mlclaw 0.5.0 → 0.6.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.
- package/.agents/skills/mlclaw/SKILL.md +16 -0
- package/Dockerfile +1 -1
- package/README.md +34 -0
- package/dist/mlclaw-space-runtime.js +6075 -316
- package/dist/mlclaw.mjs +696 -17
- package/package.json +4 -4
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,7 +15383,7 @@ 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.1",
|
|
15387
15387
|
"openclawVersion": "2026.7.1",
|
|
15388
15388
|
"brokerkitVersion": "hf-broker/v0.6.2",
|
|
15389
15389
|
"brokerkitPluginVersion": "0.4.1",
|
|
@@ -15702,6 +15702,398 @@ async function assertNoLiveForeignLease(params) {
|
|
|
15702
15702
|
);
|
|
15703
15703
|
}
|
|
15704
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
|
+
|
|
15705
16097
|
// src/mlclaw-space-runtime/model-default.ts
|
|
15706
16098
|
var DEFAULT_MODEL_ID = "zai-org/GLM-5.2";
|
|
15707
16099
|
var DEFAULT_MODEL_PROVIDER = "fireworks-ai";
|
|
@@ -20596,6 +20988,7 @@ function createRuntime(overrides = {}) {
|
|
|
20596
20988
|
dockerRunner: overrides.dockerRunner ?? new CliDockerRunner(),
|
|
20597
20989
|
podmanRunner: overrides.podmanRunner ?? new CliPodmanRunner(),
|
|
20598
20990
|
tailscaleRunner: overrides.tailscaleRunner ?? new CliTailscaleRunner(),
|
|
20991
|
+
codexDeviceLogin: overrides.codexDeviceLogin ?? loginOpenAICodexDeviceCode,
|
|
20599
20992
|
configRoot: overrides.configRoot ?? defaultConfigRoot(overrides.env ?? process4.env),
|
|
20600
20993
|
now: overrides.now ?? (() => /* @__PURE__ */ new Date()),
|
|
20601
20994
|
sleep: overrides.sleep ?? delay2,
|
|
@@ -20643,6 +21036,16 @@ function createProgram(runtimeOverrides = {}) {
|
|
|
20643
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) => {
|
|
20644
21037
|
await credentialsRepair(agent, opts, runtime);
|
|
20645
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
|
+
});
|
|
20646
21049
|
const gateway = program2.command("gateway").description("Operate a ML Claw gateway");
|
|
20647
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) => {
|
|
20648
21051
|
await gatewayStart(agent, opts, runtime);
|
|
@@ -21343,14 +21746,14 @@ async function resolveBootstrapPlan(params) {
|
|
|
21343
21746
|
existingSecrets,
|
|
21344
21747
|
runtime
|
|
21345
21748
|
});
|
|
21346
|
-
const sessionSecret = existingSecrets.MLCLAW_SESSION_SECRET ??
|
|
21749
|
+
const sessionSecret = existingSecrets.MLCLAW_SESSION_SECRET ?? randomBytes2(48).toString("base64url");
|
|
21347
21750
|
const restoredCredentialKey = existingSecrets.MLCLAW_CREDENTIAL_KEY ?? runtime.env.MLCLAW_CREDENTIAL_KEY;
|
|
21348
21751
|
if (existingManifest?.recoveredWithoutCredentialKey && !restoredCredentialKey) {
|
|
21349
21752
|
throw new Error(
|
|
21350
21753
|
"recovered deployment requires its existing MLCLAW_CREDENTIAL_KEY; restore it in the environment and rerun bootstrap"
|
|
21351
21754
|
);
|
|
21352
21755
|
}
|
|
21353
|
-
const credentialKey = restoredCredentialKey ??
|
|
21756
|
+
const credentialKey = restoredCredentialKey ?? randomBytes2(32).toString("base64url");
|
|
21354
21757
|
const credentialKeySha256 = createHash4("sha256").update(credentialKey).digest("hex");
|
|
21355
21758
|
if (existingManifest?.credentialKeySha256 && existingManifest.credentialKeySha256 !== credentialKeySha256) {
|
|
21356
21759
|
throw new Error("MLCLAW_CREDENTIAL_KEY does not match the recovered deployment");
|
|
@@ -21448,6 +21851,7 @@ async function resolveBootstrapPlan(params) {
|
|
|
21448
21851
|
gatewayLocation,
|
|
21449
21852
|
localPort,
|
|
21450
21853
|
runtimeId: gatewayLocation === "local" ? manifest.localRuntimeId : spaceRuntimeId(agentName),
|
|
21854
|
+
deploymentId: manifest.deploymentId,
|
|
21451
21855
|
...bucketPrefix ? { bucketPrefix } : {},
|
|
21452
21856
|
...effectiveTelegramProxy ? { telegramProxy: effectiveTelegramProxy } : {},
|
|
21453
21857
|
...effectiveTelegramApiRoot ? { telegramApiRoot: effectiveTelegramApiRoot } : {},
|
|
@@ -21816,7 +22220,8 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21816
22220
|
OPENCLAW_MODEL: updated.model,
|
|
21817
22221
|
MLCLAW_GATEWAY_LOCATION: updated.gatewayLocation,
|
|
21818
22222
|
MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
|
|
21819
|
-
MLCLAW_RUNTIME_ID: runtimeIdFor(updated)
|
|
22223
|
+
MLCLAW_RUNTIME_ID: runtimeIdFor(updated),
|
|
22224
|
+
MLCLAW_DEPLOYMENT_ID: updated.deploymentId
|
|
21820
22225
|
};
|
|
21821
22226
|
const reconciled = await reconcileManifest({
|
|
21822
22227
|
manifest: updated,
|
|
@@ -21846,6 +22251,17 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21846
22251
|
});
|
|
21847
22252
|
}
|
|
21848
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
|
+
}
|
|
21849
22265
|
await assertLease();
|
|
21850
22266
|
await writeLocalDeployment(runtime.configRoot, updated, updatedSecrets);
|
|
21851
22267
|
if (updated.gatewayLocation === "local") {
|
|
@@ -21873,7 +22289,8 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21873
22289
|
OPENCLAW_LIVE_DIR: SPACE_LIVE_DIR,
|
|
21874
22290
|
MLCLAW_RUNTIME_SETTINGS_FILE: `${SPACE_LIVE_DIR}/.mlclaw/settings.json`,
|
|
21875
22291
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
21876
|
-
MLCLAW_RUNTIME_ID: spaceRuntimeId(updated.agent)
|
|
22292
|
+
MLCLAW_RUNTIME_ID: spaceRuntimeId(updated.agent),
|
|
22293
|
+
MLCLAW_DEPLOYMENT_ID: updated.deploymentId
|
|
21877
22294
|
},
|
|
21878
22295
|
assertLease
|
|
21879
22296
|
);
|
|
@@ -21902,6 +22319,8 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21902
22319
|
bucket,
|
|
21903
22320
|
runtime.now()
|
|
21904
22321
|
);
|
|
22322
|
+
await assertLease();
|
|
22323
|
+
await deleteCodexCredentialControlObjects(hub, manifest.pendingTombstoneBucket, bucketPrefix);
|
|
21905
22324
|
const { pendingTombstoneBucket: _completed, ...completed } = manifest;
|
|
21906
22325
|
await assertLease();
|
|
21907
22326
|
await writeManifest(runtime.configRoot, completed);
|
|
@@ -21911,6 +22330,43 @@ async function stateAdopt(agent, opts, runtime) {
|
|
|
21911
22330
|
updated = reconciled.manifest;
|
|
21912
22331
|
runtime.stdout.log(`State bucket: ${bucket}`);
|
|
21913
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
|
+
}
|
|
21914
22370
|
async function inspectStateBucket(hub, bucket, bucketPrefix) {
|
|
21915
22371
|
await hub.assertBucketAccessible(bucket);
|
|
21916
22372
|
const client = hub.bucket(bucket);
|
|
@@ -21919,7 +22375,13 @@ async function inspectStateBucket(hub, bucket, bucketPrefix) {
|
|
|
21919
22375
|
const prefix = normalizeBucketPrefix(bucketPrefix);
|
|
21920
22376
|
const prefixWithSlash = `${prefix}/`;
|
|
21921
22377
|
const manifestPath2 = `${prefixWithSlash}${SNAPSHOT_MANIFEST_REMOTE_NAME}`;
|
|
21922
|
-
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
|
+
);
|
|
21923
22385
|
const stateEntries = payloadEntries.filter(
|
|
21924
22386
|
(entry) => entry.path === manifestPath2 || entry.path.startsWith(`${prefixWithSlash}snapshots/`) || entry.path.startsWith(`${prefixWithSlash}runtime/`)
|
|
21925
22387
|
);
|
|
@@ -21984,6 +22446,7 @@ function deploymentSecrets(params) {
|
|
|
21984
22446
|
MLCLAW_GATEWAY_LOCATION: params.gatewayLocation,
|
|
21985
22447
|
MLCLAW_RUNTIME_IMAGE: params.runtimeImage,
|
|
21986
22448
|
MLCLAW_RUNTIME_ID: params.runtimeId,
|
|
22449
|
+
MLCLAW_DEPLOYMENT_ID: params.deploymentId,
|
|
21987
22450
|
MLCLAW_SESSION_SECRET: params.sessionSecret,
|
|
21988
22451
|
MLCLAW_CREDENTIAL_KEY: params.credentialKey,
|
|
21989
22452
|
MLCLAW_OPENCLAW_PORT: String(DEFAULT_SPACE_OPENCLAW_PORT),
|
|
@@ -22144,6 +22607,7 @@ function spaceGatewayNeedsRepair(manifest, variables, runtime, allowedUsers) {
|
|
|
22144
22607
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
22145
22608
|
MLCLAW_RUNTIME_IMAGE: manifest.runtimeImage,
|
|
22146
22609
|
MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent),
|
|
22610
|
+
MLCLAW_DEPLOYMENT_ID: manifest.deploymentId,
|
|
22147
22611
|
MLCLAW_ALLOWED_USERS: allowedUsers,
|
|
22148
22612
|
MLCLAW_ADMINS: allowedUsers,
|
|
22149
22613
|
MLCLAW_CANONICAL_SPACE_ID: DEFAULT_CANONICAL_TEMPLATE_SPACE,
|
|
@@ -22199,6 +22663,7 @@ async function deploySpaceGateway(params) {
|
|
|
22199
22663
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
22200
22664
|
MLCLAW_RUNTIME_IMAGE: spaceRuntimeRef,
|
|
22201
22665
|
MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent),
|
|
22666
|
+
MLCLAW_DEPLOYMENT_ID: manifest.deploymentId,
|
|
22202
22667
|
MLCLAW_ALLOWED_USERS: params.allowedUsers,
|
|
22203
22668
|
MLCLAW_ADMINS: params.allowedUsers,
|
|
22204
22669
|
MLCLAW_CANONICAL_SPACE_ID: DEFAULT_CANONICAL_TEMPLATE_SPACE,
|
|
@@ -22237,7 +22702,7 @@ async function startLocalGateway(params) {
|
|
|
22237
22702
|
}
|
|
22238
22703
|
let secrets = await ensureDeploymentCredentialKey(runtime, manifest.agent);
|
|
22239
22704
|
if (!secrets.MLCLAW_SESSION_SECRET) {
|
|
22240
|
-
secrets = { ...secrets, MLCLAW_SESSION_SECRET:
|
|
22705
|
+
secrets = { ...secrets, MLCLAW_SESSION_SECRET: randomBytes2(48).toString("base64url") };
|
|
22241
22706
|
await writeSecretEnv(runtime.configRoot, manifest.agent, secrets);
|
|
22242
22707
|
}
|
|
22243
22708
|
const accessSecrets = localAccessSecrets(manifest.owner, localGatewayPort(manifest), secrets, manifest.networkAccess);
|
|
@@ -22638,7 +23103,8 @@ async function gatewayMigrate(agent, opts, runtime) {
|
|
|
22638
23103
|
...deploymentSecrets2,
|
|
22639
23104
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
22640
23105
|
MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
|
|
22641
|
-
MLCLAW_RUNTIME_ID: spaceRuntimeId(agent)
|
|
23106
|
+
MLCLAW_RUNTIME_ID: spaceRuntimeId(agent),
|
|
23107
|
+
MLCLAW_DEPLOYMENT_ID: updated.deploymentId
|
|
22642
23108
|
});
|
|
22643
23109
|
await writeManifest(runtime.configRoot, updated);
|
|
22644
23110
|
}
|
|
@@ -22689,6 +23155,7 @@ async function gatewayMigrate(agent, opts, runtime) {
|
|
|
22689
23155
|
MLCLAW_GATEWAY_LOCATION: "local",
|
|
22690
23156
|
MLCLAW_RUNTIME_IMAGE: updated.runtimeImage,
|
|
22691
23157
|
MLCLAW_RUNTIME_ID: updated.localRuntimeId,
|
|
23158
|
+
MLCLAW_DEPLOYMENT_ID: updated.deploymentId,
|
|
22692
23159
|
...localAccessSecrets(updated.owner, localGatewayPort(updated), secrets, updated.networkAccess)
|
|
22693
23160
|
});
|
|
22694
23161
|
await assertLease();
|
|
@@ -23164,7 +23631,7 @@ function nonEmpty(value) {
|
|
|
23164
23631
|
return trimmed ? trimmed : void 0;
|
|
23165
23632
|
}
|
|
23166
23633
|
function newLocalRuntimeId(agent) {
|
|
23167
|
-
return `local-${agent}-${
|
|
23634
|
+
return `local-${agent}-${randomBytes2(8).toString("hex")}`;
|
|
23168
23635
|
}
|
|
23169
23636
|
function runtimeIdFor(manifest) {
|
|
23170
23637
|
return manifest.gatewayLocation === "local" ? manifest.localRuntimeId : spaceRuntimeId(manifest.agent);
|
|
@@ -23187,7 +23654,7 @@ async function handoffAndStopLocalGateway(params) {
|
|
|
23187
23654
|
}
|
|
23188
23655
|
await runner.disableRestart(containerName, connection);
|
|
23189
23656
|
const handoffStartedAt = params.runtime.now();
|
|
23190
|
-
const requestId =
|
|
23657
|
+
const requestId = randomBytes2(16).toString("hex");
|
|
23191
23658
|
await writeRuntimeHandoffRequest(
|
|
23192
23659
|
params.hub,
|
|
23193
23660
|
params.manifest.bucket,
|
|
@@ -23217,7 +23684,7 @@ async function handoffAndStopLocalGateway(params) {
|
|
|
23217
23684
|
}
|
|
23218
23685
|
async function disableAndPauseSpaceGateway(params) {
|
|
23219
23686
|
const handoffStartedAt = params.runtime.now();
|
|
23220
|
-
const requestId =
|
|
23687
|
+
const requestId = randomBytes2(16).toString("hex");
|
|
23221
23688
|
const shouldWaitForHandoff = await spaceGatewayShouldWaitForHandoff(params);
|
|
23222
23689
|
if (!shouldWaitForHandoff) {
|
|
23223
23690
|
await params.hub.addSpaceVariable(params.manifest.space, "MLCLAW_GATEWAY_DISABLED", "1");
|
|
@@ -23312,7 +23779,7 @@ async function ensureDeploymentCredentialKey(runtime, agent, existing) {
|
|
|
23312
23779
|
}
|
|
23313
23780
|
const updated = {
|
|
23314
23781
|
...secrets,
|
|
23315
|
-
MLCLAW_CREDENTIAL_KEY:
|
|
23782
|
+
MLCLAW_CREDENTIAL_KEY: randomBytes2(32).toString("base64url")
|
|
23316
23783
|
};
|
|
23317
23784
|
await writeSecretEnv(runtime.configRoot, agent, updated);
|
|
23318
23785
|
return updated;
|
|
@@ -23375,6 +23842,9 @@ async function update(repoId, opts, hub, hfToken, runtime) {
|
|
|
23375
23842
|
}
|
|
23376
23843
|
await hub.addSpaceVariable(repoId, "MLCLAW_GATEWAY_LOCATION", "space");
|
|
23377
23844
|
await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_ID", spaceRuntimeId(agentName));
|
|
23845
|
+
if (localManifest) {
|
|
23846
|
+
await hub.addSpaceVariable(repoId, "MLCLAW_DEPLOYMENT_ID", localManifest.deploymentId);
|
|
23847
|
+
}
|
|
23378
23848
|
await hub.addSpaceVariable(repoId, "MLCLAW_OPENCLAW_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
|
|
23379
23849
|
await hub.addSpaceVariable(repoId, "OPENCLAW_GATEWAY_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
|
|
23380
23850
|
const bucket = variables.get("OPENCLAW_HF_STATE_BUCKET")?.value;
|
|
@@ -23410,7 +23880,8 @@ async function reconcileUpdatedDeployment(localManifest, runtimeImage, hub, runt
|
|
|
23410
23880
|
...currentSecrets,
|
|
23411
23881
|
MLCLAW_GATEWAY_LOCATION: "space",
|
|
23412
23882
|
MLCLAW_RUNTIME_IMAGE: runtimeImage,
|
|
23413
|
-
MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent)
|
|
23883
|
+
MLCLAW_RUNTIME_ID: spaceRuntimeId(manifest.agent),
|
|
23884
|
+
MLCLAW_DEPLOYMENT_ID: manifest.deploymentId
|
|
23414
23885
|
});
|
|
23415
23886
|
}
|
|
23416
23887
|
});
|
|
@@ -23589,7 +24060,7 @@ async function doctor(repoId, opts, hub, runtime) {
|
|
|
23589
24060
|
}
|
|
23590
24061
|
if (!secrets.has("MLCLAW_SESSION_SECRET")) {
|
|
23591
24062
|
if (fix) {
|
|
23592
|
-
await hub.addSpaceSecret(repoId, "MLCLAW_SESSION_SECRET",
|
|
24063
|
+
await hub.addSpaceSecret(repoId, "MLCLAW_SESSION_SECRET", randomBytes2(48).toString("base64url"));
|
|
23593
24064
|
fixed.push("set secret MLCLAW_SESSION_SECRET");
|
|
23594
24065
|
} else {
|
|
23595
24066
|
issues.push("secret MLCLAW_SESSION_SECRET is missing");
|
|
@@ -23598,7 +24069,7 @@ async function doctor(repoId, opts, hub, runtime) {
|
|
|
23598
24069
|
if (!secrets.has("MLCLAW_CREDENTIAL_KEY")) {
|
|
23599
24070
|
if (fix) {
|
|
23600
24071
|
const agent = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw";
|
|
23601
|
-
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");
|
|
23602
24073
|
await hub.addSpaceSecret(repoId, "MLCLAW_CREDENTIAL_KEY", credentialKey);
|
|
23603
24074
|
fixed.push("set secret MLCLAW_CREDENTIAL_KEY");
|
|
23604
24075
|
} else {
|
|
@@ -23917,6 +24388,106 @@ async function readOptionalRouterTokenFile(file) {
|
|
|
23917
24388
|
const parsed = parseSecretEnv(raw);
|
|
23918
24389
|
return nonEmpty(parsed.MLCLAW_ROUTER_TOKEN) ?? nonEmpty(parsed.HF_ROUTER_TOKEN) ?? nonEmpty(raw);
|
|
23919
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
|
+
}
|
|
23920
24491
|
async function credentialsStatus(requestedAgent, runtime) {
|
|
23921
24492
|
const manifest = await credentialManifest(requestedAgent, runtime);
|
|
23922
24493
|
await verifiedStoredBrokerCredential(manifest, runtime);
|
|
@@ -24021,6 +24592,113 @@ async function credentialsRepair(requestedAgent, opts, runtime) {
|
|
|
24021
24592
|
runtime.stdout.log(`HF Broker credential repaired: ${manifest.agent}`);
|
|
24022
24593
|
});
|
|
24023
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
|
+
}
|
|
24024
24702
|
async function credentialManifest(requestedAgent, runtime) {
|
|
24025
24703
|
if (requestedAgent) return await readManifest(runtime.configRoot, requestedAgent);
|
|
24026
24704
|
const manifests = await listManifests(runtime.configRoot);
|
|
@@ -24366,5 +25044,6 @@ export {
|
|
|
24366
25044
|
TELEGRAM_SLEEP_TIME,
|
|
24367
25045
|
createProgram,
|
|
24368
25046
|
main,
|
|
24369
|
-
mergeStateVolume
|
|
25047
|
+
mergeStateVolume,
|
|
25048
|
+
migrateCodexCredentialForBucketAdoption
|
|
24370
25049
|
};
|