From 10c0ccc0972325ee4a57405fb203912a6d2b5b01 Mon Sep 17 00:00:00 2001 From: 1cbyc Date: Sat, 26 Sep 2026 08:27:17 +0000 Subject: [PATCH] Validate server ports before startup Co-authored-by: insisong --- taskuary/cli.py | 6 +++++- taskuary/config.py | 12 +++++++++++- tests/test_config.py | 11 +++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/taskuary/cli.py b/taskuary/cli.py index 8af78ba7..b689a711 100644 --- a/taskuary/cli.py +++ b/taskuary/cli.py @@ -4,6 +4,10 @@ from . import __version__, config +def _port(value): + try: return config.port_number(value) + except ValueError as e: raise argparse.ArgumentTypeError(str(e)) from None + def public_url(host, port) -> str: """0.0.0.0 / :: are bind addresses, not a place a browser can go.""" shown = '127.0.0.1' if host in ('0.0.0.0', '::') else host @@ -47,7 +51,7 @@ def open_when_ready(url: str, wait, open_it=None): def main(): ap = argparse.ArgumentParser(prog='taskuary', description='Your work AI assistant - the local-first agent work hub.') ap.add_argument('--host', help='override [server].host (0.0.0.0 to listen on all interfaces)') - ap.add_argument('--port', type=int, help='override [server].port') + ap.add_argument('--port', type=_port, help='override [server].port') ap.add_argument('--no-browser', action='store_true', help="don't open a browser tab when the server starts") ap.add_argument('--debug', action='store_true', help='verbose console logging (requests, report runs, errors)') ap.add_argument('--version', action='version', version=f'taskuary {__version__}') diff --git a/taskuary/config.py b/taskuary/config.py index 9687dbf9..cc3c25ff 100644 --- a/taskuary/config.py +++ b/taskuary/config.py @@ -8,6 +8,7 @@ a token stored on the volume. """ import json, os +from loguru import logger try: import tomllib except ImportError: import tomli as tomllib # py3.10 from pathlib import Path @@ -73,12 +74,21 @@ def _write(d: dict): try: p.chmod(0o600) except OSError: pass +def port_number(value) -> int: + """A TCP port accepted by sockets and server runners.""" + try: port = int(value) + except (TypeError, ValueError): raise ValueError('port must be a number from 1 to 65535') from None + if not 1 <= port <= 65535: raise ValueError('port must be a number from 1 to 65535') + return port + def _env_server() -> dict: """Non-empty TASKUARY_* overlays. Empty is unset — an injected '' must not disable a stored token.""" out = {} h, p, t = os.getenv('TASKUARY_HOST'), os.getenv('TASKUARY_PORT'), os.getenv('TASKUARY_TOKEN') if h: out['host'] = h - if p: out['port'] = int(p) + if p: + try: out['port'] = port_number(p) + except ValueError as e: logger.warning(f'ignoring TASKUARY_PORT={p!r}: {e}') if t: out['token'] = t return out diff --git a/tests/test_config.py b/tests/test_config.py index 69fe834a..131e5a67 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -36,6 +36,17 @@ def test_env_overrides_server_bind(self): self.assertEqual((cfg['server']['host'], cfg['server']['port'], cfg['server']['token']), ('0.0.0.0', 9000, 'abc')) + def test_invalid_env_port_is_ignored(self): + for value in ('abc', '0', '70000'): + with self.subTest(value=value), mock.patch.dict(os.environ, {'TASKUARY_PORT': value}, clear=True): + self.assertEqual(config._env_server(), {}) + + def test_cli_rejects_port_outside_socket_range(self): + with mock.patch('sys.argv', ['taskuary', '--port', '70000']): + with self.assertRaises(SystemExit) as stopped: + cli.main() + self.assertEqual(stopped.exception.code, 2) + def test_env_overrides_do_not_persist_on_agent_save(self): """Runtime overlays must not round-trip through save() — that's how Docker was writing host = 0.0.0.0 and token = None onto the volume."""