tailctl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. tailctl-0.1.0/LICENSE +21 -0
  2. tailctl-0.1.0/PKG-INFO +143 -0
  3. tailctl-0.1.0/README.md +110 -0
  4. tailctl-0.1.0/pyproject.toml +77 -0
  5. tailctl-0.1.0/setup.cfg +4 -0
  6. tailctl-0.1.0/src/tailctl/__init__.py +1 -0
  7. tailctl-0.1.0/src/tailctl/cli.py +674 -0
  8. tailctl-0.1.0/src/tailctl/config.py +275 -0
  9. tailctl-0.1.0/src/tailctl/forwarder.py +123 -0
  10. tailctl-0.1.0/src/tailctl/instance_manager.py +863 -0
  11. tailctl-0.1.0/src/tailctl/liveness.py +137 -0
  12. tailctl-0.1.0/src/tailctl/log.py +43 -0
  13. tailctl-0.1.0/src/tailctl/paths.py +79 -0
  14. tailctl-0.1.0/src/tailctl/profiles.py +71 -0
  15. tailctl-0.1.0/src/tailctl/registry.py +180 -0
  16. tailctl-0.1.0/src/tailctl/signals.py +52 -0
  17. tailctl-0.1.0/src/tailctl/tailscale.py +365 -0
  18. tailctl-0.1.0/src/tailctl.egg-info/PKG-INFO +143 -0
  19. tailctl-0.1.0/src/tailctl.egg-info/SOURCES.txt +31 -0
  20. tailctl-0.1.0/src/tailctl.egg-info/dependency_links.txt +1 -0
  21. tailctl-0.1.0/src/tailctl.egg-info/entry_points.txt +2 -0
  22. tailctl-0.1.0/src/tailctl.egg-info/requires.txt +7 -0
  23. tailctl-0.1.0/src/tailctl.egg-info/top_level.txt +1 -0
  24. tailctl-0.1.0/tests/test_cli.py +272 -0
  25. tailctl-0.1.0/tests/test_config.py +267 -0
  26. tailctl-0.1.0/tests/test_fixtures.py +67 -0
  27. tailctl-0.1.0/tests/test_forwarder.py +70 -0
  28. tailctl-0.1.0/tests/test_instance_manager.py +410 -0
  29. tailctl-0.1.0/tests/test_liveness.py +133 -0
  30. tailctl-0.1.0/tests/test_log.py +68 -0
  31. tailctl-0.1.0/tests/test_paths.py +57 -0
  32. tailctl-0.1.0/tests/test_registry.py +72 -0
  33. tailctl-0.1.0/tests/test_tailscale.py +340 -0
tailctl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DRYCodeWorks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tailctl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: tailctl
3
+ Version: 0.1.0
4
+ Summary: Per-identity Tailscale networking for parallel sessions on a single Mac
5
+ Author-email: DRYCodeWorks <dan@drycodeworks.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://drycodeworks.com
8
+ Project-URL: Repository, https://github.com/DRYCodeWorks/tailctl
9
+ Project-URL: Issues, https://github.com/DRYCodeWorks/tailctl/issues
10
+ Keywords: tailscale,vpn,wireguard,networking,socks5,proxy,cli,macos,multi-account,ai-agents,parallel-sessions
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS :: MacOS X
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: System :: Networking
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: PyYAML>=6.0
27
+ Requires-Dist: psutil>=5.9
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.4; extra == "dev"
30
+ Requires-Dist: pytest-timeout>=2.2; extra == "dev"
31
+ Requires-Dist: ruff>=0.4; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ <p align="center">
35
+ <img src="https://raw.githubusercontent.com/DRYCodeWorks/tailctl/main/docs/tailctl-banner.jpg" alt="tailctl — per-identity Tailscale networking for parallel sessions on a single Mac" width="100%">
36
+ </p>
37
+
38
+ # tailctl
39
+
40
+ Per-identity Tailscale networking for parallel sessions on a single Mac.
41
+
42
+ `tailctl` gives each process (e.g. a Claude Code session in its own Ghostty window)
43
+ its **own** Tailscale identity — a private, headless userspace `tailscaled` with a
44
+ per-identity SOCKS5/HTTP proxy and localhost port-forwards — instead of switching
45
+ the machine-global Tailscale account. Many identities run at once, and the GUI
46
+ Tailscale app (the browser's default tailnet) is never touched. No FIFO queue, no
47
+ shared network, no flapping.
48
+
49
+ Agent traffic is opted in per command via proxy env vars (`tailctl run`) or stable
50
+ localhost forwards for native-TCP clients (ClickHouse, Postgres, …).
51
+
52
+ No always-on daemon beyond the per-identity tailscaled instances. Single-laptop scope.
53
+
54
+ ## Status
55
+
56
+ Working prototype — macOS-only, single-laptop scope.
57
+
58
+ ## Install
59
+
60
+ Requires macOS, Python 3.11+, and the Homebrew Tailscale daemon (see
61
+ [Requirements](#requirements)).
62
+
63
+ ```
64
+ brew install tailscale
65
+ pipx install tailctl # or: pip install tailctl
66
+ ```
67
+
68
+ Or from source:
69
+
70
+ ```
71
+ brew install tailscale
72
+ git clone https://github.com/DRYCodeWorks/tailctl && cd tailctl
73
+ python3 -m venv .venv && .venv/bin/pip install -e .
74
+ ln -sf "$PWD/.venv/bin/tailctl" ~/.local/bin/tailctl # put it on PATH
75
+ ```
76
+
77
+ ## Quick view
78
+
79
+ ```
80
+ tailctl init # scaffold ~/.tailctl/profiles.yaml
81
+ tailctl doctor # validate config + probe daemon + verify accounts
82
+ tailctl bootstrap <name> --tailnet <t> [--exit-node <e>] [--port-forward svc=host:port]
83
+ # onboard a NEW tailnet: discovers account_id,
84
+ # appends a profile (preserving comments), prints
85
+ # the key steps (mint in console -> store in BWS)
86
+ tailctl up acme-dev # spawn a userspace tailscaled for the profile
87
+ # (first time: prints a one-time auth URL)
88
+ tailctl run acme-dev -- curl http://service.internal # route a command via that identity
89
+ tailctl run acme-dev -- clickhouse-client --host 127.0.0.1 --port $CLICKHOUSE_ADDR
90
+ tailctl ps # list running instances + their proxy ports/forwards
91
+ tailctl down acme-dev # release (refcount--); stops the daemon at zero
92
+ ```
93
+
94
+ `run` sets `ALL_PROXY`/`HTTPS_PROXY`/`HTTP_PROXY` (+ `NO_PROXY`) so proxy-aware
95
+ clients route through the identity, and exports `<SERVICE>_ADDR=127.0.0.1:<port>`
96
+ for each configured `port_forward` so native-TCP clients connect to localhost.
97
+
98
+ ## profiles.yaml
99
+
100
+ ```yaml
101
+ default: acme-dev
102
+ tailscaled_binary: /opt/homebrew/opt/tailscale/bin/tailscaled
103
+ profiles:
104
+ acme-dev:
105
+ account_id: a2c4
106
+ tailnet: acme.example.ts.net
107
+ exit_node: tailscale-subnet-router-development
108
+ accept_routes: true
109
+ auth_key_env: ACME_TS_AUTHKEY # optional: env var holding a Tailscale
110
+ # auth key (from BWS) for non-interactive
111
+ # first login — no browser URL
112
+ port_forwards:
113
+ - service: clickhouse # exported as CLICKHOUSE_ADDR
114
+ remote_host: 100.64.0.10
115
+ remote_port: 9000
116
+ ```
117
+
118
+ ## Authentication
119
+
120
+ Each profile is a distinct device on its tailnet, authenticated **once** — the
121
+ login persists in the profile's statedir and is reused on every later `up`/`run`
122
+ (teardown never logs out). Two ways to do that one-time login:
123
+
124
+ - **Auth key (recommended):** set `auth_key_env` to an env var holding a
125
+ Tailscale auth key (minted in the tailnet's admin console, stored in BWS).
126
+ First `up` logs in non-interactively — no browser.
127
+ - **Interactive:** with no auth key, the first `up` prints a login URL to open
128
+ once.
129
+
130
+ Profiles that share an account but differ by exit node (e.g. Acme
131
+ dev/staging/prod) are separate profiles → separate nodes → one auth each.
132
+
133
+ ## Requirements
134
+
135
+ - macOS
136
+ - Homebrew Tailscale for the userspace daemon: `brew install tailscale`
137
+ (coexists with the GUI Tailscale.app, which keeps the browser's default tailnet)
138
+ - Python 3.11+
139
+ - Each profile's Tailscale account authenticated once per profile on first `up`
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,110 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/DRYCodeWorks/tailctl/main/docs/tailctl-banner.jpg" alt="tailctl — per-identity Tailscale networking for parallel sessions on a single Mac" width="100%">
3
+ </p>
4
+
5
+ # tailctl
6
+
7
+ Per-identity Tailscale networking for parallel sessions on a single Mac.
8
+
9
+ `tailctl` gives each process (e.g. a Claude Code session in its own Ghostty window)
10
+ its **own** Tailscale identity — a private, headless userspace `tailscaled` with a
11
+ per-identity SOCKS5/HTTP proxy and localhost port-forwards — instead of switching
12
+ the machine-global Tailscale account. Many identities run at once, and the GUI
13
+ Tailscale app (the browser's default tailnet) is never touched. No FIFO queue, no
14
+ shared network, no flapping.
15
+
16
+ Agent traffic is opted in per command via proxy env vars (`tailctl run`) or stable
17
+ localhost forwards for native-TCP clients (ClickHouse, Postgres, …).
18
+
19
+ No always-on daemon beyond the per-identity tailscaled instances. Single-laptop scope.
20
+
21
+ ## Status
22
+
23
+ Working prototype — macOS-only, single-laptop scope.
24
+
25
+ ## Install
26
+
27
+ Requires macOS, Python 3.11+, and the Homebrew Tailscale daemon (see
28
+ [Requirements](#requirements)).
29
+
30
+ ```
31
+ brew install tailscale
32
+ pipx install tailctl # or: pip install tailctl
33
+ ```
34
+
35
+ Or from source:
36
+
37
+ ```
38
+ brew install tailscale
39
+ git clone https://github.com/DRYCodeWorks/tailctl && cd tailctl
40
+ python3 -m venv .venv && .venv/bin/pip install -e .
41
+ ln -sf "$PWD/.venv/bin/tailctl" ~/.local/bin/tailctl # put it on PATH
42
+ ```
43
+
44
+ ## Quick view
45
+
46
+ ```
47
+ tailctl init # scaffold ~/.tailctl/profiles.yaml
48
+ tailctl doctor # validate config + probe daemon + verify accounts
49
+ tailctl bootstrap <name> --tailnet <t> [--exit-node <e>] [--port-forward svc=host:port]
50
+ # onboard a NEW tailnet: discovers account_id,
51
+ # appends a profile (preserving comments), prints
52
+ # the key steps (mint in console -> store in BWS)
53
+ tailctl up acme-dev # spawn a userspace tailscaled for the profile
54
+ # (first time: prints a one-time auth URL)
55
+ tailctl run acme-dev -- curl http://service.internal # route a command via that identity
56
+ tailctl run acme-dev -- clickhouse-client --host 127.0.0.1 --port $CLICKHOUSE_ADDR
57
+ tailctl ps # list running instances + their proxy ports/forwards
58
+ tailctl down acme-dev # release (refcount--); stops the daemon at zero
59
+ ```
60
+
61
+ `run` sets `ALL_PROXY`/`HTTPS_PROXY`/`HTTP_PROXY` (+ `NO_PROXY`) so proxy-aware
62
+ clients route through the identity, and exports `<SERVICE>_ADDR=127.0.0.1:<port>`
63
+ for each configured `port_forward` so native-TCP clients connect to localhost.
64
+
65
+ ## profiles.yaml
66
+
67
+ ```yaml
68
+ default: acme-dev
69
+ tailscaled_binary: /opt/homebrew/opt/tailscale/bin/tailscaled
70
+ profiles:
71
+ acme-dev:
72
+ account_id: a2c4
73
+ tailnet: acme.example.ts.net
74
+ exit_node: tailscale-subnet-router-development
75
+ accept_routes: true
76
+ auth_key_env: ACME_TS_AUTHKEY # optional: env var holding a Tailscale
77
+ # auth key (from BWS) for non-interactive
78
+ # first login — no browser URL
79
+ port_forwards:
80
+ - service: clickhouse # exported as CLICKHOUSE_ADDR
81
+ remote_host: 100.64.0.10
82
+ remote_port: 9000
83
+ ```
84
+
85
+ ## Authentication
86
+
87
+ Each profile is a distinct device on its tailnet, authenticated **once** — the
88
+ login persists in the profile's statedir and is reused on every later `up`/`run`
89
+ (teardown never logs out). Two ways to do that one-time login:
90
+
91
+ - **Auth key (recommended):** set `auth_key_env` to an env var holding a
92
+ Tailscale auth key (minted in the tailnet's admin console, stored in BWS).
93
+ First `up` logs in non-interactively — no browser.
94
+ - **Interactive:** with no auth key, the first `up` prints a login URL to open
95
+ once.
96
+
97
+ Profiles that share an account but differ by exit node (e.g. Acme
98
+ dev/staging/prod) are separate profiles → separate nodes → one auth each.
99
+
100
+ ## Requirements
101
+
102
+ - macOS
103
+ - Homebrew Tailscale for the userspace daemon: `brew install tailscale`
104
+ (coexists with the GUI Tailscale.app, which keeps the browser's default tailnet)
105
+ - Python 3.11+
106
+ - Each profile's Tailscale account authenticated once per profile on first `up`
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,77 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tailctl"
7
+ version = "0.1.0"
8
+ description = "Per-identity Tailscale networking for parallel sessions on a single Mac"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "DRYCodeWorks", email = "dan@drycodeworks.com" }]
13
+ keywords = [
14
+ "tailscale",
15
+ "vpn",
16
+ "wireguard",
17
+ "networking",
18
+ "socks5",
19
+ "proxy",
20
+ "cli",
21
+ "macos",
22
+ "multi-account",
23
+ "ai-agents",
24
+ "parallel-sessions",
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Environment :: Console",
29
+ "Intended Audience :: Developers",
30
+ "Intended Audience :: System Administrators",
31
+ "License :: OSI Approved :: MIT License",
32
+ "Operating System :: MacOS :: MacOS X",
33
+ "Programming Language :: Python :: 3",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Topic :: System :: Networking",
38
+ "Topic :: Utilities",
39
+ ]
40
+ dependencies = [
41
+ "PyYAML>=6.0",
42
+ "psutil>=5.9",
43
+ ]
44
+
45
+ [project.urls]
46
+ Homepage = "https://drycodeworks.com"
47
+ Repository = "https://github.com/DRYCodeWorks/tailctl"
48
+ Issues = "https://github.com/DRYCodeWorks/tailctl/issues"
49
+
50
+ [project.optional-dependencies]
51
+ dev = [
52
+ "pytest>=7.4",
53
+ "pytest-timeout>=2.2",
54
+ "ruff>=0.4",
55
+ ]
56
+
57
+ [project.scripts]
58
+ tailctl = "tailctl.cli:main"
59
+
60
+ [tool.setuptools.packages.find]
61
+ where = ["src"]
62
+
63
+ [tool.pytest.ini_options]
64
+ testpaths = ["tests"]
65
+ addopts = "-q --strict-markers"
66
+ markers = [
67
+ "integration: requires real tailscale CLI; not run by default",
68
+ "smoke: parallel smoke test; requires TAILCTL_RUN_REAL_TAILSCALE=1",
69
+ ]
70
+ timeout = 10
71
+
72
+ [tool.ruff]
73
+ line-length = 100
74
+ target-version = "py311"
75
+
76
+ [tool.ruff.lint]
77
+ select = ["E", "F", "W", "I", "B", "UP"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"