mlclaw 0.3.7 → 0.4.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/.agents/skills/mlclaw/SKILL.md +20 -1
- package/Dockerfile +1 -1
- package/README.md +73 -10
- package/dist/hf-state-sync.js +4 -3
- package/dist/mlclaw-space-runtime.js +247 -62
- package/dist/mlclaw.mjs +7118 -482
- package/entrypoint.sh +1 -1
- package/package.json +3 -3
- package/src/hf-state-sync/cli.ts +4 -3
|
@@ -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
|
|
5151
|
+
stateBucket,
|
|
5131
5152
|
stateMountDir,
|
|
5132
5153
|
statePrefix,
|
|
5133
|
-
gatewayLocation
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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 &&
|
|
8526
|
+
return actualBuffer.length === expectedBuffer.length && timingSafeEqual4(actualBuffer, expectedBuffer);
|
|
8471
8527
|
}
|
|
8472
8528
|
function requestHeader(headers, name) {
|
|
8473
8529
|
const value = headers[name];
|
|
@@ -8547,7 +8603,7 @@ async function configureOpenClawGateway(config2) {
|
|
|
8547
8603
|
gateway.controlUi = {
|
|
8548
8604
|
...typeof gateway.controlUi === "object" && gateway.controlUi ? gateway.controlUi : {},
|
|
8549
8605
|
dangerouslyDisableDeviceAuth: true,
|
|
8550
|
-
allowedOrigins:
|
|
8606
|
+
allowedOrigins: config2.accessOrigins,
|
|
8551
8607
|
embedSandbox: "scripts"
|
|
8552
8608
|
};
|
|
8553
8609
|
configureOpenClawModels(openclawConfig, config2);
|
|
@@ -8860,7 +8916,9 @@ function validateOpenAiApiKey(value) {
|
|
|
8860
8916
|
|
|
8861
8917
|
// src/mlclaw-space-runtime/pages.ts
|
|
8862
8918
|
function templatePage(config2) {
|
|
8863
|
-
return page(
|
|
8919
|
+
return page(
|
|
8920
|
+
"ML Claw",
|
|
8921
|
+
`
|
|
8864
8922
|
<main>
|
|
8865
8923
|
<img src="/assets/mlclaw.svg" alt="ML Claw" class="logo">
|
|
8866
8924
|
<h1>ML Claw</h1>
|
|
@@ -8880,30 +8938,85 @@ function templatePage(config2) {
|
|
|
8880
8938
|
<p class="muted">Manual duplication is for development or advanced setup only.</p>
|
|
8881
8939
|
<p class="muted">Source Space: ${escapeHtml(config2.spaceId ?? config2.canonicalSpaceId)}</p>
|
|
8882
8940
|
</main>
|
|
8883
|
-
`
|
|
8941
|
+
`
|
|
8942
|
+
);
|
|
8884
8943
|
}
|
|
8885
8944
|
function loginPage(config2, message, next = "/") {
|
|
8886
8945
|
const oauthReady = Boolean(config2.oauthClientId && config2.oauthClientSecret);
|
|
8887
8946
|
const loginPath = next === "/" ? "/oauth/login" : `/oauth/login?next=${encodeURIComponent(next)}`;
|
|
8888
8947
|
const loginHref = new URL(loginPath, config2.publicUrl).toString();
|
|
8889
|
-
return page(
|
|
8948
|
+
return page(
|
|
8949
|
+
`${config2.branding.name} Login`,
|
|
8950
|
+
`
|
|
8890
8951
|
<main>
|
|
8891
8952
|
<img src="/assets/hf-logo.svg" alt="Hugging Face" class="logo">
|
|
8892
8953
|
<h1>${escapeHtml(config2.branding.name)}</h1>
|
|
8893
8954
|
${message ? `<p class="notice">${escapeHtml(message)}</p>` : ""}
|
|
8894
8955
|
${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
8956
|
</main>
|
|
8896
|
-
`
|
|
8957
|
+
`
|
|
8958
|
+
);
|
|
8959
|
+
}
|
|
8960
|
+
function localLoginPage(config2) {
|
|
8961
|
+
return page(
|
|
8962
|
+
`${config2.branding.name} Local Access`,
|
|
8963
|
+
`
|
|
8964
|
+
<main>
|
|
8965
|
+
<img src="/assets/mlclaw.svg" alt="ML Claw" class="logo">
|
|
8966
|
+
<h1>${escapeHtml(config2.branding.name)}</h1>
|
|
8967
|
+
<p class="muted">Use the private access link printed by the ML Claw CLI.</p>
|
|
8968
|
+
<form id="local-login-form">
|
|
8969
|
+
<label for="local-access-token">Local access code</label>
|
|
8970
|
+
<input id="local-access-token" name="token" type="password" autocomplete="off" required>
|
|
8971
|
+
<button class="button" type="submit">Open gateway</button>
|
|
8972
|
+
</form>
|
|
8973
|
+
<p id="local-login-status" class="notice" role="status"></p>
|
|
8974
|
+
</main>
|
|
8975
|
+
<script>
|
|
8976
|
+
const form = document.getElementById("local-login-form");
|
|
8977
|
+
const input = document.getElementById("local-access-token");
|
|
8978
|
+
const status = document.getElementById("local-login-status");
|
|
8979
|
+
const submit = async () => {
|
|
8980
|
+
status.textContent = "Signing in...";
|
|
8981
|
+
const response = await fetch("/mlclaw/api/local-session", {
|
|
8982
|
+
method: "POST",
|
|
8983
|
+
credentials: "same-origin",
|
|
8984
|
+
headers: { "content-type": "application/json" },
|
|
8985
|
+
body: JSON.stringify({ token: input.value }),
|
|
8986
|
+
});
|
|
8987
|
+
input.value = "";
|
|
8988
|
+
if (response.ok) {
|
|
8989
|
+
location.replace("/");
|
|
8990
|
+
return;
|
|
8991
|
+
}
|
|
8992
|
+
status.textContent = "The local access link is invalid. Run mlclaw gateway status again.";
|
|
8993
|
+
};
|
|
8994
|
+
form.addEventListener("submit", (event) => {
|
|
8995
|
+
event.preventDefault();
|
|
8996
|
+
submit().catch(() => { status.textContent = "The local gateway could not be reached."; });
|
|
8997
|
+
});
|
|
8998
|
+
const token = location.hash.slice(1);
|
|
8999
|
+
if (token) {
|
|
9000
|
+
history.replaceState(null, "", location.pathname);
|
|
9001
|
+
input.value = token;
|
|
9002
|
+
submit().catch(() => { status.textContent = "The local gateway could not be reached."; });
|
|
9003
|
+
}
|
|
9004
|
+
</script>
|
|
9005
|
+
`
|
|
9006
|
+
);
|
|
8897
9007
|
}
|
|
8898
9008
|
function unauthorizedPage(username) {
|
|
8899
|
-
return page(
|
|
9009
|
+
return page(
|
|
9010
|
+
"ML Claw Access",
|
|
9011
|
+
`
|
|
8900
9012
|
<main>
|
|
8901
9013
|
<h1>Access not allowed</h1>
|
|
8902
9014
|
<p>The signed-in Hugging Face account <strong>${escapeHtml(username)}</strong> is not allowed to operate this Space.</p>
|
|
8903
9015
|
<p class="muted">Set <code>MLCLAW_ALLOWED_USERS</code> to a comma-separated list of usernames, then restart the Space.</p>
|
|
8904
9016
|
<a class="button secondary" href="/mlclaw/logout">Sign out</a>
|
|
8905
9017
|
</main>
|
|
8906
|
-
`
|
|
9018
|
+
`
|
|
9019
|
+
);
|
|
8907
9020
|
}
|
|
8908
9021
|
function page(title, body) {
|
|
8909
9022
|
return `<!doctype html>
|
|
@@ -9120,7 +9233,7 @@ function positiveNumber2(value) {
|
|
|
9120
9233
|
}
|
|
9121
9234
|
|
|
9122
9235
|
// src/mlclaw-space-runtime/cookies.ts
|
|
9123
|
-
import { createHmac as
|
|
9236
|
+
import { createHmac as createHmac5, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual5 } from "node:crypto";
|
|
9124
9237
|
function createSignedCookie(options, payload) {
|
|
9125
9238
|
const body = Buffer.from(JSON.stringify({
|
|
9126
9239
|
...payload,
|
|
@@ -9172,12 +9285,12 @@ function randomState() {
|
|
|
9172
9285
|
return randomBytes6(24).toString("base64url");
|
|
9173
9286
|
}
|
|
9174
9287
|
function sign2(value, secret) {
|
|
9175
|
-
return
|
|
9288
|
+
return createHmac5("sha256", secret).update(value).digest("base64url");
|
|
9176
9289
|
}
|
|
9177
9290
|
function signatureMatches2(a, b) {
|
|
9178
9291
|
const left = Buffer.from(a);
|
|
9179
9292
|
const right = Buffer.from(b);
|
|
9180
|
-
return left.length === right.length &&
|
|
9293
|
+
return left.length === right.length && timingSafeEqual5(left, right);
|
|
9181
9294
|
}
|
|
9182
9295
|
function parseCookies(header) {
|
|
9183
9296
|
const cookies = /* @__PURE__ */ new Map();
|
|
@@ -9220,33 +9333,39 @@ var STATE_COOKIE = "mlclaw_oauth";
|
|
|
9220
9333
|
var SESSION_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
9221
9334
|
var STATE_TTL_SECONDS = 60 * 10;
|
|
9222
9335
|
function createSessionCookie(params) {
|
|
9223
|
-
return createSignedCookie(
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9336
|
+
return createSignedCookie(
|
|
9337
|
+
{
|
|
9338
|
+
name: params.cookieName ?? SESSION_COOKIE,
|
|
9339
|
+
secret: params.sessionSecret,
|
|
9340
|
+
maxAgeSeconds: SESSION_TTL_SECONDS,
|
|
9341
|
+
secure: params.secure
|
|
9342
|
+
},
|
|
9343
|
+
{ username: params.username }
|
|
9344
|
+
);
|
|
9229
9345
|
}
|
|
9230
9346
|
function createOauthStateCookie(params) {
|
|
9231
9347
|
const state = params.state ?? randomState();
|
|
9232
9348
|
return {
|
|
9233
9349
|
state,
|
|
9234
|
-
cookie: createSignedCookie(
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9350
|
+
cookie: createSignedCookie(
|
|
9351
|
+
{
|
|
9352
|
+
name: STATE_COOKIE,
|
|
9353
|
+
secret: params.sessionSecret,
|
|
9354
|
+
maxAgeSeconds: STATE_TTL_SECONDS,
|
|
9355
|
+
secure: params.secure
|
|
9356
|
+
},
|
|
9357
|
+
{ state, next: normalizeNext(params.next), intent: params.intent ?? "login" }
|
|
9358
|
+
)
|
|
9240
9359
|
};
|
|
9241
9360
|
}
|
|
9242
|
-
function clearSessionCookie(secure) {
|
|
9243
|
-
return clearCookie(
|
|
9361
|
+
function clearSessionCookie(secure, cookieName = SESSION_COOKIE) {
|
|
9362
|
+
return clearCookie(cookieName, secure);
|
|
9244
9363
|
}
|
|
9245
9364
|
function clearOauthStateCookie(secure) {
|
|
9246
9365
|
return clearCookie(STATE_COOKIE, secure);
|
|
9247
9366
|
}
|
|
9248
|
-
function readSession(cookieHeader, sessionSecret) {
|
|
9249
|
-
return verifySignedCookie(cookieHeader,
|
|
9367
|
+
function readSession(cookieHeader, sessionSecret, cookieName = SESSION_COOKIE) {
|
|
9368
|
+
return verifySignedCookie(cookieHeader, cookieName, sessionSecret);
|
|
9250
9369
|
}
|
|
9251
9370
|
function readOauthState(cookieHeader, sessionSecret) {
|
|
9252
9371
|
return verifySignedCookie(cookieHeader, STATE_COOKIE, sessionSecret);
|
|
@@ -9564,6 +9683,7 @@ function createSpaceRuntimeApp(config2, controls) {
|
|
|
9564
9683
|
const allowBrokerKitSummary = fixedWindowRateLimit(12, 6e4);
|
|
9565
9684
|
const allowDelegatedEvents = fixedWindowRateLimit(60, 6e4);
|
|
9566
9685
|
const allowSummaryEvents = fixedWindowRateLimit(60, 6e4);
|
|
9686
|
+
const allowLocalLogin = fixedWindowRateLimit(10, 6e4);
|
|
9567
9687
|
const openAiCredentials = new OpenAiCredentialStore(config2.openaiCredentialStoreFile, config2.credentialKey);
|
|
9568
9688
|
app.get("/health", (c) => health(c, config2, controls));
|
|
9569
9689
|
app.get("/healthz", (c) => health(c, config2, controls));
|
|
@@ -9599,10 +9719,58 @@ function createSpaceRuntimeApp(config2, controls) {
|
|
|
9599
9719
|
);
|
|
9600
9720
|
app.get("/oauth/login", (c) => handleOauthLogin(c, config2));
|
|
9601
9721
|
app.get("/oauth/callback", (c) => handleOauthCallback(c, config2, controls));
|
|
9722
|
+
app.get("/mlclaw/local-login", (c) => {
|
|
9723
|
+
if (!config2.localAccessUser || !config2.localAccessToken || config2.gatewayLocation !== "local") {
|
|
9724
|
+
return c.text("not found\n", 404);
|
|
9725
|
+
}
|
|
9726
|
+
c.header(
|
|
9727
|
+
"content-security-policy",
|
|
9728
|
+
"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'"
|
|
9729
|
+
);
|
|
9730
|
+
c.header("cache-control", "no-store");
|
|
9731
|
+
c.header("referrer-policy", "no-referrer");
|
|
9732
|
+
return c.html(localLoginPage(config2));
|
|
9733
|
+
});
|
|
9734
|
+
app.post("/mlclaw/api/local-session", async (c) => {
|
|
9735
|
+
if (!config2.localAccessUser || !config2.localAccessToken || config2.gatewayLocation !== "local") {
|
|
9736
|
+
return c.json({ ok: false, error: "not found" }, 404);
|
|
9737
|
+
}
|
|
9738
|
+
const origin = c.req.header("origin");
|
|
9739
|
+
if (!origin || !config2.accessOrigins.includes(origin) || !allowLocalLogin(origin)) {
|
|
9740
|
+
return c.json({ ok: false, error: "access denied" }, 403);
|
|
9741
|
+
}
|
|
9742
|
+
const contentLength = Number.parseInt(c.req.header("content-length") ?? "0", 10);
|
|
9743
|
+
if (contentLength > 512 || !(c.req.header("content-type") ?? "").startsWith("application/json")) {
|
|
9744
|
+
return c.json({ ok: false, error: "invalid request" }, 400);
|
|
9745
|
+
}
|
|
9746
|
+
const text = await c.req.text();
|
|
9747
|
+
if (text.length > 512) {
|
|
9748
|
+
return c.json({ ok: false, error: "invalid request" }, 400);
|
|
9749
|
+
}
|
|
9750
|
+
let token;
|
|
9751
|
+
try {
|
|
9752
|
+
token = JSON.parse(text).token;
|
|
9753
|
+
} catch {
|
|
9754
|
+
return c.json({ ok: false, error: "invalid request" }, 400);
|
|
9755
|
+
}
|
|
9756
|
+
if (typeof token !== "string" || !localAccessTokenMatches(token, config2.localAccessToken)) {
|
|
9757
|
+
return c.json({ ok: false, error: "access denied" }, 403);
|
|
9758
|
+
}
|
|
9759
|
+
c.header(
|
|
9760
|
+
"set-cookie",
|
|
9761
|
+
createSessionCookie({
|
|
9762
|
+
username: config2.localAccessUser,
|
|
9763
|
+
sessionSecret: config2.sessionSecret,
|
|
9764
|
+
secure: origin.startsWith("https://"),
|
|
9765
|
+
cookieName: config2.sessionCookieName
|
|
9766
|
+
})
|
|
9767
|
+
);
|
|
9768
|
+
return c.json({ ok: true });
|
|
9769
|
+
});
|
|
9602
9770
|
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));
|
|
9771
|
+
app.get("/logout", (c) => logoutResponse(c, config2, false));
|
|
9772
|
+
app.get("/mlclaw/logout", (c) => logoutResponse(c, config2, false));
|
|
9773
|
+
app.post("/mlclaw/api/logout", (c) => logoutResponse(c, config2, true));
|
|
9606
9774
|
app.get("/mlclaw/assets/*", async (c) => {
|
|
9607
9775
|
const relative = c.req.path.slice("/mlclaw/assets/".length);
|
|
9608
9776
|
const safe = safeRelativePath(relative);
|
|
@@ -9903,7 +10071,7 @@ function handleOauthLogin(c, config2) {
|
|
|
9903
10071
|
if (!config2.oauthClientId || !config2.oauthClientSecret) {
|
|
9904
10072
|
return c.html(loginPage(config2, "Hugging Face OAuth is not configured.", next));
|
|
9905
10073
|
}
|
|
9906
|
-
const session = readSession(c.req.header("cookie"), config2.sessionSecret);
|
|
10074
|
+
const session = readSession(c.req.header("cookie"), config2.sessionSecret, config2.sessionCookieName);
|
|
9907
10075
|
const integrationsRequested = c.req.query("intent") === "integrations";
|
|
9908
10076
|
const intent = integrationsRequested && session && isAdmin(config2, session.username) ? "integrations" : "login";
|
|
9909
10077
|
const { state, cookie } = createOauthStateCookie({
|
|
@@ -9948,7 +10116,7 @@ async function handleOauthCallback(c, config2, controls) {
|
|
|
9948
10116
|
return c.html(loginPage(config2, "Hugging Face sign-in failed. Try again."), 401);
|
|
9949
10117
|
}
|
|
9950
10118
|
if (stateCookie.intent === "integrations") {
|
|
9951
|
-
const session = readSession(c.req.header("cookie"), config2.sessionSecret);
|
|
10119
|
+
const session = readSession(c.req.header("cookie"), config2.sessionSecret, config2.sessionCookieName);
|
|
9952
10120
|
if (!session || !isAdmin(config2, session.username) || session.username !== identity.username) {
|
|
9953
10121
|
return c.html(loginPage(config2, "Integration authorization requires the signed-in ML Claw administrator."), 403);
|
|
9954
10122
|
}
|
|
@@ -9971,7 +10139,8 @@ async function handleOauthCallback(c, config2, controls) {
|
|
|
9971
10139
|
createSessionCookie({
|
|
9972
10140
|
username: identity.username,
|
|
9973
10141
|
sessionSecret: config2.sessionSecret,
|
|
9974
|
-
secure: config2.cookieSecure
|
|
10142
|
+
secure: config2.cookieSecure,
|
|
10143
|
+
cookieName: config2.sessionCookieName
|
|
9975
10144
|
})
|
|
9976
10145
|
);
|
|
9977
10146
|
headers.append("set-cookie", clearOauthStateCookie(config2.cookieSecure));
|
|
@@ -10042,9 +10211,11 @@ function trustedBrokerKitHeaders(mode, origin) {
|
|
|
10042
10211
|
if (asset) headers.set("access-control-allow-origin", "null");
|
|
10043
10212
|
return headers;
|
|
10044
10213
|
}
|
|
10045
|
-
function logoutResponse(config2, json) {
|
|
10214
|
+
function logoutResponse(c, config2, json) {
|
|
10046
10215
|
const headers = new Headers();
|
|
10047
|
-
|
|
10216
|
+
const origin = c.req.header("origin");
|
|
10217
|
+
const secure = origin && config2.accessOrigins.includes(origin) ? origin.startsWith("https://") : config2.cookieSecure;
|
|
10218
|
+
headers.append("set-cookie", clearSessionCookie(secure, config2.sessionCookieName));
|
|
10048
10219
|
if (json) {
|
|
10049
10220
|
headers.set("content-type", "application/json; charset=utf-8");
|
|
10050
10221
|
return new Response(`${JSON.stringify({ ok: true })}
|
|
@@ -10054,7 +10225,7 @@ function logoutResponse(config2, json) {
|
|
|
10054
10225
|
return new Response(null, { status: 302, headers });
|
|
10055
10226
|
}
|
|
10056
10227
|
function requireAllowed(c, config2) {
|
|
10057
|
-
const session = readSession(c.req.header("cookie"), config2.sessionSecret);
|
|
10228
|
+
const session = readSession(c.req.header("cookie"), config2.sessionSecret, config2.sessionCookieName);
|
|
10058
10229
|
if (!session) {
|
|
10059
10230
|
return unauthenticated(c, config2);
|
|
10060
10231
|
}
|
|
@@ -10160,7 +10331,10 @@ function unauthenticated(c, config2) {
|
|
|
10160
10331
|
return c.json({ ok: false, error: "authentication required" }, 401);
|
|
10161
10332
|
}
|
|
10162
10333
|
if (isBrowserNavigation(c)) {
|
|
10163
|
-
return c.redirect(
|
|
10334
|
+
return c.redirect(
|
|
10335
|
+
config2.gatewayLocation === "local" ? "/mlclaw/local-login" : `/login?next=${encodeURIComponent(next)}`,
|
|
10336
|
+
302
|
|
10337
|
+
);
|
|
10164
10338
|
}
|
|
10165
10339
|
return c.html(loginPage(config2, void 0, next), 401);
|
|
10166
10340
|
}
|
|
@@ -10739,7 +10913,7 @@ async function proxyHttp(req, res, config2, identity) {
|
|
|
10739
10913
|
delete headers["accept-encoding"];
|
|
10740
10914
|
delete headers["Accept-Encoding"];
|
|
10741
10915
|
}
|
|
10742
|
-
addTrustedProxyHeaders(headers, config2, identity);
|
|
10916
|
+
addTrustedProxyHeaders(headers, config2, identity, requestAccessOrigin(req, config2));
|
|
10743
10917
|
const upstream = http2.request(
|
|
10744
10918
|
{
|
|
10745
10919
|
host: config2.openclawHost,
|
|
@@ -10797,7 +10971,7 @@ function proxyWebSocket(req, socket, head, config2, identity) {
|
|
|
10797
10971
|
headers.host = `${config2.openclawHost}:${config2.openclawPort}`;
|
|
10798
10972
|
headers.connection = "Upgrade";
|
|
10799
10973
|
headers.upgrade = req.headers.upgrade ?? "websocket";
|
|
10800
|
-
addTrustedProxyHeaders(headers, config2, identity);
|
|
10974
|
+
addTrustedProxyHeaders(headers, config2, identity, requestAccessOrigin(req, config2));
|
|
10801
10975
|
upstream.write(`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}\r
|
|
10802
10976
|
`);
|
|
10803
10977
|
for (const [key, value] of Object.entries(headers)) {
|
|
@@ -10841,19 +11015,26 @@ function sanitizeHeaders(headers) {
|
|
|
10841
11015
|
if (HOP_BY_HOP_HEADERS.has(lower)) {
|
|
10842
11016
|
continue;
|
|
10843
11017
|
}
|
|
10844
|
-
if (lower.startsWith("x-forwarded-") || lower.startsWith("x-openclaw-") || lower === "authorization") {
|
|
11018
|
+
if (lower.startsWith("x-forwarded-") || lower.startsWith("x-openclaw-") || lower.startsWith("tailscale-") || lower === "authorization") {
|
|
10845
11019
|
continue;
|
|
10846
11020
|
}
|
|
10847
11021
|
out[key] = value;
|
|
10848
11022
|
}
|
|
10849
11023
|
return out;
|
|
10850
11024
|
}
|
|
10851
|
-
function addTrustedProxyHeaders(headers, config2, identity) {
|
|
11025
|
+
function addTrustedProxyHeaders(headers, config2, identity, accessOrigin) {
|
|
10852
11026
|
headers["x-forwarded-user"] = identity.username;
|
|
10853
|
-
headers["x-forwarded-proto"] =
|
|
10854
|
-
headers["x-forwarded-host"] = new URL(
|
|
11027
|
+
headers["x-forwarded-proto"] = accessOrigin.startsWith("https://") ? "https" : "http";
|
|
11028
|
+
headers["x-forwarded-host"] = new URL(accessOrigin).host;
|
|
10855
11029
|
headers["x-openclaw-scopes"] = resolveControlUiScopes(config2, identity).join(",");
|
|
10856
11030
|
}
|
|
11031
|
+
function requestAccessOrigin(req, config2) {
|
|
11032
|
+
const host = req.headers.host?.trim().toLowerCase();
|
|
11033
|
+
if (!host) {
|
|
11034
|
+
return config2.publicUrl;
|
|
11035
|
+
}
|
|
11036
|
+
return config2.accessOrigins.find((origin) => new URL(origin).host.toLowerCase() === host) ?? config2.publicUrl;
|
|
11037
|
+
}
|
|
10857
11038
|
function resolveControlUiScopes(config2, identity) {
|
|
10858
11039
|
return config2.adminUsers.includes(identity.username) ? ADMIN_CONTROL_UI_SCOPES : USER_CONTROL_UI_SCOPES;
|
|
10859
11040
|
}
|
|
@@ -10934,7 +11115,7 @@ var SpaceRuntimeServer = class {
|
|
|
10934
11115
|
server2.on("upgrade", (req, socket, head) => {
|
|
10935
11116
|
const netSocket = socket;
|
|
10936
11117
|
try {
|
|
10937
|
-
const session = readSession(req.headers.cookie, this.config.sessionSecret);
|
|
11118
|
+
const session = readSession(req.headers.cookie, this.config.sessionSecret, this.config.sessionCookieName);
|
|
10938
11119
|
if (!session || !this.isAllowed(session.username)) {
|
|
10939
11120
|
rejectWebSocket(netSocket);
|
|
10940
11121
|
return;
|
|
@@ -11011,7 +11192,7 @@ var SpaceRuntimeServer = class {
|
|
|
11011
11192
|
res.off("close", abortRequest);
|
|
11012
11193
|
}
|
|
11013
11194
|
}
|
|
11014
|
-
const session = readSession(req.headers.cookie, this.config.sessionSecret);
|
|
11195
|
+
const session = readSession(req.headers.cookie, this.config.sessionSecret, this.config.sessionCookieName);
|
|
11015
11196
|
if (!session) {
|
|
11016
11197
|
this.sendUnauthenticated(req, res, url);
|
|
11017
11198
|
return;
|
|
@@ -11091,6 +11272,10 @@ var SpaceRuntimeServer = class {
|
|
|
11091
11272
|
}
|
|
11092
11273
|
sendUnauthenticated(req, res, url) {
|
|
11093
11274
|
const next = normalizeNext(`${url.pathname}${url.search}`);
|
|
11275
|
+
if (this.config.gatewayLocation === "local" && isBrowserNavigation2(req)) {
|
|
11276
|
+
this.sendRedirect(res, "/mlclaw/local-login");
|
|
11277
|
+
return;
|
|
11278
|
+
}
|
|
11094
11279
|
if (url.pathname === "/" && (req.method === "GET" || req.method === "HEAD")) {
|
|
11095
11280
|
this.sendHtml(res, loginPage(this.config, void 0, next));
|
|
11096
11281
|
return;
|