Ubuntu commited on
Commit ·
26ce774
1
Parent(s): e0aa0c0
Support headless Codex OAuth import for HF Spaces
Browse files- Dockerfile +2 -0
- README.md +31 -1
- import-auth-profiles.mjs +71 -0
- 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**:
|
| 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 |
-
:
|
|
|
|
|
|
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = {};
|