Integrate reviewed command-channel troubleshooting guidance

Preserve the exact product-intelligence candidate and clarify execution scope versus live channel admission without changing runtime behaviour.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot] 2026-10-02 00:00:47 +01:00
commit fc4deb6c17
3 changed files with 124 additions and 1 deletions

View file

@ -1000,6 +1000,11 @@ LXC guests** in Settings → System → General and any configured
guests and guests already linked to an online guest-local agent. In the latter
case, check that guest agent's Docker monitoring instead.
If the owning node says **Remote control blocked**, first check its
[command channel](#commands-enabled-but-remote-control-blocked). A successful
root `pct exec` check does not establish that Pulse can reach that node's agent;
do not repeat the guest probe when those results are already known.
The host-side path requires the guest's `docker` executable and
`/var/run/docker.sock` in the root `pct exec` context. A working Docker CLI in a
user's login session does not prove that this context can reach the daemon;
@ -1036,6 +1041,38 @@ Review output before posting and redact any private endpoint in an error. Do not
change LXC privilege, `keyctl`, socket permissions or Docker versions merely to
test a guess.
### Commands enabled but remote control blocked
**Remote control blocked** means the agent reports commands enabled, but Pulse
has no admitted command channel connected for it. Agent Doctor's
`commands command-capable · credential grants exec` line describes the reported
local command ceiling and credential scope, not a connected session. A fresh
monitoring report or **Automatic updates ready** also does not prove that this
separate WebSocket channel is working. Changing Proxmox API permissions cannot
repair a Pulse command-channel connection.
For a systemd agent, inspect the recent journal **locally on the affected node**:
```bash
sudo journalctl -u pulse-agent.service --since '15 minutes ago' -n 200 --no-pager --output=cat
```
Look for **Connected and registered with Pulse command server**, or
**WebSocket connection failed repeatedly, reconnecting** and its error. A
`dial websocket` error is a connection/handshake failure; `registration failed`
means registration was attempted but not accepted. Scope alone does not prove
that the credential's host/agent binding was admitted. No matching entry is
inconclusive: initial retries may be debug-only; do not restart the host or
enable server-wide debug logging to manufacture an error.
If reporting the problem, share only the failure stage and redacted error
reason (or say no command-channel entry is visible), not the full journal or
service configuration. Omit tokens, cookies, URLs, hostnames, addresses and
agent/token IDs. Keep saved identity and credentials intact while distinguishing
connection failures from admission failures; do not delete state, loosen TLS
verification or broaden permissions to force a connection. Guest-local Docker
monitoring with commands disabled remains an alternative to host-side discovery.
### Check Status
```bash
# Linux

View file

@ -1000,6 +1000,11 @@ LXC guests** in Settings → System → General and any configured
guests and guests already linked to an online guest-local agent. In the latter
case, check that guest agent's Docker monitoring instead.
If the owning node says **Remote control blocked**, first check its
[command channel](#commands-enabled-but-remote-control-blocked). A successful
root `pct exec` check does not establish that Pulse can reach that node's agent;
do not repeat the guest probe when those results are already known.
The host-side path requires the guest's `docker` executable and
`/var/run/docker.sock` in the root `pct exec` context. A working Docker CLI in a
user's login session does not prove that this context can reach the daemon;
@ -1036,6 +1041,38 @@ Review output before posting and redact any private endpoint in an error. Do not
change LXC privilege, `keyctl`, socket permissions or Docker versions merely to
test a guess.
### Commands enabled but remote control blocked
**Remote control blocked** means the agent reports commands enabled, but Pulse
has no admitted command channel connected for it. Agent Doctor's
`commands command-capable · credential grants exec` line describes the reported
local command ceiling and credential scope, not a connected session. A fresh
monitoring report or **Automatic updates ready** also does not prove that this
separate WebSocket channel is working. Changing Proxmox API permissions cannot
repair a Pulse command-channel connection.
For a systemd agent, inspect the recent journal **locally on the affected node**:
```bash
sudo journalctl -u pulse-agent.service --since '15 minutes ago' -n 200 --no-pager --output=cat
```
Look for **Connected and registered with Pulse command server**, or
**WebSocket connection failed repeatedly, reconnecting** and its error. A
`dial websocket` error is a connection/handshake failure; `registration failed`
means registration was attempted but not accepted. Scope alone does not prove
that the credential's host/agent binding was admitted. No matching entry is
inconclusive: initial retries may be debug-only; do not restart the host or
enable server-wide debug logging to manufacture an error.
If reporting the problem, share only the failure stage and redacted error
reason (or say no command-channel entry is visible), not the full journal or
service configuration. Omit tokens, cookies, URLs, hostnames, addresses and
agent/token IDs. Keep saved identity and credentials intact while distinguishing
connection failures from admission failures; do not delete state, loosen TLS
verification or broaden permissions to force a connection. Guest-local Docker
monitoring with commands disabled remains an alternative to host-side discovery.
### Check Status
```bash
# Linux

View file

@ -20,7 +20,10 @@ SECRET = "synthetic-docker-docs-secret"
def section(heading):
guide = (ROOT / "docs/UNIFIED_AGENT.md").read_text()
return guide.split("### " + heading + "\n", 1)[1].split("\n### ", 1)[0]
marker = "### " + heading + "\n"
if marker not in guide:
raise AssertionError(f"missing diagnostic section: {heading}")
return guide.split(marker, 1)[1].split("\n### ", 1)[0]
def command(heading):
@ -106,6 +109,52 @@ class DockerTroubleshootingDocsTest(unittest.TestCase):
self.assertIn(operation, recipe)
self.assertIn(operation, source)
def test_command_channel_guidance_does_not_confuse_scope_with_admission(self):
text = section("Commands enabled but remote control blocked")
for required in (
"no admitted command channel connected", "not a connected session",
"Automatic updates ready", "Changing Proxmox API permissions cannot",
"locally on the affected node", "`dial websocket`", "`registration failed`",
"No matching entry is\ninconclusive", "not the full journal",
"Omit tokens, cookies, URLs, hostnames, addresses", "Keep saved identity and credentials intact",
"do not delete state, loosen TLS", "commands disabled remains an alternative",
):
with self.subTest(required=required):
self.assertIn(required, text)
for label in ("Connected and registered with Pulse command server",
"WebSocket connection failed repeatedly, reconnecting"):
self.assertIn(label, text)
self.assertIn(label, (ROOT / "internal/hostagent/commands.go").read_text())
policy = (ROOT / "internal/api/connections_aggregator.go").read_text()
self.assertIn("agent reports command execution enabled, but no admitted command channel is connected", policy)
guest = section("Docker visible in one LXC but missing in another")
self.assertIn("#commands-enabled-but-remote-control-blocked", guest)
self.assertIn("do not repeat the guest probe", guest)
self.assertNotRegex(text, r"--property=Environment|systemctl (?:cat|restart)|--insecure")
def test_command_channel_journal_check_is_bounded_and_read_only(self):
recipe = command("Commands enabled but remote control blocked")
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
for name, body in (
("sudo", '#!/bin/sh\nexec "$@"\n'),
("journalctl", '#!/usr/bin/python3\nimport json, sys\nfrom pathlib import Path\n'
'Path("' + str(root / "argv.json") + '").write_text(json.dumps(sys.argv[1:]))\n'
'print("WebSocket connection failed repeatedly, reconnecting")\n'),
):
path = root / name
path.write_text(body)
path.chmod(0o700)
env = dict(os.environ, PATH=f"{root}:{os.environ['PATH']}", SYNTHETIC_SECRET=SECRET)
result = subprocess.run(["bash", "-eu", "-c", recipe], env=env,
capture_output=True, text=True, timeout=5)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads((root / "argv.json").read_text()), [
"-u", "pulse-agent.service", "--since", "15 minutes ago", "-n", "200",
"--no-pager", "--output=cat",
])
self.assertNotIn(SECRET, result.stdout + result.stderr)
def run_guest(self, *, socket="present", docker="ok", pct="ok", cli=True):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)