Skip to content

fix(worker): dial control.json's bindAddress instead of hardcoding loopback - #30

Merged
Eyalm321 merged 4 commits into
mainfrom
fix/worker-honours-bindaddress
Sep 18, 2026
Merged

Eyalm321 merged 4 commits into
mainfrom
fix/worker-honours-bindaddress

Conversation

@Eyalm321

Copy link
Copy Markdown
Owner

The bug

worker.rs built its control-API base URL as http://127.0.0.1:{port}, ignoring control.json's
bindAddress:

let base = format!("http://127.0.0.1:{}", disco.port);

When the app binds a specific address (the mobile-client remote-access path) that is a
single-socket bind, so loopback is not listening. Every POST /queues/<q>/claim fails with
ConnectionRefused and the worker exits before claiming anything.

Why it is worth fixing rather than working around

The failure is near-silent from the outside. Worker panes sit running/idle and tasks stay
queued with claimedBy: null — which reads as an empty queue or a botched fan-out, not a config
fault. The only tell is a worker pane's tail:

[g182-w1] online — draining 'g182'
Error: reqwest::Error { kind: Request, url: "http://127.0.0.1:41419/queues/g182/claim",
  source: ConnectError("tcp connect error", 127.0.0.1:41419, Os { code: 111, ConnectionRefused }) }

Observed against a tailnet bind, where it made every queue fan-out in the goals system drain
nothing. It was diagnosed only after mistaking it for bad task payloads.

The fix

control_cli::base_url already encodes exactly the right rule, so it is reused rather than
duplicated: dial a specific address directly (bracketing IPv6), keep loopback for an unspecified
(0.0.0.0/::) or absent bind. bindAddress is #[serde(default)], so legacy control.json
files are byte-for-byte unaffected
.

Tests

Three tests in worker::tests, driven from real control.json payloads:

  • a specific IPv4 bind → http://100.120.216.17:41419
  • a specific IPv6 bind → http://[fd7a::1]:41419 (bracketed)
  • absent and 0.0.0.0 and :: → http://127.0.0.1:41419

The expression lives in a control_base() helper so the tests call the same path run() does.
Verified by mutation: reverting control_base to the hardcoded loopback turns the two
specific-address tests red (17 passed / 2 failed), and restoring it returns 19/19. A test that
rebuilt the expression itself would have passed either way — that earlier draft is what prompted
the refactor.

Full app-crate suite green: 160 + 2 passed, 0 failed. worker.rs is rustfmt-clean (several other
files in the crate have pre-existing fmt diffs; untouched).

Not covered here

The npm MCP bridge (hyperpanes-mcp, dist/control/discovery.js) has the identical defect — it
also builds http://127.0.0.1:<port> and drops bindAddress, so mcp__hyperpanes__* tools fail
the same way. That lives outside this repo.

Eyal Mizrachi and others added 2 commits August 17, 2026 20:55
…opback

`worker.rs` built its base URL as `http://127.0.0.1:{port}`, ignoring
`control.json`'s `bindAddress`. When the app binds a specific address (the
mobile-client remote-access path) that is a single-socket bind, so loopback is
not listening: every `POST /queues/<q>/claim` fails with ConnectionRefused and
the worker exits before claiming any task.

The failure is near-silent from the outside. Worker panes sit `running`/`idle`
and tasks stay `queued` with `claimedBy: null`, which reads as an empty queue or
a bad fan-out rather than a config fault — you only see it by reading a worker
pane's tail. Observed against a tailnet bind, where it made every queue fan-out
in the goals system drain nothing.

`control_cli::base_url` already encodes the right rule and is reused here rather
than duplicated: dial a specific address directly (bracketing IPv6), keep
loopback for an unspecified (`0.0.0.0`/`::`) or absent bind, so legacy
control.json files are byte-for-byte unaffected.

The expression lives in `control_base()` so the tests exercise the same path
`run()` does — reverting it to a hardcoded loopback turns them red, which a test
that rebuilt the expression itself would not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eyalm321 added a commit that referenced this pull request Sep 18, 2026
The backport of 47c6adf left `locate` returning
`Option<(ResolveResult, Option<u32>, Option<u32>, usize, usize, usize, f32, f32)>`,
which clippy refuses as `type_complexity` — so `lint (rs/crates/terminal-widget)`
went red. The push workflow on main runs tests but no lint job, so main looked
green and PR #30 was the first thing to surface it.

An eight-field positional tuple with three bare `usize` is a swap waiting to
happen anyway, so it becomes a `PathHit` struct with documented fields rather
than a `type` alias that would only quiet the lint. One call site.

Also runs rustfmt over the crate: the backport drifted `font.rs`, and only
core was fmt-checked at merge time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Eyalm321
Eyalm321 merged commit 2b1e013 into main Sep 18, 2026
27 checks passed
Eyalm321 added a commit that referenced this pull request Sep 18, 2026
… branches

Two independent CI faults, both observed on 2026-09-17.

**Pushes to main skipped half the gate.** `lint` and `build-gui` live in
verify.yml, which was `pull_request`-only, so anything landed straight on main
was unlinted and its GUI build unverified. Main went green while carrying a
clippy `type_complexity` error and, later, a tree where `cargo build --locked`
on the app crate failed; a PR opened against main was the first thing to notice
either. verify.yml now also runs on `push: branches: [main]`. build-gui's
`contains(github.event.pull_request.labels.*.name, ...)` clause is null-safe on
push, and the `needs.changes.outputs.rust` path filter drives it as before.

**test.yml fired twice for every PR-branch push.** A bare `push:` plus
`pull_request:` produced two runs whose `github.ref` differs
(refs/heads/<branch> vs refs/pull/N/merge), so the concurrency group could not
dedupe them; the surplus run raced the real one and cancelled jobs in it. The
`verify` gate reads `cancelled` as failure, which is the whole reason PR #30 sat
red for a month on `lint=cancelled` with nothing wrong in its code. `push:` is
now `branches: [main]`, so PR branches get exactly one run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eyalm321 added a commit that referenced this pull request Sep 18, 2026
The app crate reports its own version through `CARGO_PKG_VERSION` — the About
block, the crash reporter and the self-updater all read it — so it has to match
the tag or a v0.0.29 build claims to be 0.0.28 and the updater keeps offering an
"upgrade" the user already has.

What lands since v0.0.28:

- Security: `control.json` carried the master control token world-readable (0644
  under the process umask); `control-token` and `device-tokens.json` were
  chmod'd 0600 only after the rename, leaving a window. All three now go through
  `write_atomic_private`, which creates the temp file 0600 before the first byte.
  Verified on a live instance, not just in tests.
- Rendering: glyph coverage blends in linear light rather than sRGB bytes (thin
  strokes were rendering grey); synthetic bold no longer draws `i` as a smudge;
  CJK full-width glyphs are centred in their 2-cell span instead of
  left-clamped.
- Terminal: soft-wrapped links hit-test as one link; Shift+Enter reaches a pane
  over the control API; a CLI piped into `head` exits quietly instead of a crash
  dialog.
- Sessions: a re-attached pane no longer comes back blank; one directory reached
  two ways is one project again; two threads writing atomically no longer race
  for a temp name.
- Worker: dials `control.json`'s `bindAddress` instead of hardcoding loopback,
  so queue fan-out works on a non-loopback bind (#30).
- Perf: per-frame snapshot Vec and per-glyph mask clone removed from the render
  path; the GPU stack is behind an opt-in `gpu` feature.
- Features: per-pane dictation (push-to-talk) and AT-SPI text for the terminal
  grid — both still want a live pass.

Ten of the fixes are backported from bshuler/avada-terminal and three from
xiaoyuan0459/hyperpanes, authorship preserved on each.

Co-Authored-By: Claude Opus 5 (1M context) <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.

1 participant