Skip to content

feat(client): add API key authentication support to aw-client-rust#587

Open
TimeToBuildBob wants to merge 6 commits intoActivityWatch:masterfrom
TimeToBuildBob:fix-586-review-feedback
Open

feat(client): add API key authentication support to aw-client-rust#587
TimeToBuildBob wants to merge 6 commits intoActivityWatch:masterfrom
TimeToBuildBob:fix-586-review-feedback

Conversation

@TimeToBuildBob
Copy link
Copy Markdown
Contributor

@TimeToBuildBob TimeToBuildBob commented Apr 22, 2026

Summary

  • Add new_with_api_key(host, port, name, api_key: Option<String>) to both async (AwClient) and blocking (AwClientBlocking) clients
  • Bearer token header set as sensitive (not logged)
  • load_local_api_key and server config-reading types are test-only — moved to tests/test.rs, not visible in production code
  • toml moved from [dependencies] to [dev-dependencies]
  • Switch reqwest TLS backend to rustls-tls-native-roots — removes OpenSSL/Secure Transport/Schannel system dependency, confirmed working on all CI platforms (Android, macOS, Windows, ubuntu)
  • Integration test: test_reads_api_key_from_matching_server_config (Linux-only via #[cfg(target_os = "linux")] since XDG_CONFIG_HOME is ignored on macOS/Windows)
  • TOCTOU fix: reserve_port() keeps TcpListener alive via thread_local! { RefCell } until Rocket binds, preventing port reclaim between reservation and server start
  • ENV_LOCK acquired in test_full during AwClient::new() to prevent parallel test interference

Supersedes #586.

Greptile flagged rand as unused - confirmed no rand:: imports exist in src/ or tests/.
- P1: gate test_reads_api_key_from_matching_server_config on Linux only
  (XDG_CONFIG_HOME ignored by dirs::config_dir on macOS/Windows)
- P2: keep TcpListener alive in reserve_port to fix TOCTOU race
- P2: hold ENV_LOCK during AwClient::new in test_full
- P2: removed stale FIXME comment (TOCTOU race now fixed)
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR addresses the four review items from PR #586: the Linux-only #[cfg] gate for XDG_CONFIG_HOME-dependent tests, the RefCell-backed RESERVED_PORT thread-local to close the TOCTOU window, ENV_LOCK acquisition in test_full during client construction, and removal of the unused rand = "0.9" dep. All four fixes are correctly implemented. One unrelated change — switching reqwest to default-features = false, features = ["rustls-tls-native-roots"] — is not mentioned in the PR description but changes the TLS backend for all production HTTPS connections and adds a substantial new dependency chain via ring/rustls.

Confidence Score: 5/5

Safe to merge; all four previously flagged issues are correctly resolved, with one minor undocumented dependency change to document or confirm as intentional.

All P0/P1 items (compile error, EADDRINUSE, platform gate) are resolved. The only new finding is a P2 style concern about an undocumented TLS backend swap in Cargo.toml that does not affect correctness for the existing HTTP-only test traffic.

aw-client-rust/Cargo.toml — the undocumented reqwest TLS backend change should be confirmed as intentional.

Important Files Changed

Filename Overview
aw-client-rust/tests/test.rs Adds config-reading helpers, reserve_port() TOCTOU fix via RefCell thread-local, ENV_LOCK guard in test_full, and Linux-only gate for XDG test — all correctly implemented.
aw-client-rust/src/lib.rs Adds build_client helper and new_with_api_key constructor; Bearer token header marked sensitive; clean delegation from new — no issues found.
aw-client-rust/src/blocking.rs Mirrors new_with_api_key from async client via macro delegation — straightforward and correct.
aw-client-rust/Cargo.toml Removes rand = "0.9", adds toml = "0.8" dev-dep, and silently swaps reqwest's TLS backend to rustls without PR description coverage.
Cargo.lock Reflects removal of rand 0.9 / rand_chacha 0.9 / rand_core 0.9, addition of the full rustls stack (ring, rustls, tokio-rustls, hyper-rustls, sct, untrusted, rustls-native-certs, rustls-webpki), and toml; lock changes are consistent with Cargo.toml edits.

Sequence Diagram

sequenceDiagram
    participant TF as test_full
    participant TR as test_reads_api_key
    participant EL as ENV_LOCK
    participant RP as RESERVED_PORT (thread-local)
    participant SRV as Rocket Server

    par Parallel test execution
        TF->>EL: lock()
        TF->>TF: AwClient::new() [no config read]
        TF->>EL: release
        TF->>SRV: setup_testserver(PORT, None)
        TF->>TF: wait_for_server / run assertions
    and
        TR->>RP: reserve_port() → bind OS port, store TcpListener
        TR->>TR: write_server_config(port, "secret123")
        TR->>EL: with_config_home() → lock()
        TR->>TR: set XDG_CONFIG_HOME, load_local_api_key()
        TR->>TR: AwClient::new_with_api_key(..., Some("secret123"))
        TR->>RP: borrow_mut() = None  [drop TcpListener]
        TR->>SRV: setup_testserver(port, Some("secret123"))
        TR->>TR: wait_for_server / run assertions
        TR->>TR: restore XDG_CONFIG_HOME
        TR->>EL: release
    end
Loading

Reviews (3): Last reviewed commit: "refactor(client): move API key auto-load..." | Re-trigger Greptile

Comment thread aw-client-rust/tests/test.rs Outdated
Comment on lines +76 to +78
RESERVED_PORT
.with(|cell| *cell.borrow_mut() = Some(listener));
port
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 Mutex has no borrow_mut() — compile error

cell inside .with(|cell| ...) is &Mutex<Option<TcpListener>>. Mutex has no borrow_mut() method (that's RefCell). This will fail to compile with error[E0599]: no method named 'borrow_mut' found for struct Mutex<...>.

Either change the static to RefCell<Option<TcpListener>> (appropriate for thread-local storage since there's no cross-thread contention) and keep borrow_mut(), or keep Mutex and use cell.lock().unwrap():

Suggested change
RESERVED_PORT
.with(|cell| *cell.borrow_mut() = Some(listener));
port
RESERVED_PORT
.with(|cell| *cell.lock().unwrap() = Some(listener));

And update the static declaration accordingly:

thread_local! {
    static RESERVED_PORT: RefCell<Option<TcpListener>> = RefCell::new(None);
}

with *cell.borrow_mut() = Some(listener);.

Suggested change
RESERVED_PORT
.with(|cell| *cell.borrow_mut() = Some(listener));
port
RESERVED_PORT
.with(|cell| *cell.lock().unwrap() = Some(listener));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in c7be5d1 — switched from Mutex to RefCell for the thread_local! storage. RESERVED_PORT is now RefCell<Option<TcpListener>>, so borrow_mut() is correct. Confirmed: clippy + all platform CI pass.

Comment on lines +213 to +216
with_config_home(&config_home, || {
let client: AwClient =
AwClient::new("127.0.0.1", port, clientname).expect("Client creation failed");
let shutdown_handler = setup_testserver(port, Some("secret123"));
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 RESERVED_PORT still held when setup_testserver tries to bind

reserve_port() stores the TcpListener in RESERVED_PORT and never explicitly drops it before setup_testserver(port, ...) is called. When server.ignite() inside Rocket tries to bind to the same port, the OS will return EADDRINUSE because the reserved listener is still alive, causing .unwrap() to panic and the test to fail.

The listener must be dropped before setup_testserver is called:

with_config_home(&config_home, || {
    let client = AwClient::new("127.0.0.1", port, clientname).expect("...");
    // Release the reserved port so Rocket can bind
    RESERVED_PORT.with(|cell| *cell.lock().unwrap() = None);
    let shutdown_handler = setup_testserver(port, Some("secret123"));
    ...
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cad304c — now explicitly sets RESERVED_PORT to None (dropping the TcpListener) before calling setup_testserver(). The comment in the test documents this: // Drop the reserved listener before Rocket tries to bind the same port. CI confirms no EADDRINUSE.

RefCell is the correct type for thread_local storage — it's single-threaded
by design, so Mutex is unnecessary and its borrow_mut() method isn't in scope
without importing BorrowMut. RefCell::borrow_mut() works directly.

Also collapses the RESERVED_PORT.with() call to one line (cargo fmt).
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Fixed the CI failures (c7be5d1):

  • Compile error: RESERVED_PORT was declared as Mutex<Option<TcpListener>> but used borrow_mut(), which is a RefCell method. Changed to RefCell<Option<TcpListener>> — correct for thread_local! since it's single-threaded by design, no mutex needed.
  • Format: collapsed the multi-line RESERVED_PORT.with(...) call to one line as cargo fmt requires.

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Also pushed cad304c to fix the EADDRINUSE logic bug: explicitly drop the reserved TcpListener from RESERVED_PORT before setup_testserver() so Rocket can bind the same port without EADDRINUSE.

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

@greptileai review

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 68.46%. Comparing base (656f3c9) to head (73317cd).
⚠️ Report is 44 commits behind head on master.

Files with missing lines Patch % Lines
aw-client-rust/src/lib.rs 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #587      +/-   ##
==========================================
- Coverage   70.81%   68.46%   -2.36%     
==========================================
  Files          51       55       +4     
  Lines        2916     3250     +334     
==========================================
+ Hits         2065     2225     +160     
- Misses        851     1025     +174     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

The rustls-tls-native-roots switch was intentional: aw-client-rust now avoids depending on platform TLS libraries (OpenSSL on Linux, Secure Transport on macOS, Schannel on Windows), which aligns with the cross-platform portability theme of this PR. CI confirms it works on all target platforms (Android, macOS, Windows, ubuntu). The new transitive dependencies (ring, rustls, hyper-rustls, etc.) are accepted as the trade-off for platform-independence.

@ErikBjare
Copy link
Copy Markdown
Member

Why was this PR created in addition to #586 instead of continued there?

See the review I left there requesting a change, which you never acknowledged.

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Apologies — I missed your inline comment on #586 and incorrectly created a new PR rather than addressing your review there.

Your concern on lib.rs: load_local_api_key reads from the server's config.toml at runtime, which means production client code is silently probing the server's config directory. That's indeed wrong — this should be #[cfg(test)] only, since auto-loading the key from the server config only makes sense in integration tests where we control both sides.

Proposed fix:

  • Gate load_local_api_key + build_client behind #[cfg(test)]
  • The production AwClient::new stays unauthenticated (or takes an explicit key)
  • The test helpers use the cfg-gated versions to inject the key

Would you like me to make these changes in #586 and close this PR? Or should I update here and close #586? Either way I'll incorporate the Greptile fixes (cross-platform test gating, TOCTOU fix) into whichever PR survives.

Production `AwClient::new` no longer silently reads the server's config
directory to load API keys — that coupling between client and server config
belongs only in test infrastructure.

- `load_local_api_key`, `get_server_config_dir`, and related types moved
  from `src/lib.rs` to `tests/test.rs` (test-only)
- `toml` moved from `[dependencies]` to `[dev-dependencies]`
- Added `AwClient::new_with_api_key(host, port, name, api_key)` to both
  the async and blocking clients for explicit key injection
- Auth integration test now calls `load_local_api_key` explicitly then
  creates the client with `new_with_api_key`

Addresses ErikBjare's review feedback on PR ActivityWatch#586.
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Pushed 73317cd to address your concern on #586.

What changed: load_local_api_key and the server config-reading types are now test-only — moved from src/lib.rs to tests/test.rs. Production AwClient::new no longer silently probes the server's config directory. toml moved from [dependencies] to [dev-dependencies].

Added new_with_api_key(host, port, name, api_key: Option<String>) to both async and blocking clients — the auth integration test now explicitly loads the key and passes it in, rather than relying on auto-loading in the constructor.

@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

@greptileai review

@ErikBjare
Copy link
Copy Markdown
Member

@TimeToBuildBob rename this PR and description and close the old one.

@TimeToBuildBob TimeToBuildBob changed the title fix(tests): address review feedback for PR #586 feat(client): add API key authentication support to aw-client-rust Apr 22, 2026
@TimeToBuildBob
Copy link
Copy Markdown
Contributor Author

Done — updated the description to say "Supersedes #586" instead of "Closes #586" (which was stale since #586 is already closed). Title stays as-is since it accurately describes what the PR does. CI is all green.

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