Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions nixos/services/pm-apps/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ in
tailnetAddress = lib.mkOption {
type = lib.types.str;
default = "100.99.113.103";
description = "The host's tailnet address; Grist and Pad bind it alongside loopback, as the compose files did.";
description = "The host's tailnet address; Grist and Pad bind it alongside loopback, as the compose files did, and the ready-for-agent tailnet socket forwards it to the loopback-only app (#1181).";
};
};

Expand Down Expand Up @@ -127,15 +127,45 @@ in
];
# The binary is installed by pm-stack/ready-for-agent/install.sh;
# state (SQLite) lives in ~/.local/share/ready-for-agent/ by
# upstream default. Bare --host binds all interfaces — Drew's
# 2026-10-25 call (LAN/tailnet fine; no public route exists).
ExecStart = "${home}/.local/bin/ready-for-agent start --no-open --host";
# upstream default. No --host flag: upstream's default bind is
# loopback only. The all-interfaces bind (bare --host) was
# retired by #1181 — it also claimed any future WAN-facing
# interface, broader than the stack's access model (loopback +
# tailnet; public only behind a password). Tailnet access is
# restored by the ready-for-agent-tailnet socket below, which
# forwards the tailnet address to the loopback listener.
ExecStart = "${home}/.local/bin/ready-for-agent start --no-open";
Restart = "on-failure";
RestartSec = 10;
};

Install.WantedBy = [ "default.target" ];
};

# Tailnet entry point for ready-for-agent (#1181): the app itself
# binds loopback only (single-address --host upstream), so the
# tailnet address is served by a socket-activated systemd proxy
# forwarding to 127.0.0.1:6056 — the same loopback + tailnet scope
# Grist and Pad get from their dual port publishes.
systemd.user.sockets.ready-for-agent-tailnet = {
Socket = {
ListenStream = "${cfg.tailnetAddress}:6056";
};
Comment on lines +151 to +153

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 Allow the tailnet socket to bind before Tailscale is ready

If the lingering user manager starts before Tailscale assigns cfg.tailnetAddress at boot, this socket fails with EADDRNOTAVAIL. It is enabled through the user's sockets.target, with neither address-readiness ordering nor a retry mechanism, so the later appearance of the tailnet address does not restore remote access without restarting the socket. Set FreeBind = true so the listener can be established before the address exists; the systemd socket documentation states that this option defaults to false and recommends it when binding a specific IP address.

Useful? React with 👍 / 👎.


Install.WantedBy = [ "sockets.target" ];
};

systemd.user.services.ready-for-agent-tailnet = {
Unit = {
Description = "ready-for-agent tailnet forward (socket-activated proxy to loopback)";
Requires = [ "ready-for-agent.service" ];
After = [ "ready-for-agent.service" ];
};

Service = {
ExecStart = "/run/current-system/sw/bin/systemd-socket-proxyd 127.0.0.1:6056";

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 Resolve the proxy executable from the systemd package

On the NixOS deployment, this points into the system profile's bin directory, but systemd-socket-proxyd is installed as a helper under lib/systemd, not bin; a repo-wide search found no wrapper or symlink exposing it at this path. Consequently, incoming tailnet connections activate a service that fails to execute, while the app now accepts only loopback connections, removing remote access. Reference ${config.systemd.package}/lib/systemd/systemd-socket-proxyd instead. The upstream service example likewise uses ExecStart=/usr/lib/systemd/systemd-socket-proxyd.

Useful? React with 👍 / 👎.

};
};
};
};
}
2 changes: 1 addition & 1 deletion pm-stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ health gate):

| Layer | Piece | Version / pin | Reach | Deployment definition |
| --------- | --------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------- |
| Dispatch | ready-for-agent | npm `ready-for-agent@1.1.0` (no container image exists upstream) | UI + GraphQL on `6056`, all interfaces (loopback + tailnet + LAN) | `ready-for-agent/` |
| Dispatch | ready-for-agent | npm `ready-for-agent@1.1.0` (no container image exists upstream) | UI + GraphQL on `6056`, loopback + tailnet (app loopback-only; tailnet via socket proxy, #1181) | `ready-for-agent/` |
| Planning | Pad | `ghcr.io/perpetualsoftware/pad:0.18.1`, pinned by digest | `127.0.0.1:7777` + host tailnet address (`PM_STACK_TAILNET_IP`):7777; no tunnel hostname | `pad/` |
| Tables | Grist | `gristlabs/grist-oss:v1.7.21`, pinned by digest | `127.0.0.1:8484` + host tailnet address (`PM_STACK_TAILNET_IP`):8484; no tunnel hostname | `grist/` |
| Dashboard | Aggregator | this repo (Bun app, user unit `pm-aggregator`) | `127.0.0.1:4870` + tailnet `amd-6.raptor-pumpkinseed.ts.net:4870`; no public route | `aggregator/` |
Expand Down
2 changes: 1 addition & 1 deletion pm-stack/aggregator/src/topology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function buildServices(cfg: Config): ServiceDef[] {
id: "ready-for-agent",
name: "ready-for-agent",
port: configuredPort(cfg.readyForAgent.baseUrl, 6056),
bind: "0.0.0.0 — every interface, incl. LAN (bind scope tracked in #1181)",
bind: "loopback + tailnet — app on 127.0.0.1 only; tailnet address via ready-for-agent-tailnet socket proxy (aligned in #1181)",
runsAs: "ready-for-agent.service",
probe: "http",
loopbackUrl: cfg.readyForAgent.baseUrl,
Expand Down
15 changes: 10 additions & 5 deletions pm-stack/ready-for-agent/systemd/ready-for-agent.service
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@
# and enables it. State (SQLite) lives in
# ~/.local/share/ready-for-agent/ by upstream default.
#
# The UI + GraphQL listen on port 6056. Since 2026-10-25 the unit
# binds all interfaces (bare --host): loopback + tailnet + LAN —
# Drew's call (LAN/tailnet access fine; a public endpoint only
# behind a password). No public route is configured.
# The UI + GraphQL listen on port 6056. The unit binds loopback
# only (no --host flag: upstream's default), aligned with the rest
# of the stack by #1181 — the 2026-10-25 all-interfaces bind (bare
# --host) also claimed any future WAN-facing interface, broader
# than the settled access model (loopback + tailnet; a public
# endpoint only behind a password). On the NixOS host the live unit
# comes from nixos/services/pm-apps (home-manager), which adds a
# socket-activated proxy serving the tailnet address; a standalone
# install of this file is loopback-only.
#
# PATH is explicit: the harness shells out to git, gh, and the
# fleet's OpenCode 2 (`opencode` from the per-user profile — v1 is
Expand All @@ -32,7 +37,7 @@ Wants=network-online.target
Type=simple
WorkingDirectory=%h/pm-stack/ready-for-agent
Environment=PATH=%h/.local/bin:/etc/profiles/per-user/user/bin:%h/.nix-profile/bin:/run/current-system/sw/bin:/usr/bin:/bin
ExecStart=%h/.local/bin/ready-for-agent start --no-open --host
ExecStart=%h/.local/bin/ready-for-agent start --no-open
Restart=on-failure
RestartSec=10

Expand Down
Loading