Skip to content

fix(daemon): prevent two daemons from running at once (split-brain)#104

Merged
ronaldeddings merged 3 commits into
Hacker-Valley-Media:mainfrom
vitorfhc:fix/daemon-singleton-gate
Jun 17, 2026
Merged

fix(daemon): prevent two daemons from running at once (split-brain)#104
ronaldeddings merged 3 commits into
Hacker-Valley-Media:mainfrom
vitorfhc:fix/daemon-singleton-gate

Conversation

@vitorfhc

@vitorfhc vitorfhc commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

A CLI command targeting a named context fails with context '<id>' not found (no extensions connected) even though the extension is installed and connected. interceptor contexts is empty.

The cause is two daemons running at once, each owning half the system:

Process Owns Has the extension?
Daemon A (older) WebSocket server :19222 ✅ extension + context registered here
Daemon B (newer) /tmp/interceptor.sock (the CLI's socket) ❌ empty

The CLI talks to the socket → reaches Daemon B → sees zero extensions. The extension is connected to Daemon A → orphaned from the CLI. Split brain.

Root cause

The singleton election was driven only by the pid file, which is advisory and racy. The one genuinely OS-exclusive resource — the WebSocket listen port — was treated as optional:

  • A daemon that failed to bind :19222 logged continuing without WebSocket and kept running anyway.
  • Before that, it had already unconditionally unlinked and rebound the CLI socket, stealing it from the real daemon.

So a process that lost the only real race survived as a half-daemon: it owned the socket (and the CLI) but had no extension.

Fix

Make the WebSocket port the atomic singleton token, acquired before the CLI socket:

  1. Bind :19222 first. The OS guarantees exactly one binder.
  2. If the bind fails, another singleton is already live → exit instead of becoming a second, extension-less daemon (previously: "continue").
  3. Only the process that holds the port may clear/rebind the CLI socket (removed the unconditional unlink).

The pid-file election is kept as a fast path; the port bind is the authoritative backstop that closes the race.

Verification

  • decideSingletonGate() — new pure helper with unit tests for the win/lose outcomes.
  • Integration (isolated ports): starting a second --standalone daemon while the first holds the port → the second exits and the first's socket inode is unchanged (not hijacked). Confirmed both for the pid-file fast path and for the stale-pid-file case that actually triggered the bug:
    ws port 29232 already held by another daemon — another singleton is
    already running; exiting this duplicate (Failed to start server. Is port 29232 in use?)
    
  • bun test daemon/transport suite: 35 pass, 0 fail.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enforced stricter single-instance daemon behavior across both standalone and native modes.
  • Bug Fixes

    • Daemon now exits immediately when the WebSocket singleton instance can’t be acquired, instead of falling back to running without WebSocket.
    • Improved startup socket handling, including cleanup before binding Unix IPC sockets on non-Windows systems.
  • Tests

    • Added coverage for the singleton gating decision logic (serve vs. exit) in daemon lifecycle tests.

Two daemons could run at once, split-braining the system: one process
owned the CLI socket while another owned the WebSocket port the extension
connects to. The CLI then talked to an extension-less daemon and reported
"no extensions connected" / "context '<id>' not found", even though the
extension was connected — just to the other daemon.

Root cause: the singleton election was driven only by the pid file, which
is advisory and racy. The one OS-exclusive resource (the WS listen port)
was treated as optional — a daemon that failed to bind it logged
"continuing without WebSocket" and kept running, after unconditionally
unlinking and rebinding the CLI socket.

Fix: bind the WS port BEFORE claiming the CLI socket and treat it as the
authoritative singleton token. If the bind fails, another singleton is
already live, so exit instead of becoming a second, extension-less daemon.
Only the process that holds the port may clear/rebind the socket.

- decideSingletonGate(): tested pure decision for the win/lose outcomes
- index.ts: acquire WS port first; fatal on loss; remove the unconditional
  socket unlink so a loser can never hijack the CLI socket
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d70540c-eefd-4ce8-abb1-1ee99c071c26

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3b498 and 642dd3e.

📒 Files selected for processing (1)
  • daemon/index.ts

📝 Walkthrough

Walkthrough

The daemon startup now enforces a single shared WebSocket port via early singleton gating. Early WS binding is attempted upfront, and decideSingletonGate determines whether the daemon should serve or exit. If the port is unavailable, the process exits immediately. Socket binding is refactored to clean up stale files and properly shut down the WS server on errors.

Changes

WebSocket Singleton Gating and Daemon Startup

Layer / File(s) Summary
Singleton Gate Decision Contract
daemon/lifecycle.ts
New SingletonGateDecision type and decideSingletonGate function return serve when WebSocket port is acquired; otherwise return exit with exit code 0 and reason text varying by daemon role.
Early WebSocket Binding with Singleton Gate
daemon/index.ts
Daemon imports decideSingletonGate, attempts WS server startup early, calls the gate with WebSocket acquire status, and exits immediately if the gate denies serve; otherwise logs listening and continues.
Socket Binding with File Cleanup and Error Recovery
daemon/index.ts
Socket bind is refactored to clear stale SOCKET_PATH immediately before listen (non-Windows), removes the prior fallback allowing continuation without WebSocket, and now stops the WS server and exits on socket listen failure.
Singleton Gate Decision Tests
test/daemon-lifecycle.test.ts
New test cases verify gate returns serve when WebSocket port is acquired and exit with code 0 when unavailable, covering both standalone and native daemon roles.

Sequence Diagram

sequenceDiagram
  participant Daemon
  participant WSServer as WebSocket Server
  participant Gate as Singleton Gate
  participant CLISocket as CLI Socket
  Daemon->>WSServer: startWsServer()
  WSServer-->>Daemon: port acquired or failed
  Daemon->>Gate: decideSingletonGate(wsPortAcquired)
  alt Port acquired
    Gate-->>Daemon: { action: serve }
    Daemon->>CLISocket: Bind CLI socket
    CLISocket-->>Daemon: Listening
  else Port unavailable
    Gate-->>Daemon: { action: exit, exitCode: 0 }
    Daemon->>Daemon: Exit process
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A singleton gate now stands tall,
No two daemons heed the call.
Early binding, clean and true,
One WebSocket port, forever new. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title precisely describes the main change: preventing split-brain daemon scenarios by implementing singleton election. It aligns with the PR's core objective and the file changes, which introduce singleton gate logic and WebSocket port-based election.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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

🧹 Nitpick comments (2)
daemon/index.ts (1)

901-901: 💤 Low value

Consider more generic log message for WS binding failures.

The message assumes EADDRINUSE (port already in use), but other errors like EACCES (permission denied) are possible. While wsBindError.message is appended for debugging, a more generic prefix would be clearer.

♻️ Optional refinement
-  log(`ws port ${WS_PORT} already held by another daemon — ${singletonGate.reason}${wsBindError ? ` (${wsBindError.message})` : ""}`)
+  log(`failed to acquire ws port ${WS_PORT} — ${singletonGate.reason}${wsBindError ? ` (${wsBindError.message})` : ""}`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@daemon/index.ts` at line 901, Change the hardcoded "already held" log to a
more generic bind-failure message: update the log call that references WS_PORT,
singletonGate.reason and wsBindError to say something like "failed to bind ws
port" (or "WS port bind failure") and then append singletonGate.reason and the
wsBindError.message (if present) for details; ensure the prefix no longer
assumes EADDRINUSE so it covers EACCES and other errors while preserving the
existing diagnostic info.
test/daemon-lifecycle.test.ts (1)

95-105: 💤 Low value

Test coverage for singleton gate exit scenarios is good.

Both standalone and native daemon exit cases are verified with correct exitCode. Optionally, you could also verify the reason field differs between standalone and native modes for completeness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/daemon-lifecycle.test.ts` around lines 95 - 105, Add assertions to the
existing tests that also verify the returned decision.reason differs between
standalone and native modes: call decideSingletonGate for both wsPortAcquired:
false with standalone: true and standalone: false, then assert decision.reason
(or decision.reason?.message) contains the expected distinct text for each case
(e.g., mentions "standalone" vs "native" or whichever unique strings are
produced by decideSingletonGate). Keep the existing action and exitCode
assertions and only add these extra expect(...) checks to the two tests
referencing decideSingletonGate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@daemon/index.ts`:
- Line 901: Change the hardcoded "already held" log to a more generic
bind-failure message: update the log call that references WS_PORT,
singletonGate.reason and wsBindError to say something like "failed to bind ws
port" (or "WS port bind failure") and then append singletonGate.reason and the
wsBindError.message (if present) for details; ensure the prefix no longer
assumes EADDRINUSE so it covers EACCES and other errors while preserving the
existing diagnostic info.

In `@test/daemon-lifecycle.test.ts`:
- Around line 95-105: Add assertions to the existing tests that also verify the
returned decision.reason differs between standalone and native modes: call
decideSingletonGate for both wsPortAcquired: false with standalone: true and
standalone: false, then assert decision.reason (or decision.reason?.message)
contains the expected distinct text for each case (e.g., mentions "standalone"
vs "native" or whichever unique strings are produced by decideSingletonGate).
Keep the existing action and exitCode assertions and only add these extra
expect(...) checks to the two tests referencing decideSingletonGate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b77a42b0-436b-4c49-9f6b-a5ca6c859eb7

📥 Commits

Reviewing files that changed from the base of the PR and between 86e7eb6 and 5e3b498.

📒 Files selected for processing (3)
  • daemon/index.ts
  • daemon/lifecycle.ts
  • test/daemon-lifecycle.test.ts

ronaldeddings and others added 2 commits June 17, 2026 14:52
Resolve the daemon/index.ts conflict by taking main's post-0.17.2 structure
(the 0.17.2 release rewrote the daemon and added the native relay architecture).
The atomic WS-port singleton gate is reapplied onto that structure in the next
commit. daemon/lifecycle.ts (decideSingletonGate + SingletonGateDecision) and the
unit tests auto-merged cleanly from the original PR commit 5e3b498@vitorfhc's
authorship on the core helper and tests is preserved.
…architecture

Reapplies @vitorfhc's WS-port singleton-gate fix (PR Hacker-Valley-Media#104) onto the post-0.17.2
daemon. The 0.17.2 release rewrote daemon/index.ts and added the native relay
architecture, so the original diff no longer applied cleanly; the fix's intent is
unchanged and the split-brain bug it targets is still live on main.

- Extract the WS server into startWsServer() and acquire the WS port BEFORE the
  CLI socket — the port is the one OS-exclusive singleton token (validated against
  Bun docs/source + Linux/BSD man pages: Bun binds exclusively by default;
  macOS/Windows ignore reusePort; Linux defaults it off).
- decideSingletonGate(): a daemon that loses the WS-port race exits cleanly instead
  of "continuing without WebSocket" as a second, extension-less singleton.
- Only the WS-port holder may clear/rebind the CLI socket (removed the unconditional
  unlink that let a loser hijack it); scoped to the non-Windows branch.
- Stop the WS server if the CLI socket listen then fails.

decideSingletonGate + its unit tests auto-merged from the original commit 5e3b498,
so @vitorfhc's authorship on the core helper and tests is preserved. Verified:
typecheck clean, bun test 477 pass / 0 fail, capability-blind audit passes, and a
live two-daemon test confirms the duplicate exits without hijacking the socket
(split-brain reproduced on main, gone with this change).

Co-authored-by: Vitor Falcao <vitorfhcosta@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ronaldeddings

Copy link
Copy Markdown
Collaborator

Thanks for this, @vitorfhc — sharp diagnosis. Before integrating I validated every technical claim in the PR against primary sources (Bun docs/issues/blog + the Bun.Serve API reference, Linux socket(7)/unix(7)/bind(2)/flock(2), the LWN SO_REUSEPORT writeup, FreeBSD/macOS setsockopt(2), and the canonical SO socket-reuse answer). It all checks out on our stack (Bun 1.3.11, macOS-primary):

  • The WS listen port is genuinely OS-exclusive — without reusePort, Bun.serve throws EADDRINUSE; macOS/Windows ignore reusePort entirely, and Linux has defaulted it off since Bun #1443. So "the OS guarantees exactly one binder" holds.
  • The pid-file election really is advisory/racy (TOCTOU + stale-PID reuse), and the unconditional socket unlink was a genuine hijack vector.

Your branch had gone stale against main: the 0.17.2 release rewrote daemon/index.ts and added the native relay architecture, so the original diff no longer applied. To keep this as your PR (and your authorship) rather than opening a new one, I pushed two commits to your branch via maintainer-edit access:

  1. e40de16 — merge main (0.17.2). decideSingletonGate + your unit tests auto-merged cleanly from your original commit 5e3b498, so your git blame on the core helper and tests is preserved.
  2. 642dd3e — reapply the gate onto the new structure: extract startWsServer(), acquire the WS port before the CLI socket, exit-on-loss via decideSingletonGate, scope the socket unlink to the WS-port holder (non-Windows), and stop the WS server if the socket listen then fails.

Verification on the merged result:

  • bun test 477 pass / 0 fail, typecheck clean (all 3 configs), capability-blind audit passes.
  • Live two-daemon repro on isolated ports: without the fix the second daemon hijacks the CLI socket (inode changes) and logs continuing without WebSocket = split-brain; with the fix it logs ws port … already held … exiting this duplicate and exits, socket inode unchanged, the original daemon untouched.

The PR is now MERGEABLE. Thanks again — this directly protects --context routing. 🙏

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@daemon/index.ts`:
- Around line 1197-1231: The code allows WebSocket clients to self-assert native
delegation privileges by simply sending a NATIVE_REGISTER_TYPE request without
any credential verification, then immediately honoring privileged
NATIVE_DELEGATE_TYPE actions via the forwardDelegateToBridge function. Before
setting the __native flag in the NATIVE_REGISTER_TYPE handler and before
processing the NATIVE_DELEGATE_TYPE request in the subsequent block, add
validation logic to require a non-self-asserted capability or nonce credential
from the injected agent path to verify that the WebSocket client is genuinely
authorized to perform native operations, rather than allowing any client to
claim these privileges.
- Around line 538-551: The socket file cleanup via clearDaemonRuntimeFiles() is
invoked within bootstrapDaemonRole() before the WS port bind attempt, creating a
race condition where the socket could be cleared before WS acquisition is
confirmed. Move the socket cleanup operation to occur after successful WS port
acquisition and singleton gate confirmation (around the existing conditional
socket cleanup near line 1144-1146) to ensure the WS token is proven before
removing the socket file and prevent competing processes from acquiring the
token and orphaned socket path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d70540c-eefd-4ce8-abb1-1ee99c071c26

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3b498 and 642dd3e.

📒 Files selected for processing (1)
  • daemon/index.ts

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@daemon/index.ts`:
- Around line 1197-1231: The code allows WebSocket clients to self-assert native
delegation privileges by simply sending a NATIVE_REGISTER_TYPE request without
any credential verification, then immediately honoring privileged
NATIVE_DELEGATE_TYPE actions via the forwardDelegateToBridge function. Before
setting the __native flag in the NATIVE_REGISTER_TYPE handler and before
processing the NATIVE_DELEGATE_TYPE request in the subsequent block, add
validation logic to require a non-self-asserted capability or nonce credential
from the injected agent path to verify that the WebSocket client is genuinely
authorized to perform native operations, rather than allowing any client to
claim these privileges.
- Around line 538-551: The socket file cleanup via clearDaemonRuntimeFiles() is
invoked within bootstrapDaemonRole() before the WS port bind attempt, creating a
race condition where the socket could be cleared before WS acquisition is
confirmed. Move the socket cleanup operation to occur after successful WS port
acquisition and singleton gate confirmation (around the existing conditional
socket cleanup near line 1144-1146) to ensure the WS token is proven before
removing the socket file and prevent competing processes from acquiring the
token and orphaned socket path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4d70540c-eefd-4ce8-abb1-1ee99c071c26

📥 Commits

Reviewing files that changed from the base of the PR and between 5e3b498 and 642dd3e.

📒 Files selected for processing (1)
  • daemon/index.ts
🛑 Comments failed to post (2)
daemon/index.ts (2)

538-551: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect pre-gate runtime cleanup to verify it cannot unlink the daemon IPC socket.

rg -n -C8 'function clearDaemonRuntimeFiles|const clearDaemonRuntimeFiles|clearDaemonRuntimeFiles|socketPath|SOCKET_PATH|unlink' daemon/lifecycle.ts daemon/index.ts

Repository: Hacker-Valley-Media/Interceptor

Length of output: 17074


Pre-gate socket cleanup violates singleton ordering; move socket removal after WS acquisition.

The code calls bootstrapDaemonRole() at line 538, which invokes clearDaemonRuntimeFiles(deps, decision.reason) at line 526. This helper removes SOCKET_PATH (confirmed in daemon/lifecycle.ts:85), but only after the WS port bind is attempted at line 546. This ordering inverts the PR objective: the socket file must not be removed until after the WS singleton token is proven. A process that clears the socket early, fails the WS bind, and then exits will allow a competing process to acquire the WS token and claim the now-cleared socket path before losing the WS race completes.

Move clearDaemonRuntimeFiles() to after the WS gate succeeds (around line 1144–1146, where the socket file is already cleared again conditionally).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@daemon/index.ts` around lines 538 - 551, The socket file cleanup via
clearDaemonRuntimeFiles() is invoked within bootstrapDaemonRole() before the WS
port bind attempt, creating a race condition where the socket could be cleared
before WS acquisition is confirmed. Move the socket cleanup operation to occur
after successful WS port acquisition and singleton gate confirmation (around the
existing conditional socket cleanup near line 1144-1146) to ensure the WS token
is proven before removing the socket file and prevent competing processes from
acquiring the token and orphaned socket path.

1197-1231: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t let WebSocket clients self-assert native delegation privileges.

NATIVE_REGISTER_TYPE marks the socket as native, and NATIVE_DELEGATE_TYPE then forwards directly to the bridge before context validation. Require a non-self-asserted capability/nonce from the injected agent path before setting __native or honoring delegated bridge actions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@daemon/index.ts` around lines 1197 - 1231, The code allows WebSocket clients
to self-assert native delegation privileges by simply sending a
NATIVE_REGISTER_TYPE request without any credential verification, then
immediately honoring privileged NATIVE_DELEGATE_TYPE actions via the
forwardDelegateToBridge function. Before setting the __native flag in the
NATIVE_REGISTER_TYPE handler and before processing the NATIVE_DELEGATE_TYPE
request in the subsequent block, add validation logic to require a
non-self-asserted capability or nonce credential from the injected agent path to
verify that the WebSocket client is genuinely authorized to perform native
operations, rather than allowing any client to claim these privileges.

@ronaldeddings ronaldeddings merged commit fa0d6e3 into Hacker-Valley-Media:main Jun 17, 2026
2 checks passed
@ronaldeddings

Copy link
Copy Markdown
Collaborator

@vitorfhc that last comment was AI-assisted. I wanted to personally drop a THANK YOU for the contribution. This will help a lot with stability.

trillium pushed a commit to trillium/Interceptor that referenced this pull request Jun 18, 2026
…l flags

Bumps every surface to 0.17.3. The headline is a daemon stability fix that
hardens multi-context / multi-browser routing, plus first-class install
support for Chrome's sibling channels.

Daemon single-instance gate (Hacker-Valley-Media#104)
- Fixes a split-brain where two daemons could run at once — one owning the
  CLI socket, another owning the WebSocket port the extension connects to —
  so the CLI talked to an extension-less daemon and reported
  "context '<id>' not found (no extensions connected)" even though the
  extension was connected (just to the other daemon).
- The WebSocket listen port is now the atomic singleton token: it is bound
  BEFORE the CLI socket, and a daemon that loses that race exits cleanly
  instead of surviving as a second, extension-less daemon that unlinks and
  hijacks the CLI socket. The pid-file election is kept as a fast path; the
  port bind is the authoritative backstop.
- Stress-validated: hundreds of concurrent duplicate-daemon spawns, a full
  kill-and-respawn thundering herd, and ~120 concurrent tab-opens across
  three live extensions — zero split-brains, zero socket hijacks, zero
  mis-routes. (PR by @vitorfhc; reapplied onto the 0.17.2 relay architecture.)

Chrome channel install flags (Hacker-Valley-Media#98)
- scripts/install.sh adds --chrome-beta, --chrome-canary, --chrome-dev, and
  --chrome-for-testing as first-class install targets (Darwin), so you can
  run stable + Beta side by side, each routed to a distinct --context.
- Correct native-messaging host directory for Chrome for Testing (Chrome 146+
  uses a dedicated ~/Library/.../Google/ChromeForTesting dir). (PR by @trillium.)

Internal / CI
- bash -n shell-syntax gate over scripts/*.sh (Hacker-Valley-Media#108); install-modes test
  updates for the channel changes + first-installed default (Hacker-Valley-Media#109).

Rollup for users updating from 0.16.9
- This release also carries the full 0.17.x line: Electron/Chromium CDP app
  control, the Native Runtime Agent, and the capability-blind Extension Fabric.

Version: 0.17.2 -> 0.17.3 (package.json + extension manifest; the build stamps
the CLI / daemon / bridge from package.json).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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