generated from coulomb/repo-seed
Fixing bridge to haskelseed
This commit is contained in:
7
.codex/config.toml
Normal file
7
.codex/config.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[mcp_servers.ops-bridge]
|
||||
command = "uv"
|
||||
args = [
|
||||
"run",
|
||||
"python",
|
||||
"src/bridge/mcp_server/server.py",
|
||||
]
|
||||
19
Makefile
19
Makefile
@@ -1,13 +1,22 @@
|
||||
.PHONY: test lint install mcp-http mcp-stop
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
test:
|
||||
.PHONY: help setup test lint install mcp-http mcp-stop
|
||||
|
||||
help: ## List available make targets
|
||||
@awk 'BEGIN {FS = ":.*## "}; /^[a-zA-Z0-9_.-]+:.*## / {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
setup: ## Sync dependencies and install the bridge CLI wrapper
|
||||
uv sync --all-groups
|
||||
uv tool install -e . --force
|
||||
|
||||
test: ## Run the test suite
|
||||
uv run pytest
|
||||
|
||||
lint:
|
||||
lint: ## Run ruff lint checks
|
||||
uv run ruff check .
|
||||
|
||||
install:
|
||||
uv tool install -e .
|
||||
install: ## Install the bridge CLI wrapper
|
||||
uv tool install -e . --force
|
||||
|
||||
mcp-http: ## Start MCP server in SSE mode (default port 8002)
|
||||
BRIDGE_MCP_PORT=$${BRIDGE_MCP_PORT:-8002} uv run python src/bridge/mcp_server/server.py --http
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""End-to-end tunnel diagnostics for OpsBridge."""
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
@@ -13,6 +14,38 @@ from bridge.models import BridgeState, TunnelConfig
|
||||
from bridge.state import StateManager, _pid_alive
|
||||
|
||||
|
||||
def _remote_port_probe_command(remote_port: int) -> str:
|
||||
"""Build a portable remote shell probe for a listening TCP port."""
|
||||
return (
|
||||
f"port={remote_port}; "
|
||||
"if command -v ss >/dev/null 2>&1; then "
|
||||
"ss -tnlp 2>/dev/null | grep -q \":$port \" && echo ok || echo closed; "
|
||||
"elif command -v netstat >/dev/null 2>&1; then "
|
||||
"netstat -tnlp 2>/dev/null | "
|
||||
"grep -q \"[.:]$port[[:space:]]\" && echo ok || echo closed; "
|
||||
"else "
|
||||
"hex=$(printf '%04X' \"$port\"); "
|
||||
"awk -v p=\":$hex\" "
|
||||
"'NR > 1 && $4 == \"0A\" && index($2, p) { found = 1 } "
|
||||
"END { print found ? \"ok\" : \"closed\" }' "
|
||||
"/proc/net/tcp /proc/net/tcp6 2>/dev/null; "
|
||||
"fi"
|
||||
)
|
||||
|
||||
|
||||
def _probe_local_port(local_port: int) -> str:
|
||||
"""Check whether the local side of an SSH -L tunnel is accepting TCP."""
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", local_port), timeout=5):
|
||||
return "listening"
|
||||
except ConnectionRefusedError:
|
||||
return "closed"
|
||||
except socket.timeout:
|
||||
return "error:timeout"
|
||||
except OSError as e:
|
||||
return f"error:{e}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TunnelCheckResult:
|
||||
tunnel: str
|
||||
@@ -52,7 +85,10 @@ def check_tunnel(cfg: TunnelConfig, state_mgr: StateManager) -> TunnelCheckResul
|
||||
and ssh_process != "ok"
|
||||
)
|
||||
|
||||
# 3. SSH probe for remote port
|
||||
# 3. Port probe: reverse tunnels listen remotely; local tunnels listen here.
|
||||
if cfg.direction == "local":
|
||||
remote_port = _probe_local_port(cfg.local_port)
|
||||
else:
|
||||
key_path = str(Path(cfg.ssh_key).expanduser())
|
||||
cmd = [
|
||||
"ssh",
|
||||
@@ -61,7 +97,7 @@ def check_tunnel(cfg: TunnelConfig, state_mgr: StateManager) -> TunnelCheckResul
|
||||
"-o", "ConnectTimeout=5",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
f"{cfg.ssh_user}@{cfg.host}",
|
||||
f"ss -tnlp 2>/dev/null | grep -q ':{cfg.remote_port} ' && echo ok || echo closed",
|
||||
_remote_port_probe_command(cfg.remote_port),
|
||||
]
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
|
||||
@@ -6,7 +6,11 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from bridge.diagnostics import check_all_tunnels, check_tunnel
|
||||
from bridge.diagnostics import (
|
||||
_remote_port_probe_command,
|
||||
check_all_tunnels,
|
||||
check_tunnel,
|
||||
)
|
||||
from bridge.models import BridgeState, TunnelConfig
|
||||
from bridge.state import StateManager
|
||||
|
||||
@@ -32,6 +36,14 @@ def state_mgr(tmp_path):
|
||||
|
||||
|
||||
class TestCheckTunnel:
|
||||
def test_remote_port_probe_has_minimal_host_fallback(self):
|
||||
"""Remote probe supports minimal hosts without ss/netstat."""
|
||||
command = _remote_port_probe_command(18000)
|
||||
assert "command -v ss" in command
|
||||
assert "command -v netstat" in command
|
||||
assert "/proc/net/tcp" in command
|
||||
assert "/proc/net/tcp6" in command
|
||||
|
||||
def test_no_pid(self, tcfg, state_mgr):
|
||||
"""No PID file → ssh_process='no_pid', ok=False."""
|
||||
with patch("bridge.diagnostics.subprocess.run") as mock_run:
|
||||
@@ -83,6 +95,29 @@ class TestCheckTunnel:
|
||||
assert result.remote_port == "closed"
|
||||
assert result.ok is False
|
||||
|
||||
def test_local_direction_checks_local_port(self, tcfg, state_mgr):
|
||||
"""Local tunnels verify the local listener instead of a remote -R port."""
|
||||
local_cfg = TunnelConfig(
|
||||
name="local-tunnel",
|
||||
host="haskelseed.local",
|
||||
remote_port=1234,
|
||||
local_port=11234,
|
||||
ssh_user="root",
|
||||
ssh_key="~/.ssh/id_ops",
|
||||
actor="adm-bernd",
|
||||
direction="local",
|
||||
)
|
||||
state_mgr.write_pid("local-tunnel", 12345)
|
||||
with (
|
||||
patch("bridge.diagnostics._pid_alive", return_value=True),
|
||||
patch("bridge.diagnostics._probe_local_port", return_value="listening"),
|
||||
patch("bridge.diagnostics.subprocess.run") as mock_run,
|
||||
):
|
||||
result = check_tunnel(local_cfg, state_mgr)
|
||||
mock_run.assert_not_called()
|
||||
assert result.remote_port == "listening"
|
||||
assert result.ok is True
|
||||
|
||||
def test_ssh_timeout(self, tcfg, state_mgr):
|
||||
"""SSH probe timeout → remote_port='error:timeout'."""
|
||||
state_mgr.write_pid("test-tunnel", 12345)
|
||||
|
||||
56
workplans/ADHOC-2026-06-14.md
Normal file
56
workplans/ADHOC-2026-06-14.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: ADHOC-2026-06-14
|
||||
type: workplan
|
||||
title: "Ad hoc ops-bridge fixes for 2026-06-14"
|
||||
domain: custodian
|
||||
repo: ops-bridge
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: ops-bridge
|
||||
created: "2026-06-14"
|
||||
updated: "2026-06-14"
|
||||
state_hub_workstream_id: "fbc2ef7e-626f-4c6a-bdf8-c69bf29097ce"
|
||||
---
|
||||
|
||||
## Fix haskelseed bridge diagnostics
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-06-14-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "ffe6b8d8-889c-4ec4-8b64-00b77f86e39f"
|
||||
```
|
||||
|
||||
`haskelseed` is an Alpine host without `ss`, so `bridge check` reported
|
||||
reverse tunnel ports as closed even while SSH reverse listeners were present.
|
||||
Updated diagnostics to fall back from `ss` to `netstat` and then
|
||||
`/proc/net/tcp`/`tcp6`. Also fixed local-direction diagnostics so
|
||||
`nix-daemon-haskelseed` checks the local `-L` listener instead of probing a
|
||||
remote reverse port.
|
||||
|
||||
Verification:
|
||||
|
||||
- `state-hub-haskelseed` responded through `127.0.0.1:18000/state/health`.
|
||||
- `bridge check --json` reported all configured tunnels `ok: true`.
|
||||
- `python3 -m pytest tests/test_cli.py tests/test_diagnostics.py` passed.
|
||||
|
||||
## Make default target safe and add setup
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-06-14-T02
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "3b932955-0d75-4b95-9821-92bfa2dadbd0"
|
||||
```
|
||||
|
||||
Changed `make` to default to a help listing that only shows targets with
|
||||
`##` comments. Added `make setup` to run `uv sync --all-groups` and reinstall
|
||||
the editable `bridge` CLI wrapper through `uv tool install -e . --force`.
|
||||
|
||||
Verification:
|
||||
|
||||
- `uv sync --all-groups` succeeded and installed the project environment.
|
||||
- `make` listed targets only and did not run tests or setup.
|
||||
- `make setup` succeeded and installed the `bridge` executable.
|
||||
- `make test` passed all 235 tests.
|
||||
- `make lint` passed.
|
||||
Reference in New Issue
Block a user