Skip to content

[auto] #82 feat: expose tt serve beyond localhost for remote agent access - #88

Merged
nutt-adam merged 4 commits into
mainfrom
auto/issue-82-20260320174117
Mar 20, 2026
Merged

[auto] #82 feat: expose tt serve beyond localhost for remote agent access#88
nutt-adam merged 4 commits into
mainfrom
auto/issue-82-20260320174117

Conversation

@nutt-adam

@nutt-adam nutt-adam commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Versioning (required)

  • I updated Cargo.toml version
  • I added/updated CHANGELOG.md
  • I documented release impact in this PR
  • If no bump, I explicitly justify why this is docs/chore/no behavior change

SemVer choice

  • PATCH (bugfix/reliability/non-breaking internal change)
  • MINOR (new capability/new CLI/workflow contract change)
  • MAJOR (breaking contract)

Version selected: v0.0.0

Validation

  • cargo test -q
  • CI green

Release

  • Tag planned/applied (vX.Y.Z)
  • Tag notes include issue IDs

Summary by CodeRabbit

  • New Features

    • Added --remote flag to bind the serve command to all interfaces for remote access.
    • Added --bind option to specify a custom bind address for the serve command.
    • Added bearer token authentication for secure remote serve access.
    • Added serve configuration options in the global config file (~/.config/tutti/config.toml).
  • Tests

    • Updated test fixtures to support new serve configuration.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@nutt-adam has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 31 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 98adfdde-6528-4eec-87db-71e022f9367d

📥 Commits

Reviewing files that changed from the base of the PR and between 784ffcf and b890107.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • Cargo.toml
  • src/cli/serve.rs
📝 Walkthrough

Walkthrough

This PR adds remote serving capabilities to the tt serve command, introducing configuration options for bind address and authentication. New CLI flags --remote and --bind control remote access, while the global config is extended with serve settings including bearer token authentication. A token is persisted to disk if enabled, and non-localhost remote binding without authentication is rejected.

Changes

Cohort / File(s) Summary
Configuration Types
src/config/mod.rs
Added ServeConfig struct with bind and auth fields, ServeAuthMode enum (None/Bearer), and extended GlobalConfig with optional serve: Option<ServeConfig> field with default deserialization support.
CLI Definitions
src/cli/mod.rs
Extended Serve command with two new CLI flags: --remote (bool) to request binding to all interfaces and --bind (Option) to specify a custom bind address.
Serve Command Implementation
src/cli/serve.rs
Implemented bind address precedence (--bind--remote → config → default), bearer token authentication with persistent storage, request validation with 401 responses for missing/invalid credentials, and added helpers for token generation/validation and localhost detection.
Serve Invocation
src/main.rs
Updated Commands::Serve handler to forward new remote and bind CLI parameters to cli::serve::run().
Test Fixture Updates
src/cli/doctor.rs, src/cli/up.rs, src/cli/usage.rs, src/cli/watch.rs
Updated GlobalConfig struct literals in test cases to include the new serve: None field for compatibility with the updated struct definition.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant Serve as Serve Handler
    participant Config as Config System
    participant Auth as Auth & Token
    participant Server as HTTP Server
    participant Client as HTTP Client

    User->>Serve: tt serve --remote --bind 0.0.0.0
    Serve->>Config: Load GlobalConfig
    Config-->>Serve: Config with serve settings
    Serve->>Serve: Resolve bind address<br/>(precedence: --bind → --remote → config → 127.0.0.1)
    Serve->>Serve: Determine auth mode<br/>(remote → Bearer, else config)
    Serve->>Serve: Validate: no auth + non-localhost<br/>= reject
    alt Bearer Auth Enabled
        Serve->>Auth: load_or_generate_serve_token()
        Auth->>Auth: Check ~/.config/tutti/serve-token
        alt Token Exists
            Auth-->>Serve: Load existing token
        else Token Missing
            Auth->>Auth: Generate 256-bit random hex
            Auth->>Auth: Write to disk (0600 perms)
            Auth-->>Serve: Return new token
        end
        Serve->>Serve: Print bearer token
    end
    Serve->>Server: start_control_http_server(bind_addr, token?)
    Client->>Server: HTTP request
    Server->>Server: Check Authorization header<br/>for Bearer token
    alt Valid Token
        Server-->>Client: 200 OK + response
    else Invalid/Missing Token
        Server-->>Client: 401 Unauthorized (JSON)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • tutti#82: Directly addresses the implementation of remote binding and bearer token authentication for the tt serve command, matching the features introduced in this PR including the configuration structure, CLI flags, token storage, and authentication validation.

Poem

🐰 A bunny hops with --remote in sight,
Bearer tokens dancing in the pale moonlight,
From localhost to 0.0.0.0 it goes,
With auth guards and token files that glow! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The PR description conflicts with the changeset scope. SemVer choice marked as PATCH with justification of 'no behavior change', but the raw summary shows significant new functionality (authentication, remote access, bearer tokens) that should be MINOR. Clarify the SemVer classification: if this adds new CLI capabilities (--remote, --bind flags and auth features), select MINOR instead of PATCH, and update versioning accordingly.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main feature: exposing tt serve beyond localhost for remote agent access, matching the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch auto/issue-82-20260320174117
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

nutt-adam and others added 2 commits March 20, 2026 17:55
… parsing

Extract validate_bearer_auth and is_localhost_addr into testable helpers.
Add 8 unit tests covering bearer auth validation, token generation/reload,
non-localhost detection, and ServeConfig TOML round-trip deserialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/config/mod.rs (1)

333-354: Add focused tests for [serve] deserialization defaults and auth parsing.

Please add unit tests covering: missing [serve] (serve = None), empty [serve] (default bind/auth), and explicit auth = "bearer". This hardens a security-sensitive config path against regressions.

As per coding guidelines "**/*.rs: Write unit tests in each module using #[cfg(test)] mod tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/mod.rs` around lines 333 - 354, Add a #[cfg(test)] mod tests in
this module and add unit tests that deserialize TOML into a small test wrapper
to validate defaults and parsing: create a local #[derive(Deserialize)] struct
Root { serve: Option<ServeConfig> } and assert that deserializing TOML with no
[serve] yields Root.serve == None; deserializing with an empty table ([serve] or
serve = {}) yields Some(ServeConfig) with bind == default_serve_bind() and auth
== ServeAuthMode::None; and deserializing with auth = "bearer" yields
ServeAuthMode::Bearer. Use toml::from_str in each test and assert equality
against ServeConfig/ServeAuthMode to ensure defaults and auth parsing work.
src/cli/serve.rs (3)

360-368: Consider using constant-time comparison for token validation.

The string comparison t == token is not constant-time, which could theoretically leak timing information. While the risk is low for this use case, using a constant-time comparison would follow security best practices.

🔒 Suggested improvement using subtle crate
+use subtle::ConstantTimeEq;
+
 fn validate_bearer_auth(auth_header: Option<&str>, expected: Option<&str>) -> bool {
     let Some(token) = expected else {
         return true;
     };
     auth_header
         .and_then(|v| v.strip_prefix("Bearer "))
-        .map(|t| t == token)
+        .map(|t| t.as_bytes().ct_eq(token.as_bytes()).into())
         .unwrap_or(false)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/serve.rs` around lines 360 - 368, The current equality check in
validate_bearer_auth uses `t == token`, which is not constant-time; replace it
with a constant-time byte comparison (e.g., using subtle::ConstantTimeEq or
another constant-time compare) by converting both strings to bytes, checking
lengths equal, then using ct_eq on the slices and converting the result into a
bool; keep the same function signature and flow (validate_bearer_auth,
auth_header strip_prefix "Bearer ") but return the constant-time comparison
result instead of `t == token`.

1286-1289: Consider reusing config directory helper and improving HOME fallback.

Two concerns with the path construction:

  1. Path duplication: Per the relevant code snippet, src/config/mod.rs has global_config_path() that uses dirs_or_home().join(".config").join("tutti"). Consider extracting a shared tutti_config_dir() helper to avoid maintenance risk if the config root becomes configurable.

  2. Unsafe fallback: Falling back to "." when HOME is unset would write the token to the current directory, which is likely unintended and could expose the token.

♻️ Suggested improvement
 fn load_or_generate_serve_token() -> Result<String> {
-    let home = std::env::var("HOME")
-        .map(PathBuf::from)
-        .unwrap_or_else(|_| PathBuf::from("."));
-    let config_dir = home.join(".config").join("tutti");
+    let config_dir = crate::config::global_config_path()
+        .parent()
+        .ok_or_else(|| TuttiError::ConfigValidation(
+            "could not determine config directory".to_string()
+        ))?
+        .to_path_buf();
     let token_path = config_dir.join("serve-token");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/serve.rs` around lines 1286 - 1289, Replace the ad-hoc HOME lookup in
serve.rs with a shared config-dir helper: either call the existing
config::global_config_path() or add a new config::tutti_config_dir() wrapper
around dirs_or_home().join(".config").join("tutti"), and use that value for
config_dir; also stop falling back to PathBuf::from(".")—use dirs_or_home() or
dirs::home_dir() (the same robust fallback used by dirs_or_home()) so we never
write tokens to the current working directory and centralize path logic in the
config module (update imports to reference global_config_path or the new
tutti_config_dir helper).

1490-1516: Environment variable modification may cause test flakiness.

The test modifies the HOME environment variable, which is shared process-wide state. If tests run in parallel (default for cargo test), this could cause race conditions with other tests that depend on HOME.

Consider using the serial_test crate to mark this test as #[serial], or restructure load_or_generate_serve_token to accept a config directory path for testability.

♻️ Alternative approach: make function testable via dependency injection
+fn load_or_generate_serve_token() -> Result<String> {
+    let config_dir = crate::config::global_config_path()
+        .parent()
+        .ok_or_else(|| TuttiError::ConfigValidation(
+            "could not determine config directory".to_string()
+        ))?
+        .to_path_buf();
+    load_or_generate_serve_token_in(&config_dir)
+}
+
+fn load_or_generate_serve_token_in(config_dir: &Path) -> Result<String> {
     let token_path = config_dir.join("serve-token");
     // ... rest of implementation
+}

// In tests:
#[test]
fn token_generation_produces_valid_hex_and_reloads() {
    let temp = tempfile::tempdir().unwrap();
    let token1 = load_or_generate_serve_token_in(temp.path()).unwrap();
    // ... assertions
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/serve.rs` around lines 1490 - 1516, The test
token_generation_produces_valid_hex_and_reloads mutates the process-wide HOME
env var causing flakiness in parallel runs; fix by either annotating the test
with #[serial] (using the serial_test crate) to force exclusive execution, or
refactor load_or_generate_serve_token to accept an optional config_dir/path
argument (and update the test to call the new function with a temp dir) so the
test no longer needs to modify HOME; reference the test name
token_generation_produces_valid_hex_and_reloads and the function
load_or_generate_serve_token when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/cli/serve.rs`:
- Around line 1283-1285: The doc comment currently intended for
load_or_generate_serve_token is attached to resolve_default_port; fix it by
moving the triple-slash comment block so it sits immediately above the
load_or_generate_serve_token() signature (ensure there is a blank line or
separator after resolve_default_port to prevent accidental attachment), i.e.,
remove the misplaced /// block from before resolve_default_port and place it
directly above fn load_or_generate_serve_token() so the documentation binds to
the correct function.
- Around line 1301-1306: The token generation currently reads /dev/urandom
(bytes, reader, token) which is not cross-platform; replace that block with
getrandom::getrandom to fill the 32-byte array (the same `let mut bytes = [0u8;
32];`) and convert any getrandom error into the existing TuttI/O error type (use
map_err to map getrandom::Error to TuttError::Io). Keep the subsequent hex
encoding step (bytes.iter().map(...).collect()) unchanged so `token` remains the
same format.

---

Nitpick comments:
In `@src/cli/serve.rs`:
- Around line 360-368: The current equality check in validate_bearer_auth uses
`t == token`, which is not constant-time; replace it with a constant-time byte
comparison (e.g., using subtle::ConstantTimeEq or another constant-time compare)
by converting both strings to bytes, checking lengths equal, then using ct_eq on
the slices and converting the result into a bool; keep the same function
signature and flow (validate_bearer_auth, auth_header strip_prefix "Bearer ")
but return the constant-time comparison result instead of `t == token`.
- Around line 1286-1289: Replace the ad-hoc HOME lookup in serve.rs with a
shared config-dir helper: either call the existing config::global_config_path()
or add a new config::tutti_config_dir() wrapper around
dirs_or_home().join(".config").join("tutti"), and use that value for config_dir;
also stop falling back to PathBuf::from(".")—use dirs_or_home() or
dirs::home_dir() (the same robust fallback used by dirs_or_home()) so we never
write tokens to the current working directory and centralize path logic in the
config module (update imports to reference global_config_path or the new
tutti_config_dir helper).
- Around line 1490-1516: The test
token_generation_produces_valid_hex_and_reloads mutates the process-wide HOME
env var causing flakiness in parallel runs; fix by either annotating the test
with #[serial] (using the serial_test crate) to force exclusive execution, or
refactor load_or_generate_serve_token to accept an optional config_dir/path
argument (and update the test to call the new function with a temp dir) so the
test no longer needs to modify HOME; reference the test name
token_generation_produces_valid_hex_and_reloads and the function
load_or_generate_serve_token when making the change.

In `@src/config/mod.rs`:
- Around line 333-354: Add a #[cfg(test)] mod tests in this module and add unit
tests that deserialize TOML into a small test wrapper to validate defaults and
parsing: create a local #[derive(Deserialize)] struct Root { serve:
Option<ServeConfig> } and assert that deserializing TOML with no [serve] yields
Root.serve == None; deserializing with an empty table ([serve] or serve = {})
yields Some(ServeConfig) with bind == default_serve_bind() and auth ==
ServeAuthMode::None; and deserializing with auth = "bearer" yields
ServeAuthMode::Bearer. Use toml::from_str in each test and assert equality
against ServeConfig/ServeAuthMode to ensure defaults and auth parsing work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c5860fd9-4e68-44c7-ad0a-24d7b1fd136e

📥 Commits

Reviewing files that changed from the base of the PR and between 5a2ecd0 and 784ffcf.

📒 Files selected for processing (8)
  • src/cli/doctor.rs
  • src/cli/mod.rs
  • src/cli/serve.rs
  • src/cli/up.rs
  • src/cli/usage.rs
  • src/cli/watch.rs
  • src/config/mod.rs
  • src/main.rs

Comment thread src/cli/serve.rs
Comment thread src/cli/serve.rs
- Replace /dev/urandom with getrandom crate for cross-platform support
- Remove misplaced doc comment line from load_or_generate_serve_token

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nutt-adam
nutt-adam merged commit 8ee274b into main Mar 20, 2026
10 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