Skip to content

Add browser-based device authorization#16

Merged
IliaMManolov merged 3 commits into
mainfrom
feat/device-authorization
Jul 23, 2026
Merged

Add browser-based device authorization#16
IliaMManolov merged 3 commits into
mainfrom
feat/device-authorization

Conversation

@IliaMManolov

@IliaMManolov IliaMManolov commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add OAuth-style device authorization with PKCE, browser login, logout, and scoped client credentials
  • reload rotated credentials without restarting while surfacing needs_reauth and needs_upgrade states
  • isolate server secrets by issuer and installation, securely replace credential files, and terminate child process trees on auth changes or shutdown
  • move client server actions to the scoped request API while retaining the legacy API-key compatibility path

Validation

  • cargo test --workspace (95 tests)
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo package -p edison-stdiod --allow-dirty
  • changed-file prek hooks
  • offline GoTrue and real Playwright device approval, denial, reauthorization, revocation, and MCP round-trip coverage with the matching edison-watch branch

Summary by cubic

Adds OAuth-style device authorization with PKCE for edison-stdiod, enabling browser login/logout and scoped client credentials. Also hardens daemon/tunnel behavior, storage safety, and server diagnostics.

  • New Features

    • Browser/device auth with PKCE: login opens the browser; logout revokes and clears local credentials.
    • Scoped client credentials with a narrow /api/v1/client/... surface; legacy API-key flows remain compatible for add/list/remove where valid.
    • Daemon reacts to auth changes: live credential reloads, needs_reauth/needs_upgrade states, and clean child process tree termination on auth changes or shutdown.
    • Safer on-disk handling: per-issuer/installation env stores and atomic private file replacement for credentials and env data.
    • Clearer local server diagnostics: bounded stderr tail with secret redaction and clearer terminal errors on startup failures.
  • Bug Fixes

    • Account changes (user/org) revoke the old token and clear per-user secrets; backend switches are allowed when no issuer-bound auth exists.
    • Auth client hardening: disable HTTP redirects; accept token_type case-insensitively.
    • WebSocket stability: abort the sink task on all exit paths; drop stale scoped tokens on backend mismatch.
    • Child reliability: restart a server if its outbound queue overflows; share child handles with pumps for accurate exit codes.
    • Windows replacement safety: move aside the current credential file before replacing to avoid data loss.

Written for commit e98d81f. Summary will update on new commits.

Review in cubic

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@IliaMManolov

Copy link
Copy Markdown
Collaborator Author

Companion backend/dashboard PR: https://github.com/Edison-Watch/edison-watch/pull/1077

@cubic-dev-ai cubic-dev-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.

All reported issues were addressed across 24 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread crates/edison-stdiod/src/cli/login.rs Outdated
Comment thread crates/edison-stdiod/src/config_extra_tests.rs
Comment thread crates/edison-stdiod/src/secure_file.rs Outdated
Comment thread crates/edison-stdiod/src/tunnel.rs Outdated
Comment thread crates/edison-stdiod/src/auth.rs
Comment thread crates/edison-stdiod/src/config.rs Outdated
Comment thread crates/edison-stdiod/src/auth.rs Outdated

@cubic-dev-ai cubic-dev-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.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/edison-stdiod/src/daemon.rs Outdated
Comment thread crates/edison-stdiod/src/proc.rs Outdated
- Treat a user or org change on a reused installation as an account change
  so the previous per-user secret is cleared and the old credential revoked
- Restrict the --backend mismatch error to configs holding issuer-bound
  auth state so first-run/logged-out configs can switch servers
- Disable HTTP redirects on the auth client so the PKCE verifier and tokens
  are never re-posted to a redirect target
- Accept token_type case-insensitively per RFC 6749
- Abort the WS sink task on every run_frame_loop exit path, including
  reasoned closes and recv errors
- Move the current credential file aside instead of deleting it during
  Windows replacement so a failure never loses the previous contents
- Restart a child whose outbound queue overflows instead of leaving it
  alive while dropping subsequent MCP requests
- Share the child handle with the pumps so terminal diagnostics report the
  real exit code, and assert it in the exit-status regression test
- Assert stale scoped tokens are dropped in the invalid-saved-backend test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-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.

3 issues found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crates/edison-stdiod/src/proc.rs">

<violation number="1" location="crates/edison-stdiod/src/proc.rs:646">
P1: Child-process descendants can survive a later restart, logout, or shutdown: the new status poll reaps the direct child before `shutdown` tries to kill its process tree, causing `child.id()` to be unavailable and both tree-kill branches to be skipped. Preserving a process-group/job handle or performing tree termination before reaping would keep descendant MCP processes from leaking.</violation>
</file>

<file name="crates/edison-stdiod/src/secure_file.rs">

<violation number="1" location="crates/edison-stdiod/src/secure_file.rs:96">
P2: When the destination path is an existing directory and the initial Windows rename returns one of the handled errors, this fallback moves that directory to the backup name and installs the credential file in its place. That silently relocates arbitrary directory contents; checking and rejecting directory targets before moving `path` would preserve the existing failure behavior.</violation>

<violation number="2" location="crates/edison-stdiod/src/secure_file.rs:96">
P2: A daemon crash or forced termination after this rename but before `tmp -> path` leaves Windows `config.toml`/`server_envs.json` absent, even though the old bytes remain in `.bak`. The next startup therefore loses the stored auth or environment state that this backup is intended to preserve; stale-backup recovery is needed before treating the canonical file as missing.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

async fn child_exit_status(child: Option<&SharedChild>) -> Option<ExitStatus> {
let child = child?;
for _ in 0..10 {
if let Ok(Some(status)) = child.lock().await.try_wait() {

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: Child-process descendants can survive a later restart, logout, or shutdown: the new status poll reaps the direct child before shutdown tries to kill its process tree, causing child.id() to be unavailable and both tree-kill branches to be skipped. Preserving a process-group/job handle or performing tree termination before reaping would keep descendant MCP processes from leaking.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/edison-stdiod/src/proc.rs, line 646:

<comment>Child-process descendants can survive a later restart, logout, or shutdown: the new status poll reaps the direct child before `shutdown` tries to kill its process tree, causing `child.id()` to be unavailable and both tree-kill branches to be skipped. Preserving a process-group/job handle or performing tree termination before reaping would keep descendant MCP processes from leaking.</comment>

<file context>
@@ -619,20 +626,38 @@ impl ChildServer {
+async fn child_exit_status(child: Option<&SharedChild>) -> Option<ExitStatus> {
+    let child = child?;
+    for _ in 0..10 {
+        if let Ok(Some(status)) = child.lock().await.try_wait() {
+            return Some(status);
+        }
</file context>

// destination is absent remains, but never one where no copy of
// the data exists.
let backup = unique_sibling_path(path, "bak");
match std::fs::rename(path, &backup) {

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: When the destination path is an existing directory and the initial Windows rename returns one of the handled errors, this fallback moves that directory to the backup name and installs the credential file in its place. That silently relocates arbitrary directory contents; checking and rejecting directory targets before moving path would preserve the existing failure behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/edison-stdiod/src/secure_file.rs, line 96:

<comment>When the destination path is an existing directory and the initial Windows rename returns one of the handled errors, this fallback moves that directory to the backup name and installs the credential file in its place. That silently relocates arbitrary directory contents; checking and rejecting directory targets before moving `path` would preserve the existing failure behavior.</comment>

<file context>
@@ -81,14 +85,32 @@ fn replace(tmp: &Path, path: &Path) -> io::Result<()> {
+            // destination is absent remains, but never one where no copy of
+            // the data exists.
+            let backup = unique_sibling_path(path, "bak");
+            match std::fs::rename(path, &backup) {
                 Ok(()) => {}
-                Err(error) if error.kind() == io::ErrorKind::NotFound => {}
</file context>
Suggested change
match std::fs::rename(path, &backup) {
if std::fs::metadata(path)
.map(|metadata| metadata.is_dir())
.unwrap_or(false)
{
return Err(io::Error::new(
io::ErrorKind::IsADirectory,
format!("destination is a directory: {}", path.display()),
));
}
match std::fs::rename(path, &backup) {

// destination is absent remains, but never one where no copy of
// the data exists.
let backup = unique_sibling_path(path, "bak");
match std::fs::rename(path, &backup) {

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: A daemon crash or forced termination after this rename but before tmp -> path leaves Windows config.toml/server_envs.json absent, even though the old bytes remain in .bak. The next startup therefore loses the stored auth or environment state that this backup is intended to preserve; stale-backup recovery is needed before treating the canonical file as missing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/edison-stdiod/src/secure_file.rs, line 96:

<comment>A daemon crash or forced termination after this rename but before `tmp -> path` leaves Windows `config.toml`/`server_envs.json` absent, even though the old bytes remain in `.bak`. The next startup therefore loses the stored auth or environment state that this backup is intended to preserve; stale-backup recovery is needed before treating the canonical file as missing.</comment>

<file context>
@@ -81,14 +85,32 @@ fn replace(tmp: &Path, path: &Path) -> io::Result<()> {
+            // destination is absent remains, but never one where no copy of
+            // the data exists.
+            let backup = unique_sibling_path(path, "bak");
+            match std::fs::rename(path, &backup) {
                 Ok(()) => {}
-                Err(error) if error.kind() == io::ErrorKind::NotFound => {}
</file context>

@IliaMManolov
IliaMManolov merged commit bf1fbf1 into main Jul 23, 2026
2 of 4 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