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/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.4.10",
15386
+ "packageVersion": "0.6.0",
15387
15387
  "openclawVersion": "2026.7.1",
15388
- "brokerkitVersion": "hf-broker/v0.5.2",
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 ?? randomBytes(48).toString("base64url");
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 ?? randomBytes(32).toString("base64url");
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 payloadEntries = fileEntries.filter((entry) => !entry.path.startsWith(".mlclaw/"));
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: randomBytes(48).toString("base64url") };
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}-${randomBytes(8).toString("hex")}`;
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 = randomBytes(16).toString("hex");
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 = randomBytes(16).toString("hex");
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: randomBytes(32).toString("base64url")
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", randomBytes(48).toString("base64url"));
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") : randomBytes(32).toString("base64url");
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
  };