heinzel-tailscale 0.9.1__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. heinzel_tailscale-0.9.1/.gitignore +15 -0
  2. heinzel_tailscale-0.9.1/CHANGELOG.md +8 -0
  3. heinzel_tailscale-0.9.1/LICENSE +21 -0
  4. heinzel_tailscale-0.9.1/PKG-INFO +68 -0
  5. heinzel_tailscale-0.9.1/README.md +54 -0
  6. heinzel_tailscale-0.9.1/plugin-lock.json +41 -0
  7. heinzel_tailscale-0.9.1/pyproject.toml +131 -0
  8. heinzel_tailscale-0.9.1/requirements.lock +1163 -0
  9. heinzel_tailscale-0.9.1/src/heinzel_tailscale/__init__.py +72 -0
  10. heinzel_tailscale-0.9.1/src/heinzel_tailscale/_cli.py +164 -0
  11. heinzel_tailscale-0.9.1/src/heinzel_tailscale/_localapi.py +189 -0
  12. heinzel_tailscale-0.9.1/src/heinzel_tailscale/_lock.py +53 -0
  13. heinzel_tailscale-0.9.1/src/heinzel_tailscale/_mapping.py +138 -0
  14. heinzel_tailscale-0.9.1/src/heinzel_tailscale/_models.py +60 -0
  15. heinzel_tailscale-0.9.1/src/heinzel_tailscale/_readiness.py +32 -0
  16. heinzel_tailscale-0.9.1/src/heinzel_tailscale/_service.py +296 -0
  17. heinzel_tailscale-0.9.1/tests/browser/web_test.py +151 -0
  18. heinzel_tailscale-0.9.1/tests/conftest.py +21 -0
  19. heinzel_tailscale-0.9.1/tests/fixtures/local_http.py +13 -0
  20. heinzel_tailscale-0.9.1/tests/fixtures/tailscale_web.py +161 -0
  21. heinzel_tailscale-0.9.1/tests/integration/cli_test.py +141 -0
  22. heinzel_tailscale-0.9.1/tests/integration/host_test.py +552 -0
  23. heinzel_tailscale-0.9.1/tests/integration/lock_test.py +48 -0
  24. heinzel_tailscale-0.9.1/tests/integration/origin_policy_test.py +82 -0
  25. heinzel_tailscale-0.9.1/tests/unit/cli_test.py +74 -0
  26. heinzel_tailscale-0.9.1/tests/unit/localapi_test.py +166 -0
  27. heinzel_tailscale-0.9.1/tests/unit/mapping_test.py +131 -0
  28. heinzel_tailscale-0.9.1/tests/unit/readiness_test.py +66 -0
  29. heinzel_tailscale-0.9.1/tests/unit/status_test.py +185 -0
  30. heinzel_tailscale-0.9.1/web/build.py +32 -0
  31. heinzel_tailscale-0.9.1/web/e2e/tailscale.mjs +183 -0
  32. heinzel_tailscale-0.9.1/web/package.json +8 -0
  33. heinzel_tailscale-0.9.1/web/pnpm-lock.yaml +24 -0
@@ -0,0 +1,15 @@
1
+ .venv/
2
+ .tools/
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .pyright/
7
+ .coverage*
8
+ htmlcov/
9
+ node_modules/
10
+ dist/
11
+ release-dist/
12
+ *.egg-info/
13
+ .acceptance/
14
+ .preview/
15
+ .candidates/
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.9.1
4
+
5
+ Move this plugin to its independent repository project for the Heinzel 0.9.1 plugin contract.
6
+ Own its dependencies, tests and artifacts; consume public contracts without concrete provider dependencies.
7
+
8
+ Earlier history is preserved in the source repository named in README.md.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Helmut Hoffer von Ankershoffen
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.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.5
2
+ Name: heinzel-tailscale
3
+ Version: 0.9.1
4
+ Summary: Share the authenticated web app privately through Tailscale Serve: a Heinzelmännchen plugin.
5
+ Project-URL: Source, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/tree/main/plugins/heinzel-tailscale
6
+ Project-URL: Changelog, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/blob/main/plugins/heinzel-tailscale/CHANGELOG.md
7
+ Project-URL: Issues, https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/issues
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: heinzel<0.10,>=0.9.1
12
+ Requires-Dist: httpx>=0.28
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Tailscale private web sharing
16
+
17
+ A first-party adapter for the installed Tailscale client. It keeps the web app's bearer/device authentication and
18
+ loopback listener; tailnet membership does not grant access to the backend owner's operations or terminal.
19
+
20
+ Install the released plugin from Discover Plugins, or run `hz system plugin add tailscale --confirm`. The core PyPI
21
+ package does not bundle this separate plugin distribution, so add it explicitly after installing Heinzel. Tailscale
22
+ itself must already be installed and signed in; hz does not install it, log in, grant HTTPS consent or change OS
23
+ permissions for you.
24
+ On macOS it also detects the CLI bundled in `/Applications/Tailscale.app` and explicitly selects CLI mode, as
25
+ described in [Tailscale's CLI documentation](https://tailscale.com/docs/reference/tailscale-cli?tab=macos).
26
+
27
+ In Settings or the footer's sharing icon, turn **Share over Tailscale** on. The control checks the node and existing
28
+ Serve mappings, merges its exact HTTPS origin into your user configuration and configures its loopback target. The
29
+ running web app applies the updated origin policy immediately; it does not require a backend restart. It shows ON and
30
+ a URL only after the owning authenticated web app verifies its actual port, active origin policy and matching Serve
31
+ configuration. A CLI/stdio MCP request saves the same intent but reports verification pending until that web owner
32
+ verifies it. Status reads never enable sharing.
33
+
34
+ ```sh
35
+ hz tailscale status --json
36
+ hz tailscale enable --json
37
+ hz tailscale disable --json
38
+ ```
39
+
40
+ The corresponding private operations are available through authenticated API and stdio MCP. They are absent from
41
+ the unauthenticated REST and HTTP MCP heads. In-process calls use the actual bound web port. A separate CLI retains
42
+ a previously observed port override; it cannot discover an arbitrary ephemeral web listener or claim it is ready.
43
+
44
+ **Copy URL** copies a working link with the current `#token=...` session fragment when the authenticated session has
45
+ one. Treat that link like a password: anyone who receives it can use that session until it is revoked or expires. If
46
+ there is no session token, the control copies only the address. A fresh phone browser also needs authentication: use
47
+ the separate **Copy sign-in link** action from the session owner, then pair that browser. Pairing a device never grants
48
+ it authority to pair another device. See [private web sharing](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/PRIVATE_WEB_SHARING.md) for authentication
49
+ and revocation.
50
+
51
+ Turning sharing off removes only the still-matching hz root handler and any origin permission this plugin added.
52
+ Other paths, listeners, services and manually configured origins remain. An existing conflicting root, public
53
+ Funnel setting, foreground session or higher-priority origin policy is refused. A changed Serve revision or unknown
54
+ write outcome stops without retry or rollback; inspect current status before taking another action.
55
+ Matching an existing manual handler does not make it owned by hz, so enabling refuses to adopt it. OFF without an
56
+ owned request is non-destructive. Completed removal relinquishes ownership before cleanup; repeating OFF cannot
57
+ delete a later manual recreation. An uncertain create remains unverified and needs inspection before deletion.
58
+
59
+ `tailscale.https_port` chooses an alternate HTTPS port when another app owns the default. Disable a previous request
60
+ before changing its external origin. A Tailscale version must provide the conditional Serve LocalAPI and one of the
61
+ recognized local endpoint formats. Unsupported client installations report an actionable error; no sudo or remote
62
+ endpoint fallback is attempted. The exact node DNS name must be eligible for HTTPS certificates in Tailscale's own
63
+ configuration. Readiness verifies the local deployment; actual phone access still depends on Tailscale login,
64
+ network policy and the remote browser's credential.
65
+
66
+ Development and release commands: [repository guide](../../README.md).
67
+
68
+ Migration provenance: heinzel, before the 0.9 plugin-boundary migration.
@@ -0,0 +1,54 @@
1
+ # Tailscale private web sharing
2
+
3
+ A first-party adapter for the installed Tailscale client. It keeps the web app's bearer/device authentication and
4
+ loopback listener; tailnet membership does not grant access to the backend owner's operations or terminal.
5
+
6
+ Install the released plugin from Discover Plugins, or run `hz system plugin add tailscale --confirm`. The core PyPI
7
+ package does not bundle this separate plugin distribution, so add it explicitly after installing Heinzel. Tailscale
8
+ itself must already be installed and signed in; hz does not install it, log in, grant HTTPS consent or change OS
9
+ permissions for you.
10
+ On macOS it also detects the CLI bundled in `/Applications/Tailscale.app` and explicitly selects CLI mode, as
11
+ described in [Tailscale's CLI documentation](https://tailscale.com/docs/reference/tailscale-cli?tab=macos).
12
+
13
+ In Settings or the footer's sharing icon, turn **Share over Tailscale** on. The control checks the node and existing
14
+ Serve mappings, merges its exact HTTPS origin into your user configuration and configures its loopback target. The
15
+ running web app applies the updated origin policy immediately; it does not require a backend restart. It shows ON and
16
+ a URL only after the owning authenticated web app verifies its actual port, active origin policy and matching Serve
17
+ configuration. A CLI/stdio MCP request saves the same intent but reports verification pending until that web owner
18
+ verifies it. Status reads never enable sharing.
19
+
20
+ ```sh
21
+ hz tailscale status --json
22
+ hz tailscale enable --json
23
+ hz tailscale disable --json
24
+ ```
25
+
26
+ The corresponding private operations are available through authenticated API and stdio MCP. They are absent from
27
+ the unauthenticated REST and HTTP MCP heads. In-process calls use the actual bound web port. A separate CLI retains
28
+ a previously observed port override; it cannot discover an arbitrary ephemeral web listener or claim it is ready.
29
+
30
+ **Copy URL** copies a working link with the current `#token=...` session fragment when the authenticated session has
31
+ one. Treat that link like a password: anyone who receives it can use that session until it is revoked or expires. If
32
+ there is no session token, the control copies only the address. A fresh phone browser also needs authentication: use
33
+ the separate **Copy sign-in link** action from the session owner, then pair that browser. Pairing a device never grants
34
+ it authority to pair another device. See [private web sharing](https://github.com/helmut-hoffer-von-ankershoffen/heinzel/blob/main/docs/PRIVATE_WEB_SHARING.md) for authentication
35
+ and revocation.
36
+
37
+ Turning sharing off removes only the still-matching hz root handler and any origin permission this plugin added.
38
+ Other paths, listeners, services and manually configured origins remain. An existing conflicting root, public
39
+ Funnel setting, foreground session or higher-priority origin policy is refused. A changed Serve revision or unknown
40
+ write outcome stops without retry or rollback; inspect current status before taking another action.
41
+ Matching an existing manual handler does not make it owned by hz, so enabling refuses to adopt it. OFF without an
42
+ owned request is non-destructive. Completed removal relinquishes ownership before cleanup; repeating OFF cannot
43
+ delete a later manual recreation. An uncertain create remains unverified and needs inspection before deletion.
44
+
45
+ `tailscale.https_port` chooses an alternate HTTPS port when another app owns the default. Disable a previous request
46
+ before changing its external origin. A Tailscale version must provide the conditional Serve LocalAPI and one of the
47
+ recognized local endpoint formats. Unsupported client installations report an actionable error; no sudo or remote
48
+ endpoint fallback is attempted. The exact node DNS name must be eligible for HTTPS certificates in Tailscale's own
49
+ configuration. Readiness verifies the local deployment; actual phone access still depends on Tailscale login,
50
+ network policy and the remote browser's credential.
51
+
52
+ Development and release commands: [repository guide](../../README.md).
53
+
54
+ Migration provenance: heinzel, before the 0.9 plugin-boundary migration.
@@ -0,0 +1,41 @@
1
+ {
2
+ "format": 1,
3
+ "sdk": 1,
4
+ "inputs": [
5
+ "coverage>=7.12",
6
+ "editables>=0.5",
7
+ "hatchling>=1.29.0",
8
+ "heinzel>=0.9.1,<0.10",
9
+ "httpx>=0.28",
10
+ "mypy>=1.19.0,<3",
11
+ "pyright[nodejs]==1.1.414",
12
+ "pytest-timeout>=2.4.0,<3",
13
+ "pytest-xdist>=3.8",
14
+ "pytest>=9.0.3,<10",
15
+ "ruff==0.17.2"
16
+ ],
17
+ "core": {
18
+ "version": "0.9.1",
19
+ "dependencies": [
20
+ "croniter>=6.2.4",
21
+ "fastapi>=0.161",
22
+ "keyring>=25.6",
23
+ "mcp>=2.3",
24
+ "packaging>=25.0",
25
+ "platformdirs>=4.5",
26
+ "pluggy<2,>=1.6.0",
27
+ "psutil==7.2.2",
28
+ "pydantic-ai-slim==2.61.0",
29
+ "pydantic-ai-slim[anthropic]==2.61.0; extra == \"anthropic\"",
30
+ "pydantic-ai-slim[openai]==2.61.0; extra == \"openai\"",
31
+ "pydantic-settings<3,>=2.11.0",
32
+ "pydantic<3,>=2.12.0",
33
+ "pyyaml<7,>=6.0.2",
34
+ "rich>=14.2.0",
35
+ "tomlkit>=0.13",
36
+ "typer>=0.23.0",
37
+ "uvicorn>=0.54",
38
+ "wsproto>=1.3"
39
+ ]
40
+ }
41
+ }
@@ -0,0 +1,131 @@
1
+ [project]
2
+ name = "heinzel-tailscale"
3
+ version = "0.9.1"
4
+ description = "Share the authenticated web app privately through Tailscale Serve: a Heinzelmännchen plugin."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ dependencies = ["heinzel>=0.9.1,<0.10", "httpx>=0.28"]
10
+
11
+ [project.urls]
12
+ Source = "https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/tree/main/plugins/heinzel-tailscale"
13
+ Changelog = "https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/blob/main/plugins/heinzel-tailscale/CHANGELOG.md"
14
+ Issues = "https://github.com/helmut-hoffer-von-ankershoffen/heinzel-plugins/issues"
15
+
16
+ [project.entry-points."heinzel.modules"]
17
+ tailscale = "heinzel_tailscale"
18
+
19
+ [build-system]
20
+ requires = ["hatchling>=1.29.0"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [dependency-groups]
24
+ test = ["editables>=0.5", "pytest>=9.0.3,<10", "pytest-timeout>=2.4.0,<3", "pytest-xdist>=3.8", "ruff==0.17.2", "mypy>=1.19.0,<3", "coverage>=7.12", "pyright[nodejs]==1.1.414"]
25
+
26
+
27
+ [tool.ruff]
28
+ line-length = 120
29
+ fix = true
30
+ extend-exclude = ["template"] # rendered and checked by `make template-test`
31
+
32
+ [tool.ruff.lint]
33
+ select = ["ALL"]
34
+ # pydoclint (DOC): docstring sections must match what the code raises, returns and yields. Preview rules, opted into
35
+ # one by one so no other preview rule sneaks in.
36
+ preview = true
37
+ explicit-preview-rules = true
38
+ extend-select = ["DOC201", "DOC202", "DOC402", "DOC403", "DOC501", "DOC502"]
39
+ ignore = [
40
+ "COM812", # conflicts with the ruff formatter
41
+ "CPY001", # no copyright header in OSS files
42
+ "D203", # incompatible with D211
43
+ "DOC201", # a return value is documented by the type and the summary line; Returns: only when it adds meaning
44
+ "DOC502", # documenting exceptions raised by callees is useful for callers
45
+ "D213", # incompatible with D212
46
+ "FBT001", # boolean positional arguments are idiomatic for Typer options
47
+ "FBT002", # same as FBT001
48
+ "ISC001", # conflicts with the ruff formatter
49
+ "TD003", # todos don't need an issue link
50
+ "FIX002", # todos are allowed
51
+ ]
52
+
53
+ [tool.ruff.lint.per-file-ignores]
54
+ ".claude/hooks/*.py" = [
55
+ "D103", # hook entry points are documented by the module docstring
56
+ "INP001", # hooks are scripts, not a package
57
+ "S603", # subprocess calls with fixed argument lists
58
+ "S607", # tools (uv, git, make) are resolved from PATH on purpose
59
+ "T201", # hooks talk to the agent through stdout/stderr
60
+ ]
61
+ "fuzz/*.py" = [
62
+ "INP001", # fuzz targets are scripts, not a package
63
+ ]
64
+ "hatch_build.py" = [
65
+ "INP001", # the build hook hatch loads by path, not a package
66
+ ]
67
+ "scripts/*.py" = [
68
+ "INP001", # scripts are not a package
69
+ "T201", # scripts report on stdout/stderr
70
+ ]
71
+ "**/tests/**/*.py" = [
72
+ "ANN", # fixtures and tests don't need full annotations
73
+ "DOC", # test docstrings describe behaviour, not interfaces
74
+ "ARG", # fixtures are used for their side effects
75
+ "D", # test names are the documentation
76
+ "INP001", # tests are not a package
77
+ "PLC0415", # local imports keep fixtures isolated
78
+ "PLR2004", # magic values are fine in assertions
79
+ "S101", # asserts are what tests do
80
+ "S105", # fake tokens and passwords are test data (credentials, adapters for tracker and document services)
81
+ "S106", # same as S105, passed as arguments
82
+ "S603", # subprocess calls with trusted input
83
+ "S607", # partial executable paths (uv, hz) are resolved from PATH on purpose
84
+ "SLF001", # private access is needed for solitary unit tests
85
+ ]
86
+
87
+ [tool.ruff.lint.pydocstyle]
88
+ convention = "google"
89
+
90
+ [tool.ruff.format]
91
+ docstring-code-format = true
92
+
93
+ [tool.mypy]
94
+ strict = true
95
+ mypy_path = ["src"]
96
+ files = ["src"]
97
+ plugins = ["pydantic.mypy"]
98
+
99
+ [tool.pydantic-mypy]
100
+ init_forbid_extra = true
101
+ init_typed = true
102
+ warn_required_dynamic_aliases = true
103
+
104
+ [tool.pytest]
105
+ minversion = "9.0"
106
+ strict = true
107
+ testpaths = ["tests"]
108
+ addopts = ["-ra", "--import-mode=importlib"]
109
+ filterwarnings = ["error"]
110
+ markers = ["unit: Offline isolated behavior.", "integration: Offline public-contract integration.", "e2e: Installed artifacts or real external services.", "perf: Explicit timing tests.", "long_running: Extended end-to-end budget."]
111
+
112
+ [tool.heinzel.plugin]
113
+ profiles = ["standard"]
114
+ requires = ["core:plugin-api@1"]
115
+ provides = []
116
+
117
+ [tool.coverage.run]
118
+ source = ["heinzel_tailscale"]
119
+ branch = true
120
+
121
+ [tool.coverage.report]
122
+ fail_under = 90
123
+ show_missing = true
124
+
125
+ [tool.pyright]
126
+ typeCheckingMode = "strict"
127
+ include = ["src"]
128
+ pythonVersion = "3.12"
129
+
130
+ [tool.hatch.build.targets.sdist]
131
+ exclude = [".venv", ".acceptance", ".preview", "dist", "web/node_modules", "**/__pycache__", ".pytest_cache", ".mypy_cache", ".coverage*"]