Skip to content

feat(config): add user-scope .env file support#112

Merged
donk8r merged 5 commits into
masterfrom
feature/login
Jul 19, 2026
Merged

feat(config): add user-scope .env file support#112
donk8r merged 5 commits into
masterfrom
feature/login

Conversation

@donk8r

@donk8r donk8r commented Jul 19, 2026

Copy link
Copy Markdown
Member
  • Load .env from shared config directory before project-local .env
  • Establish precedence: system < user-scope < project-local
  • Document new loading order in environment and provider guides
  • Remove Groq provider from documentation table

- Load .env from shared config directory before project-local .env
- Establish precedence: system < user-scope < project-local
- Document new loading order in environment and provider guides
- Remove Groq provider from documentation table
@github-actions

Copy link
Copy Markdown

Octomind — developer:brief (octohub:glm)

📦 Brief: Add user-scope .env loading from shared config directory

Overall risk: 🟡 MEDIUM · Cards: 1

# Change Risk Confidence
1 .env loading now reads a user-scope file from <config_dir>/.env before the project-local .env, establishing a three-tier precedence 🟡 ●●●

Card 1/1: User-scope `.env` file loaded before project-local `.env` · 🟡 · ●●●

INTENT
Allow users to set API keys once in a shared config-directory .env that applies across all projects, with project-local .env still winning on conflicts. Commit message: "feat(config): add user-scope .env file support."

WHAT CHANGED

  • load_dotenv_override() now loads <config_dir>/.env (user-scope) before ./.env (project-local), both overriding system environment variables. Later loads win, preserving the precedence: system < user-scope < project-local.
  • get_config_dir() is called to resolve the shared config directory; the .env file is only loaded if it exists on disk.
  • dotenv_loaded flag is set to true if either file loads successfully — source tracking (get_source) remains binary (System vs DotEnv) and cannot distinguish which .env file a variable came from.
  • Documentation updated in two guides to describe the new three-tier load order; Groq provider removed from the provider compatibility table.

IMPACT RADIUS

  • Called from main.rs at startup (line 89–92) — the single entry point for all CLI, ACP, and WebSocket modes. Every launch path now attempts the user-scope load.
  • get_source() in env_source.rs is used by config --show and API-key source detection. It still works correctly — both .env files are categorized as DotEnv, and the pre-dotenv snapshot comparison logic is unchanged.

RISK
🟡 MEDIUM. A parse error in the user-scope .env aborts loading before the project-local .env is attempted. The ? operator on dotenvy::from_filename_override(&user_env) propagates the error immediately, skipping the project-local load entirely. In main.rs the error is caught and printed as a warning, but the project-local .env — which may contain the keys the user actually needs — is silently never loaded. A malformed shared .env would thus break all project-local key overrides with only a warning.

// User-scope error here → project-local .env never reached
dotenvy::from_filename_override(&user_env)?;

QUESTIONS

  • Should a user-scope .env parse failure fall through to the project-local .env (e.g., log + continue) rather than aborting the entire load chain? The current behavior means a bad shared file silently disables project-local overrides.

DIVERGENCE
🧩 Incomplete changeget_source() returns EnvSource::DotEnv for variables from either .env file, but the new documentation describes two distinct sources. The source tracker cannot tell the user which .env a key came from, which may cause confusion when debugging precedence conflicts.

📎 Source

📂 Files changed (3 files, ~34 lines)

{tokens:201207,cost:0.28912679999999996}

- Implement RFC 8628 device flow for CLI authentication
- Add polling mechanism to retrieve API keys after browser confirmation
- Implement secure storage of API keys in user config .env file
- Add support for custom API and panel URLs via environment variables
- Implement login command for Octomind Cloud
- Add URL repointing logic for local callback handling
- Add environment variable upsert utility for API keys
- Integrate login command into main CLI dispatcher
@github-actions

Copy link
Copy Markdown

Octomind — developer:brief (octohub:glm)

📦 Brief: New octomind login command — device-authorization flow (RFC 8628) that polls the API and stores the minted key in the user-scope .env

Overall risk: 🟢 LOW · Cards: 1

# Change Risk Confidence
1 CLI login subcommand: starts device-auth, polls for token, upserts OCTOHUB_API_KEY into <config_dir>/.env with 0o600 perms 🟢 ●●●
Card 1/1: Device-authorization login flow stores API key in user-scope .env · 🟢 · ●●●

INTENT
Enable octomind login as a first-class CLI subcommand so users can authenticate against Octomind Cloud without manually copying API keys — the same device-code pattern gh auth login uses (RFC 8628).

WHAT CHANGED

  • New Login subcommand registered in the CLI (Commands::Login) and dispatched in run_with_cleanup; takes --no-browser flag for headless/SSH environments.
  • execute() calls POST /api/v1/auth/cli to start a device flow, displays a user code + verification URL, opens the browser (or prints the URL), then polls POST /api/v1/auth/cli/token at the server-supplied interval until the key arrives, the code expires, or a 15-minute deadline passes.
  • On success, the API key is upserted into <config_dir>/.env as OCTOHUB_API_KEY, replacing any prior value (including export-prefixed lines) while preserving all other variables. File permissions are set to 0o600 on Unix.
  • The key is also set in the current process's environment via std::env::set_var so subsequent commands in the same shell invocation work immediately.
  • API responses use a (Option<String>, Option<Value>) envelope — the first element is an error code string, the second is the data payload. Non-2xx responses carry the same envelope, so HTTP status is never inspected directly.
  • OCTOMIND_API_URL and OCTOMIND_PANEL_URL env vars allow overriding the API and browser-confirmation endpoints for local development.

IMPACT RADIUS

  • The stored key is consumed by the existing startup path: load_dotenv_override() in src/config/env_source.rs loads <config_dir>/.env first, then cwd .env overrides it. Every octomind process already reads this file, so no consumer changes were needed.
  • The block_open/block_row/block_close_ok/key_width display functions are reused from src/session/chat/tool_display.rs — same rendering path as /help and other CLI commands.

RISK
🟢 LOW. The std::fs::read_to_string(&path).unwrap_or_default() in write_key silently returns an empty string on any read error — including permission-denied on an existing file — which would then be overwritten by the write. This conflicts with the project's own "no unwrap_or_else(|_| default) that swallows real errors" guideline. In practice the risk is minimal: if the user can't read the file they almost certainly can't write it either, so the write would fail with a visible error. The polling loop correctly checks the deadline before each sleep, and the interval is clamped to 1–30s.

DIVERGENCE
🧩 Incomplete changeOCTOHUB_API_KEY is written and set in the environment, but no code in the codebase reads it yet (grep finds only the login module itself). The key presumably flows through octolib or the provider layer at runtime via environment-variable consumption, but this can't be verified within this repo. The login command is functional as a credential-acquisition step regardless.

📎 Source

📂 Files changed (3 files, ~263 lines)

{tokens:148641,cost:0.2216574}

donk8r added 3 commits July 19, 2026 17:00
- Add account client for control-plane API authentication
- Implement session persistence and automatic JWT refresh logic
- Add /usage command to CLI and chat to track spend and quotas
- Integrate visual progress bars for budget, storage, and network usage
- Add --force flag to login for re-authentication
- Display account identity, plan, and sign-in state in config and login
- Centralize session management and environment variable handling
- Add error handling for unsigned-in states and API failures
- Add machine_id to persist a unique identifier per device
- Send device_id during login to prevent cross-machine key collisions
- Display server-assigned key name in login output
- Clean up formatting in agent command list
@github-actions

Copy link
Copy Markdown

Octomind — developer:brief (octohub:glm)

I have all the context I need. Let me compile the brief.

📦 Brief: Account authentication, session persistence, and usage tracking added to the CLI

Overall risk: 🟡 MEDIUM · Cards: 3

# Change Risk Confidence
1 New account client with JWT session persistence, auto-refresh, and device-scoped login keys 🟡 ●●●
2 /usage session command showing spend, quotas, and resource bars 🟢 ●●●
3 Config display now shows Octomind Cloud sign-in state alongside provider keys 🟢 ●●●

Card 1/3: Account client — panel session persistence, JWT auto-refresh, and machine-specific device IDs · 🟡 MEDIUM · ●●●

INTENT
Establish a control-plane API client so the CLI can authenticate as a signed-in Octomind account (not just use a hub key), with automatic JWT refresh that keeps a logged-in CLI logged in. The second commit adds a per-machine device ID so signing in on one machine doesn't invalidate keys on others.

WHAT CHANGED

  • New src/account.rs module: session struct (jwt + refresh_token + api_url) persisted to auth.json at 0600; session() reads it and rejects sessions minted for a different API host.
  • get() does an authenticated GET, and on unauthorized/token_expired it refreshes the JWT once (trading the refresh token for a new JWT), persists it, and retries — exactly mirroring the browser panel's behavior.
  • whoami() and usage() are thin wrappers over get() returning Option<T> so "not signed in" is a normal state, not an error.
  • machine_id() generates a 12-char UUID prefix, persists it to machine-id next to the config (not in auth.json — it must survive sign-out), and sends it as device_id during login so the server scopes the minted key to this machine.
  • login.rs refactored: constants moved to account.rs, --force flag added to bypass the "already signed in" check, login now persists both the hub key (.env) and a panel session (auth.json), and displays the server-assigned key_name.

IMPACT RADIUS

  • login.rs is the sole consumer of post_public, save_session, machine_id, and Session construction. The login flow now makes an extra whoami() call before starting (to check if already signed in) and after completing (to display account email).
  • config.rs calls account::session() (synchronous file read, no network) to show sign-in state — see Card 3.
  • usage.rs calls account::usage() and account::whoami() — see Card 2.
  • clear_session() is defined but has no caller anywhere in the codebase — no octomind logout command exists.

RISK
🟡 MEDIUM. clear_session() is dead code — the account module can create and persist sessions but provides no way to remove them from the CLI. A user who wants to sign out must manually delete auth.json. This looks like half of a feature (logout command not yet wired).

The whoami() pre-check in login.rs (line 85) propagates network errors via ?. If the user has a stale session and no network, whoami() attempts a refresh (network call), which fails and aborts login before the device flow even starts. The user would need to manually delete auth.json or use --force (but --force only skips the whoami check, not the session file). This is a narrow edge case but could confuse a user with a stale session on an offline machine.

DIVERGENCE
🧩 Incomplete changeclear_session() at src/account.rs:116 is exported but never called. No logout subcommand or session command invokes it. The session can be created and refreshed but not removed through the CLI.

📎 Source

Card 2/3: /usage command — account spend and quota display with visual bars · 🟢 LOW · ●●●

INTENT
Give users a way to see their account-level spend, budget caps, and resource usage (storage, network, balance) from within a chat session, so they can tell if they're about to hit a limit without leaving the CLI.

WHAT CHANGED

  • New /usage session command registered in the command dispatch, help listing, autocomplete array, and ACP available commands.
  • handle_usage() calls account::usage() (returns Option<Usage>); None renders a "not signed in" block, errors render a CommandOutput::Error with a login hint.
  • When signed in, it makes a best-effort whoami() call (errors swallowed via .ok().flatten()) to show the account email — the numbers are the point, the email is a nicety.
  • display_usage() renders three spend windows (4h, week, month) as colored progress bars with threshold-based coloring (green < 70%, yellow 70–90%, red ≥ 90%), plus storage/network bars and a balance row. The summary line shows the tightest window's percentage of cap.

IMPACT RADIUS

  • New CommandOutput::Usage variant added to the enum in commands/mod.rs with display_usage dispatched in the render() match.
  • UsageWindow struct is pub-exported from commands/mod.rs via pub use usage::UsageWindow — needed by the CommandOutput::Usage variant.
  • No existing commands or display functions are modified.

RISK
🟢 LOW. Isolated addition — new command, new output variant, new display function. No existing behavior altered. The two sequential API calls (usage + whoami) are both Option-returning with graceful degradation.

📎 Source

Card 3/3: Config display shows Octomind Cloud sign-in state · 🟢 LOW · ●●●

INTENT
Show whether the user is signed in to Octomind Cloud in the /config output, alongside the existing provider API key rows, so the user can see at a glance whether account commands will work.

WHAT CHANGED

  • New render_octomind_cloud_row() added to both execute() and show_configuration() in config.rs, placed after the Cloudflare row in the API keys section.
  • The row distinguishes three states: signed in (✓ "signed in"), hub key set but no panel session (✓ "key set, not signed in"), and neither (✗ "not set (octomind login)").
  • Unlike other rows that check env vars, this one calls account::session() (a synchronous file read of auth.json) — no network call.

IMPACT RADIUS

  • Called from two places in config.rs — the interactive config edit flow and the read-only show_configuration.
  • account::session() is a pure file read with no side effects; safe to call synchronously in the config display path.

RISK
🟢 LOW. Display-only change. session() reads a file and returns Option; no network, no state mutation. The three-state logic correctly handles the case where a user has a hub key (models work) but no panel session (account commands won't).

📎 Source

📂 Files changed (11 files, ~573 lines)

{tokens:263591,cost:0.3902644}

@github-actions

Copy link
Copy Markdown

Octomind — developer:brief (octohub:glm)

📦 Brief: Remove unused clear_session function from account module

Overall risk: 🟢 LOW · Cards: 1

# Change Risk Confidence
1 Dead-code removal of account::clear_session() — no callers anywhere in the crate 🟢 ●●●
Card 1/1: Dead-code removal of unused session-clearing function · 🟢 · ●●●

INTENT
Remove clear_session() from src/account.rs — it was defined but never called anywhere in the codebase.

WHAT CHANGED

  • account::clear_session() deleted. It deleted the session file from disk if it existed.
  • No callers exist: grep across all .rs files finds zero references to account::clear_session. The clear_session symbols in session/context.rs and session/dedup.rs are unrelated functions in different modules.

IMPACT RADIUS
Isolated change — no downstream impact detected. The function was private to the account module's public surface but had no consumers.

RISK
🟢 LOW. No significant risk. Pure dead-code removal with no callers to break.

📎 Source

📂 Files changed (1 file, ~8 lines)
  • src/account.rs — account/session persistence module; unused clear_session removed

{tokens:83393,cost:0.1201402}

@donk8r
donk8r merged commit 81cc5a1 into master Jul 19, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant