Skill: deadlysilent/codex-profiler (version 1.0.27)

--- name: codex-profiler description: Maintained Codex operations skill: unified /codex_usage + /codex_auth path. Standalone codex-usage/codex-auth are deprecated. --- > ✅ **Maintained path:** use `codex-profiler` for all Codex profile operations. > Standalone `codex-usage` and `codex-auth` skills are deprecated. This skill consolidates both scripts: - `scripts/codex_usage.py` (usage/limits) - `scripts/codex_auth.py` (OAuth helper for start/status) For auth/profile mutation, this skill now standardizes on gateway-native `openclaw models auth ...` commands. ## Safe defaults - Usage checks are read-only by default. - Auth state is runtime-managed; one-shot direct file edits are unreliable and can be overwritten by in-memory/cooldown state. - Treat `auth-profiles.json` as gateway-managed state. Never mutate it directly in normal operations. - Prefer gateway-native auth mutation commands (`openclaw models auth ...`, `openclaw models auth order ...`) over script-level file writes. - Use dry-run/read-only preflight first, then apply, then verify (strict anti-drift flow below). - See `RISK.md` for allowed/denied operation boundaries. ## Commands ### Usage - `/codex_usage` → selector (default / all / discovered profiles) - `/codex_usage <profile>` ### Auth - `/codex_auth` → selector (profiles) - `/codex_auth <profile>` - `/codex_auth finish <profile> <callback_url>` (helper only; profile/order mutation must use gateway-native commands) ## UX requirements (cross-channel) For `/codex_usage`, send immediate progress message first as a separate message: - "Running Codex usage checks now…" Delivery rule: - If progress is sent through channel message tool path, send final result through the same path (same target/session), then return `NO_REPLY`. - Avoid mixed delivery (tool progress + plain reply final). For auth/profile/order mutation, warn that writes are gateway-managed and enforce verify-after-apply: - "I will apply this via `openclaw models auth ...` and then verify with `models status` + `auth order get`." - "I won’t hand-edit auth files directly because runtime state can drift/overwrite one-shot edits." ### Interaction adapter - If inline buttons are supported: use selector buttons. - If inline buttons are not supported: use text fallback prompts. - Apply duplicate-request suppression per user for ~20s. - Never echo full callback URLs in responses. ## Profile removal policy (MANDATORY) 1. **Best method (default): operational retire, not hard delete** - Remove the target profile from active provider order (`openclaw models auth order set ...`) so it is never selected. - Keep profile data intact unless the user explicitly requests permanent deletion. 2. **Hard delete only on explicit user instruction** - Perform permanent profile deletion only when the user clearly asks to hard delete/remove permanently. - If gateway-native delete is unavailable in the installed OpenClaw version, do not improvise risky live edits; use a controlled maintenance window flow. ## Strict anti-drift auth mutation flow (MANDATORY) For auth/profile/order changes, use this exact 3-step flow: 1) **Preflight (read-only)** ```bash openclaw models status --json openclaw models auth order get --provider openai-codex --agent <agent-id> ``` 2) **Apply (gateway-native command)** ```bash openclaw models auth order set --provider openai-codex --agent <agent-id> <profile1> <profile2> # or openclaw models auth order clear --provider openai-codex --agent <agent-id> # or provider login flow openclaw models auth login --provider openai-codex ``` 3) **Verify (post-apply, no assumptions)** ```bash openclaw models status --json openclaw models auth order get --provider openai-codex --agent <agent-id> ``` Never skip verification. If results mismatch expectation, do not hand-edit files; diagnose and re-apply via gateway-native commands. ## How to run ```bash # Usage checks (read-only) python3 skills/codex-profiler/scripts/codex_usage.py --profile all --timeout-sec 25 --retries 1 --debug python3 skills/codex-profiler/scripts/codex_usage.py --profile all --format text # OAuth helper (callback parsing/status only) python3 skills/codex-profiler/scripts/codex_auth.py start --profile default python3 skills/codex-profiler/scripts/codex_auth.py status ``` ## Safety posture - No remote shell execution (`curl|bash`, `wget|sh`) is allowed by this skill. - No `sudo`/SSH/system-level host mutation commands are part of this skill path. - Usage checks are restricted to trusted HTTPS endpoint host allowlist (`chatgpt.com`). - Callback URLs and token material must be treated as sensitive and never echoed in full. ## Multi-account rotation guidance When asked about running multiple Codex accounts/profiles, rotation policy, or fallback strategy, read: - `references/multi-account-rotation.md` Use the short template for quick chat answers and the deep-dive template for setup/troubleshooting requests. ## Notes - Uses auth profiles at `~/.openclaw/agents/main/agent/auth-profiles.json` by default. - Current source of truth is `auth-profiles.json`; `auth.json` is legacy compatibility and should not be used as primary state. - If profile routing behaves unexpectedly, check for mixed state (missing/stale `auth-profiles.json`, leftover legacy files, or stale runtime cooldown) before assuming model fallback bugs. - Same `accountId` across two profile labels is not automatically a defect. In this environment, multiple labels can map to the same workspace/account identity while still operating correctly. Treat it as healthy when both profiles are auth-valid and usable in `/codex_usage`; investigate only when auth failures or routing failures appear. - Codex usage endpoint: `https://chatgpt.com/backend-api/wham/usage`. - Usage script now surfaces `401` as [REDACTED_SECRET] with a clear hint, while still returning local profile health. - Usage output now includes top-level `summary`, `formatted_profiles`, and `suggested_user_message` for cleaner slash-command formatting. - Preferred strict output format for `/codex_usage` (single line per profile): - `🟢 \`%profile%\` — 5h **%5h_left%** | week **%week_left%** | r5 **%5h_reset%** | rw **%week_reset%**` - Use `🟠` when limited and `🔴` when unusable/auth-invalid. - No preface, no table, no trailing summary unless explicitly requested. - OAuth flow: OpenAI auth endpoints + localhost callback on port 1455. - Preferred mutation path is gateway-native (`openclaw models auth ...` / `openclaw models auth order ...`) with mandatory preflight + verify. - `codex_auth.py status --profile <profile>` remains useful for per-profile helper status checks. - Keep temporary payload/token artifacts only until verification succeeds, then clean them up. - Codex CLI installation is not required for usage endpoint reads in this skill path.

path: RISK.md
content: # Risk Policy — codex-profiler

## Safe defaults
- Usage checks are read-only by default.
- Auth apply is explicit and should be queued safely.
- Profile delete actions require explicit confirmation.

## Allowed operations
- Local auth profile inspection and updates for Codex profile management.
- Codex usage probe via trusted HTTPS endpoint (`chatgpt.com`).
- OAuth start/finish handling for Codex profile refresh.

## Denied operations
- No remote shell execution (`curl|bash`, `wget|sh`).
- No `sudo`/SSH/system package or host mutation commands.
- No outbound calls to untrusted hosts for usage checks.
- Never echo full callback URLs/tokens in chat output.


path: SECURITY.md
content: # Security Notes

This consolidated skill bundles `codex_usage.py` and `codex_auth.py`.

## Scope
- Read-only usage checks (`codex_usage.py`).
- OAuth profile refresh/setup (`codex_auth.py`).

## Data Handling
- Sensitive fields are not emitted in output.
- Usage output applies scrub rules for token-like keys.
- Never echo full callback URLs in responses.

## Network Egress
- Usage checks call trusted ChatGPT usage endpoint.
- OAuth flow uses OpenAI auth endpoints.

## Operational Safety
- Auth queued apply mode may restart gateway.
- Revert command + backups are provided before restart-capable operations.


path: _meta.json
content: ownerId: kn7a95mjnzxazjp414temaznks813rwc
slug: codex-profiler
version: 1.0.26
publishedAt: 1773763428710

path: references/multi-account-rotation.md
content: # Codex Multi-Account Rotation Guide

Use this guide when a user asks about running multiple Codex accounts/profiles, rotating profiles, or improving resilience when one profile fails usage checks.

## Scope and safety
- Only use legitimately authorized accounts.
- Do not automate bypasses of provider limits or anti-abuse controls.
- Prefer transparent failover and clear user messaging.

## Quick answer template (short mode)
Use this when the user asks casually:

"Yes — you can set up multiple Codex profiles and rotate between them. Add each account as its own `openai-codex:<name>` profile, keep one as `default`, and fail over on repeated 401/403/429 errors with cooldown before retrying. You don’t need Codex CLI just for usage checks. If usage endpoint auth is rejected (401), keep local profile health output and switch to another healthy profile."

## Setup pattern
1. Create distinct profile IDs:
   - `openai-codex:default`
   - `openai-codex:work`
   - `openai-codex:personal`
   - `openai-codex:backup`
2. Keep names stable and descriptive (owner/purpose).
3. Ensure each profile has valid OAuth material in `auth-profiles.json`.
4. Keep one primary default and at least one fallback.

## Rotation policy (recommended)
Use deterministic, debuggable rotation:

1. Try `default` first.
2. On profile error, classify and act:
   - `401 auth_not_accepted_by_usage_endpoint`: endpoint rejected token/session format. Mark profile degraded for this check path and fail over.
   - `403 forbidden_by_usage_endpoint`: likely permission/scope issue. Fail over.
   - `429` or rate-limit style failures: fail over + cooldown.
   - transport timeout/network: retry once, then fail over.
3. Cooldown degraded profile (e.g., 15-30 min for 401/403, shorter for transient network issues).
4. Re-test cooled-down profiles later before promoting back to default.

## Operational thresholds
- Retry count: 1
- Request timeout: 20-25s
- Failover trigger: 2 consecutive hard failures for the same profile
- Recovery trigger: 2 consecutive successful checks before promotion

## Suggested user-facing messages
### Healthy
"Usage check complete. Primary profile is healthy; fallback profiles available."

### Fallback activated
"Primary profile had repeated endpoint auth/rate-limit errors, so I switched to a fallback profile and continued."

### All degraded
"All configured Codex profiles are currently degraded for remote usage checks. I can still report local token/profile health while you refresh or rotate credentials."

## Safe cleanup / profile removal
Prefer detach-only first, then hard delete if requested.

- Detach only (safer):
```bash
python3 skills/codex-profiler/scripts/codex_usage.py --delete-profile openai-codex:work --confirm-delete
```

- Hard delete (permanent, with backup):
```bash
python3 skills/codex-profiler/scripts/codex_usage.py --delete-profile openai-codex:work --confirm-delete --hard-delete
```

## Deep-dive response template
Use when user asks "how do I set it up?":

1. "Create 2-4 Codex profiles (default + fallbacks) with clear names."
2. "Run usage check on `all` profiles to baseline health and recent failures."
3. "Route requests through `default`, fail over on repeated 401/403/429 or timeout failures."
4. "Apply cooldown before retrying failed profiles; auto-promote only after repeated success."
5. "Use detach-only delete first for bad profiles; hard-delete only when sure."
6. "No Codex CLI install is required for this usage-check flow."

## Troubleshooting notes
- `401` on `chatgpt.com/backend-api/wham/usage` can happen even with valid OAuth profile state.
- Treat `401` as endpoint auth incompatibility for that profile/session path, not immediate evidence that local profile storage is broken.
- Always avoid printing full callback URLs or tokens in chat output.


path: scripts/codex_auth.py
content: #!/usr/bin/env python3
import argparse
import base64
import hashlib
import json
import os
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path
import fcntl
import shutil
import subprocess
import shlex
import copy

AUTH_PATH_DEFAULT = str(Path.home() / ".openclaw" / "agents" / "main" / "agent" / "auth-profiles.json")
PENDING_PATH = "/tmp/openclaw/codex-auth-pending.json"

CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize"
TOKEN_URL = "https://auth.openai.com/oauth/token"
REDIRECT_URI = "http://localhost:1455/auth/callback"
SCOPE = "openid profile email offline_access"
JWT_CLAIM_PATH = "https://api.openai.com/auth"
OPENCLAW_CONFIG_PATH = str(Path.home() / ".openclaw" / "openclaw.json")
BACKUP_DIR = "/tmp/openclaw/safety-backups"
APPLY_STATUS_PATH = "/tmp/openclaw/codex-auth-apply-last.json"


def safe_profile_slug(profile_id: str) -> str:
    return (profile_id or "unknown").replace(":", "_")


def per_profile_status_path(profile_id: str) -> str:
    return f"/tmp/openclaw/codex-auth-apply-last-{safe_profile_slug(profile_id)}.json"


def b64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).decode().rstrip("=")


def generate_pkce():
    verifier = b64url(os.urandom(32))
    challenge = b64url(hashlib.sha256(verifier.encode()).digest())
    return verifier, challenge


def create_state():
    return b64url(os.urandom(16))


def decode_jwt(token: str):
    try:
        parts = token.split(".")
        if len(parts) != 3:
            return None
        payload = parts[1]
        payload += "=" * (-len(payload) % 4)
        raw = base64.urlsafe_b64decode(payload.encode())
        return json.loads(raw.decode())
    except Exception:
        return None


def get_account_id(access_token: str):
    payload = decode_jwt(access_token) or {}
    auth = payload.get(JWT_CLAIM_PATH) or {}
    account_id = auth.get("chatgpt_account_id")
    return account_id if isinstance(account_id, str) and account_id else None


def parse_callback_input(value: str):
    value = value.strip()
    if not value:
        return None, None
    try:
        u = urllib.parse.urlparse(value)
        qs = urllib.parse.parse_qs(u.query)
        code = (qs.get("code") or [None])[0]
        state = (qs.get("state") or [None])[0]
        return code, state
    except Exception:
        pass
    if value.startswith("code=") or "&" in value:
        qs = urllib.parse.parse_qs(value)
        code = (qs.get("code") or [None])[0]
        state = (qs.get("state") or [None])[0]
        return code, state
    if "#" in value:
        code, state = value.split("#", 1)
        return code, state
    return value, None


def resolve_profile_id(selector: str):
    selector = (selector or "default").strip()
    if selector.startswith("openai-codex:"):
        return selector
    if selector == "default":
        return "openai-codex:default"
    return f"openai-codex:{selector}"


def guard_default_mutation(profile_id: str, allow_default: bool):
    if profile_id == "openai-codex:default" and not allow_default:
        print(json.dumps({
            "ok": False,
            "error": "default_profile_protected",
            "profile": profile_id,
            "hint": "Refusing to mutate/auth default profile unless explicitly allowed. Re-run with --allow-default if this is intentional.",
        }, indent=2))
        sys.exit(2)


def assert_only_target_profile_changed(before_profiles, after_profiles, target_profile, context):
    before_profiles = before_profiles or {}
    after_profiles = after_profiles or {}
    keys = set(before_profiles.keys()) | set(after_profiles.keys())
    drift = []
    for k in sorted(keys):
        if k == target_profile:
            continue
        if before_profiles.get(k) != after_profiles.get(k):
            drift.append(k)
    if drift:
        raise RuntimeError(
            f"profile_drift_detected:{context}:target={target_profile}:changed={','.join(drift)}"
        )


def read_json(path):
    if not os.path.exists(path):
        return {}
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


def write_json_atomic(path, data):
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)
        f.write("\n")
    os.replace(tmp, path)


def backup_file(path, tag):
    os.makedirs(BACKUP_DIR, exist_ok=True)
    ts = int(time.time())
    base = os.path.basename(path)
    out = os.path.join(BACKUP_DIR, f"{base}.{tag}.{ts}.bak")
    if os.path.exists(path):
        shutil.copy2(path, out)
    else:
        Path(out).write_text("", encoding="utf-8")
    return out


def build_revert_command(backups):
    ops = []
    if backups.get("config"):
        ops.append(f"cp '{backups['config']}' '{OPENCLAW_CONFIG_PATH}'")
    if backups.get("auth"):
        ops.append(f"cp '{backups['auth']}' '{AUTH_PATH_DEFAULT}'")
    ops.append("openclaw gateway restart")
    return " && ".join(ops)


def ensure_profile_declared_in_config_obj(cfg, profile_id):
    auth = cfg.setdefault("auth", {})
    profiles = auth.setdefault("profiles", {})
    before_profiles = copy.deepcopy(profiles)
    existing = profiles.get(profile_id)
    wanted = {"provider": "openai-codex", "mode": "oauth"}
    changed = existing != wanted
    if changed:
        profiles[profile_id] = wanted
    assert_only_target_profile_changed(before_profiles, profiles, profile_id, "config")
    return changed


def ensure_profile_declared_in_config(profile_id):
    cfg = read_json(OPENCLAW_CONFIG_PATH)
    changed = ensure_profile_declared_in_config_obj(cfg, profile_id)
    backup = None
    if changed:
        backup = backup_file(OPENCLAW_CONFIG_PATH, "before-codex-auth")
        write_json_atomic(OPENCLAW_CONFIG_PATH, cfg)
    return {"changed": changed, "backup": backup}


def save_pending(profile_id, verifier, state):
    pending = read_json(PENDING_PATH)
    pending[profile_id] = {
        "verifier": verifier,
        "state": state,
        "createdAt": int(time.time() * 1000)
    }
    os.makedirs(os.path.dirname(PENDING_PATH), exist_ok=True)
    write_json_atomic(PENDING_PATH, pending)


def load_pending(profile_id):
    pending = read_json(PENDING_PATH)
    return pending.get(profile_id)


def exchange_code(code, verifier):
    body = urllib.parse.urlencode({
        "grant_type": "authorization_code",
        "client_id": CLIENT_ID,
        "code": code,
        "code_verifier": verifier,
        "redirect_uri": REDIRECT_URI,
    }).encode()
    req = urllib.request.Request(TOKEN_URL, data=body, headers={"Content-Type": "application/x-www-form-urlencoded"})
    with urllib.request.urlopen(req, timeout=30) as resp:
        raw = resp.read().decode("utf-8", errors="replace")
    data = json.loads(raw)
    if not data.get("access_token") or not data.get("refresh_token") or not isinstance(data.get("expires_in"), (int, float)):
        raise RuntimeError("Token exchange missing fields")
    return {
        "access": data["access_token"],
        "refresh": data["refresh_token"],
        "expires": int(time.time() * 1000) + int(data["expires_in"] * 1000),
    }


def upsert_auth_profile_obj(store, profile_id, credentials):
    store.setdefault("profiles", {})
    store.setdefault("usageStats", {})
    store.setdefault("order", {})
    store["profiles"][profile_id] = {
        "provider": "openai-codex",
        "type": "oauth",
        "access": credentials["access"],
        "refresh": credentials["refresh"],
        "expires": credentials["expires"],
        "accountId": credentials["accountId"],
    }

    provider_order = store["order"].get("openai-codex")
    if not isinstance(provider_order, list):
        provider_order = []
    if profile_id not in provider_order:
        provider_order.append(profile_id)
    store["order"]["openai-codex"] = provider_order
    return store


def update_auth_profile(auth_path, profile_id, credentials):
    os.makedirs(os.path.dirname(auth_path), exist_ok=True)
    backup = backup_file(auth_path, "before-codex-auth")
    # lock + read existing
    with open(auth_path, "a+", encoding="utf-8") as f:
        fcntl.flock(f, fcntl.LOCK_EX)
        f.seek(0)
        try:
            raw = f.read().strip()
            store = json.loads(raw) if raw else {}
        except Exception:
            store = {}

        before_profiles = copy.deepcopy((store.get("profiles") or {}))
        store = upsert_auth_profile_obj(store, profile_id, credentials)
        assert_only_target_profile_changed(before_profiles, store.get("profiles") or {}, profile_id, "auth")
        write_json_atomic(auth_path, store)
        fcntl.flock(f, fcntl.LOCK_UN)
    return backup


def build_auth_url(verifier, state):
    params = {
        "response_type": "code",
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "scope": SCOPE,
        "code_challenge": b64url(hashlib.sha256(verifier.encode()).digest()),
        "code_challenge_method": "S256",
        "state": state,
        "id_token_add_organizations": "true",
        "codex_cli_simplified_flow": "true",
        "originator": "pi",
    }
    return AUTHORIZE_URL + "?" + urllib.parse.urlencode(params)


def run_cmd(cmd):
    return subprocess.run(cmd, capture_output=True, text=True)


def stop_gateway_processes():
    # Avoid `openclaw gateway stop` because CLI config parsing may fail.
    subprocess.run(["pkill", "-TERM", "-f", "^openclaw-gateway$"], capture_output=True, text=True)
    time.sleep(3)
    subprocess.run(["pkill", "-KILL", "-f", "^openclaw-gateway$"], capture_output=True, text=True)
    time.sleep(1)


def start_gateway_process():
    os.makedirs("/tmp/openclaw", exist_ok=True)
    log_path = "/tmp/openclaw/codex-auth-gateway-start.log"
    lf = open(log_path, "a", encoding="utf-8")

    # Prefer direct gateway binary when present.
    gw_bin = shutil.which("openclaw-gateway")
    if gw_bin:
        subprocess.Popen([gw_bin], stdout=lf, stderr=lf, stdin=subprocess.DEVNULL, start_new_session=True)
        return log_path

    # Fallback for installs where only the CLI is on PATH.
    oc_bin = shutil.which("openclaw")
    if not oc_bin:
        raise FileNotFoundError("Neither 'openclaw-gateway' nor 'openclaw' found on PATH")

    subprocess.run([oc_bin, "gateway", "start"], stdout=lf, stderr=lf, stdin=subprocess.DEVNULL, check=False)
    return log_path


def wait_gateway_live(timeout_sec=25):
    deadline = time.time() + timeout_sec
    url = "http://127.0.0.1:18789/health"
    last_err = None
    while time.time() < deadline:
        try:
            with urllib.request.urlopen(url, timeout=3) as resp:
                raw = resp.read().decode("utf-8", errors="replace")
                if '"ok":true' in raw or '"status":"live"' in raw:
                    return True, raw[:200]
        except Exception as e:
            last_err = str(e)
        time.sleep(1)
    return False, (last_err or "health_check_timeout")


def apply_with_gateway_restart(payload_path, auth_path):
    payload = read_json(payload_path)
    profile_id = payload["profile"]
    tokens = payload["tokens"]

    # backups + revert command
    cfg_bak = backup_file(OPENCLAW_CONFIG_PATH, "before-codex-auth-apply")
    auth_bak = backup_file(auth_path, "before-codex-auth-apply")
    revert_cmd = f"cp '{cfg_bak}' '{OPENCLAW_CONFIG_PATH}' && cp '{auth_bak}' '{auth_path}' && openclaw gateway restart"

    err = []
    ok = True

    # Build staged copies first (while gateway may still be running)
    stage_dir = "/tmp/openclaw"
    os.makedirs(stage_dir, exist_ok=True)
    ts = int(time.time())
    staged_cfg = os.path.join(stage_dir, f"openclaw.staged.{ts}.json")
    staged_auth = os.path.join(stage_dir, f"auth-profiles.staged.{ts}.json")

    try:
        if os.path.exists(OPENCLAW_CONFIG_PATH):
            shutil.copy2(OPENCLAW_CONFIG_PATH, staged_cfg)
        else:
            write_json_atomic(staged_cfg, {})
        if os.path.exists(auth_path):
            shutil.copy2(auth_path, staged_auth)
        else:
            write_json_atomic(staged_auth, {})

        cfg = read_json(staged_cfg)
        ensure_profile_declared_in_config_obj(cfg, profile_id)
        write_json_atomic(staged_cfg, cfg)

        store = read_json(staged_auth)
        before_profiles = copy.deepcopy((store.get("profiles") or {}))
        store = upsert_auth_profile_obj(store, profile_id, tokens)
        assert_only_target_profile_changed(before_profiles, store.get("profiles") or {}, profile_id, "apply_auth")
        write_json_atomic(staged_auth, store)

        if not (read_json(staged_auth).get("profiles", {}).get(profile_id)):
            raise RuntimeError("staged_auth_missing_profile_after_write")
    except Exception as e:
        ok = False
        err.append({"step": "stage", "error": str(e)})

    # Activate staged files only while gateway is stopped
    if ok:
        try:
            stop_gateway_processes()
        except Exception as e:
            ok = False
            err.append({"step": "stop", "error": str(e)})

    if ok:
        try:
            shutil.copy2(staged_cfg, OPENCLAW_CONFIG_PATH)
            os.makedirs(os.path.dirname(auth_path), exist_ok=True)
            shutil.copy2(staged_auth, auth_path)
        except Exception as e:
            ok = False
            err.append({"step": "activate", "error": str(e)})

    gateway_start_log = None
    if ok:
        try:
            gateway_start_log = start_gateway_process()
            live_ok, health_note = wait_gateway_live(timeout_sec=25)
            if not live_ok:
                ok = False
                err.append({"step": "start", "error": f"gateway_not_live:{health_note}"})
        except Exception as e:
            ok = False
            err.append({"step": "start", "error": str(e)})

    out = {
        "ok": ok,
        "profile": profile_id,
        "revert_command": revert_cmd,
        "staged": {
            "config": staged_cfg,
            "auth": staged_auth,
        },
        "gateway_start_log": gateway_start_log,
        "errors": err,
        "ts": int(time.time() * 1000),
    }
    write_json_atomic(APPLY_STATUS_PATH, out)
    write_json_atomic(per_profile_status_path(profile_id), out)
    return out


def main():
    ap = argparse.ArgumentParser()
    sub = ap.add_subparsers(dest="cmd", required=True)

    s_start = sub.add_parser("start")
    s_start.add_argument("--profile", default="default")
    s_start.add_argument("--auth-path", default=AUTH_PATH_DEFAULT)
    s_start.add_argument("--allow-default", action="store_true", help="Required to mutate/auth the default profile")

    s_finish = sub.add_parser("finish")
    s_finish.add_argument("--profile", default="default")
    s_finish.add_argument("--callback-url", required=True)
    s_finish.add_argument("--auth-path", default=AUTH_PATH_DEFAULT)
    s_finish.add_argument("--queue-apply", action="store_true", default=True, help="Queue stop/write/start apply script in background (strict default)")
    s_finish.add_argument("--allow-default", action="store_true", help="Required to mutate/auth the default profile")
    s_finish.add_argument("--allow-unsafe-direct", action="store_true", help="Allow direct in-process writes without off-host stop/write/start (not recommended)")

    s_apply = sub.add_parser("apply")
    s_apply.add_argument("--payload", required=True)
    s_apply.add_argument("--auth-path", default=AUTH_PATH_DEFAULT)
    s_apply.add_argument("--allow-default", action="store_true", help="Required to mutate/auth the default profile")

    s_status = sub.add_parser("status")
    s_status.add_argument("--profile", default=None, help="Optional profile suffix/id for per-profile apply status")

    args = ap.parse_args()

    if args.cmd == "status":
        chosen = APPLY_STATUS_PATH
        if args.profile:
            pid = resolve_profile_id(args.profile)
            candidate = per_profile_status_path(pid)
            if os.path.exists(candidate):
                chosen = candidate
        if os.path.exists(chosen):
            out = read_json(chosen)
            if isinstance(out, dict):
                out.setdefault("status_path", chosen)
            print(json.dumps(out, indent=2))
        else:
            print(json.dumps({"ok": False, "error": "no_status", "status_path": chosen}, indent=2))
        return

    if args.cmd == "apply":
        payload_preview = read_json(args.payload)
        guard_default_mutation(payload_preview.get("profile"), getattr(args, "allow_default", False))
        out = apply_with_gateway_restart(args.payload, args.auth_path)
        print(json.dumps(out, indent=2))
        if not out.get("ok"):
            sys.exit(2)
        return

    if args.cmd == "start":
        profile_id = resolve_profile_id(args.profile)
        guard_default_mutation(profile_id, getattr(args, "allow_default", False))
        verifier, _challenge = generate_pkce()
        state = create_state()
        save_pending(profile_id, verifier, state)
        url = build_auth_url(verifier, state)
        print(json.dumps({
            "ok": True,
            "profile": profile_id,
            "login_url": url,
            "redirect_uri": REDIRECT_URI,
            "instructions": "Open the login_url in your browser, then paste the localhost redirect URL back using the finish command.",
        }, indent=2))
        return

    if args.cmd == "finish":
        profile_id = resolve_profile_id(args.profile)
        guard_default_mutation(profile_id, getattr(args, "allow_default", False))
        pending = load_pending(profile_id)
        if not pending:
            print(json.dumps({"ok": False, "error": "no_pending_flow", "profile": profile_id}, indent=2))
            sys.exit(2)
        code, state = parse_callback_input(args.callback_url)
        if not code:
            print(json.dumps({"ok": False, "error": "missing_code"}, indent=2))
            sys.exit(2)
        if pending.get("state") and state and pending["state"] != state:
            print(json.dumps({"ok": False, "error": "state_mismatch"}, indent=2))
            sys.exit(2)
        tokens = exchange_code(code, pending["verifier"])
        account_id = get_account_id(tokens["access"])
        if not account_id:
            print(json.dumps({"ok": False, "error": "missing_account_id"}, indent=2))
            sys.exit(2)
        tokens["accountId"] = account_id

        if args.queue_apply:
            os.makedirs("/tmp/openclaw", exist_ok=True)
            safe_profile = safe_profile_slug(profile_id)
            payload_path = f"/tmp/openclaw/codex-auth-apply-{safe_profile}.json"
            script_path = f"/tmp/openclaw/codex-auth-apply-{safe_profile}.sh"
            log_path = f"/tmp/openclaw/codex-auth-apply-{safe_profile}.log"
            status_path = per_profile_status_path(profile_id)
            write_json_atomic(payload_path, {"profile": profile_id, "tokens": tokens})

            py = shlex.quote(sys.executable or "python3")
            this_file = shlex.quote(os.path.abspath(__file__))
            payload_q = shlex.quote(payload_path)
            auth_q = shlex.quote(args.auth_path)
            log_q = shlex.quote(log_path)
            allow_default_flag = " --allow-default" if (profile_id == "openai-codex:default" and args.allow_default) else ""

            script = f"""#!/usr/bin/env bash
set -euo pipefail
{{
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] codex-auth apply start profile={profile_id}";
  {py} {this_file} apply --payload {payload_q} --auth-path {auth_q}{allow_default_flag};
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] codex-auth apply done profile={profile_id}";
}} >> {log_q} 2>&1
"""
            Path(script_path).write_text(script, encoding="utf-8")
            os.chmod(script_path, 0o700)

            launcher = "nohup"
            unit_name = f"codex-auth-apply-{safe_profile}-{int(time.time())}"
            try:
                sd = subprocess.run(
                    ["systemd-run", "--user", "--unit", unit_name, "--collect", "/bin/bash", script_path],
                    capture_output=True,
                    text=True,
                    timeout=15,
                )
                if sd.returncode == 0:
                    launcher = "systemd-run"
                else:
                    subprocess.Popen(["nohup", "bash", script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
            except Exception:
                subprocess.Popen(["nohup", "bash", script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)

            print(json.dumps({
                "ok": True,
                "profile": profile_id,
                "accountId": account_id,
                "expires": tokens["expires"],
                "queued": True,
                "launcher": launcher,
                "warning": "Strict mode: off-host stop/write/start apply script scheduled. Avoid long-running tasks.",
                "status_command": f"python3 {os.path.abspath(__file__)} status --profile {args.profile}",
                "status_path": status_path,
                "log_path": log_path,
                "script_path": script_path,
                "payload_path": payload_path,
            }, indent=2))
            return

        if not args.allow_unsafe_direct:
            print(json.dumps({
                "ok": False,
                "error": "strict_mode_requires_queue_apply",
                "hint": "This skill now enforces off-host queue apply by default. Re-run with --queue-apply (default) or explicitly pass --allow-unsafe-direct if you truly need direct write.",
            }, indent=2))
            sys.exit(2)

        # Unsafe direct path (explicitly opt-in only)
        cfg_res = ensure_profile_declared_in_config(profile_id)
        auth_backup = update_auth_profile(args.auth_path, profile_id, tokens)
        backups = {"config": cfg_res.get("backup"), "auth": auth_backup}
        revert_cmd = build_revert_command(backups)
        out = {
            "ok": True,
            "profile": profile_id,
            "accountId": account_id,
            "expires": tokens["expires"],
            "message": "Auth profile updated (unsafe direct path).",
            "restart_required": bool(cfg_res.get("changed")),
            "pre_restart_message": "Run this command to revert back if failed",
            "revert_command": revert_cmd,
        }
        if cfg_res.get("changed"):
            out["config_note"] = "Profile declaration added to openclaw.json; restart gateway to apply config profile mapping."
        print(json.dumps(out, indent=2))


if __name__ == "__main__":
    main()


path: scripts/codex_usage.py
content: #!/usr/bin/env python3
import argparse
import datetime as dt
import json
import os
import shutil
import socket
import sys
import time
import subprocess
import tempfile
import shlex
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

DEFAULT_AUTH_PATH = str(Path.home() / ".openclaw" / "agents" / "main" / "agent" / "auth-profiles.json")
DEFAULT_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage"
ALLOWED_ENDPOINT_HOSTS = {"chatgpt.com"}


def fmt_ts(ms):
    if not ms:
        return "n/a"
    try:
        return dt.datetime.fromtimestamp(ms / 1000, tz=dt.timezone.utc).isoformat()
    except Exception:
        return "n/a"


def fmt_expiry(raw):
    if not raw:
        return "n/a"
    ts = float(raw)
    if ts > 10_000_000_000:
        ts = ts / 1000.0
    now = dt.datetime.now(dt.timezone.utc)
    exp = dt.datetime.fromtimestamp(ts, tz=dt.timezone.utc)
    delta = exp - now
    sign = "-" if delta.total_seconds() < 0 else "+"
    mins = int(abs(delta.total_seconds()) // 60)
    return f"{exp.isoformat()} ({sign}{mins}m)"


def load_store(path):
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


def save_store(path, store):
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    tmp = p.with_suffix(p.suffix + ".tmp")
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(store, f, indent=2)
        f.write("\n")
    os.replace(tmp, p)


def backup_store(path):
    p = Path(path)
    if not p.exists():
        return None
    stamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    backup_path = p.with_name(f"{p.name}.bak.delete-{stamp}")
    shutil.copy2(p, backup_path)
    return str(backup_path)


def write_json_atomic(path, obj):
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile("w", delete=False, dir=str(p.parent), encoding="utf-8") as tf:
        json.dump(obj, tf, indent=2)
        tf.write("\n")
        tmp = tf.name
    os.replace(tmp, p)


def launch_offhost_script(script_path, unit_name):
    try:
        sd = subprocess.run(
            ["systemd-run", "--user", "--unit", unit_name, "--collect", "/bin/bash", script_path],
            capture_output=True,
            text=True,
            timeout=15,
        )
        if sd.returncode == 0:
            return "systemd-run", (sd.stdout or "").strip()
    except Exception:
        pass

    subprocess.Popen(["nohup", "bash", script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
    return "nohup", ""


def schedule_offhost_delete_apply(auth_path, mutated_store, targets, mode):
    os.makedirs("/tmp/openclaw", exist_ok=True)
    ts = int(time.time())
    safe = "-".join([t.replace(":", "_") for t in sorted(set(targets))])[:120] or "none"
    staged_auth_path = f"/tmp/openclaw/codex-delete-auth-staged-{safe}-{ts}.json"
    script_path = f"/tmp/openclaw/codex-delete-apply-{safe}-{ts}.sh"
    log_path = f"/tmp/openclaw/codex-delete-apply-{safe}-{ts}.log"
    status_path = f"/tmp/openclaw/codex-delete-apply-{safe}-{ts}.status.json"

    write_json_atomic(staged_auth_path, mutated_store)

    auth_q = shlex.quote(auth_path)
    staged_q = shlex.quote(staged_auth_path)
    log_q = shlex.quote(log_path)
    status_q = shlex.quote(status_path)
    mode_q = shlex.quote(mode)
    targets_json_q = shlex.quote(json.dumps(sorted(set(targets))))
    auth_py = json.dumps(auth_path)
    status_py = json.dumps(status_path)

    script = f"""#!/usr/bin/env bash
set -euo pipefail
exec >>{log_q} 2>&1

echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] codex-delete apply start mode={mode} targets={','.join(sorted(set(targets)))}"
cp {auth_q} {auth_q}.bak.offhost-delete.$(date -u +%Y%m%dT%H%M%SZ)

pkill -TERM -f '^openclaw-gateway$' || true
sleep 3
pkill -KILL -f '^openclaw-gateway$' || true
sleep 1

cp {staged_q} {auth_q}
nohup openclaw-gateway >/tmp/openclaw/openclaw-gateway-offhost-delete-start.log 2>&1 < /dev/null &
sleep 5

python3 - <<'PY'
import json, time, urllib.request
from pathlib import Path
status = {{
  'ok': False,
  'step': 'post_restart_check',
  'ts': int(time.time()*1000),
  'mode': {mode_q},
  'targets': json.loads({targets_json_q}),
}}
auth = json.loads(Path({auth_py}).read_text(encoding='utf-8'))
profiles = auth.get('profiles') or {{}}
status['present_after'] = [t for t in status['targets'] if t in profiles]
try:
  with urllib.request.urlopen('http://127.0.0.1:18789/health', timeout=4) as r:
    status['gateway_health'] = r.read().decode('utf-8', errors='replace')[:200]
except Exception as e:
  status['gateway_health'] = f'error:{{e}}'
# Success here means gateway returned and target mutation should be in place.
status['ok'] = True
Path({status_py}).write_text(json.dumps(status, indent=2) + '\n', encoding='utf-8')
print(json.dumps(status))
PY

echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] codex-delete apply end"
"""
    Path(script_path).write_text(script, encoding="utf-8")
    os.chmod(script_path, 0o700)

    launcher, launch_note = launch_offhost_script(script_path, f"codex-delete-apply-{ts}")
    return {
        "staged_auth_path": staged_auth_path,
        "script_path": script_path,
        "log_path": log_path,
        "status_path": status_path,
        "launcher": launcher,
        "launcher_note": launch_note,
    }


def list_codex_profiles(store):
    profiles = store.get("profiles", {})
    ids = []
    for pid, cred in profiles.items():
        if cred.get("provider") == "openai-codex":
            ids.append(pid)
    return sorted(ids)


def resolve_targets(store, selector):
    codex_ids = list_codex_profiles(store)
    if not codex_ids:
        return []

    selector = (selector or "all").strip()
    if selector in ("all", "both"):
        return codex_ids

    if selector == "default":
        if "openai-codex:default" in codex_ids:
            return ["openai-codex:default"]
        return [codex_ids[0]]

    if selector in codex_ids:
        return [selector]

    if not selector.startswith("openai-codex:"):
        expanded = f"openai-codex:{selector}"
        if expanded in codex_ids:
            return [expanded]

    return []


def detach_targets(store, targets):
    targets = sorted(set(targets or []))
    profiles = store.get("profiles", {})

    order = store.setdefault("order", {})
    codex_order = order.get("openai-codex", []) or []
    codex_order = [pid for pid in codex_order if pid not in set(targets)]
    order["openai-codex"] = codex_order

    last_good = store.setdefault("lastGood", {})
    if last_good.get("openai-codex") in set(targets):
        last_good["openai-codex"] = codex_order[0] if codex_order else None

    detached = [pid for pid in targets if pid in profiles]
    store["order"] = order
    store["lastGood"] = last_good

    return {
        "detached": detached,
        "remaining": list_codex_profiles(store),
        "order": codex_order,
        "lastGood": last_good.get("openai-codex"),
    }


def hard_delete_targets(store, targets):
    targets = sorted(set(targets or []))
    profiles = store.get("profiles", {})
    usage_stats = store.get("usageStats", {})

    deleted = []
    for pid in targets:
        if pid in profiles:
            profiles.pop(pid, None)
            deleted.append(pid)
        usage_stats.pop(pid, None)

    detach_result = detach_targets(store, targets)
    store["profiles"] = profiles
    store["usageStats"] = usage_stats

    return {
        "deleted": deleted,
        "remaining": detach_result["remaining"],
        "order": detach_result["order"],
        "lastGood": detach_result["lastGood"],
    }


def validate_endpoint(raw_endpoint):
    endpoint = (raw_endpoint or "").strip() or DEFAULT_ENDPOINT
    parsed = urllib.parse.urlparse(endpoint)
    if parsed.scheme.lower() != "https":
        return DEFAULT_ENDPOINT, False, "endpoint_forced_to_default_non_https"
    host = (parsed.hostname or "").lower()
    if host not in ALLOWED_ENDPOINT_HOSTS:
        return DEFAULT_ENDPOINT, False, "endpoint_forced_to_default_untrusted_host"
    return endpoint, True, None


def endpoint_reachable(endpoint, timeout_sec=6):
    """Return True when host/path is reachable over HTTPS, even if auth is rejected.

    This preflight is network reachability only; 401/403 should still be considered
    reachable so the main usage call can classify auth behavior precisely.
    """
    req = urllib.request.Request(endpoint, method="HEAD", headers={"User-Agent": "CodexBar"})
    try:
        with urllib.request.urlopen(req, timeout=timeout_sec):
            return True
    except urllib.error.HTTPError as e:
        # Host/path reachable; endpoint answered with HTTP status.
        return True
    except Exception:
        return False


def call_remote_usage(endpoint, token, account_id=None, timeout_sec=20, retries=1):
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
        "User-Agent": "CodexBar",
    }
    if account_id:
        headers["ChatGPT-Account-Id"] = str(account_id)

    last_err = None
    for attempt in range(1, retries + 2):
        start = time.time()
        req = urllib.request.Request(endpoint, headers=headers)
        try:
            with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
                body = resp.read().decode("utf-8", errors="replace")
                elapsed_ms = int((time.time() - start) * 1000)
                parse_error = False
                try:
                    payload = json.loads(body)
                except Exception:
                    payload = {}
                    parse_error = True
                return {
                    "ok": True,
                    "status": int(resp.status),
                    "payload": payload,
                    "attempt": attempt,
                    "elapsed_ms": elapsed_ms,
                    "parse_error": parse_error,
                }
        except urllib.error.HTTPError as e:
            body = e.read().decode("utf-8", errors="replace")
            elapsed_ms = int((time.time() - start) * 1000)
            try:
                payload = json.loads(body)
            except Exception:
                payload = {}
            if e.code >= 500 and attempt <= retries + 1:
                last_err = {
                    "ok": False,
                    "status": int(e.code),
                    "error": "server_error",
                    "payload": payload,
                    "attempt": attempt,
                    "elapsed_ms": elapsed_ms,
                }
                time.sleep(min(1.5 * attempt, 4))
                continue
            err_type = "http_error"
            if int(e.code) == 401:
                err_type = "auth_not_accepted_by_usage_endpoint"
            elif int(e.code) == 403:
                err_type = "forbidden_by_usage_endpoint"
            return {
                "ok": False,
                "status": int(e.code),
                "error": err_type,
                "payload": payload,
                "attempt": attempt,
                "elapsed_ms": elapsed_ms,
            }
        except socket.timeout:
            elapsed_ms = int((time.time() - start) * 1000)
            last_err = {
                "ok": False,
                "status": None,
                "error": "timeout",
                "attempt": attempt,
                "elapsed_ms": elapsed_ms,
            }
            if attempt <= retries + 1:
                time.sleep(min(1.5 * attempt, 4))
                continue
        except Exception as e:
            elapsed_ms = int((time.time() - start) * 1000)
            last_err = {
                "ok": False,
                "status": None,
                "error": "request_exception",
                "message": str(e),
                "attempt": attempt,
                "elapsed_ms": elapsed_ms,
            }
            if attempt <= retries + 1:
                time.sleep(min(1.5 * attempt, 4))
                continue

    return last_err or {"ok": False, "error": "unknown"}


def format_reset_in(seconds):
    if not isinstance(seconds, (int, float)):
        return "n/a"
    total = int(max(0, seconds))
    if total < 3600:
        mins = max(1, total // 60)
        return f"{mins} minutes"
    hours = total // 3600
    mins = (total % 3600) // 60
    if mins == 0:
        return f"{hours}h"
    return f"{hours}h {mins}m"


def format_reset_at(ts):
    if not isinstance(ts, (int, float)):
        return "n/a"
    d = dt.datetime.fromtimestamp(ts, tz=dt.timezone.utc).astimezone()
    return d.strftime("%d/%m/%Y, %H:%M")


def format_duration_dhm(seconds):
    if not isinstance(seconds, (int, float)):
        return "n/a"
    total = int(max(0, seconds))
    days = total // 86400
    hours = (total % 86400) // 3600
    minutes = (total % 3600) // 60
    return f"{days} Days, {hours} Hours, {minutes} Minutes"


def format_pct(value):
    if not isinstance(value, (int, float)):
        return "n/a"
    return f"{int(round(float(value)))}% left"


def format_bool_icon(flag):
    return "✅" if bool(flag) else "❌"


def short_profile_name(profile_id):
    if not isinstance(profile_id, str):
        return str(profile_id)
    return profile_id.split(":", 1)[1] if ":" in profile_id else profile_id


def short_reset_display(reset_at):
    if not isinstance(reset_at, str) or reset_at in {"n/a", "not reported"}:
        return "n/a"
    # Input format from format_reset_at: "dd/mm/yyyy, HH:MM"
    parts = [p.strip() for p in reset_at.split(",", 1)]
    if len(parts) == 2:
        return f"{parts[0]} {parts[1]}"
    return reset_at


def parse_codex_usage(payload):
    rl = (payload or {}).get("rate_limit") or {}
    pw = rl.get("primary_window") or {}
    sw = rl.get("secondary_window") or {}

    def infer_window_label(secs, fallback_label):
        if not isinstance(secs, (int, float)) or secs <= 0:
            return fallback_label
        secs_i = int(secs)
        if secs_i % 86400 == 0:
            days = secs_i // 86400
            if days == 7:
                return "Week"
            return f"{days}d"
        if secs_i % 3600 == 0:
            return f"{secs_i // 3600}h"
        if secs_i >= 86400:
            return f"{round(secs_i / 86400)}d"
        return f"{round(secs_i / 3600)}h"

    def win(obj, fallback_label):
        if not obj:
            return None
        secs = obj.get("limit_window_seconds")
        reset = obj.get("reset_at")
        reset_after = obj.get("reset_after_seconds")
        label = infer_window_label(secs, fallback_label)
        used = obj.get("used_percent")
        remaining = None
        if isinstance(used, (int, float)):
            remaining = max(0, 100 - float(used))
        return {
            "label": label,
            "used_percent": used,
            "remaining_percent": remaining,
            "reset_in": format_reset_in(reset_after),
            "reset_at": format_reset_at(reset),
            "reset_after_seconds": reset_after,
            "limit_window_seconds": secs,
        }

    windows = []
    p = win(pw, "5h")
    s = win(sw, "Week")
    if p:
        windows.append(p)
    if s:
        windows.append(s)

    plan = payload.get("plan_type") if isinstance(payload, dict) else None
    credits = None
    try:
        bal = payload.get("credits", {}).get("balance")
        if bal is not None:
            credits = float(bal)
    except Exception:
        credits = None

    return {
        "plan": plan,
        "credits_balance": credits,
        "allowed": rl.get("allowed"),
        "limit_reached": rl.get("limit_reached"),
        "windows": windows,
    }


def build_template_line(profile_report):
    # Friendly auth-failure rendering (401 from usage endpoint)
    if int(profile_report.get("remote_status") or 0) == 401:
        pid = profile_report.get("profile") or "unknown"
        suffix = pid.split(":", 1)[1] if ":" in pid else pid
        return (
            f"🔴 `{short_profile_name(pid)}` — auth invalid (401) | run `/codex_auth {suffix}`"
        )

    remote = profile_report.get("remote_usage") or {}
    windows = remote.get("windows") or []

    def to_row(w):
        return {
            "remaining": format_pct(w.get("remaining_percent")),
            "remaining_raw": w.get("remaining_percent"),
            "reset_at": w.get("reset_at") or "n/a",
            "time_left": format_duration_dhm(w.get("reset_after_seconds")),
            "secs": w.get("limit_window_seconds"),
            "label": str(w.get("label") or "Window"),
        }

    def derive_usage_state(remote_usage, win_rows, fallback_ok):
        # Prefer explicit WHAM booleans, but guard with window evidence to avoid stale-looking output.
        allowed = remote_usage.get("allowed")
        limit_reached = remote_usage.get("limit_reached")

        max_window = None
        for w in win_rows:
            secs = w.get("secs")
            if isinstance(secs, (int, float)):
                if max_window is None or int(secs) > int(max_window.get("secs") or 0):
                    max_window = w

        max_window_exhausted = False
        if max_window and isinstance(max_window.get("remaining_raw"), (int, float)):
            max_window_exhausted = float(max_window.get("remaining_raw")) <= 0.0

        if isinstance(limit_reached, bool):
            limited = limit_reached or max_window_exhausted
        elif max_window is not None:
            limited = max_window_exhausted
        else:
            limited = False

        if isinstance(allowed, bool):
            usable = allowed and not limited
        else:
            usable = bool(fallback_ok) and not limited

        return usable, limited

    short = {"label": "5h", "remaining": "not reported", "reset_at": "not reported", "time_left": "not reported", "secs": None, "remaining_raw": None}
    longw = {"label": "Week", "remaining": "not reported", "reset_at": "not reported", "time_left": "not reported", "secs": None, "remaining_raw": None}

    rows = []
    for w in windows:
        row = to_row(w)
        rows.append(row)
        secs = row.get("secs")
        # Prefer nearest short window around 5h; otherwise any <= 24h.
        if isinstance(secs, (int, float)) and secs <= 86400:
            if short.get("secs") is None or abs(int(secs) - 18000) < abs(int(short.get("secs") or 18000) - 18000):
                short = row
        # Long window = largest window > 24h.
        if isinstance(secs, (int, float)) and secs > 86400:
            if longw.get("secs") is None or int(secs) > int(longw.get("secs") or 0):
                longw = row

    usable, limited = derive_usage_state(remote, rows, profile_report.get("remote_usage_ok"))

    if limited:
        status = "🟠"
    else:
        status = "🟢" if usable else "🔴"

    pid = short_profile_name(profile_report.get('profile'))
    short_label = (short.get('label') or '5h').lower()
    long_label = (longw.get('label') or 'week').lower()
    short_reset_tag = 'r5' if short_label.startswith('5') else f"r{short_label}"
    long_reset_tag = 'rw' if long_label.startswith('week') else f"r{long_label}"

    return (
        f"{status} `{pid}` — {short_label} **{short['remaining']}** | {long_label} **{longw['remaining']}** | "
        f"{short_reset_tag} **{short_reset_display(short['reset_at'])}** | {long_reset_tag} **{short_reset_display(longw['reset_at'])}**"
    )


def render_text_output(out):
    # Strict default text format for slash usage:
    # - one compact line per profile
    # - no preface/progress text
    # - no summary footer
    lines = []
    for line in out.get("formatted_profiles") or []:
        lines.append(line)

    # Keep actionable auth hints if present.
    msg = out.get("suggested_user_message")
    if msg and "Auth invalid" in msg:
        lines.append(msg)

    return "\n".join(lines).strip() + "\n"


def scrub_sensitive(obj):
    if isinstance(obj, dict):
        out = {}
        for k, v in obj.items():
            lk = str(k).lower()
            if lk in {"access", "refresh", "token", "authorization", "api_key", "apikey"}:
                out[k] = "[redacted]"
            else:
                out[k] = scrub_sensitive(v)
        return out
    if isinstance(obj, list):
        return [scrub_sensitive(v) for v in obj]
    return obj


def report_profile(store, profile_id, include_remote=False, endpoint=None, timeout_sec=20, retries=1, debug=False):
    profiles = store.get("profiles", {})
    usage_stats = store.get("usageStats", {})

    p = profiles.get(profile_id)
    if not p:
        return {"profile": profile_id, "status": "missing"}

    u = usage_stats.get(profile_id, {})

    result = {
        "profile": profile_id,
        "provider": p.get("provider"),
        "type": p.get("type"),
        "token_expiry": fmt_expiry(p.get("expires")),
        "last_used": fmt_ts(u.get("lastUsed")),
        "last_failure": fmt_ts(u.get("lastFailureAt")),
        "cooldown_until": fmt_ts(u.get("cooldownUntil")),
        "error_count": u.get("errorCount", 0),
        "failure_counts": u.get("failureCounts", {}),
    }

    if include_remote and endpoint:
        [REDACTED_SECRET]"access")
        account_id = p.get("accountId")
        if [REDACTED_SECRET] = call_remote_usage(endpoint, token, account_id=account_id, timeout_sec=timeout_sec, retries=retries)
            result["remote_usage_ok"] = bool(remote.get("ok"))
            if remote.get("ok"):
                result["remote_usage"] = parse_codex_usage(remote.get("payload") or {})
            else:
                result["remote_error"] = remote.get("error") or "request_failed"
                if remote.get("status") is not None:
                    result["remote_status"] = remote.get("status")
                if int(remote.get("status") or 0) == 401:
                    result["remote_hint"] = (
                        "usage_endpoint_rejected_token_format_or_session; "
                        "this endpoint may require a different auth/session type than stored OAuth tokens"
                    )
            if debug:
                result["remote_debug"] = {
                    "attempt": remote.get("attempt"),
                    "elapsed_ms": remote.get("elapsed_ms"),
                    "status": remote.get("status"),
                    "endpoint": endpoint,
                }
        else:
            result["remote_usage_ok"] = False
            result["remote_error"] = "missing_access_token"

    return result


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--profile", default="all", help="all|default|<profile-id>|<suffix>")
    ap.add_argument("--auth-path", default=DEFAULT_AUTH_PATH)
    ap.add_argument("--timeout-sec", type=int, default=20)
    ap.add_argument("--retries", type=int, default=1, help="number of retries after first attempt")
    ap.add_argument("--debug", action="store_true")
    ap.add_argument(
        "--delete-profile",
        default=None,
        help="Delete codex profile(s): all|default|<profile-id>|<suffix>",
    )
    ap.add_argument(
        "--confirm-delete",
        action="store_true",
        help="Required safeguard to perform mutation (without this flag, script only previews).",
    )
    ap.add_argument(
        "--hard-delete",
        action="store_true",
        help="Permanently delete profile+usage entries. Default is safer detach-only (keeps token/profile entry).",
    )
    ap.add_argument("--dry-run", action="store_true", help="Show delete result without writing auth file")
    ap.add_argument("--ack-gateway-restart", action="store_true", help="Required confirmation: mutation will stop gateway, write auth store, and restart gateway")
    ap.add_argument("--allow-unsafe-direct", action="store_true", help="Allow direct in-process auth write for delete mutation (not recommended)")
    ap.add_argument("--format", choices=["json", "text"], default="json", help="Output format for usage checks")
    args = ap.parse_args()

    try:
        store = load_store(args.auth_path)
    except Exception as e:
        print(json.dumps({"ok": False, "error": f"failed_to_read_auth_profiles: {e}"}))
        sys.exit(1)

    if args.delete_profile:
        delete_targets_list = resolve_targets(store, args.delete_profile)
        if not delete_targets_list:
            print(
                json.dumps(
                    {
                        "ok": False,
                        "error": "no_matching_codex_profiles_for_delete",
                        "selector": args.delete_profile,
                        "available_profiles": list_codex_profiles(store),
                        "hint": "Use --delete-profile all/default/<profile-id>/<suffix>",
                    },
                    indent=2,
                )
            )
            sys.exit(3)

        mode = "hard_delete" if args.hard_delete else "detach_only"

        # Verification safeguard: require explicit confirmation flag before mutating auth store.
        if not args.confirm_delete:
            print(
                json.dumps(
                    {
                        "ok": False,
                        "error": "delete_confirmation_required",
                        "action": "delete_profiles_preview",
                        "mode": mode,
                        "selector": args.delete_profile,
                        "would_mutate": delete_targets_list,
                        "auth_path": args.auth_path,
                        "safety_note": "Default mode is detach-only (removes from order/lastGood, keeps token/profile).",
                        "backup_note": "A timestamped backup will be created automatically before mutation.",
                        "how_to_confirm": "Re-run with --confirm-delete --ack-gateway-restart (optionally add --dry-run first; use --hard-delete for permanent removal).",
                    },
                    indent=2,
                )
            )
            sys.exit(4)

        if args.hard_delete:
            mutate_result = hard_delete_targets(store, delete_targets_list)
            action = "hard_delete_profiles"
        else:
            mutate_result = detach_targets(store, delete_targets_list)
            action = "detach_profiles"

        if args.dry_run:
            print(
                json.dumps(
                    {
                        "ok": True,
                        "action": action,
                        "mode": mode,
                        "dry_run": True,
                        "auth_path": args.auth_path,
                        **mutate_result,
                    },
                    indent=2,
                )
            )
            return

        if not args.ack_gateway_restart:
            print(
                json.dumps(
                    {
                        "ok": False,
                        "error": "gateway_restart_ack_required",
                        "action": action,
                        "mode": mode,
                        "targets": delete_targets_list,
                        "warning": "This operation will stop the gateway, write auth profile changes, then restart the gateway.",
                        "how_to_confirm": "Re-run with --confirm-delete --ack-gateway-restart (optionally add --hard-delete).",
                    },
                    indent=2,
                )
            )
            sys.exit(5)

        if args.allow_unsafe_direct:
            backup_path = backup_store(args.auth_path)
            save_store(args.auth_path, store)
            print(
                json.dumps(
                    {
                        "ok": True,
                        "action": action,
                        "mode": mode,
                        "dry_run": False,
                        "unsafe_direct": True,
                        "auth_path": args.auth_path,
                        "backup_path": backup_path,
                        **mutate_result,
                    },
                    indent=2,
                )
            )
            return

        queue_meta = schedule_offhost_delete_apply(args.auth_path, store, delete_targets_list, mode)
        print(
            json.dumps(
                {
                    "ok": True,
                    "action": action,
                    "mode": mode,
                    "dry_run": False,
                    "queued": True,
                    "warning": "Gateway stop/write/start apply has been queued off-host.",
                    "auth_path": args.auth_path,
                    **mutate_result,
                    **queue_meta,
                },
                indent=2,
            )
        )
        return

    targets = resolve_targets(store, args.profile)
    if not targets:
        print(json.dumps({
            "ok": False,
            "error": "no_matching_codex_profiles",
            "available_profiles": list_codex_profiles(store),
            "hint": "Use --profile all/default/<profile-id>/<suffix>",
        }, indent=2))
        sys.exit(2)

    raw_endpoint = os.getenv("CODEX_USAGE_ENDPOINT", DEFAULT_ENDPOINT).strip()
    endpoint, endpoint_ok, endpoint_reason = validate_endpoint(raw_endpoint)
    include_remote = endpoint_ok and endpoint_reachable(endpoint)

    reports = []
    for pid in targets:
        reports.append(
            report_profile(
                store,
                pid,
                include_remote=include_remote,
                endpoint=endpoint,
                timeout_sec=max(5, args.timeout_sec),
                retries=max(0, args.retries),
                debug=args.debug,
            )
        )

    ok_remote = sum(1 for p in reports if p.get("remote_usage_ok") is True)
    failed_remote = sum(1 for p in reports if p.get("remote_usage_ok") is False)

    out = {
        "ok": True,
        "source": "auth-profiles",
        "remote_endpoint": endpoint,
        "remote_endpoint_enabled": include_remote,
        "profiles": reports,
        "progress_message": "Running Codex usage checks now…",
        "response_template": "🟢 `%profile%` — 5h **%5h_left%** | wk **%week_left%** | r5 **%5h_reset%** | rw **%week_reset%**",
    }
    if endpoint_reason:
        out["remote_endpoint_note"] = endpoint_reason
    if endpoint_ok and not include_remote:
        out["remote_endpoint_note"] = "endpoint_unreachable_local_health_only"

    auth_rejects = [p.get("profile") for p in reports if int(p.get("remote_status") or 0) == 401]
    if auth_rejects:
        out["remote_auth_note"] = "usage_endpoint_rejected_available_oauth_tokens_401"
        out["remote_auth_affected_profiles"] = auth_rejects

    out["summary"] = {
        "profiles_checked": len(reports),
        "remote_ok_count": ok_remote,
        "remote_failed_count": failed_remote,
    }
    out["formatted_profiles"] = [build_template_line(r) for r in reports]

    if auth_rejects:
        cmds = ", ".join([f"/codex_auth {p.split(':',1)[1] if ':' in p else p}" for p in auth_rejects])
        out["suggested_user_message"] = (
            "Auth invalid for profile(s): " + ", ".join(auth_rejects) + ". "
            "OAuth token was rejected by OpenAI usage endpoint (401). "
            "Refresh with: " + cmds
        )
    elif failed_remote > 0 and ok_remote == 0:
        out["suggested_user_message"] = (
            "Remote usage checks are currently unavailable; showing local profile health only."
        )
    else:
        out["suggested_user_message"] = "Codex usage check completed."

    if args.format == "text":
        print(render_text_output(scrub_sensitive(out)), end="")
    else:
        print(json.dumps(scrub_sensitive(out), indent=2))


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)