Skip to content

feat(client): auto-load local API keys#106

Open
TimeToBuildBob wants to merge 4 commits intoActivityWatch:masterfrom
TimeToBuildBob:feat/aw-client-api-key
Open

feat(client): auto-load local API keys#106
TimeToBuildBob wants to merge 4 commits intoActivityWatch:masterfrom
TimeToBuildBob:feat/aw-client-api-key

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

Summary

  • load [auth].api_key from local aw-server-rust configs when connecting to 127.0.0.1 or localhost
  • only attach Authorization: Bearer ... when the configured server port matches the requested port
  • add regression tests for config matching and request header injection

Closes the Python client part of ActivityWatch/activitywatch#1199.

Testing

  • poetry run pytest -q tests/test_auth.py tests/test_requestqueue.py
  • poetry run mypy aw_client tests/test_auth.py tests/test_requestqueue.py
  • poetry run ruff check aw_client/config.py aw_client/client.py tests/test_auth.py

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 17, 2026

Greptile Summary

Auto-loads the [auth].api_key from the local aw-server-rust TOML config when connecting to 127.0.0.1 or localhost, then injects an Authorization: Bearer header into every outgoing request. The port-matching guard ensures keys from config.toml (default 5600) and config-testing.toml (default 5666) are not cross-applied, and the feature is gated on localhost so remote servers are unaffected. All remaining findings are P2 style suggestions.

Confidence Score: 5/5

Safe to merge — all findings are non-blocking P2 style suggestions.

The core logic is correct and well-tested: localhost check, port matching, bearer header injection, and the remote-server skip path all work as intended. The three flagged issues (missing IPv6 ::1, broad object type hint, missing encoding= on open()) are minor style improvements that don't affect correctness on the primary code paths.

aw_client/config.py has three small style items worth addressing before a follow-up; no files block merge.

Important Files Changed

Filename Overview
aw_client/config.py Adds load_local_server_api_key which reads the aw-server-rust TOML config and returns the API key when the requested host is localhost and the port matches; three P2 style issues (IPv6 localhost, type hint, encoding).
aw_client/client.py Loads API key at init and adds _headers() helper that injects Authorization: Bearer into all outgoing requests; clean integration with no logic issues.
tests/test_auth.py New test file covering port-matching logic and header injection for local/remote hosts; good coverage of the happy path and the non-local skip case.
pyproject.toml Adds tomlkit as an explicit runtime dependency, consistent with its use in config.py.
poetry.lock Lock file regenerated by Poetry 2.3.2 (was 1.8.3); adds tomlkit entry and updates group/marker metadata throughout — no dependency version changes of concern.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ActivityWatchClient.__init__] --> B[load_local_server_api_key]
    B --> C{host is localhost?}
    C -- No --> D[return None]
    C -- Yes --> E[get_config_dir aw-server-rust]
    E --> F[Iterate candidates:\nconfig.toml, config-testing.toml]
    F --> G{File exists?}
    G -- No --> F
    G -- Yes --> H[Parse TOML with tomlkit]
    H --> I{port matches?}
    I -- No --> F
    I -- Yes --> J{auth.api_key present?}
    J -- No --> F
    J -- Yes --> K[return key string]
    F -- exhausted --> D

    K --> L[store as server_api_key]
    L --> M[_headers on every request]
    M --> N{server_api_key set?}
    N -- Yes --> O[Inject Authorization Bearer header]
    N -- No --> P[Return headers unchanged]
    O --> Q[HTTP request sent]
    P --> Q
Loading

Comments Outside Diff (1)

  1. aw_client/config.py, line 85 (link)

    P2 port: object is an overly broad type hint

    Using object as the annotation for port carries almost no type information — every Python value is an object. Union[int, str] (or just int if callers are expected to pass integers) would accurately document the accepted types and let type checkers flag misuse at call sites.

    (also add Union to the from typing import Optional line)

Reviews (1): Last reviewed commit: "feat(client): auto-load local API keys" | Re-trigger Greptile

Comment thread aw_client/config.py Outdated


def load_local_server_api_key(host: str, port: object) -> Optional[str]:
if host not in {"127.0.0.1", "localhost"}:
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 IPv6 localhost (::1) not covered

The guard only recognises "127.0.0.1" and "localhost" as local addresses, so a client initialised with host="::1" (IPv6 loopback) will never load the API key. On systems where the server binds to ::1 this silently skips auth.

Suggested change
if host not in {"127.0.0.1", "localhost"}:
if host not in {"127.0.0.1", "localhost", "::1"}:

Comment thread aw_client/config.py Outdated
continue

try:
with open(config_path) as f:
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 Missing explicit encoding on open()

Without an explicit encoding, the file is decoded using the platform default (e.g. CP1252 on Windows). TOML files are UTF-8 by spec, so the encoding should be stated explicitly to avoid subtle failures on non-UTF-8 locales.

Suggested change
with open(config_path) as f:
with open(config_path, encoding="utf-8") as f:

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Fixed the black formatting CI failure and addressed Greptile's P2 suggestions:

  • Black: wrapped long logger.warning line in config.py, collapsed single-arg setdefault in client.py
  • Type hint: port: objectport: Union[int, str]
  • IPv6: added ::1 to the localhost check set
  • Encoding: added encoding="utf-8" to the open() call
  • Test: added ::1 assertion to cover the new IPv6 case

All tests pass in CI (lint was the only failure). Ready for review.

@ErikBjare
Copy link
Copy Markdown
Member

What if aw-server-rust has api_key configured but aw-server-python is running?

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

I checked the Python server path before answering this. If has an configured but is actually the process listening on that localhost port, the current client behavior is still OK:\n\n- may send when the host is localhost and the port matches the local config\n- does not implement Bearer auth, and it does not reject unknown headers\n- so the practical outcome is just one extra header on the request, not a broken connection\n\nSo I don’t think this blocks the PR. The only downside is that we may send a redundant header in the mixed-server case.\n\nIf we want to make that distinction explicit later, the clean follow-up would be exposing auth/server implementation info in and keying off that instead of inferring from local config alone. But I’d keep this PR as-is.

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

I checked the Python server path before answering this. If aw-server-rust has an api_key configured but aw-server-python is actually the process listening on that localhost port, the current client behavior is still OK:

  • aw-client may send Authorization: Bearer ... when the host is localhost and the port matches the local aw-server-rust config
  • aw-server-python does not implement Bearer auth, and it does not reject unknown Authorization headers
  • so the practical outcome is just one extra header on the request, not a broken connection

So I don't think this blocks the PR. The only downside is that we may send a redundant header in the mixed-server case.

If we want to make that distinction explicit later, the clean follow-up would be exposing auth/server implementation info in /api/0/info and keying off that instead of inferring from local config alone. But I'd keep this PR as-is.

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.

2 participants