mlclaw 0.4.10 → 0.6.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 +14 -0
- package/Dockerfile +3 -2
- package/README.md +35 -2
- package/assets/hf-tooling/manifest.json +9 -0
- package/assets/hf-tooling/skills/hf-broker/SKILL.md +126 -0
- package/dist/hf-tooling-seed.js +17 -0
- package/dist/mlclaw-space-runtime.js +6029 -301
- package/dist/mlclaw.mjs +698 -18
- package/entrypoint.sh +33 -1
- package/package.json +5 -5
package/dist/mlclaw.mjs
CHANGED
|
@@ -8956,7 +8956,7 @@ import fs15 from "node:fs/promises";
|
|
|
8956
8956
|
import { realpathSync } from "node:fs";
|
|
8957
8957
|
import os8 from "node:os";
|
|
8958
8958
|
import process4 from "node:process";
|
|
8959
|
-
import { createHash as createHash4, randomBytes, randomUUID as randomUUID2 } from "node:crypto";
|
|
8959
|
+
import { createHash as createHash4, randomBytes as randomBytes2, randomUUID as randomUUID2 } from "node:crypto";
|
|
8960
8960
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
8961
8961
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
8962
8962
|
|
|
@@ -15383,9 +15383,9 @@ function nextLink(header) {
|
|
|
15383
15383
|
|
|
15384
15384
|
// src/mlclaw/release-config.generated.ts
|
|
15385
15385
|
var RELEASE_CONFIG = {
|
|
15386
|
-
"packageVersion": "0.
|
|
15386
|
+
"packageVersion": "0.6.0",
|
|
15387
15387
|
"openclawVersion": "2026.7.1",
|
|
15388
|
-
"brokerkitVersion": "hf-broker/v0.
|
|
15388
|
+
"brokerkitVersion": "hf-broker/v0.6.2",
|
|
15389
15389
|
"brokerkitPluginVersion": "0.4.1",
|
|
15390
15390
|
"runtimeImageRepository": "ghcr.io/huggingface/mlclaw"
|
|
15391
15391
|
};
|
|
@@ -15530,6 +15530,7 @@ RUN apt-get update \\
|
|
|
15530
15530
|
&& rm -rf /var/lib/apt/lists/*
|
|
15531
15531
|
RUN python3 -m pip install --break-system-packages --no-cache-dir \\
|
|
15532
15532
|
"huggingface_hub==1.19.0" \\
|
|
15533
|
+
"hf-xet==1.5.2" \\
|
|
15533
15534
|
"datasets==5.0.0" \\
|
|
15534
15535
|
"safetensors==0.8.0" \\
|
|
15535
15536
|
"fastapi==0.137.1" \\
|
|
@@ -15701,6 +15702,398 @@ async function assertNoLiveForeignLease(params) {
|
|
|
15701
15702
|
);
|
|
15702
15703
|
}
|
|
15703
15704
|
|
|
15705
|
+
// src/mlclaw/codex-auth.ts
|
|
15706
|
+
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto";
|
|
15707
|
+
var CODEX_AUTH_OBJECT_BASENAME = ".mlclaw/codex-auth.enc";
|
|
15708
|
+
var CODEX_AUTH_REVOCATION_BASENAME = ".mlclaw/codex-auth.revoked";
|
|
15709
|
+
function codexAuthObjectPath(statePrefix) {
|
|
15710
|
+
return `${normalizeBucketPrefix(statePrefix)}/${CODEX_AUTH_OBJECT_BASENAME}`;
|
|
15711
|
+
}
|
|
15712
|
+
function codexAuthRevocationObjectPath(statePrefix) {
|
|
15713
|
+
return `${normalizeBucketPrefix(statePrefix)}/${CODEX_AUTH_REVOCATION_BASENAME}`;
|
|
15714
|
+
}
|
|
15715
|
+
function codexAuthContext(params) {
|
|
15716
|
+
return compactContext({
|
|
15717
|
+
...params.deploymentId ? { deploymentId: params.deploymentId } : {},
|
|
15718
|
+
...params.bucket ? { bucket: params.bucket } : {},
|
|
15719
|
+
statePrefix: normalizeBucketPrefix(params.statePrefix)
|
|
15720
|
+
});
|
|
15721
|
+
}
|
|
15722
|
+
function encodeCodexAuthDocument(params) {
|
|
15723
|
+
if (!params.authJson || typeof params.authJson !== "object" || Array.isArray(params.authJson)) {
|
|
15724
|
+
throw new Error("Codex auth.json must contain a JSON object");
|
|
15725
|
+
}
|
|
15726
|
+
const authJson = params.authJson;
|
|
15727
|
+
const authMode = typeof authJson.auth_mode === "string" ? authJson.auth_mode : void 0;
|
|
15728
|
+
if (authMode && authMode !== "chatgpt") {
|
|
15729
|
+
throw new Error("Codex auth.json is not a ChatGPT account login");
|
|
15730
|
+
}
|
|
15731
|
+
if (!authMode && !("tokens" in authJson)) {
|
|
15732
|
+
throw new Error("Codex auth.json does not look like account credentials");
|
|
15733
|
+
}
|
|
15734
|
+
return {
|
|
15735
|
+
version: 1,
|
|
15736
|
+
kind: "codex-auth",
|
|
15737
|
+
authJson,
|
|
15738
|
+
updatedAt: params.now.toISOString()
|
|
15739
|
+
};
|
|
15740
|
+
}
|
|
15741
|
+
function encryptCodexAuthDocument(params) {
|
|
15742
|
+
const context = compactContext(params.context);
|
|
15743
|
+
const key = deriveCodexAuthKey(params.secret);
|
|
15744
|
+
const iv = randomBytes(12);
|
|
15745
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
15746
|
+
cipher.setAAD(contextAad(context));
|
|
15747
|
+
const plaintext = Buffer.from(JSON.stringify(params.document), "utf8");
|
|
15748
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
15749
|
+
const envelope = {
|
|
15750
|
+
version: 1,
|
|
15751
|
+
kind: "codex-auth",
|
|
15752
|
+
algorithm: "aes-256-gcm",
|
|
15753
|
+
context,
|
|
15754
|
+
iv: iv.toString("base64url"),
|
|
15755
|
+
tag: cipher.getAuthTag().toString("base64url"),
|
|
15756
|
+
ciphertext: ciphertext.toString("base64url")
|
|
15757
|
+
};
|
|
15758
|
+
return `${JSON.stringify(envelope)}
|
|
15759
|
+
`;
|
|
15760
|
+
}
|
|
15761
|
+
function decryptCodexAuthDocument(params) {
|
|
15762
|
+
const envelope = JSON.parse(params.encrypted);
|
|
15763
|
+
if (envelope.version !== 1 || envelope.kind !== "codex-auth" || envelope.algorithm !== "aes-256-gcm" || !envelope.context || typeof envelope.context !== "object" || !envelope.iv || !envelope.tag || !envelope.ciphertext) {
|
|
15764
|
+
throw new Error("invalid Codex auth envelope");
|
|
15765
|
+
}
|
|
15766
|
+
const context = compactContext(envelope.context);
|
|
15767
|
+
assertContextMatches(context, params.expectedContext);
|
|
15768
|
+
const key = deriveCodexAuthKey(params.secret);
|
|
15769
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64url"));
|
|
15770
|
+
decipher.setAAD(contextAad(context));
|
|
15771
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
|
|
15772
|
+
const plaintext = Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, "base64url")), decipher.final()]);
|
|
15773
|
+
return decodeCodexAuthDocument(JSON.parse(plaintext.toString("utf8")));
|
|
15774
|
+
}
|
|
15775
|
+
function decodeCodexAuthDocument(value) {
|
|
15776
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
15777
|
+
throw new Error("invalid Codex auth document");
|
|
15778
|
+
}
|
|
15779
|
+
const record2 = value;
|
|
15780
|
+
if (record2.version !== 1 || record2.kind !== "codex-auth") {
|
|
15781
|
+
throw new Error("invalid Codex auth document");
|
|
15782
|
+
}
|
|
15783
|
+
if (!record2.authJson || typeof record2.authJson !== "object" || Array.isArray(record2.authJson)) {
|
|
15784
|
+
throw new Error("invalid Codex auth document");
|
|
15785
|
+
}
|
|
15786
|
+
const updatedAt = typeof record2.updatedAt === "string" ? record2.updatedAt : void 0;
|
|
15787
|
+
if (!updatedAt || Number.isNaN(Date.parse(updatedAt))) {
|
|
15788
|
+
throw new Error("invalid Codex auth document");
|
|
15789
|
+
}
|
|
15790
|
+
return {
|
|
15791
|
+
version: 1,
|
|
15792
|
+
kind: "codex-auth",
|
|
15793
|
+
authJson: record2.authJson,
|
|
15794
|
+
updatedAt
|
|
15795
|
+
};
|
|
15796
|
+
}
|
|
15797
|
+
function deriveCodexAuthKey(secret) {
|
|
15798
|
+
return Buffer.from(
|
|
15799
|
+
hkdfSync("sha256", Buffer.from(secret, "utf8"), Buffer.alloc(0), Buffer.from("mlclaw:codex-auth:v1"), 32)
|
|
15800
|
+
);
|
|
15801
|
+
}
|
|
15802
|
+
function compactContext(context) {
|
|
15803
|
+
const statePrefix = normalizeBucketPrefix(context.statePrefix);
|
|
15804
|
+
return {
|
|
15805
|
+
...context.deploymentId ? { deploymentId: context.deploymentId } : {},
|
|
15806
|
+
...context.bucket ? { bucket: context.bucket } : {},
|
|
15807
|
+
statePrefix
|
|
15808
|
+
};
|
|
15809
|
+
}
|
|
15810
|
+
function assertContextMatches(observed, expected) {
|
|
15811
|
+
if (!expected) return;
|
|
15812
|
+
const normalized = compactContext(expected);
|
|
15813
|
+
for (const key of ["deploymentId", "bucket", "statePrefix"]) {
|
|
15814
|
+
if (normalized[key] && observed[key] !== normalized[key]) {
|
|
15815
|
+
throw new Error("Codex auth context does not match this deployment");
|
|
15816
|
+
}
|
|
15817
|
+
}
|
|
15818
|
+
}
|
|
15819
|
+
function contextAad(context) {
|
|
15820
|
+
return Buffer.from(JSON.stringify(compactContext(context)), "utf8");
|
|
15821
|
+
}
|
|
15822
|
+
|
|
15823
|
+
// src/mlclaw/openai-codex-device-auth.ts
|
|
15824
|
+
var OPENAI_AUTH_BASE_URL = "https://auth.openai.com";
|
|
15825
|
+
var OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
15826
|
+
var OPENAI_CODEX_DEVICE_VERIFICATION_URL = `${OPENAI_AUTH_BASE_URL}/codex/device`;
|
|
15827
|
+
var OPENAI_CODEX_DEVICE_CALLBACK_URL = `${OPENAI_AUTH_BASE_URL}/deviceauth/callback`;
|
|
15828
|
+
var OPENAI_CODEX_DEVICE_TIMEOUT_MS = 15 * 6e4;
|
|
15829
|
+
var DEFAULT_POLL_INTERVAL_MS = 5e3;
|
|
15830
|
+
var MIN_POLL_INTERVAL_MS = 1e3;
|
|
15831
|
+
var MAX_RESPONSE_BYTES = 256 * 1024;
|
|
15832
|
+
var JWT_AUTH_CLAIM = "https://api.openai.com/auth";
|
|
15833
|
+
async function loginOpenAICodexDeviceCode(options) {
|
|
15834
|
+
throwIfAborted(options.signal);
|
|
15835
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
15836
|
+
const now = options.now ?? Date.now;
|
|
15837
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
15838
|
+
const device = await requestDeviceCode(fetchFn, options.signal);
|
|
15839
|
+
await options.onVerification({
|
|
15840
|
+
verificationUrl: OPENAI_CODEX_DEVICE_VERIFICATION_URL,
|
|
15841
|
+
userCode: device.userCode,
|
|
15842
|
+
expiresInMs: OPENAI_CODEX_DEVICE_TIMEOUT_MS
|
|
15843
|
+
});
|
|
15844
|
+
const authorization = await pollDeviceAuthorization({
|
|
15845
|
+
fetchFn,
|
|
15846
|
+
device,
|
|
15847
|
+
now,
|
|
15848
|
+
sleep,
|
|
15849
|
+
...options.signal ? { signal: options.signal } : {}
|
|
15850
|
+
});
|
|
15851
|
+
return await exchangeDeviceAuthorization(fetchFn, authorization, now, options.signal);
|
|
15852
|
+
}
|
|
15853
|
+
function codexAuthJsonFromOAuthCredential(credential, now) {
|
|
15854
|
+
return {
|
|
15855
|
+
auth_mode: "chatgpt",
|
|
15856
|
+
OPENAI_API_KEY: null,
|
|
15857
|
+
tokens: {
|
|
15858
|
+
...credential.idToken ? { id_token: credential.idToken } : {},
|
|
15859
|
+
access_token: credential.access,
|
|
15860
|
+
refresh_token: credential.refresh,
|
|
15861
|
+
account_id: credential.accountId,
|
|
15862
|
+
expires_at: credential.expires
|
|
15863
|
+
},
|
|
15864
|
+
last_refresh: now.toISOString()
|
|
15865
|
+
};
|
|
15866
|
+
}
|
|
15867
|
+
async function requestDeviceCode(fetchFn, signal) {
|
|
15868
|
+
const response = await fetchFn(`${OPENAI_AUTH_BASE_URL}/api/accounts/deviceauth/usercode`, {
|
|
15869
|
+
method: "POST",
|
|
15870
|
+
headers: requestHeaders("application/json"),
|
|
15871
|
+
body: JSON.stringify({ client_id: OPENAI_CODEX_CLIENT_ID }),
|
|
15872
|
+
...signal ? { signal } : {}
|
|
15873
|
+
});
|
|
15874
|
+
const body = await readJsonObject(response);
|
|
15875
|
+
if (!response.ok) {
|
|
15876
|
+
throw responseError("OpenAI device code request failed", response, body);
|
|
15877
|
+
}
|
|
15878
|
+
const deviceAuthId = nonEmptyString(body.device_auth_id);
|
|
15879
|
+
const userCode = nonEmptyString(body.user_code) ?? nonEmptyString(body.usercode);
|
|
15880
|
+
const intervalMs = secondsToSafeMilliseconds(body.interval);
|
|
15881
|
+
if (!deviceAuthId || !userCode) {
|
|
15882
|
+
throw new Error("OpenAI device code response was missing required fields");
|
|
15883
|
+
}
|
|
15884
|
+
return {
|
|
15885
|
+
deviceAuthId,
|
|
15886
|
+
userCode,
|
|
15887
|
+
intervalMs: Math.min(
|
|
15888
|
+
OPENAI_CODEX_DEVICE_TIMEOUT_MS,
|
|
15889
|
+
Math.max(MIN_POLL_INTERVAL_MS, intervalMs ?? DEFAULT_POLL_INTERVAL_MS)
|
|
15890
|
+
)
|
|
15891
|
+
};
|
|
15892
|
+
}
|
|
15893
|
+
async function pollDeviceAuthorization(params) {
|
|
15894
|
+
const deadline = params.now() + OPENAI_CODEX_DEVICE_TIMEOUT_MS;
|
|
15895
|
+
let intervalMs = params.device.intervalMs;
|
|
15896
|
+
while (params.now() < deadline) {
|
|
15897
|
+
throwIfAborted(params.signal);
|
|
15898
|
+
const response = await params.fetchFn(`${OPENAI_AUTH_BASE_URL}/api/accounts/deviceauth/token`, {
|
|
15899
|
+
method: "POST",
|
|
15900
|
+
headers: requestHeaders("application/json"),
|
|
15901
|
+
body: JSON.stringify({
|
|
15902
|
+
device_auth_id: params.device.deviceAuthId,
|
|
15903
|
+
user_code: params.device.userCode
|
|
15904
|
+
}),
|
|
15905
|
+
...params.signal ? { signal: params.signal } : {}
|
|
15906
|
+
});
|
|
15907
|
+
const body = await readJsonObject(response);
|
|
15908
|
+
if (response.ok) {
|
|
15909
|
+
const authorizationCode = nonEmptyString(body.authorization_code);
|
|
15910
|
+
const codeVerifier = nonEmptyString(body.code_verifier);
|
|
15911
|
+
if (!authorizationCode || !codeVerifier) {
|
|
15912
|
+
throw new Error("OpenAI device authorization response was missing required fields");
|
|
15913
|
+
}
|
|
15914
|
+
return { authorizationCode, codeVerifier };
|
|
15915
|
+
}
|
|
15916
|
+
const errorCode = oauthErrorCode(body);
|
|
15917
|
+
if (response.status === 403 || response.status === 404 || errorCode === "deviceauth_authorization_pending") {
|
|
15918
|
+
await params.sleep(Math.min(intervalMs, Math.max(0, deadline - params.now())), params.signal);
|
|
15919
|
+
continue;
|
|
15920
|
+
}
|
|
15921
|
+
if (errorCode === "slow_down") {
|
|
15922
|
+
intervalMs += 5e3;
|
|
15923
|
+
await params.sleep(Math.min(intervalMs, Math.max(0, deadline - params.now())), params.signal);
|
|
15924
|
+
continue;
|
|
15925
|
+
}
|
|
15926
|
+
throw responseError("OpenAI device authorization failed", response, body);
|
|
15927
|
+
}
|
|
15928
|
+
throw new Error("OpenAI device authorization timed out after 15 minutes");
|
|
15929
|
+
}
|
|
15930
|
+
async function exchangeDeviceAuthorization(fetchFn, authorization, now, signal) {
|
|
15931
|
+
const response = await fetchFn(`${OPENAI_AUTH_BASE_URL}/oauth/token`, {
|
|
15932
|
+
method: "POST",
|
|
15933
|
+
headers: requestHeaders("application/x-www-form-urlencoded"),
|
|
15934
|
+
body: new URLSearchParams({
|
|
15935
|
+
grant_type: "authorization_code",
|
|
15936
|
+
client_id: OPENAI_CODEX_CLIENT_ID,
|
|
15937
|
+
code: authorization.authorizationCode,
|
|
15938
|
+
code_verifier: authorization.codeVerifier,
|
|
15939
|
+
redirect_uri: OPENAI_CODEX_DEVICE_CALLBACK_URL
|
|
15940
|
+
}),
|
|
15941
|
+
...signal ? { signal } : {}
|
|
15942
|
+
});
|
|
15943
|
+
const body = await readJsonObject(response);
|
|
15944
|
+
if (!response.ok) {
|
|
15945
|
+
throw responseError("OpenAI device token exchange failed", response, body);
|
|
15946
|
+
}
|
|
15947
|
+
return credentialFromTokenResponse(body, now);
|
|
15948
|
+
}
|
|
15949
|
+
function requestHeaders(contentType) {
|
|
15950
|
+
return {
|
|
15951
|
+
"Content-Type": contentType,
|
|
15952
|
+
originator: "mlclaw",
|
|
15953
|
+
"User-Agent": "mlclaw"
|
|
15954
|
+
};
|
|
15955
|
+
}
|
|
15956
|
+
async function readJsonObject(response) {
|
|
15957
|
+
const text = await readResponseTextLimited(response);
|
|
15958
|
+
if (!text) return {};
|
|
15959
|
+
let parsed;
|
|
15960
|
+
try {
|
|
15961
|
+
parsed = JSON.parse(text);
|
|
15962
|
+
} catch {
|
|
15963
|
+
throw new Error("OpenAI OAuth response was not valid JSON");
|
|
15964
|
+
}
|
|
15965
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15966
|
+
throw new Error("OpenAI OAuth response was not a JSON object");
|
|
15967
|
+
}
|
|
15968
|
+
return parsed;
|
|
15969
|
+
}
|
|
15970
|
+
async function readResponseTextLimited(response) {
|
|
15971
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
15972
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_BYTES) {
|
|
15973
|
+
await response.body?.cancel().catch(() => void 0);
|
|
15974
|
+
throw new Error("OpenAI OAuth response exceeded the size limit");
|
|
15975
|
+
}
|
|
15976
|
+
if (!response.body) return "";
|
|
15977
|
+
const reader = response.body.getReader();
|
|
15978
|
+
const chunks = [];
|
|
15979
|
+
let total = 0;
|
|
15980
|
+
try {
|
|
15981
|
+
while (true) {
|
|
15982
|
+
const { done, value } = await reader.read();
|
|
15983
|
+
if (done) break;
|
|
15984
|
+
total += value.byteLength;
|
|
15985
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
15986
|
+
await reader.cancel().catch(() => void 0);
|
|
15987
|
+
throw new Error("OpenAI OAuth response exceeded the size limit");
|
|
15988
|
+
}
|
|
15989
|
+
chunks.push(value);
|
|
15990
|
+
}
|
|
15991
|
+
} finally {
|
|
15992
|
+
reader.releaseLock();
|
|
15993
|
+
}
|
|
15994
|
+
const merged = new Uint8Array(total);
|
|
15995
|
+
let offset = 0;
|
|
15996
|
+
for (const chunk of chunks) {
|
|
15997
|
+
merged.set(chunk, offset);
|
|
15998
|
+
offset += chunk.byteLength;
|
|
15999
|
+
}
|
|
16000
|
+
return new TextDecoder().decode(merged);
|
|
16001
|
+
}
|
|
16002
|
+
function responseError(prefix, response, body) {
|
|
16003
|
+
const code = oauthErrorCode(body);
|
|
16004
|
+
const safeCode = code ? sanitizeErrorText(code) : void 0;
|
|
16005
|
+
return new Error(`${prefix} (HTTP ${response.status})${safeCode ? `: ${safeCode}` : ""}`);
|
|
16006
|
+
}
|
|
16007
|
+
function oauthErrorCode(body) {
|
|
16008
|
+
const error = body.error;
|
|
16009
|
+
if (typeof error === "string") return nonEmptyString(error);
|
|
16010
|
+
if (error && typeof error === "object" && !Array.isArray(error)) {
|
|
16011
|
+
return nonEmptyString(error.code);
|
|
16012
|
+
}
|
|
16013
|
+
return void 0;
|
|
16014
|
+
}
|
|
16015
|
+
function sanitizeErrorText(value) {
|
|
16016
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ").replace(/\s+/gu, " ").trim().slice(0, 500);
|
|
16017
|
+
}
|
|
16018
|
+
function credentialFromTokenResponse(body, now) {
|
|
16019
|
+
const access = nonEmptyString(body.access_token);
|
|
16020
|
+
const refresh = nonEmptyString(body.refresh_token);
|
|
16021
|
+
const expiresInMs = secondsToSafeMilliseconds(body.expires_in);
|
|
16022
|
+
if (!access || !refresh || expiresInMs === void 0) {
|
|
16023
|
+
throw new Error("OpenAI token response was missing required fields");
|
|
16024
|
+
}
|
|
16025
|
+
const claims = accessTokenClaims(access);
|
|
16026
|
+
const idToken = nonEmptyString(body.id_token);
|
|
16027
|
+
return {
|
|
16028
|
+
access,
|
|
16029
|
+
refresh,
|
|
16030
|
+
expires: now() + expiresInMs,
|
|
16031
|
+
accountId: claims.accountId,
|
|
16032
|
+
...idToken ? { idToken } : {}
|
|
16033
|
+
};
|
|
16034
|
+
}
|
|
16035
|
+
function accessTokenClaims(accessToken) {
|
|
16036
|
+
const parts = accessToken.split(".");
|
|
16037
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
16038
|
+
throw new Error("OpenAI access token was not a JWT");
|
|
16039
|
+
}
|
|
16040
|
+
let payload;
|
|
16041
|
+
try {
|
|
16042
|
+
payload = objectValue(JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")), "access token JWT payload");
|
|
16043
|
+
} catch {
|
|
16044
|
+
throw new Error("OpenAI access token JWT was invalid");
|
|
16045
|
+
}
|
|
16046
|
+
const claim = payload[JWT_AUTH_CLAIM];
|
|
16047
|
+
const accountId = claim && typeof claim === "object" && !Array.isArray(claim) ? nonEmptyString(claim.chatgpt_account_id) : void 0;
|
|
16048
|
+
const expiresSeconds = finiteNonNegativeNumber(payload.exp);
|
|
16049
|
+
if (!accountId) {
|
|
16050
|
+
throw new Error("OpenAI access token did not contain a ChatGPT account ID");
|
|
16051
|
+
}
|
|
16052
|
+
if (expiresSeconds === void 0 || !Number.isSafeInteger(expiresSeconds * 1e3)) {
|
|
16053
|
+
throw new Error("OpenAI access token did not contain a valid expiry");
|
|
16054
|
+
}
|
|
16055
|
+
return { accountId, expires: expiresSeconds * 1e3 };
|
|
16056
|
+
}
|
|
16057
|
+
function nonEmptyString(value) {
|
|
16058
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
16059
|
+
}
|
|
16060
|
+
function objectValue(value, label) {
|
|
16061
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
16062
|
+
throw new Error(`${label} was not an object`);
|
|
16063
|
+
}
|
|
16064
|
+
return value;
|
|
16065
|
+
}
|
|
16066
|
+
function finiteNonNegativeNumber(value) {
|
|
16067
|
+
const number = typeof value === "string" && value.trim() ? Number(value) : value;
|
|
16068
|
+
return typeof number === "number" && Number.isFinite(number) && number >= 0 ? number : void 0;
|
|
16069
|
+
}
|
|
16070
|
+
function secondsToSafeMilliseconds(value) {
|
|
16071
|
+
const seconds = typeof value === "string" && value.trim() ? Number(value) : value;
|
|
16072
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) return void 0;
|
|
16073
|
+
const milliseconds = Math.floor(seconds * 1e3);
|
|
16074
|
+
return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
|
|
16075
|
+
}
|
|
16076
|
+
function throwIfAborted(signal) {
|
|
16077
|
+
if (signal?.aborted) throw new Error("OpenAI device login cancelled");
|
|
16078
|
+
}
|
|
16079
|
+
async function abortableSleep(milliseconds, signal) {
|
|
16080
|
+
throwIfAborted(signal);
|
|
16081
|
+
await new Promise((resolve, reject) => {
|
|
16082
|
+
const finish = () => {
|
|
16083
|
+
signal?.removeEventListener("abort", abort);
|
|
16084
|
+
resolve();
|
|
16085
|
+
};
|
|
16086
|
+
const timer = setTimeout(finish, milliseconds);
|
|
16087
|
+
const abort = () => {
|
|
16088
|
+
clearTimeout(timer);
|
|
16089
|
+
signal?.removeEventListener("abort", abort);
|
|
16090
|
+
reject(new Error("OpenAI device login cancelled"));
|
|
16091
|
+
};
|
|
16092
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
16093
|
+
if (signal?.aborted) abort();
|
|
16094
|
+
});
|
|
16095
|
+
}
|
|
16096
|
+
|
|
15704
16097
|
// src/mlclaw-space-runtime/model-default.ts
|
|
15705
16098
|
var DEFAULT_MODEL_ID = "zai-org/GLM-5.2";
|
|
15706
16099
|
var DEFAULT_MODEL_PROVIDER = "fireworks-ai";
|
|
@@ -20595,6 +20988,7 @@ function createRuntime(overrides = {}) {
|
|
|
20595
20988
|
dockerRunner: overrides.dockerRunner ?? new CliDockerRunner(),
|
|
20596
20989
|
podmanRunner: overrides.podmanRunner ?? new CliPodmanRunner(),
|
|
20597
20990
|
tailscaleRunner: overrides.tailscaleRunner ?? new CliTailscaleRunner(),
|
|
20991
|
+
codexDeviceLogin: overrides.codexDeviceLogin ?? loginOpenAICodexDeviceCode,
|
|
20598
20992
|
configRoot: overrides.configRoot ?? defaultConfigRoot(overrides.env ?? process4.env),
|
|
20599
20993
|
now: overrides.now ?? (() => /* @__PURE__ */ new Date()),
|
|
20600
20994
|
sleep: overrides.sleep ?? delay2,
|
|
@@ -20642,6 +21036,16 @@ function createProgram(runtimeOverrides = {}) {
|
|
|
20642
21036
|
credentials.command("repair").description("Replace and apply the dedicated HF Broker credential").argument("[agent]", "Agent name; inferred when exactly one deployment exists").option("--broker-hf-token-file <path>", "File containing MLCLAW_BROKER_HF_TOKEN=... or a raw token").action(async (agent, opts) => {
|
|
20643
21037
|
await credentialsRepair(agent, opts, runtime);
|
|
20644
21038
|
});
|
|
21039
|
+
const codexCredentials = credentials.command("codex").description("Manage deployment-scoped OpenAI Codex account credentials");
|
|
21040
|
+
codexCredentials.command("login").description("Connect a Codex ChatGPT account to one ML Claw deployment").argument("[agent]", "Agent name; inferred when exactly one deployment exists").action(async (agent) => {
|
|
21041
|
+
await codexCredentialsLogin(agent, runtime);
|
|
21042
|
+
});
|
|
21043
|
+
codexCredentials.command("status").description("Show the Codex credential status for one ML Claw deployment").argument("[agent]", "Agent name; inferred when exactly one deployment exists").action(async (agent) => {
|
|
21044
|
+
await codexCredentialsStatus(agent, runtime);
|
|
21045
|
+
});
|
|
21046
|
+
codexCredentials.command("logout").description("Remove Codex account credentials from one ML Claw deployment").argument("[agent]", "Agent name; inferred when exactly one deployment exists").action(async (agent) => {
|
|
21047
|
+
await codexCredentialsLogout(agent, runtime);
|
|
21048
|
+
});
|
|
20645
21049
|
const gateway = program2.command("gateway").description("Operate a ML Claw gateway");
|
|
20646
21050
|
gateway.command("start").argument("<agent>", "Agent name").option("--docker-context <name>", "Set Docker context only when the deployment has no pinned context").option("--local-port <port>", "Loopback port for the local gateway", parseLocalPort).option("--tailscale <off|direct|serve>", "Tailnet access mode", parseTailscaleMode).option("--tailscale-port <port>", "Tailnet listener or Serve HTTPS port", parseLocalPort).option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if another live runtime lease is present", false).action(async (agent, opts) => {
|
|
20647
21051
|
await gatewayStart(agent, opts, runtime);
|
|
@@ -21342,14 +21746,14 @@ async function resolveBootstrapPlan(params) {
|
|
|
21342
21746
|
existingSecrets,
|
|
21343
21747
|
runtime
|
|
21344
21748
|
});
|
|
21345
|
-
const sessionSecret = existingSecrets.MLCLAW_SESSION_SECRET ??
|
|
21749
|
+
const sessionSecret = existingSecrets.MLCLAW_SESSION_SECRET ?? randomBytes2(48).toString("base64url");
|
|
21346
21750
|
const restoredCredentialKey = existingSecrets.MLCLAW_CREDENTIAL_KEY ?? runtime.env.MLCLAW_CREDENTIAL_KEY;
|
|
21347
21751
|
if (existingManifest?.recoveredWithoutCredentialKey && !restoredCredentialKey) {
|
|
21348
21752
|
throw new Error(
|
|
21349
21753
|
"recovered deployment requires its existing MLCLAW_CREDENTIAL_KEY; restore it in the environment and rerun bootstrap"
|
|
21350
21754
|
);
|
|
21351
21755
|
}
|
|
21352
|
-
const credentialKey = restoredCredentialKey ??
|
|
21756
|
+
const credentialKey = restoredCredentialKey ?? randomBytes2(32).toString("base64url");
|
|
21353
21757
|
const credentialKeySha256 = createHash4("sha256").update(credentialKey).digest("hex");
|
|
21354
21758
|
if (existingManifest?.credentialKeySha256 && existingManifest.credentialKeySha256 !== credentialKeySha256) {
|
|
21355
21759
|
throw new Error("MLCLAW_CREDENTIAL_KEY does not match the recovered deployment");
|
|
@@ -21447,6 +21851,7 @@ async function resolveBootstrapPlan(params) {
|
|
|
21447
21851
|
gatewayLocation,
|
|
21448
21852
|
localPort,
|
|
21449
21853
|
runtimeId: gatewayLocation === "local" ? manifest.localRuntimeId : spaceRuntimeId(agentName),
|
|
21854
|
+
deploymentId: manifest.deploymentId,
|
|
21450
21855
|
...bucketPrefix ? { bucketPrefix } : {},
|
|
21451
21856
|
...effectiveTelegramProxy ? { telegramProxy: effectiveTelegramProxy } : {},
|
|
21452
21857
|
...effectiveTelegramApiRoot ? { telegramApiRoot: effectiveTelegramApiRoot } : {},
|
|
@@ -21815,7 +22220,8 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21815
22220
|
OPENCLAW_MODEL: updated.model,
|
|
21816
22221
|
MLCLAW_GATEWAY_LOCATION: updated.gatewayLocation,
|
|
21817
22222
|
MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
|
|
21818
|
-
MLCLAW_RUNTIME_ID: runtimeIdFor(updated)
|
|
22223
|
+
MLCLAW_RUNTIME_ID: runtimeIdFor(updated),
|
|
22224
|
+
MLCLAW_DEPLOYMENT_ID: updated.deploymentId
|
|
21819
22225
|
};
|
|
21820
22226
|
const reconciled = await reconcileManifest({
|
|
21821
22227
|
manifest: updated,
|
|
@@ -21845,6 +22251,17 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21845
22251
|
});
|
|
21846
22252
|
}
|
|
21847
22253
|
}
|
|
22254
|
+
if (bucketChanged) {
|
|
22255
|
+
await assertLease();
|
|
22256
|
+
await migrateCodexCredentialForBucketAdoption({
|
|
22257
|
+
hub,
|
|
22258
|
+
manifest: current,
|
|
22259
|
+
targetBucket: updated.bucket,
|
|
22260
|
+
statePrefix: bucketPrefix,
|
|
22261
|
+
credentialKey: nonEmpty(secrets.MLCLAW_CREDENTIAL_KEY),
|
|
22262
|
+
now: runtime.now()
|
|
22263
|
+
});
|
|
22264
|
+
}
|
|
21848
22265
|
await assertLease();
|
|
21849
22266
|
await writeLocalDeployment(runtime.configRoot, updated, updatedSecrets);
|
|
21850
22267
|
if (updated.gatewayLocation === "local") {
|
|
@@ -21872,7 +22289,8 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21872
22289
|
OPENCLAW_LIVE_DIR: SPACE_LIVE_DIR,
|
|
21873
22290
|
MLCLAW_RUNTIME_SETTINGS_FILE: `${SPACE_LIVE_DIR}/.mlclaw/settings.json`,
|
|
21874
22291
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
21875
|
-
MLCLAW_RUNTIME_ID: spaceRuntimeId(updated.agent)
|
|
22292
|
+
MLCLAW_RUNTIME_ID: spaceRuntimeId(updated.agent),
|
|
22293
|
+
MLCLAW_DEPLOYMENT_ID: updated.deploymentId
|
|
21876
22294
|
},
|
|
21877
22295
|
assertLease
|
|
21878
22296
|
);
|
|
@@ -21901,6 +22319,8 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21901
22319
|
bucket,
|
|
21902
22320
|
runtime.now()
|
|
21903
22321
|
);
|
|
22322
|
+
await assertLease();
|
|
22323
|
+
await deleteCodexCredentialControlObjects(hub, manifest.pendingTombstoneBucket, bucketPrefix);
|
|
21904
22324
|
const { pendingTombstoneBucket: _completed, ...completed } = manifest;
|
|
21905
22325
|
await assertLease();
|
|
21906
22326
|
await writeManifest(runtime.configRoot, completed);
|
|
@@ -21910,6 +22330,43 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21910
22330
|
updated = reconciled.manifest;
|
|
21911
22331
|
runtime.stdout.log(`State bucket: ${bucket}`);
|
|
21912
22332
|
}
|
|
22333
|
+
async function migrateCodexCredentialForBucketAdoption(params) {
|
|
22334
|
+
const revoked = await codexCredentialIsRevoked(params.hub, params.manifest.bucket, params.statePrefix);
|
|
22335
|
+
const encrypted = revoked ? void 0 : await readCodexCredentialBundle({
|
|
22336
|
+
hub: params.hub,
|
|
22337
|
+
manifest: params.manifest,
|
|
22338
|
+
statePrefix: params.statePrefix
|
|
22339
|
+
});
|
|
22340
|
+
const targetManifest = { ...params.manifest, bucket: params.targetBucket };
|
|
22341
|
+
if (encrypted) {
|
|
22342
|
+
if (!params.credentialKey) {
|
|
22343
|
+
throw new Error("cannot move Codex credentials without MLCLAW_CREDENTIAL_KEY");
|
|
22344
|
+
}
|
|
22345
|
+
const document = decryptCodexAuthDocument({
|
|
22346
|
+
encrypted,
|
|
22347
|
+
secret: params.credentialKey,
|
|
22348
|
+
expectedContext: codexContextForManifest(params.manifest, params.statePrefix)
|
|
22349
|
+
});
|
|
22350
|
+
await writeCodexCredentialBundle({
|
|
22351
|
+
manifest: targetManifest,
|
|
22352
|
+
statePrefix: params.statePrefix,
|
|
22353
|
+
credentialKey: params.credentialKey,
|
|
22354
|
+
document,
|
|
22355
|
+
hub: params.hub
|
|
22356
|
+
});
|
|
22357
|
+
await deleteCodexRevocationMarker(params.hub, params.targetBucket, params.statePrefix);
|
|
22358
|
+
return;
|
|
22359
|
+
}
|
|
22360
|
+
if (revoked) {
|
|
22361
|
+
await writeCodexRevocationMarker(params.hub, targetManifest, params.statePrefix, params.now);
|
|
22362
|
+
await params.hub.bucket(params.targetBucket).deleteFiles([codexAuthObjectPath(params.statePrefix)]);
|
|
22363
|
+
return;
|
|
22364
|
+
}
|
|
22365
|
+
await deleteCodexCredentialControlObjects(params.hub, params.targetBucket, params.statePrefix);
|
|
22366
|
+
}
|
|
22367
|
+
async function deleteCodexCredentialControlObjects(hub, bucket, statePrefix) {
|
|
22368
|
+
await hub.bucket(bucket).deleteFiles([codexAuthObjectPath(statePrefix), codexAuthRevocationObjectPath(statePrefix)]);
|
|
22369
|
+
}
|
|
21913
22370
|
async function inspectStateBucket(hub, bucket, bucketPrefix) {
|
|
21914
22371
|
await hub.assertBucketAccessible(bucket);
|
|
21915
22372
|
const client = hub.bucket(bucket);
|
|
@@ -21918,7 +22375,13 @@ async function inspectStateBucket(hub, bucket, bucketPrefix) {
|
|
|
21918
22375
|
const prefix = normalizeBucketPrefix(bucketPrefix);
|
|
21919
22376
|
const prefixWithSlash = `${prefix}/`;
|
|
21920
22377
|
const manifestPath2 = `${prefixWithSlash}${SNAPSHOT_MANIFEST_REMOTE_NAME}`;
|
|
21921
|
-
const
|
|
22378
|
+
const prefixedControlPath = `${prefixWithSlash}.mlclaw/`;
|
|
22379
|
+
const credentialPayloadPaths = new Set(
|
|
22380
|
+
fileEntries.some((entry) => entry.path === DEPLOYMENT_PATH) ? [] : [codexAuthObjectPath(bucketPrefix)]
|
|
22381
|
+
);
|
|
22382
|
+
const payloadEntries = fileEntries.filter(
|
|
22383
|
+
(entry) => !entry.path.startsWith(".mlclaw/") && (!entry.path.startsWith(prefixedControlPath) || credentialPayloadPaths.has(entry.path))
|
|
22384
|
+
);
|
|
21922
22385
|
const stateEntries = payloadEntries.filter(
|
|
21923
22386
|
(entry) => entry.path === manifestPath2 || entry.path.startsWith(`${prefixWithSlash}snapshots/`) || entry.path.startsWith(`${prefixWithSlash}runtime/`)
|
|
21924
22387
|
);
|
|
@@ -21983,6 +22446,7 @@ function deploymentSecrets(params) {
|
|
|
21983
22446
|
MLCLAW_GATEWAY_LOCATION: params.gatewayLocation,
|
|
21984
22447
|
MLCLAW_RUNTIME_IMAGE: params.runtimeImage,
|
|
21985
22448
|
MLCLAW_RUNTIME_ID: params.runtimeId,
|
|
22449
|
+
MLCLAW_DEPLOYMENT_ID: params.deploymentId,
|
|
21986
22450
|
MLCLAW_SESSION_SECRET: params.sessionSecret,
|
|
21987
22451
|
MLCLAW_CREDENTIAL_KEY: params.credentialKey,
|
|
21988
22452
|
MLCLAW_OPENCLAW_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT),
|
|
@@ -22143,6 +22607,7 @@ function spaceGatewayNeedsRepair(manifest, variables, runtime, allowedUsers) {
|
|
|
22143
22607
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
22144
22608
|
MLCLAW_RUNTIME_IMAGE: manifest.runtimeImage,
|
|
22145
22609
|
MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent),
|
|
22610
|
+
MLCLAW_DEPLOYMENT_ID: manifest.deploymentId,
|
|
22146
22611
|
MLCLAW_ALLOWED_USERS: allowedUsers,
|
|
22147
22612
|
MLCLAW_ADMINS: allowedUsers,
|
|
22148
22613
|
MLCLAW_CANONICAL_SPACE_ID: DEFAULT_CANONICAL_TEMPLATE_SPACE,
|
|
@@ -22198,6 +22663,7 @@ async function deploySpaceGateway(params) {
|
|
|
22198
22663
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
22199
22664
|
MLCLAW_RUNTIME_IMAGE: spaceRuntimeRef,
|
|
22200
22665
|
MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent),
|
|
22666
|
+
MLCLAW_DEPLOYMENT_ID: manifest.deploymentId,
|
|
22201
22667
|
MLCLAW_ALLOWED_USERS: params.allowedUsers,
|
|
22202
22668
|
MLCLAW_ADMINS: params.allowedUsers,
|
|
22203
22669
|
MLCLAW_CANONICAL_SPACE_ID: DEFAULT_CANONICAL_TEMPLATE_SPACE,
|
|
@@ -22236,7 +22702,7 @@ async function startLocalGateway(params) {
|
|
|
22236
22702
|
}
|
|
22237
22703
|
let secrets = await ensureDeploymentCredentialKey(runtime, manifest.agent);
|
|
22238
22704
|
if (!secrets.MLCLAW_SESSION_SECRET) {
|
|
22239
|
-
secrets = { ...secrets, MLCLAW_SESSION_SECRET:
|
|
22705
|
+
secrets = { ...secrets, MLCLAW_SESSION_SECRET: randomBytes2(48).toString("base64url") };
|
|
22240
22706
|
await writeSecretEnv(runtime.configRoot, manifest.agent, secrets);
|
|
22241
22707
|
}
|
|
22242
22708
|
const accessSecrets = localAccessSecrets(manifest.owner, localGatewayPort(manifest), secrets, manifest.networkAccess);
|
|
@@ -22637,7 +23103,8 @@ async function gatewayMigrate(agent, opts, runtime) {
|
|
|
22637
23103
|
...deploymentSecrets2,
|
|
22638
23104
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
22639
23105
|
MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
|
|
22640
|
-
MLCLAW_RUNTIME_ID: spaceRuntimeId(agent)
|
|
23106
|
+
MLCLAW_RUNTIME_ID: spaceRuntimeId(agent),
|
|
23107
|
+
MLCLAW_DEPLOYMENT_ID: updated.deploymentId
|
|
22641
23108
|
});
|
|
22642
23109
|
await writeManifest(runtime.configRoot, updated);
|
|
22643
23110
|
}
|
|
@@ -22688,6 +23155,7 @@ async function gatewayMigrate(agent, opts, runtime) {
|
|
|
22688
23155
|
MLCLAW_GATEWAY_LOCATION: "local",
|
|
22689
23156
|
MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
|
|
22690
23157
|
MLCLAW_RUNTIME_ID: updated.localRuntimeId,
|
|
23158
|
+
MLCLAW_DEPLOYMENT_ID: updated.deploymentId,
|
|
22691
23159
|
...localAccessSecrets(updated.owner, localGatewayPort(updated), secrets, updated.networkAccess)
|
|
22692
23160
|
});
|
|
22693
23161
|
await assertLease();
|
|
@@ -23163,7 +23631,7 @@ function nonEmpty(value) {
|
|
|
23163
23631
|
return trimmed ? trimmed : void 0;
|
|
23164
23632
|
}
|
|
23165
23633
|
function newLocalRuntimeId(agent) {
|
|
23166
|
-
return `local-${agent}-${
|
|
23634
|
+
return `local-${agent}-${randomBytes2(8).toString("hex")}`;
|
|
23167
23635
|
}
|
|
23168
23636
|
function runtimeIdFor(manifest) {
|
|
23169
23637
|
return manifest.gatewayLocation === "local" ? manifest.localRuntimeId : spaceRuntimeId(manifest.agent);
|
|
@@ -23186,7 +23654,7 @@ async function handoffAndStopLocalGateway(params) {
|
|
|
23186
23654
|
}
|
|
23187
23655
|
await runner.disableRestart(containerName, connection);
|
|
23188
23656
|
const handoffStartedAt = params.runtime.now();
|
|
23189
|
-
const requestId =
|
|
23657
|
+
const requestId = randomBytes2(16).toString("hex");
|
|
23190
23658
|
await writeRuntimeHandoffRequest(
|
|
23191
23659
|
params.hub,
|
|
23192
23660
|
params.manifest.bucket,
|
|
@@ -23216,7 +23684,7 @@ async function handoffAndStopLocalGateway(params) {
|
|
|
23216
23684
|
}
|
|
23217
23685
|
async function disableAndPauseSpaceGateway(params) {
|
|
23218
23686
|
const handoffStartedAt = params.runtime.now();
|
|
23219
|
-
const requestId =
|
|
23687
|
+
const requestId = randomBytes2(16).toString("hex");
|
|
23220
23688
|
const shouldWaitForHandoff = await spaceGatewayShouldWaitForHandoff(params);
|
|
23221
23689
|
if (!shouldWaitForHandoff) {
|
|
23222
23690
|
await params.hub.addSpaceVariable(params.manifest.space, "MLCLAW_GATEWAY_DISABLED", "1");
|
|
@@ -23311,7 +23779,7 @@ async function ensureDeploymentCredentialKey(runtime, agent, existing) {
|
|
|
23311
23779
|
}
|
|
23312
23780
|
const updated = {
|
|
23313
23781
|
...secrets,
|
|
23314
|
-
MLCLAW_CREDENTIAL_KEY:
|
|
23782
|
+
MLCLAW_CREDENTIAL_KEY: randomBytes2(32).toString("base64url")
|
|
23315
23783
|
};
|
|
23316
23784
|
await writeSecretEnv(runtime.configRoot, agent, updated);
|
|
23317
23785
|
return updated;
|
|
@@ -23374,6 +23842,9 @@ async function update(repoId, opts, hub, hfToken, runtime) {
|
|
|
23374
23842
|
}
|
|
23375
23843
|
await hub.addSpaceVariable(repoId, "MLCLAW_GATEWAY_LOCATION", "space");
|
|
23376
23844
|
await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_ID", spaceRuntimeId(agentName));
|
|
23845
|
+
if (localManifest) {
|
|
23846
|
+
await hub.addSpaceVariable(repoId, "MLCLAW_DEPLOYMENT_ID", localManifest.deploymentId);
|
|
23847
|
+
}
|
|
23377
23848
|
await hub.addSpaceVariable(repoId, "MLCLAW_OPENCLAW_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
|
|
23378
23849
|
await hub.addSpaceVariable(repoId, "OPENCLAW_GATEWAY_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
|
|
23379
23850
|
const bucket = variables.get("OPENCLAW_HF_STATE_BUCKET")?.value;
|
|
@@ -23409,7 +23880,8 @@ async function reconcileUpdatedDeployment(localManifest, runtimeImage, hub, runt
|
|
|
23409
23880
|
...currentSecrets,
|
|
23410
23881
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
23411
23882
|
MLCLAW_RUNTIME_IMAGE: runtimeImage,
|
|
23412
|
-
MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent)
|
|
23883
|
+
MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent),
|
|
23884
|
+
MLCLAW_DEPLOYMENT_ID: manifest.deploymentId
|
|
23413
23885
|
});
|
|
23414
23886
|
}
|
|
23415
23887
|
});
|
|
@@ -23588,7 +24060,7 @@ async function doctor(repoId, opts, hub, runtime) {
|
|
|
23588
24060
|
}
|
|
23589
24061
|
if (!secrets.has("MLCLAW_SESSION_SECRET")) {
|
|
23590
24062
|
if (fix) {
|
|
23591
|
-
await hub.addSpaceSecret(repoId, "MLCLAW_SESSION_SECRET",
|
|
24063
|
+
await hub.addSpaceSecret(repoId, "MLCLAW_SESSION_SECRET", randomBytes2(48).toString("base64url"));
|
|
23592
24064
|
fixed.push("set secret MLCLAW_SESSION_SECRET");
|
|
23593
24065
|
} else {
|
|
23594
24066
|
issues.push("secret MLCLAW_SESSION_SECRET is missing");
|
|
@@ -23597,7 +24069,7 @@ async function doctor(repoId, opts, hub, runtime) {
|
|
|
23597
24069
|
if (!secrets.has("MLCLAW_CREDENTIAL_KEY")) {
|
|
23598
24070
|
if (fix) {
|
|
23599
24071
|
const agent = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw";
|
|
23600
|
-
const credentialKey = await manifestExists(runtime.configRoot, agent) ? requiredSecret(await ensureDeploymentCredentialKey(runtime, agent), "MLCLAW_CREDENTIAL_KEY") :
|
|
24072
|
+
const credentialKey = await manifestExists(runtime.configRoot, agent) ? requiredSecret(await ensureDeploymentCredentialKey(runtime, agent), "MLCLAW_CREDENTIAL_KEY") : randomBytes2(32).toString("base64url");
|
|
23601
24073
|
await hub.addSpaceSecret(repoId, "MLCLAW_CREDENTIAL_KEY", credentialKey);
|
|
23602
24074
|
fixed.push("set secret MLCLAW_CREDENTIAL_KEY");
|
|
23603
24075
|
} else {
|
|
@@ -23916,6 +24388,106 @@ async function readOptionalRouterTokenFile(file) {
|
|
|
23916
24388
|
const parsed = parseSecretEnv(raw);
|
|
23917
24389
|
return nonEmpty(parsed.MLCLAW_ROUTER_TOKEN) ?? nonEmpty(parsed.HF_ROUTER_TOKEN) ?? nonEmpty(raw);
|
|
23918
24390
|
}
|
|
24391
|
+
async function codexCredentialsLogin(requestedAgent, runtime) {
|
|
24392
|
+
const manifest = await credentialManifest(requestedAgent, runtime);
|
|
24393
|
+
await withDeploymentLock(runtime.configRoot, deploymentLockKey(manifest), async () => {
|
|
24394
|
+
const current = await readManifest(runtime.configRoot, manifest.agent);
|
|
24395
|
+
const secrets = await readSecretEnv(runtime.configRoot, current.agent);
|
|
24396
|
+
const credentialKey = await verifiedDeploymentCredentialKey(current, secrets, runtime);
|
|
24397
|
+
const statePrefix = persistedBucketPrefix(secrets);
|
|
24398
|
+
const authJson = await runDirectCodexDeviceLogin(current, runtime);
|
|
24399
|
+
const document = encodeCodexAuthDocument({ authJson, now: runtime.now() });
|
|
24400
|
+
const token = await runtime.readToken(runtime.env);
|
|
24401
|
+
const hub = runtime.hubFactory(token);
|
|
24402
|
+
const control = await hub.deploymentControlStore(current.owner, current.deploymentId);
|
|
24403
|
+
const operation = newOperation(current, runtime.now());
|
|
24404
|
+
let lease = await acquireControlLease(control, current, operation, runtime.now());
|
|
24405
|
+
let renewalError;
|
|
24406
|
+
let renewal = Promise.resolve();
|
|
24407
|
+
const renewalTimer = setInterval(() => {
|
|
24408
|
+
renewal = renewal.then(async () => {
|
|
24409
|
+
if (renewalError) return;
|
|
24410
|
+
try {
|
|
24411
|
+
lease = await renewControlLease(control, lease, runtime.now());
|
|
24412
|
+
} catch (error) {
|
|
24413
|
+
renewalError = error;
|
|
24414
|
+
}
|
|
24415
|
+
});
|
|
24416
|
+
}, 45e3);
|
|
24417
|
+
const assertLease = async () => {
|
|
24418
|
+
await renewal;
|
|
24419
|
+
if (renewalError) throw renewalError;
|
|
24420
|
+
await assertControlLease(control, lease, runtime.now());
|
|
24421
|
+
if (renewalError) throw renewalError;
|
|
24422
|
+
};
|
|
24423
|
+
try {
|
|
24424
|
+
await assertLease();
|
|
24425
|
+
await writeCodexCredentialBundle({ manifest: current, statePrefix, credentialKey, document, hub });
|
|
24426
|
+
await assertLease();
|
|
24427
|
+
await deleteCodexRevocationMarker(hub, current.bucket, statePrefix);
|
|
24428
|
+
await assertLease();
|
|
24429
|
+
await ensureRuntimeDeploymentId(current, secrets, hub, runtime, assertLease);
|
|
24430
|
+
await assertLease();
|
|
24431
|
+
await restartDeploymentForCredentialChange(current, runtime, hub, assertLease);
|
|
24432
|
+
} finally {
|
|
24433
|
+
clearInterval(renewalTimer);
|
|
24434
|
+
await renewal;
|
|
24435
|
+
await releaseControlLease(control, lease);
|
|
24436
|
+
}
|
|
24437
|
+
runtime.stdout.log(`Codex credentials connected: ${current.agent}`);
|
|
24438
|
+
});
|
|
24439
|
+
}
|
|
24440
|
+
async function codexCredentialsStatus(requestedAgent, runtime) {
|
|
24441
|
+
const manifest = await credentialManifest(requestedAgent, runtime);
|
|
24442
|
+
const secrets = await readSecretEnv(runtime.configRoot, manifest.agent);
|
|
24443
|
+
const credentialKey = await verifiedDeploymentCredentialKey(manifest, secrets, runtime);
|
|
24444
|
+
const token = await runtime.readToken(runtime.env);
|
|
24445
|
+
const hub = runtime.hubFactory(token);
|
|
24446
|
+
const statePrefix = persistedBucketPrefix(secrets);
|
|
24447
|
+
const revoked = await codexCredentialIsRevoked(hub, manifest.bucket, statePrefix);
|
|
24448
|
+
const encrypted = revoked ? void 0 : await readCodexCredentialBundle({ hub, manifest, statePrefix });
|
|
24449
|
+
if (!encrypted) {
|
|
24450
|
+
runtime.stdout.log(`Agent: ${manifest.agent}`);
|
|
24451
|
+
runtime.stdout.log("Codex account: not configured");
|
|
24452
|
+
return;
|
|
24453
|
+
}
|
|
24454
|
+
const document = decryptCodexAuthDocument({
|
|
24455
|
+
encrypted,
|
|
24456
|
+
secret: credentialKey,
|
|
24457
|
+
expectedContext: codexContextForManifest(manifest, statePrefix)
|
|
24458
|
+
});
|
|
24459
|
+
runtime.stdout.log(`Agent: ${manifest.agent}`);
|
|
24460
|
+
runtime.stdout.log("Codex account: configured");
|
|
24461
|
+
runtime.stdout.log(`Updated: ${document.updatedAt}`);
|
|
24462
|
+
runtime.stdout.log(`Credential object: ${codexAuthObjectPath(statePrefix)}`);
|
|
24463
|
+
}
|
|
24464
|
+
async function codexCredentialsLogout(requestedAgent, runtime) {
|
|
24465
|
+
const manifest = await credentialManifest(requestedAgent, runtime);
|
|
24466
|
+
await withDeploymentLock(runtime.configRoot, deploymentLockKey(manifest), async () => {
|
|
24467
|
+
const current = await readManifest(runtime.configRoot, manifest.agent);
|
|
24468
|
+
const secrets = await readSecretEnv(runtime.configRoot, current.agent);
|
|
24469
|
+
await verifiedDeploymentCredentialKey(current, secrets, runtime);
|
|
24470
|
+
const statePrefix = persistedBucketPrefix(secrets);
|
|
24471
|
+
const token = await runtime.readToken(runtime.env);
|
|
24472
|
+
const hub = runtime.hubFactory(token);
|
|
24473
|
+
const control = await hub.deploymentControlStore(current.owner, current.deploymentId);
|
|
24474
|
+
const operation = newOperation(current, runtime.now());
|
|
24475
|
+
const lease = await acquireControlLease(control, current, operation, runtime.now());
|
|
24476
|
+
try {
|
|
24477
|
+
await assertControlLease(control, lease, runtime.now());
|
|
24478
|
+
await writeCodexRevocationMarker(hub, current, statePrefix, runtime.now());
|
|
24479
|
+
await assertControlLease(control, lease, runtime.now());
|
|
24480
|
+
await hub.bucket(current.bucket).deleteFiles([codexAuthObjectPath(statePrefix)]);
|
|
24481
|
+
await assertControlLease(control, lease, runtime.now());
|
|
24482
|
+
await restartDeploymentForCredentialChange(current, runtime, hub, async () => {
|
|
24483
|
+
await assertControlLease(control, lease, runtime.now());
|
|
24484
|
+
});
|
|
24485
|
+
} finally {
|
|
24486
|
+
await releaseControlLease(control, lease);
|
|
24487
|
+
}
|
|
24488
|
+
runtime.stdout.log(`Codex credentials removed: ${current.agent}`);
|
|
24489
|
+
});
|
|
24490
|
+
}
|
|
23919
24491
|
async function credentialsStatus(requestedAgent, runtime) {
|
|
23920
24492
|
const manifest = await credentialManifest(requestedAgent, runtime);
|
|
23921
24493
|
await verifiedStoredBrokerCredential(manifest, runtime);
|
|
@@ -24020,6 +24592,113 @@ async function credentialsRepair(requestedAgent, opts, runtime) {
|
|
|
24020
24592
|
runtime.stdout.log(`HF Broker credential repaired: ${manifest.agent}`);
|
|
24021
24593
|
});
|
|
24022
24594
|
}
|
|
24595
|
+
async function verifiedDeploymentCredentialKey(manifest, secrets, runtime) {
|
|
24596
|
+
if (manifest.recoveredWithoutCredentialKey) {
|
|
24597
|
+
throw new Error(
|
|
24598
|
+
`deployment ${manifest.agent} was recovered without its credential key; run bootstrap or repair from a machine that has the original key`
|
|
24599
|
+
);
|
|
24600
|
+
}
|
|
24601
|
+
const credentialKey = nonEmpty(secrets.MLCLAW_CREDENTIAL_KEY);
|
|
24602
|
+
if (!credentialKey) {
|
|
24603
|
+
throw new Error(`deployment ${manifest.agent} has no MLCLAW_CREDENTIAL_KEY`);
|
|
24604
|
+
}
|
|
24605
|
+
const fingerprint = createHash4("sha256").update(credentialKey).digest("hex");
|
|
24606
|
+
if (manifest.credentialKeySha256 && manifest.credentialKeySha256 !== fingerprint) {
|
|
24607
|
+
throw new Error("local credential key fingerprint does not match the deployment manifest");
|
|
24608
|
+
}
|
|
24609
|
+
const token = await runtime.readToken(runtime.env);
|
|
24610
|
+
const identity = await readDeploymentIdentity(runtime.hubFactory(token).bucket(manifest.bucket));
|
|
24611
|
+
if (!identity) {
|
|
24612
|
+
throw new Error(`bucket ${manifest.bucket} has no canonical deployment identity`);
|
|
24613
|
+
}
|
|
24614
|
+
if (identity.deploymentId !== manifest.deploymentId || identity.bucket !== manifest.bucket) {
|
|
24615
|
+
throw new Error(`bucket ${manifest.bucket} belongs to another deployment`);
|
|
24616
|
+
}
|
|
24617
|
+
if (identity.credentialKeySha256 !== fingerprint) {
|
|
24618
|
+
throw new Error("local credential key fingerprint does not match canonical deployment identity");
|
|
24619
|
+
}
|
|
24620
|
+
return credentialKey;
|
|
24621
|
+
}
|
|
24622
|
+
async function runDirectCodexDeviceLogin(manifest, runtime) {
|
|
24623
|
+
runtime.stdout.log(`Connecting a ChatGPT account to deployment ${manifest.agent}.`);
|
|
24624
|
+
const credential = await runtime.codexDeviceLogin({
|
|
24625
|
+
onVerification: (verification) => printCodexDeviceVerification(verification, runtime)
|
|
24626
|
+
});
|
|
24627
|
+
return codexAuthJsonFromOAuthCredential(credential, runtime.now());
|
|
24628
|
+
}
|
|
24629
|
+
function printCodexDeviceVerification(verification, runtime) {
|
|
24630
|
+
runtime.stdout.log(`Open: ${verification.verificationUrl}`);
|
|
24631
|
+
runtime.stdout.log(`Code: ${verification.userCode}`);
|
|
24632
|
+
runtime.stdout.log("Waiting for OpenAI authorization...");
|
|
24633
|
+
}
|
|
24634
|
+
function codexContextForManifest(manifest, statePrefix) {
|
|
24635
|
+
return codexAuthContext({
|
|
24636
|
+
deploymentId: manifest.deploymentId,
|
|
24637
|
+
bucket: manifest.bucket,
|
|
24638
|
+
...statePrefix ? { statePrefix } : {}
|
|
24639
|
+
});
|
|
24640
|
+
}
|
|
24641
|
+
async function writeCodexCredentialBundle(params) {
|
|
24642
|
+
const encrypted = encryptCodexAuthDocument({
|
|
24643
|
+
document: params.document,
|
|
24644
|
+
secret: params.credentialKey,
|
|
24645
|
+
context: codexContextForManifest(params.manifest, params.statePrefix)
|
|
24646
|
+
});
|
|
24647
|
+
await params.hub.bucket(params.manifest.bucket).uploadFiles([{ path: codexAuthObjectPath(params.statePrefix), content: new Blob([encrypted]) }]);
|
|
24648
|
+
}
|
|
24649
|
+
async function readCodexCredentialBundle(params) {
|
|
24650
|
+
const blob = await params.hub.bucket(params.manifest.bucket).downloadFile(codexAuthObjectPath(params.statePrefix));
|
|
24651
|
+
return blob ? await blob.text() : void 0;
|
|
24652
|
+
}
|
|
24653
|
+
async function codexCredentialIsRevoked(hub, bucket, statePrefix) {
|
|
24654
|
+
return Boolean(await hub.bucket(bucket).downloadFile(codexAuthRevocationObjectPath(statePrefix)));
|
|
24655
|
+
}
|
|
24656
|
+
async function writeCodexRevocationMarker(hub, manifest, statePrefix, now) {
|
|
24657
|
+
const content = new Blob([
|
|
24658
|
+
`${JSON.stringify({
|
|
24659
|
+
version: 1,
|
|
24660
|
+
kind: "codex-auth-revocation",
|
|
24661
|
+
deploymentId: manifest.deploymentId,
|
|
24662
|
+
bucket: manifest.bucket,
|
|
24663
|
+
statePrefix: normalizeBucketPrefix(statePrefix),
|
|
24664
|
+
revokedAt: now.toISOString()
|
|
24665
|
+
})}
|
|
24666
|
+
`
|
|
24667
|
+
]);
|
|
24668
|
+
await hub.bucket(manifest.bucket).uploadFiles([{ path: codexAuthRevocationObjectPath(statePrefix), content }]);
|
|
24669
|
+
}
|
|
24670
|
+
async function deleteCodexRevocationMarker(hub, bucket, statePrefix) {
|
|
24671
|
+
await hub.bucket(bucket).deleteFiles([codexAuthRevocationObjectPath(statePrefix)]);
|
|
24672
|
+
}
|
|
24673
|
+
async function ensureRuntimeDeploymentId(manifest, secrets, hub, runtime, assertLease) {
|
|
24674
|
+
if (secrets.MLCLAW_DEPLOYMENT_ID === manifest.deploymentId) return;
|
|
24675
|
+
if (manifest.gatewayLocation === "space") {
|
|
24676
|
+
await assertLease();
|
|
24677
|
+
await hub.addSpaceVariable(manifest.space, "MLCLAW_DEPLOYMENT_ID", manifest.deploymentId);
|
|
24678
|
+
return;
|
|
24679
|
+
}
|
|
24680
|
+
await assertLease();
|
|
24681
|
+
await writeSecretEnv(runtime.configRoot, manifest.agent, {
|
|
24682
|
+
...secrets,
|
|
24683
|
+
MLCLAW_DEPLOYMENT_ID: manifest.deploymentId
|
|
24684
|
+
});
|
|
24685
|
+
}
|
|
24686
|
+
async function restartDeploymentForCredentialChange(manifest, runtime, hub, assertLease) {
|
|
24687
|
+
if (manifest.gatewayLocation === "space") {
|
|
24688
|
+
await assertLease();
|
|
24689
|
+
await hub.restartSpace(manifest.space, true);
|
|
24690
|
+
runtime.stdout.log(`Space gateway restart requested: ${manifest.space}`);
|
|
24691
|
+
return;
|
|
24692
|
+
}
|
|
24693
|
+
const runner = localRunnerFor(manifest, runtime);
|
|
24694
|
+
const existing = await runner.inspect(containerNameFor(manifest.agent), localConnectionFor(manifest));
|
|
24695
|
+
if (!existing?.running) {
|
|
24696
|
+
runtime.stdout.log(`Local gateway is stopped; credentials will apply when ${manifest.agent} starts`);
|
|
24697
|
+
return;
|
|
24698
|
+
}
|
|
24699
|
+
await assertLease();
|
|
24700
|
+
await startLocalGateway({ manifest, runtime, pull: false, refresh: true, existing, assertLease });
|
|
24701
|
+
}
|
|
24023
24702
|
async function credentialManifest(requestedAgent, runtime) {
|
|
24024
24703
|
if (requestedAgent) return await readManifest(runtime.configRoot, requestedAgent);
|
|
24025
24704
|
const manifests = await listManifests(runtime.configRoot);
|
|
@@ -24365,5 +25044,6 @@ export {
|
|
|
24365
25044
|
TELEGRAM_SLEEP_TIME,
|
|
24366
25045
|
createProgram,
|
|
24367
25046
|
main,
|
|
24368
|
-
mergeStateVolume
|
|
25047
|
+
mergeStateVolume,
|
|
25048
|
+
migrateCodexCredentialForBucketAdoption
|
|
24369
25049
|
};
|