Ubuntu commited on
Commit
26ce774
·
1 Parent(s): e0aa0c0

Support headless Codex OAuth import for HF Spaces

Browse files
Files changed (4) hide show
  1. Dockerfile +2 -0
  2. README.md +31 -1
  3. import-auth-profiles.mjs +71 -0
  4. setup-hf-config.mjs +11 -2
Dockerfile CHANGED
@@ -24,6 +24,7 @@ RUN git clone --depth 1 --branch "${OPENCLAW_REF}" "${OPENCLAW_REPO}" . \
24
  && pnpm install --frozen-lockfile \
25
  && pnpm build
26
  COPY setup-hf-config.mjs /app/setup-hf-config.mjs
 
27
  COPY sync-external-storage.mjs /app/sync-external-storage.mjs
28
  ENV OPENCLAW_PREFER_PNPM=1
29
  RUN pnpm ui:build
@@ -44,6 +45,7 @@ RUN printf '%s\n' \
44
  ' mkdir -p /home/user/.openclaw' \
45
  'fi' \
46
  'node /app/setup-hf-config.mjs' \
 
47
  'if [ "${OPENCLAW_ENABLE_GEMINI_CLI_AUTH:-1}" = "1" ]; then' \
48
  ' node /app/openclaw.mjs plugins enable google-gemini-cli-auth >/tmp/openclaw-gemini-cli.log 2>&1 || true' \
49
  'fi' \
 
24
  && pnpm install --frozen-lockfile \
25
  && pnpm build
26
  COPY setup-hf-config.mjs /app/setup-hf-config.mjs
27
+ COPY import-auth-profiles.mjs /app/import-auth-profiles.mjs
28
  COPY sync-external-storage.mjs /app/sync-external-storage.mjs
29
  ENV OPENCLAW_PREFER_PNPM=1
30
  RUN pnpm ui:build
 
45
  ' mkdir -p /home/user/.openclaw' \
46
  'fi' \
47
  'node /app/setup-hf-config.mjs' \
48
+ 'node /app/import-auth-profiles.mjs' \
49
  'if [ "${OPENCLAW_ENABLE_GEMINI_CLI_AUTH:-1}" = "1" ]; then' \
50
  ' node /app/openclaw.mjs plugins enable google-gemini-cli-auth >/tmp/openclaw-gemini-cli.log 2>&1 || true' \
51
  'fi' \
README.md CHANGED
@@ -46,7 +46,7 @@ When the logs show `listening on ws://0.0.0.0:7860`, open your Space’s URL (e.
46
  - **The startup script now writes both `primary` and `fallbacks`.**
47
  - **Codex fallback only works if Codex itself is already authenticated and usable.** If `openai-codex/gpt-5.4` still lacks auth, fallback will still fail when traffic reaches it.
48
  - **OpenCode Zen** also works well in Hugging Face Spaces. Set `OPENCODE_API_KEY` and use a default model such as `opencode/claude-opus-4-6`.
49
- - **Codex auth remains the special case**: if your `openai-codex/gpt-5.4` path depends on OAuth-backed credentials, use a separate environment that can complete the login flow first, or expect the Codex fallback to fail.
50
  - **Telegram minimal setup** only needs `TELEGRAM_BOT_TOKEN`.
51
  - **Telegram advanced settings** are optional and only needed when you want allowlists, group restrictions, or mention-only behavior.
52
 
@@ -145,12 +145,19 @@ The startup script `setup-hf-config.mjs` reads the following from **Secrets** or
145
  | Env variable | Config path | Format |
146
  |--------------|------------|--------|
147
  | `OPENCLAW_MODEL_PRIMARY` | `agents.defaults.model.primary` | Primary model ref string |
 
 
148
  | `OPENCLAW_MODEL_FALLBACK_1` | `agents.defaults.model.fallbacks[0]` | First fallback model ref |
149
  | `OPENCLAW_MODEL_FALLBACK_2` | `agents.defaults.model.fallbacks[1]` | Second fallback model ref |
 
150
  | `OPENCLAW_GATEWAY_TOKEN` | `gateway.auth.mode` + `gateway.auth.token` | Any string |
151
  | `OPENCLAW_GATEWAY_PASSWORD` | `gateway.auth.mode` + `gateway.auth.password` | Any string (token wins if both set) |
152
  | `OPENCLAW_GATEWAY_TRUSTED_PROXIES` | `gateway.trustedProxies` | Comma-separated IPs |
153
  | `OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS` | `gateway.controlUi.allowedOrigins` | Comma-separated origins (e.g. `https://you.hf.space`) |
 
 
 
 
154
  | `OPENCLAW_ENABLE_GEMINI_CLI_AUTH` | startup plugin bootstrap | `1` / `0`; enables `google-gemini-cli-auth` before gateway start |
155
  | `TELEGRAM_BOT_TOKEN` | `channels.telegram.botToken` | BotFather token |
156
  | `OPENCLAW_TELEGRAM_DM_POLICY` | `channels.telegram.dmPolicy` | `pairing` \| `allowlist` \| `open` \| `disabled` |
@@ -176,6 +183,29 @@ The startup script `setup-hf-config.mjs` reads the following from **Secrets** or
176
  | `OPENCLAW_CONTROL_UI_ENABLED` | `gateway.controlUi.enabled` | `0` to disable Control UI. |
177
  | `OPENCLAW_CODEX_OAUTH_*` | auth profile store | Depends on upstream OpenClaw support and your runtime's ability to complete the OAuth flow with persistent auth storage. |
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  To add more, extend `setup-hf-config.mjs` (or your copy) to read the env, parse it, and set the corresponding keys on `config.gateway` or `config.agents` before `fs.writeFileSync`. Schema reference: [Configuration](https://docs.openclaw.ai/gateway/configuration).
180
 
181
  ## Optional Space variables (build args)
 
46
  - **The startup script now writes both `primary` and `fallbacks`.**
47
  - **Codex fallback only works if Codex itself is already authenticated and usable.** If `openai-codex/gpt-5.4` still lacks auth, fallback will still fail when traffic reaches it.
48
  - **OpenCode Zen** also works well in Hugging Face Spaces. Set `OPENCODE_API_KEY` and use a default model such as `opencode/claude-opus-4-6`.
49
+ - **Codex auth remains the special case**: in headless Spaces, the reliable path is to complete Codex OAuth elsewhere, then import the resulting OpenClaw auth profile via a Space Secret (see below). Running the interactive OAuth flow inside the Space is usually unreliable.
50
  - **Telegram minimal setup** only needs `TELEGRAM_BOT_TOKEN`.
51
  - **Telegram advanced settings** are optional and only needed when you want allowlists, group restrictions, or mention-only behavior.
52
 
 
145
  | Env variable | Config path | Format |
146
  |--------------|------------|--------|
147
  | `OPENCLAW_MODEL_PRIMARY` | `agents.defaults.model.primary` | Primary model ref string |
148
+ | `OPENCLAW_DEFAULT_MODEL` | `agents.defaults.model.primary` | Legacy alias for primary model |
149
+ | `OPENCLAW_HF_DEFAULT_MODEL` | `agents.defaults.model.primary` | Legacy alias for primary model |
150
  | `OPENCLAW_MODEL_FALLBACK_1` | `agents.defaults.model.fallbacks[0]` | First fallback model ref |
151
  | `OPENCLAW_MODEL_FALLBACK_2` | `agents.defaults.model.fallbacks[1]` | Second fallback model ref |
152
+ | `OPENCLAW_FALLBACK_MODELS` | `agents.defaults.model.fallbacks` | Comma-separated fallback model refs |
153
  | `OPENCLAW_GATEWAY_TOKEN` | `gateway.auth.mode` + `gateway.auth.token` | Any string |
154
  | `OPENCLAW_GATEWAY_PASSWORD` | `gateway.auth.mode` + `gateway.auth.password` | Any string (token wins if both set) |
155
  | `OPENCLAW_GATEWAY_TRUSTED_PROXIES` | `gateway.trustedProxies` | Comma-separated IPs |
156
  | `OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS` | `gateway.controlUi.allowedOrigins` | Comma-separated origins (e.g. `https://you.hf.space`) |
157
+ | `OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN` | `gateway.controlUi.allowedOrigins` | Legacy single-origin alias |
158
+ | `OPENCLAW_AUTH_PROFILES_JSON_B64` | startup auth import | Base64-encoded full `auth-profiles.json` exported from another OpenClaw host |
159
+ | `OPENCLAW_OPENAI_CODEX_PROFILE_B64` | startup auth import | Base64-encoded JSON object for one `openai-codex:default` profile |
160
+ | `OPENCLAW_LEGACY_OAUTH_JSON_B64` | startup auth import | Base64-encoded legacy `credentials/oauth.json` import file |
161
  | `OPENCLAW_ENABLE_GEMINI_CLI_AUTH` | startup plugin bootstrap | `1` / `0`; enables `google-gemini-cli-auth` before gateway start |
162
  | `TELEGRAM_BOT_TOKEN` | `channels.telegram.botToken` | BotFather token |
163
  | `OPENCLAW_TELEGRAM_DM_POLICY` | `channels.telegram.dmPolicy` | `pairing` \| `allowlist` \| `open` \| `disabled` |
 
183
  | `OPENCLAW_CONTROL_UI_ENABLED` | `gateway.controlUi.enabled` | `0` to disable Control UI. |
184
  | `OPENCLAW_CODEX_OAUTH_*` | auth profile store | Depends on upstream OpenClaw support and your runtime's ability to complete the OAuth flow with persistent auth storage. |
185
 
186
+ ### Headless Codex OAuth import for Spaces
187
+
188
+ For `openai-codex/gpt-5.4` on Hugging Face Spaces, the reliable path is:
189
+
190
+ 1. Complete `openclaw models auth login --provider openai-codex` on another machine.
191
+ 2. Copy the resulting `~/.openclaw/agents/main/agent/auth-profiles.json`.
192
+ 3. Base64-encode it and store it in a Space Secret named `OPENCLAW_AUTH_PROFILES_JSON_B64`.
193
+ 4. On startup, this repo imports that file into the Space state automatically.
194
+
195
+ If you only want the Codex profile instead of the whole file, you can provide a single profile JSON object in `OPENCLAW_OPENAI_CODEX_PROFILE_B64`.
196
+
197
+ Example encoding command:
198
+
199
+ ```bash
200
+ base64 -w0 ~/.openclaw/agents/main/agent/auth-profiles.json
201
+ ```
202
+
203
+ After restart, verify in Space logs that startup printed something like:
204
+
205
+ ```text
206
+ [openclaw-hf-auth-import] imported=auth-profiles profiles=...
207
+ ```
208
+
209
  To add more, extend `setup-hf-config.mjs` (or your copy) to read the env, parse it, and set the corresponding keys on `config.gateway` or `config.agents` before `fs.writeFileSync`. Schema reference: [Configuration](https://docs.openclaw.ai/gateway/configuration).
210
 
211
  ## Optional Space variables (build args)
import-auth-profiles.mjs ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const home = process.env.OPENCLAW_HOME || process.env.HOME || '/home/user';
6
+ const stateDir = path.join(home, '.openclaw');
7
+ const agentDir = path.join(stateDir, 'agents', 'main', 'agent');
8
+ const authProfilesPath = path.join(agentDir, 'auth-profiles.json');
9
+ const legacyOauthPath = path.join(stateDir, 'credentials', 'oauth.json');
10
+
11
+ function ensureDir(p) { fs.mkdirSync(p, { recursive: true }); }
12
+ function parseJson(raw, label) {
13
+ try { return JSON.parse(raw); } catch (err) { throw new Error(`Failed to parse ${label}: ${err.message}`); }
14
+ }
15
+ function writeJson(p, data) { ensureDir(path.dirname(p)); fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n', 'utf8'); }
16
+ function readFileMaybe(p) { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } }
17
+
18
+ function normalizeAuthProfiles(data) {
19
+ if (!data || typeof data !== 'object') throw new Error('auth profiles payload must be a JSON object');
20
+ if (!data.profiles || typeof data.profiles !== 'object') throw new Error('auth profiles payload missing profiles object');
21
+ return {
22
+ version: typeof data.version === 'number' ? data.version : 1,
23
+ profiles: data.profiles,
24
+ usageStats: data.usageStats && typeof data.usageStats === 'object' ? data.usageStats : {},
25
+ };
26
+ }
27
+
28
+ function upsertCodexProfile(existing, codexProfile) {
29
+ const next = normalizeAuthProfiles(existing && typeof existing === 'object' ? existing : { profiles: {} });
30
+ next.profiles['openai-codex:default'] = {
31
+ ...next.profiles['openai-codex:default'],
32
+ ...codexProfile,
33
+ provider: 'openai-codex',
34
+ };
35
+ return next;
36
+ }
37
+
38
+ const authProfilesB64 = process.env.OPENCLAW_AUTH_PROFILES_JSON_B64?.trim();
39
+ const authProfilesJson = process.env.OPENCLAW_AUTH_PROFILES_JSON?.trim();
40
+ const codexProfileB64 = process.env.OPENCLAW_OPENAI_CODEX_PROFILE_B64?.trim();
41
+ const codexProfileJson = process.env.OPENCLAW_OPENAI_CODEX_PROFILE_JSON?.trim();
42
+ const legacyOauthB64 = process.env.OPENCLAW_LEGACY_OAUTH_JSON_B64?.trim();
43
+ const legacyOauthJson = process.env.OPENCLAW_LEGACY_OAUTH_JSON?.trim();
44
+
45
+ let imported = 'none';
46
+ let detail = '';
47
+
48
+ if (authProfilesB64 || authProfilesJson) {
49
+ const raw = authProfilesJson || Buffer.from(authProfilesB64, 'base64').toString('utf8');
50
+ const parsed = normalizeAuthProfiles(parseJson(raw, 'OPENCLAW_AUTH_PROFILES_JSON(_B64)'));
51
+ writeJson(authProfilesPath, parsed);
52
+ imported = 'auth-profiles';
53
+ detail = `profiles=${Object.keys(parsed.profiles).length}`;
54
+ } else if (codexProfileB64 || codexProfileJson) {
55
+ const raw = codexProfileJson || Buffer.from(codexProfileB64, 'base64').toString('utf8');
56
+ const codexProfile = parseJson(raw, 'OPENCLAW_OPENAI_CODEX_PROFILE_JSON(_B64)');
57
+ const existingRaw = readFileMaybe(authProfilesPath);
58
+ const existing = existingRaw ? parseJson(existingRaw, authProfilesPath) : { version: 1, profiles: {}, usageStats: {} };
59
+ const merged = upsertCodexProfile(existing, codexProfile);
60
+ writeJson(authProfilesPath, merged);
61
+ imported = 'codex-profile';
62
+ detail = 'profile=openai-codex:default';
63
+ } else if (legacyOauthB64 || legacyOauthJson) {
64
+ const raw = legacyOauthJson || Buffer.from(legacyOauthB64, 'base64').toString('utf8');
65
+ const parsed = parseJson(raw, 'OPENCLAW_LEGACY_OAUTH_JSON(_B64)');
66
+ writeJson(legacyOauthPath, parsed);
67
+ imported = 'legacy-oauth';
68
+ detail = 'wrote credentials/oauth.json';
69
+ }
70
+
71
+ console.log(`[openclaw-hf-auth-import] imported=${imported}${detail ? ' ' + detail : ''} path=${authProfilesPath}`);
setup-hf-config.mjs CHANGED
@@ -61,13 +61,18 @@ function readIndexedEnv(prefix) {
61
  }
62
 
63
  const fallbackModelsFromIndexedEnv = readIndexedEnv("OPENCLAW_MODEL_FALLBACK_");
 
64
  const defaultModel =
65
  process.env.OPENCLAW_MODEL_PRIMARY?.trim() ||
 
 
66
  "huggingface/Qwen/Qwen3-8B";
67
  const fallbackModels =
68
  fallbackModelsFromIndexedEnv.length > 0
69
  ? fallbackModelsFromIndexedEnv
70
- : ["huggingface/deepseek-ai/DeepSeek-R1", "openai-codex/gpt-5.4"];
 
 
71
  const gatewayToken = readGatewayToken();
72
  const gatewayPassword = process.env.OPENCLAW_GATEWAY_PASSWORD?.trim();
73
  const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN?.trim();
@@ -97,7 +102,11 @@ const trustedProxies =
97
  ? parseCsv(trustedProxiesRaw)
98
  : DEFAULT_HF_TRUSTED_PROXY_IPS;
99
  // Comma-separated origins allowed for Control UI/WebSocket (e.g. https://your-space.hf.space)
100
- const allowedOriginsRaw = process.env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS?.trim();
 
 
 
 
101
  const allowedOrigins = parseCsv(allowedOriginsRaw);
102
 
103
  let config = {};
 
61
  }
62
 
63
  const fallbackModelsFromIndexedEnv = readIndexedEnv("OPENCLAW_MODEL_FALLBACK_");
64
+ const fallbackModelsFromCsv = parseCsv(process.env.OPENCLAW_FALLBACK_MODELS?.trim());
65
  const defaultModel =
66
  process.env.OPENCLAW_MODEL_PRIMARY?.trim() ||
67
+ process.env.OPENCLAW_DEFAULT_MODEL?.trim() ||
68
+ process.env.OPENCLAW_HF_DEFAULT_MODEL?.trim() ||
69
  "huggingface/Qwen/Qwen3-8B";
70
  const fallbackModels =
71
  fallbackModelsFromIndexedEnv.length > 0
72
  ? fallbackModelsFromIndexedEnv
73
+ : fallbackModelsFromCsv.length > 0
74
+ ? fallbackModelsFromCsv
75
+ : ["huggingface/deepseek-ai/DeepSeek-R1", "openai-codex/gpt-5.4"];
76
  const gatewayToken = readGatewayToken();
77
  const gatewayPassword = process.env.OPENCLAW_GATEWAY_PASSWORD?.trim();
78
  const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN?.trim();
 
102
  ? parseCsv(trustedProxiesRaw)
103
  : DEFAULT_HF_TRUSTED_PROXY_IPS;
104
  // Comma-separated origins allowed for Control UI/WebSocket (e.g. https://your-space.hf.space)
105
+ const allowedOriginsRaw = (
106
+ process.env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS?.trim() ||
107
+ process.env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGIN?.trim() ||
108
+ ''
109
+ );
110
  const allowedOrigins = parseCsv(allowedOriginsRaw);
111
 
112
  let config = {};