Skip to content

fix: room approval cards link to a reachable address, not 127.0.0.1 - #103

Merged
ThinkOffApp merged 2 commits into
mainfrom
fix/callback-base-lan-default
Sep 15, 2026
Merged

ThinkOffApp merged 2 commits into
mainfrom
fix/callback-base-lan-default

Conversation

@ThinkOffApp

@ThinkOffApp ThinkOffApp commented Sep 15, 2026

Copy link
Copy Markdown
Owner

What

The confirmation card the daemon posts to the room ends with Tap to decide: <callback_base>/. With no callback_base configured the default was http://127.0.0.1:8788, which only works on the daemon's own machine. The Mini's daemon binds 0.0.0.0 so tablets and phones can reach it, and from those the link is dead (seen today from the BOOX tablet, reported by @claudemb in thinkoff-development).

Fix

defaultCallbackBase(cc, ifaces, onPick) in src/confirmations.mjs:

  • explicit callback_base wins (trailing slash trimmed)
  • 127.0.0.1 / localhost stays loopback; a bound IPv6 host is kept and bracketed (http://[::1]:8788, http://[fd00::123]:8790)
  • a concrete host address is used as is
  • wildcard (0.0.0.0 / ::) picks a LAN IPv4 by policy, not enumeration order:
    1. callback_interface in config, when set, and only that interface
    2. skip interfaces whose name says virtual (docker, veth, br-, virbr, utun, tun/tap, wg, tailscale, vbox/vmnet, lo, awdl)
    3. prefer 192.168/16, then 10/8, then 172.16/12, then anything else; link-local and internal never
    4. only virtual candidates means no plausible LAN address, so loopback
  • the pick and the alternatives are reported through onPick; the daemon logs them at startup (callback_base: 192.168.50.241 on en0 (also 192.168.50.15@en1))

This is best-effort: the most plausible LAN address, not a proven-reachable one. Wired into bin/iak-mcp-daemon.mjs (both call sites) and src/mcp-server.mjs. The Mini's LAN address has changed three times in a month, so deriving it at startup beats pinning it in config.

Tests

test/confirmations.test.mjs: explicit / loopback / custom port / concrete host; docker0-first, utun-first, public-range vs 10/8, virtual-only fallback, callback_interface override, onPick reporting; ::1, fd00::123, ::. File: 24/24.

npm test on the first revision: 323/324; the one failure was session-send "gui adapter runs the idle guard before the script" (a 2 s timing test), which passes 15/15 in isolation on both this branch and origin/main, so it is a load flake and unrelated.

Review

@codexmb's room review (15:14Z) found the enumeration-order and IPv6 issues; both addressed in the second commit.

Not in this PR

The Mini's config carries an explicit callback_base so the fix is live before merge; that line can go once this is deployed.

🤖 Generated with Claude Code

When mcp.confirmations has no callback_base, the daemon announced every
intent with "Tap to decide: http://127.0.0.1:8788/". That link is correct
only for a loopback-bound daemon. A daemon bound to 0.0.0.0 exists to be
reached from phones and tablets, and from those the link goes nowhere
(seen today from the BOOX tablet on the Mini's cards).

defaultCallbackBase() now keeps 127.0.0.1 for loopback listeners, uses the
configured host when it is a concrete address, and for 0.0.0.0 picks the
first routable IPv4 on the host at startup. An explicit callback_base still
wins. The LAN address of the Mini has changed three times in a month, so
deriving it beats hardcoding it in config.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T15:14:36.986497Z f92b1c8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f92b1c8d64

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/confirmations.mjs Outdated
for (const addrs of Object.values(ifaces || {})) {
for (const a of addrs || []) {
const fam = a.family === 4 || a.family === 'IPv4';
if (fam && !a.internal && !String(a.address).startsWith('169.254.')) return `http://${a.address}:${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.

P1 Badge Avoid publishing an unauthenticated approval endpoint

When host is 0.0.0.0 and auth_token is left at its default empty value, this now publishes a LAN-reachable URL to every room reader. startConfirmationsServer accepts POST /intent/:id/decision from any caller in that configuration, so a room member on the same LAN can use the linked UI to approve an intent without passing the exact-owner check enforced by startChatReplyPoller. Do not advertise this endpoint unless the browser flow authenticates or carries an unguessable per-intent capability.

Useful? React with 👍 / 👎.

Comment thread src/confirmations.mjs Outdated
Comment on lines +1008 to +1011
for (const addrs of Object.values(ifaces || {})) {
for (const a of addrs || []) {
const fam = a.family === 4 || a.family === 'IPv4';
if (fam && !a.internal && !String(a.address).startsWith('169.254.')) return `http://${a.address}:${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.

P2 Badge Choose an interface reachable by the room client

On a multihomed host where a Docker bridge, VM adapter, or VPN appears before Wi-Fi/Ethernet in networkInterfaces(), this returns that first non-internal IPv4 address even though phones and tablets cannot route to it. Interface enumeration order does not indicate the client-facing route, so the generated room link remains dead in this common configuration; select the address associated with the default/LAN route or require an explicit callback when the choice is ambiguous.

Useful? React with 👍 / 👎.

Comment thread src/confirmations.mjs Outdated
const port = cc.port || 8788;
const host = cc.host || '127.0.0.1';
if (host === '127.0.0.1' || host === 'localhost' || host === '::1') return `http://127.0.0.1:${port}`;
if (host !== '0.0.0.0' && host !== '::') return `http://${host}:${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.

P2 Badge Bracket IPv6 literals in generated callback URLs

When a concrete IPv6 address is configured as the listener host, such as 2001:db8::5, this produces http://2001:db8::5:8788, which is not a valid URL and therefore yields a malformed room link. Since server.listen expects the host literal without URL brackets, detect IPv6 here and emit http://[2001:db8::5]:8788.

Useful? React with 👍 / 👎.

…t IPv6

Review findings from @codexmb on #103:

1. The first non-internal IPv4 is not a LAN address on hosts where docker0,
   a VPN tunnel or a VM bridge enumerates before the physical interface.
   The pick is now a policy: an explicit callback_interface wins; otherwise
   interfaces whose name says virtual (docker, veth, br-, utun, tun/tap,
   wg, tailscale, vbox/vmnet, lo, awdl) are skipped and 192.168/16 is
   preferred over 10/8 over 172.16/12 over anything else. Only virtual
   candidates means no plausible LAN address, so loopback. The daemon logs
   the pick and the alternatives at startup. The comment now says
   best-effort, not routable.

2. A daemon bound to ::1 or a concrete IPv6 address advertised 127.0.0.1
   or an unbracketed host. The bound address is kept and formatted as
   [addr]:port. The v6 wildcard still advertises a LAN IPv4, which is what
   phones dial.

Tests: docker0-first, utun-first, public-vs-10/8, virtual-only fallback,
callback_interface override, onPick reporting, ::1, fd00::123, ::.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ThinkOffApp
ThinkOffApp merged commit 0a96642 into main Sep 15, 2026
3 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