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.
- tailctl-0.1.0/LICENSE +21 -0
- tailctl-0.1.0/PKG-INFO +143 -0
- tailctl-0.1.0/README.md +110 -0
- tailctl-0.1.0/pyproject.toml +77 -0
- tailctl-0.1.0/setup.cfg +4 -0
- tailctl-0.1.0/src/tailctl/__init__.py +1 -0
- tailctl-0.1.0/src/tailctl/cli.py +674 -0
- tailctl-0.1.0/src/tailctl/config.py +275 -0
- tailctl-0.1.0/src/tailctl/forwarder.py +123 -0
- tailctl-0.1.0/src/tailctl/instance_manager.py +863 -0
- tailctl-0.1.0/src/tailctl/liveness.py +137 -0
- tailctl-0.1.0/src/tailctl/log.py +43 -0
- tailctl-0.1.0/src/tailctl/paths.py +79 -0
- tailctl-0.1.0/src/tailctl/profiles.py +71 -0
- tailctl-0.1.0/src/tailctl/registry.py +180 -0
- tailctl-0.1.0/src/tailctl/signals.py +52 -0
- tailctl-0.1.0/src/tailctl/tailscale.py +365 -0
- tailctl-0.1.0/src/tailctl.egg-info/PKG-INFO +143 -0
- tailctl-0.1.0/src/tailctl.egg-info/SOURCES.txt +31 -0
- tailctl-0.1.0/src/tailctl.egg-info/dependency_links.txt +1 -0
- tailctl-0.1.0/src/tailctl.egg-info/entry_points.txt +2 -0
- tailctl-0.1.0/src/tailctl.egg-info/requires.txt +7 -0
- tailctl-0.1.0/src/tailctl.egg-info/top_level.txt +1 -0
- tailctl-0.1.0/tests/test_cli.py +272 -0
- tailctl-0.1.0/tests/test_config.py +267 -0
- tailctl-0.1.0/tests/test_fixtures.py +67 -0
- tailctl-0.1.0/tests/test_forwarder.py +70 -0
- tailctl-0.1.0/tests/test_instance_manager.py +410 -0
- tailctl-0.1.0/tests/test_liveness.py +133 -0
- tailctl-0.1.0/tests/test_log.py +68 -0
- tailctl-0.1.0/tests/test_paths.py +57 -0
- tailctl-0.1.0/tests/test_registry.py +72 -0
- 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
|
tailctl-0.1.0/README.md
ADDED
|
@@ -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"]
|
tailctl-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|