Skip to content

Persist gateway state and default API keys in SQLite - #38

Open
maiphucgiang wants to merge 4 commits into
mainfrom
feat/sqlite-state-default-key
Open

maiphucgiang wants to merge 4 commits into
mainfrom
feat/sqlite-state-default-key

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Closes #37.

Changes

  • Support uv run converter.py and python3 converter.py without a required .env. Optionally load the current directory's .env, with explicit CLI > process environment > .env > saved SQLite values; show locked configuration sources in the WebUI.
  • Generate a cb- default API key only when needed, save it privately in SQLite, and display it once through the controlling terminal (/dev/tty or Windows CONOUT$) after the listener starts, never through stdout/stderr. Restarts reuse it without printing it again; external overrides do not replace the saved default. Key material stays out of persistent logs and public settings snapshots.
  • Consolidate sessions, cooldowns, model blocks, credit/trial history, model catalogs and usage snapshots into control.sqlite3. Import legacy JSON once in a validated transaction, including markers for absent files, and preserve account ownership, session revocation, trial reservations/backoff and in-memory routing caches.
  • Keep upstream credentials in .info files and audit data in the separate logs.sqlite3. Retire --log / CODEBUDDY2API_LOG text output with an explicit warning, add generated-key redaction, and synchronize concise English/Chinese documentation.
  • Declare the already locked python-dotenv package as a direct dependency.
  • Stop startup on authoritative SQLite read failures without deleting sessions; retain retryable epoch state. Publish CodeQL analyses against the checked-out PR head SHA.

Verification

  • All 60 backend regression scripts pass in an offline, read-only-source sandbox (2 existing conditional skips). Coverage includes 33 SQLite/startup tests, 4 dedicated terminal tests and a full-process no-TTY startup smoke test using the Dockerfile command: real controlling PTYs confirm redirected stdout/stderr never receive the key; session and cache read failures preserve stored state and fail closed.
  • WebUI: 133 unit tests, 14 mocked browser cases and 2 real-management-API browser flows pass; formatting, lint, types and production build pass. Targeted Python static checks and staged diff checks pass.
  • The original implementation passed seven isolated native scenarios exercising real python3/uv launches with PTYs and loopback HTTP: first-key disclosure, restart/session survival, dotenv and CLI precedence, restored defaults without revived sessions, log secrecy, headless rejection and occupied-port recovery.
  • Before the review follow-up, cold-backed-up and upgraded the existing local tmux instance, then restarted it twice. All 8 credential files and original settings were retained; the verification session and CSRF token survived both restarts, authentication checks passed, and legacy JSON stayed unchanged. The verification session was logged out afterward.
  • Native/runtime evidence is from Linux/WSL; Windows deployments should use a private user directory with appropriate ACLs.

Scope and rollback

  • The default-key announcement marker commits only after terminal write/flush succeeds. Failed writes, failed commits and interrupted transactions leave the same key pending; a crash after display but before commit may repeat the notice on recovery.
  • Control storage upgrades to schema 2. Stop the gateway and back up the whole data directory before upgrading; migration failures stop startup rather than discard critical state. Legacy JSON remains backup-only after import.
  • Older readers cannot open the upgraded control database. A downgrade needs matching state migration: stale JSON or an outdated backup must not overwrite newer claims, dispatches or session revocations.
  • Existing /v1/* and /admin/* addresses and explicit-empty-key compatibility are preserved. CODEBUDDY2API_ALLOW_OPEN_NOAUTH=true also permits unconfigured headless inference (as in the shipped image); management stays locked, and an existing key remains authoritative. First-time headless/non-loopback deployments should configure a key explicitly; protect the control database and its backups because they contain the saved default key and management sessions.

Summary by Sourcery

Persist gateway state and securely manage default API keys through SQLite-backed startup, migration, logging, and configuration handling.

New Features:

  • Generate, securely persist, and one-time announce a default API key for interactive local startup while supporting optional .env loading and clear configuration precedence.
  • Persist gateway runtime state, sessions, caches, credit and trial history, catalogs, and usage snapshots in SQLite with validated one-time migration from legacy JSON files.
  • Expose configuration sources such as .env and generated defaults in the WebUI.

Bug Fixes:

  • Prevent startup from silently discarding authoritative SQLite state when reads or migrations fail, including preserving sessions and retryable key-announcement state.
  • Prevent generated API keys and sensitive data from appearing in redirected terminal streams, persistent logs, or public settings snapshots.
  • Ensure legacy state imports reject invalid, unsafe, or credential-bearing data and cannot be repeated to resurrect obsolete state.

Enhancements:

  • Replace free-form text logging with concise SQLite audit events and warn when retired text-log options are used.
  • Keep upstream credentials in .info files while consolidating operational state and strengthening SQLite transaction and failure handling.

Build:

  • Declare python-dotenv as a direct dependency and update locked dependency metadata.

CI:

  • Run CodeQL analysis against the checked-out pull-request head commit.

Documentation:

  • Update English and Chinese deployment, configuration, WebUI, upgrade, rollback, and data-protection guidance for SQLite-backed state and generated keys.

Tests:

  • Add comprehensive SQLite migration, persistence, failure-handling, key-disclosure, terminal, no-auth, logging, and startup precedence coverage.

Load optional native .env settings and retain a generated default key across restarts, with one-time terminal disclosure.

Migrate runtime snapshots into the control database, keep audit logs separate, and retire text log output.
@sourcery-ai

sourcery-ai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR upgrades gateway runtime persistence to a validated schema-2 SQLite control database, adds secure optional dotenv/default-key startup resolution with one-time disclosure, retires text logging in favor of redacted SQLite auditing, and updates tests, WebUI source indicators, dependencies, and bilingual operational documentation.

Sequence diagram for startup key resolution and one-time disclosure

sequenceDiagram
    participant CLI as CLI
    participant Startup as startup.py
    participant Control as control.sqlite3
    participant Server as Uvicorn
    actor Operator

    CLI->>Startup: load_startup_env()
    Startup->>Startup: resolve_startup_key()
    alt explicit CLI, process environment, or .env key
        Startup-->>CLI: use configured key
    else no configured key
        Startup->>Control: default_key(create=True)
        Control-->>Startup: saved cb- key, pending announcement
        Startup-->>CLI: configure generated key
    end
    CLI->>Server: run_server()
    Server-->>Startup: listener started
    opt generated key pending
        Startup->>Control: claim_announcement(key)
        Control-->>Startup: claim succeeds once
        Startup-->>Operator: display key on interactive terminal
    end
Loading

Flow diagram for validated legacy state migration

flowchart TD
    Start[Gateway startup] --> Open[Open control.sqlite3]
    Open --> Migrate[StateStore.migrate]
    Migrate --> Check{Import marker exists?}
    Check -- yes --> Next[Skip legacy source]
    Check -- no --> Read[Read bounded legacy JSON]
    Read --> Validate[Validate schema and reject credentials]
    Validate -->|valid or absent| Transaction[Write state and import marker]
    Validate -->|invalid or failure| Rollback[Rollback transaction and stop startup]
    Transaction --> Next
    Next --> Complete[Start runtime services]
Loading

File-Level Changes

Change Details Files
Centralize runtime state and gateway secrets in a schema-2 SQLite control store with validated, one-time legacy migration.
  • Add SQLite namespaces for sessions, cooldowns, model blocks, credits, trials, catalogs, usage, import markers, and generated-key metadata.
  • Migrate legacy JSON atomically, validate schemas and secret exclusion, record absent files, and keep legacy files unchanged as backups.
  • Route runtime ledgers, caches, session persistence, reservations, and revocation through shared transactional state storage.
app/control_store.py
app/state_store.py
app/admin_auth.py
app/credential_cooldowns.py
app/credits.py
app/model_blocks.py
app/trial_rewards.py
app/usage_snapshots.py
app/runtime_management.py
tests/test_control_store.py
tests/test_sqlite_state.py
tests/test_identity_sync.py
tests/test_runtime_endpoints.py
tests/webui_fixture.py
Implement startup configuration resolution, persisted default API-key lifecycle, and one-time interactive disclosure.
  • Make .env optional and load only the current-directory file without overriding process environment variables.
  • Enforce CLI > process environment > .env > saved SQLite settings precedence and expose dotenv/generated sources as locked WebUI configuration.
  • Generate cb- keys only for local interactive first startup, atomically reuse them across concurrent restarts, and announce them only after the listener succeeds.
  • Reject hidden-key first starts for non-loopback or non-TTY deployments while preserving explicit empty-key compatibility.
app/startup.py
app/runtime_management.py
converter.py
app/settings.py
web/src/pages/Settings.tsx
tests/test_environment_config.py
tests/test_sqlite_state.py
Retire text logging and strengthen protection against generated-key and credential leakage.
  • Replace free-form text-file runtime logging with allowlisted SQLite audit events and warn when retired log options are supplied.
  • Redact cb- keys in audit and general safe logging paths, while excluding key material from snapshots and persistent diagnostics.
  • Keep upstream credentials in .info files and document the separate control and audit databases.
converter.py
app/audit_store.py
app/safe_logging.py
app/settings.py
tests/test_runtime_endpoints.py
tests/test_sqlite_state.py
Update deployment guidance, localized documentation, and dependency declarations for the new persistence and startup model.
  • Document zero-config uv/python launches, startup precedence, default-key handling, migration/rollback requirements, backup boundaries, and filesystem protection.
  • Synchronize English and Chinese README and operational documentation.
  • Declare python-dotenv as a direct locked dependency.
README.md
README.zh-CN.md
docs/advanced.md
docs/advanced.zh-CN.md
docs/deployment.md
docs/deployment.zh-CN.md
docs/webui.md
docs/webui.zh-CN.md
pyproject.toml
requirements.in
requirements.txt
uv.lock

Assessment against linked issues

Issue Objective Addressed Explanation
#37 Allow direct Windows/native execution to configure the gateway listener port through CLI arguments, process environment variables, or the current directory's .env file.
#37 Allow direct Windows/native execution to configure the API key through CLI arguments, process environment variables, or the current directory's .env file, while providing a persisted default key when no key is explicitly configured.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-22T00:26:48.394176Z b20f2fb New commits
🔒 Security Review Completed 2026-09-21T20:21:37.882615Z ca4cfb2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, and 1 other issue

Security issues:

  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="app/admin_auth.py" line_range="223-227" />
<code_context>
     def _revoke(self):
         """Revoke the persisted snapshot; returns False when it could not be cleared."""
+        if self._store is not None:
+            try:
+                self._store.delete("sessions")
+            except (OSError, ValueError, sqlite3.Error) as error:
+                self._storage_failed(error)
+                return False
+            self._storage_ok()
+            return True
</code_context>
<issue_to_address>
**issue (broader_impact):** SQLite read failures are not handled consistently by the migrated runtime caches: `AdminAuth._restore` converts them into session revocation, but `CredentialCooldowns._load`, `UsageSnapshots._load`, and the corresponding catalog/ledger loaders allow `sqlite3.Error` to escape during object construction. A damaged or temporarily locked control database therefore aborts startup in some cache paths while silently discarding sessions in another, rather than applying one documented failure policy.

**Triggers:** When `control.sqlite3` is locked, corrupt, or otherwise raises a SQLite error while a runtime snapshot is loaded.

**Suggested fix:** Define and apply a single startup policy for control-state read failures—preferably fail startup for authoritative state and explicitly degrade only rebuildable caches, with diagnostics.
</issue_to_address>

### Comment 2
<location path="app/control_store.py" line_range="113" />
<code_context>
            self._db.execute(f"PRAGMA user_version={self.SCHEMA_VERSION}")
</code_context>
<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and this changes the authentication trust boundary by replacing an absent key with a persisted, shared default key and one-time announcement, so a mistake can expose or lock out management and API access from the moment it ships. Reverting the code does not undo generated keys, migrated state, or SQLite-authoritative runtime data, so recovery requires deliberate state handling.

Blocking findings: app/admin_auth.py:227, app/control_store.py:113


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread app/admin_auth.py Outdated
Comment thread app/control_store.py Outdated
Comment thread app/startup.py Dismissed
Preserve authoritative SQLite state on read failures and leave failed session epochs inactive for retry.

Publish CodeQL results against the checked-out pull request head and use a literal schema-version statement.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e61ae221f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/startup.py
Comment on lines +66 to +67
if key is None and config["host"] not in ("127.0.0.1", "::1", "localhost"):
raise ValueError("非回环监听请显式设置 API key;默认密钥仅在本地首次启动时生成")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor the explicit no-auth opt-in before requiring a key

When CODEBUDDY2API_KEY is genuinely unset, a non-loopback launch now raises here before converter.main() reaches its existing CODEBUDDY2API_ALLOW_OPEN_NOAUTH check. This breaks the shipped image's default direct-run configuration: the Dockerfile binds 0.0.0.0 and sets CODEBUDDY2API_ALLOW_OPEN_NOAUTH=true, but does not define CODEBUDDY2API_KEY, so the container exits during initialization. Account for the explicit unsafe opt-in here, or ensure the image supplies an explicit empty-key source.

Useful? React with 👍 / 👎.

Use the existing unsafe opt-in for unconfigured headless launches while keeping saved and externally supplied keys authoritative.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22778a6c28

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/startup.py Outdated
Comment on lines +93 to +95
if config["state_store"].claim_announcement(key):
terminal.write(f"\n默认 API key:{key}\n已保存到 control.sqlite3,仅显示这一次;管理登录与 API 请求共用。\n")
terminal.flush()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the key after a failed terminal disclosure

When the controlling terminal disappears or write()/flush() fails after claim_announcement() commits, the server exits but the database already records the key as announced. On the next launch default_key() therefore returns pending=False, so the gateway starts with a generated API key that was never fully disclosed and cannot be recovered through the UI or standard output. Handle terminal-write failures by invalidating/resetting this generated key or otherwise restoring a recoverable pending state before shutdown.

Useful? React with 👍 / 👎.

Commit the announcement marker after terminal flush under an exclusive transaction. Roll back failed writes, interrupted disclosures and failed commits so the saved key remains recoverable.

Document possible redisclosure after a crash between terminal output and commit.
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.

windows端直接运行有俩问题:无法设置端口号,无法设置API key

2 participants