clauster 0.2.0__tar.gz → 0.2.2__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 (128) hide show
  1. {clauster-0.2.0 → clauster-0.2.2}/.bestpractices.json +5 -5
  2. {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/lint.yml +2 -2
  3. clauster-0.2.2/.github/workflows/release-please.yml +139 -0
  4. clauster-0.2.2/.release-please-manifest.json +3 -0
  5. {clauster-0.2.0 → clauster-0.2.2}/CHANGELOG.md +19 -0
  6. {clauster-0.2.0 → clauster-0.2.2}/Dockerfile +3 -1
  7. {clauster-0.2.0 → clauster-0.2.2}/PKG-INFO +31 -16
  8. {clauster-0.2.0 → clauster-0.2.2}/README.md +30 -15
  9. {clauster-0.2.0 → clauster-0.2.2}/clauster.yml.example +2 -1
  10. {clauster-0.2.0 → clauster-0.2.2}/pyproject.toml +1 -1
  11. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/__init__.py +1 -1
  12. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/config.py +30 -11
  13. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/ops.py +7 -8
  14. {clauster-0.2.0 → clauster-0.2.2}/tests/test_config.py +19 -1
  15. {clauster-0.2.0 → clauster-0.2.2}/uv.lock +1 -1
  16. clauster-0.2.0/.github/workflows/release-please.yml +0 -60
  17. clauster-0.2.0/.release-please-manifest.json +0 -3
  18. {clauster-0.2.0 → clauster-0.2.2}/.coderabbit.yaml +0 -0
  19. {clauster-0.2.0 → clauster-0.2.2}/.dockerignore +0 -0
  20. {clauster-0.2.0 → clauster-0.2.2}/.github/CODEOWNERS +0 -0
  21. {clauster-0.2.0 → clauster-0.2.2}/.github/settings.yml +0 -0
  22. {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/ci.yml +0 -0
  23. {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/pr-title.yml +0 -0
  24. {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/release.yml +0 -0
  25. {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/scorecard.yml +0 -0
  26. {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/security.yml +0 -0
  27. {clauster-0.2.0 → clauster-0.2.2}/.gitignore +0 -0
  28. {clauster-0.2.0 → clauster-0.2.2}/.markdownlint-cli2.yaml +0 -0
  29. {clauster-0.2.0 → clauster-0.2.2}/.pre-commit-config.yaml +0 -0
  30. {clauster-0.2.0 → clauster-0.2.2}/.yamllint.yaml +0 -0
  31. {clauster-0.2.0 → clauster-0.2.2}/CONTRIBUTING.md +0 -0
  32. {clauster-0.2.0 → clauster-0.2.2}/LICENSE +0 -0
  33. {clauster-0.2.0 → clauster-0.2.2}/SECURITY.md +0 -0
  34. {clauster-0.2.0 → clauster-0.2.2}/THIRD_PARTY_NOTICES.md +0 -0
  35. {clauster-0.2.0 → clauster-0.2.2}/UPGRADING.md +0 -0
  36. {clauster-0.2.0 → clauster-0.2.2}/clauster.spec +0 -0
  37. {clauster-0.2.0 → clauster-0.2.2}/codecov.yml +0 -0
  38. {clauster-0.2.0 → clauster-0.2.2}/docker/entrypoint.sh +0 -0
  39. {clauster-0.2.0 → clauster-0.2.2}/docs/screenshots/dashboard-dark.png +0 -0
  40. {clauster-0.2.0 → clauster-0.2.2}/docs/screenshots/dashboard-light.png +0 -0
  41. {clauster-0.2.0 → clauster-0.2.2}/docs/screenshots/login-dark.png +0 -0
  42. {clauster-0.2.0 → clauster-0.2.2}/docs/screenshots/new-project-clone.png +0 -0
  43. {clauster-0.2.0 → clauster-0.2.2}/package-lock.json +0 -0
  44. {clauster-0.2.0 → clauster-0.2.2}/package.json +0 -0
  45. {clauster-0.2.0 → clauster-0.2.2}/release-please-config.json +0 -0
  46. {clauster-0.2.0 → clauster-0.2.2}/renovate.json +0 -0
  47. {clauster-0.2.0 → clauster-0.2.2}/scripts/build-binary.sh +0 -0
  48. {clauster-0.2.0 → clauster-0.2.2}/scripts/lint-docs.sh +0 -0
  49. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/__main__.py +0 -0
  50. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/app.py +0 -0
  51. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/auth.py +0 -0
  52. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/bridge_log.py +0 -0
  53. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/claude_cli.py +0 -0
  54. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/claude_md.py +0 -0
  55. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/clone_jobs.py +0 -0
  56. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/discovery.py +0 -0
  57. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/environments.py +0 -0
  58. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/hooks/__init__.py +0 -0
  59. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/hooks/resume_recap.py +0 -0
  60. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/inspector.py +0 -0
  61. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/logstream.py +0 -0
  62. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/models.py +0 -0
  63. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/pointers.py +0 -0
  64. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/procutil.py +0 -0
  65. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/provisioning.py +0 -0
  66. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/pty_keeper.py +0 -0
  67. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/recap.py +0 -0
  68. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/redact.py +0 -0
  69. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/runner.py +0 -0
  70. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/state.py +0 -0
  71. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/alpine.LICENSE +0 -0
  72. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/alpine.min.js +0 -0
  73. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/clauster.css +0 -0
  74. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/favicon.svg +0 -0
  75. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/iconoir/LICENSE +0 -0
  76. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/iconoir/README.md +0 -0
  77. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/tabler/LICENSE +0 -0
  78. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/tabler/README.md +0 -0
  79. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/tabler/css/tabler.min.css +0 -0
  80. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/tabler/js/tabler.min.js +0 -0
  81. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/versions.txt +0 -0
  82. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/templates/_iconoir_sprite.html +0 -0
  83. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/templates/_project_card.html +0 -0
  84. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/templates/dashboard.html +0 -0
  85. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/templates/login.html +0 -0
  86. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/trust.py +0 -0
  87. {clauster-0.2.0 → clauster-0.2.2}/src/clauster/usage.py +0 -0
  88. {clauster-0.2.0 → clauster-0.2.2}/tests/E2E_CHECKLIST.md +0 -0
  89. {clauster-0.2.0 → clauster-0.2.2}/tests/conftest.py +0 -0
  90. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/bridge-logs/test1-bridge-debug.log +0 -0
  91. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/fake_claude/claude +0 -0
  92. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/fake_claude/claude.cmd +0 -0
  93. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/fake_git/git +0 -0
  94. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/fake_git/git.cmd +0 -0
  95. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/pointers/dockerize2.bridge-pointer.json +0 -0
  96. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/pointers/test1.bridge-pointer.json +0 -0
  97. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/pointers/test2.bridge-pointer.json +0 -0
  98. {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/transcripts/test1-session.jsonl +0 -0
  99. {clauster-0.2.0 → clauster-0.2.2}/tests/test_app.py +0 -0
  100. {clauster-0.2.0 → clauster-0.2.2}/tests/test_app_auth.py +0 -0
  101. {clauster-0.2.0 → clauster-0.2.2}/tests/test_app_instances.py +0 -0
  102. {clauster-0.2.0 → clauster-0.2.2}/tests/test_app_routes.py +0 -0
  103. {clauster-0.2.0 → clauster-0.2.2}/tests/test_auth.py +0 -0
  104. {clauster-0.2.0 → clauster-0.2.2}/tests/test_bridge_log.py +0 -0
  105. {clauster-0.2.0 → clauster-0.2.2}/tests/test_claude_md.py +0 -0
  106. {clauster-0.2.0 → clauster-0.2.2}/tests/test_clone_jobs.py +0 -0
  107. {clauster-0.2.0 → clauster-0.2.2}/tests/test_discovery.py +0 -0
  108. {clauster-0.2.0 → clauster-0.2.2}/tests/test_environments.py +0 -0
  109. {clauster-0.2.0 → clauster-0.2.2}/tests/test_fixtures.py +0 -0
  110. {clauster-0.2.0 → clauster-0.2.2}/tests/test_inspector.py +0 -0
  111. {clauster-0.2.0 → clauster-0.2.2}/tests/test_logstream.py +0 -0
  112. {clauster-0.2.0 → clauster-0.2.2}/tests/test_logtail.py +0 -0
  113. {clauster-0.2.0 → clauster-0.2.2}/tests/test_main.py +0 -0
  114. {clauster-0.2.0 → clauster-0.2.2}/tests/test_ops.py +0 -0
  115. {clauster-0.2.0 → clauster-0.2.2}/tests/test_pointers.py +0 -0
  116. {clauster-0.2.0 → clauster-0.2.2}/tests/test_procutil.py +0 -0
  117. {clauster-0.2.0 → clauster-0.2.2}/tests/test_provisioning.py +0 -0
  118. {clauster-0.2.0 → clauster-0.2.2}/tests/test_pty_keeper.py +0 -0
  119. {clauster-0.2.0 → clauster-0.2.2}/tests/test_recap.py +0 -0
  120. {clauster-0.2.0 → clauster-0.2.2}/tests/test_redact.py +0 -0
  121. {clauster-0.2.0 → clauster-0.2.2}/tests/test_runner.py +0 -0
  122. {clauster-0.2.0 → clauster-0.2.2}/tests/test_runner_pty.py +0 -0
  123. {clauster-0.2.0 → clauster-0.2.2}/tests/test_runner_recap.py +0 -0
  124. {clauster-0.2.0 → clauster-0.2.2}/tests/test_spawn_controls.py +0 -0
  125. {clauster-0.2.0 → clauster-0.2.2}/tests/test_state.py +0 -0
  126. {clauster-0.2.0 → clauster-0.2.2}/tests/test_trust.py +0 -0
  127. {clauster-0.2.0 → clauster-0.2.2}/tests/test_urls.py +0 -0
  128. {clauster-0.2.0 → clauster-0.2.2}/tests/test_usage.py +0 -0
@@ -37,12 +37,12 @@
37
37
  "version_unique_justification": "Each release has a unique Semantic Version identifier maintained in pyproject.toml and src/clauster/__init__.py, which makes every build uniquely identifiable. Release tagging is automated via release-please (.release-please-manifest.json / release-please-config.json) and produces a vX.Y.Z git tag when a release is published.",
38
38
  "version_semver_status": "Met",
39
39
  "version_semver_justification": "The project uses Semantic Versioning (MAJOR.MINOR.PATCH).",
40
- "version_tags_status": "Unmet",
41
- "version_tags_justification": "No release has been cut yet, so no semantic-version git tags exist. Tagging is configured and automated via release-please and will produce vX.Y.Z tags once the first release is published.",
42
- "release_notes_status": "N/A",
43
- "release_notes_justification": "No release has been published, so there are no release notes to provide. Releases are published through the release-please workflow, which generates human-readable, grouped release notes (a CHANGELOG entry and a tagged GitHub Release) from the Conventional Commit history.",
40
+ "version_tags_status": "Met",
41
+ "version_tags_justification": "Releases are cut as semantic-version git tags by the automated release-please workflow. Published tags include v0.2.0 and v0.2.1 (https://github.com/schubydoo/clauster/tags), each tagging the corresponding release commit on main.",
42
+ "release_notes_status": "Met",
43
+ "release_notes_justification": "Each release provides human-readable release notes: release-please generates a curated, grouped CHANGELOG.md entry (https://github.com/schubydoo/clauster/blob/main/CHANGELOG.md) and a matching tagged GitHub Release (e.g. v0.2.0, v0.2.1 at https://github.com/schubydoo/clauster/releases) from the Conventional Commit history, organized into Features / Bug Fixes sections. These are a curated summary, not the raw version-control log.",
44
44
  "release_notes_vulns_status": "N/A",
45
- "release_notes_vulns_justification": "No release has shipped a security fix and no publicly-known vulnerability has required disclosure. When a release fixes one, release-please records it in the generated release notes (Bug Fixes section).",
45
+ "release_notes_vulns_justification": "No published release has fixed a publicly-known (e.g. CVE-assigned) run-time vulnerability, so there is nothing to identify in the release notes. When a release does fix one, release-please records it in the generated notes (Bug Fixes section) and this would become Met.",
46
46
  "report_process_status": "Met",
47
47
  "report_process_justification": "Bugs are reported via GitHub Issues; security issues via SECURITY.md. https://github.com/schubydoo/clauster/issues",
48
48
  "report_tracker_status": "Met",
@@ -43,10 +43,10 @@ jobs:
43
43
  # drifts across image refreshes) and install the version-pinned, integrity-
44
44
  # checked package from the lockfile via `npm ci`. yamllint comes from
45
45
  # `uv sync --extra dev` above.
46
- - name: Set up Node 20
46
+ - name: Set up Node 24
47
47
  uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
48
48
  with:
49
- node-version: "20"
49
+ node-version: "24"
50
50
  cache: npm
51
51
  - name: Install Node deps
52
52
  run: npm ci
@@ -0,0 +1,139 @@
1
+ name: Release Please
2
+
3
+ # Release Please reads Conventional Commits on main and opens a "release PR"
4
+ # that bumps the version (src/clauster/__init__.py + pyproject.toml) and rewrites
5
+ # CHANGELOG.md. Merging that PR tags the commit (e.g. `v0.2.0`), which fires
6
+ # release.yml (`on: push: tags`).
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: release-please-${{ github.ref }}
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ release-please:
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 10
23
+ # GITHUB_TOKEN stays read-only: the App token (minted below) does all the
24
+ # writing, so the default token needs no write scope here.
25
+ permissions:
26
+ contents: read
27
+ pull-requests: read
28
+ steps:
29
+ # Mint a short-lived installation token for the Clauster release GitHub
30
+ # App so the release PR + version-bump commits are authored by the app's
31
+ # bot identity, NOT a human account. Crucially this is *not* the default
32
+ # GITHUB_TOKEN, so the tag pushed when the release PR merges still
33
+ # *triggers* release.yml (GITHUB_TOKEN-pushed tags don't — anti-recursion).
34
+ #
35
+ # Requires (set up once, manually):
36
+ # - a GitHub App owned by the repo owner, installed on this repo, with
37
+ # repo permissions Contents: R/W + Pull requests: R/W;
38
+ # - secrets RELEASE_PLEASE_APP_ID (numeric App ID) and
39
+ # RELEASE_PLEASE_APP_PRIVATE_KEY (the App's .pem private key).
40
+ # Until those exist this step 401s — merge this only after they're added.
41
+ # Supersedes the old RELEASE_PLEASE_TOKEN PAT (which attributed PRs to a
42
+ # real user); that secret can be deleted once this is live.
43
+ - name: Mint GitHub App token
44
+ id: app-token
45
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
46
+ with:
47
+ app-id: ${{ secrets.RELEASE_PLEASE_APP_ID }}
48
+ private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
49
+ # Scope the minted token to exactly what release-please needs, rather
50
+ # than inheriting the installation's full permission set (zizmor
51
+ # least-privilege). Add permission-issues: write only if you enable
52
+ # release-please's label-on-issue features.
53
+ permission-contents: write
54
+ permission-pull-requests: write
55
+
56
+ - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
57
+ with:
58
+ token: ${{ steps.app-token.outputs.token }}
59
+ config-file: release-please-config.json
60
+ manifest-file: .release-please-manifest.json
61
+
62
+ # --- Keep uv.lock's editable self-version in sync with the release bump ---
63
+ # release-please bumps pyproject.toml's version but NOT uv.lock, so the lock's
64
+ # `[[package]] name = "clauster"` version drifts behind each release — a
65
+ # papercut where every local `uv` run then regenerates uv.lock into a stray
66
+ # diff. When a release PR is open, regenerate uv.lock on that PR branch so the
67
+ # lock bumps together with pyproject and lands in the same merge. A no-op on
68
+ # every other push (and when the lock is already in sync). It pushes only to
69
+ # the bot's own `release-please--*` branch, never to main.
70
+ - name: Detect open release PR
71
+ id: relpr
72
+ env:
73
+ GH_TOKEN: ${{ github.token }} # read-only default token is enough for `pr list`
74
+ run: |
75
+ set -euo pipefail
76
+ # Pick the release PR by *authenticated identity*, not just the branch
77
+ # name: it must be authored by the release App, target main, and be an
78
+ # in-repo (non-fork) branch. The branch-name prefix alone would let any
79
+ # collaborator open a `release-please--*` PR and have us check it out +
80
+ # run `uv lock` (which can execute the project's build backend) in this
81
+ # trusted push context with the app token in scope. gh renders an App
82
+ # author as `app/<slug>`.
83
+ branch="$(gh pr list --repo "$GITHUB_REPOSITORY" --state open \
84
+ --json headRefName,baseRefName,author,isCrossRepository \
85
+ --jq '[.[]
86
+ | select(.headRefName | startswith("release-please--"))
87
+ | select(.baseRefName == "main")
88
+ | select(.isCrossRepository == false)
89
+ | select(.author.login == "app/clauster-release-bot")
90
+ ][0].headRefName // ""')"
91
+ echo "branch=$branch" >> "$GITHUB_OUTPUT"
92
+ if [ -n "$branch" ]; then echo "Release PR branch: $branch"; else echo "No open release PR."; fi
93
+
94
+ - name: Check out repo
95
+ if: ${{ steps.relpr.outputs.branch != '' }}
96
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
97
+ with:
98
+ fetch-depth: 0
99
+ persist-credentials: false
100
+
101
+ - name: Set up uv
102
+ if: ${{ steps.relpr.outputs.branch != '' }}
103
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
104
+ with:
105
+ # No cache: this job can push, and setup-uv's default caching is a
106
+ # cache-poisoning vector (zizmor). A one-off `uv lock` doesn't need it.
107
+ enable-cache: false
108
+
109
+ - name: Sync uv.lock on the release PR
110
+ if: ${{ steps.relpr.outputs.branch != '' }}
111
+ env:
112
+ # App token (contents: write) so the push is authorized AND keeps the
113
+ # release PR bot-authored. Branch via env (not inlined) — no injection.
114
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
115
+ PR_BRANCH: ${{ steps.relpr.outputs.branch }}
116
+ run: |
117
+ set -euo pipefail
118
+ # Defensive: only ever touch the bot's release branch, never anything else.
119
+ case "$PR_BRANCH" in
120
+ release-please--*) ;;
121
+ *) echo "refusing to push to unexpected branch '$PR_BRANCH'"; exit 1 ;;
122
+ esac
123
+ git fetch --quiet origin "$PR_BRANCH"
124
+ git checkout -B "$PR_BRANCH" FETCH_HEAD
125
+ uv lock
126
+ if git diff --quiet -- uv.lock; then
127
+ echo "uv.lock already in sync — nothing to commit."
128
+ exit 0
129
+ fi
130
+ # Attribute the commit to the release bot (fall back to the Actions bot).
131
+ bot_user="clauster-release-bot[bot]"
132
+ bot_id="$(gh api "/users/${bot_user}" --jq '.id' 2>/dev/null || true)"
133
+ if [ -z "$bot_id" ]; then bot_user="github-actions[bot]"; bot_id="41898282"; fi
134
+ git config user.name "$bot_user"
135
+ git config user.email "${bot_id}+${bot_user}@users.noreply.github.com"
136
+ git add uv.lock
137
+ git commit -m "chore: sync uv.lock to the release version"
138
+ git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${PR_BRANCH}"
139
+ echo "Pushed uv.lock sync to $PR_BRANCH."
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.2.2"
3
+ }
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.2](https://github.com/schubydoo/clauster/compare/v0.2.1...v0.2.2) (2026-06-03)
4
+
5
+
6
+ ### Security
7
+
8
+ * **This is a security release.** A non-loopback bind (e.g. `0.0.0.0` or a LAN IP) could serve the dashboard **unauthenticated** when `auth.enabled` was left at its default `false` — even with a password configured — because the runtime guard only enforces auth when `auth.enabled` is set, while config validation did not require it. The config validator now refuses to start a non-loopback bind unless authentication is actually enforced (`auth.enabled: true` together with `auth.password_required` + a hash, or `auth.reverse_proxy.enabled`; or the explicit `auth.allow_unauthenticated_network` opt-out). All prior releases (≤ 0.2.1) are affected, including the Docker image. **Upgrade, and on any networked deployment set `auth.enabled: true`.** See [GHSA-h4g2-xfmw-q2c9](https://github.com/schubydoo/clauster/security/advisories/GHSA-h4g2-xfmw-q2c9).
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **auth:** refuse non-loopback bind unless auth is actually enforced ([#88](https://github.com/schubydoo/clauster/issues/88)) ([d89d753](https://github.com/schubydoo/clauster/commit/d89d753120c2246eea1838cea9528aa7658eb36f))
14
+
15
+ ## [0.2.1](https://github.com/schubydoo/clauster/compare/v0.2.0...v0.2.1) (2026-06-03)
16
+
17
+
18
+ ### Documentation
19
+
20
+ * absolute GitHub URLs in README so images render on PyPI ([#79](https://github.com/schubydoo/clauster/issues/79)) ([1feef42](https://github.com/schubydoo/clauster/commit/1feef42b91a82b2d31063aa448c90e5a0688fb6a))
21
+
3
22
  ## [0.2.0](https://github.com/schubydoo/clauster/compare/v0.1.0...v0.2.0) (2026-06-03)
4
23
 
5
24
 
@@ -47,7 +47,9 @@ COPY --from=builder /app/.venv /app/.venv
47
47
  ENV PATH="/app/.venv/bin:$PATH" \
48
48
  PYTHONUNBUFFERED=1 \
49
49
  # Bind all interfaces (a container is useless on loopback). host!=loopback
50
- # makes clauster REQUIRE auth — set CLAUSTER_AUTH_PASSWORD_HASH or it exits.
50
+ # makes clauster REQUIRE enforced auth — set CLAUSTER_AUTH_ENABLED=true +
51
+ # CLAUSTER_AUTH_PASSWORD_REQUIRED=true + CLAUSTER_AUTH_PASSWORD_HASH (or
52
+ # reverse-proxy trust), or it exits on start. See README "Docker".
51
53
  CLAUSTER_HOST=0.0.0.0 \
52
54
  CLAUSTER_PORT=7621 \
53
55
  CLAUSTER_LOG_FORMAT=json \
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clauster
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Self-hosted web UI for spawning and managing Claude Code remote-control bridges on a remote host.
5
5
  Project-URL: Homepage, https://github.com/schubydoo/clauster
6
6
  Project-URL: Repository, https://github.com/schubydoo/clauster
@@ -60,14 +60,14 @@ Description-Content-Type: text/markdown
60
60
 
61
61
  <p align="center">
62
62
  <img alt="Python 3.11+" src="https://img.shields.io/badge/python-3.11%2B-blue.svg">
63
- <a href="LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg"></a>
63
+ <a href="https://github.com/schubydoo/clauster/blob/main/LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg"></a>
64
64
  <a href="https://github.com/schubydoo/clauster/pkgs/container/clauster"><img alt="GHCR" src="https://img.shields.io/badge/ghcr.io-clauster-2496ED?logo=docker&logoColor=white"></a>
65
65
  <a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"></a>
66
66
  <a href="https://pre-commit.com/"><img alt="pre-commit" src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white"></a>
67
67
  </p>
68
68
 
69
69
  <p align="center">
70
- <img src="docs/screenshots/dashboard-dark.png" alt="Clauster dashboard" width="860">
70
+ <img src="https://raw.githubusercontent.com/schubydoo/clauster/main/docs/screenshots/dashboard-dark.png" alt="Clauster dashboard" width="860">
71
71
  </p>
72
72
 
73
73
  Anthropic's first-party tooling assumes terminal access on the host to spawn a
@@ -83,17 +83,17 @@ a project, start a bridge, and attach to it from `claude.ai/code` or the mobile
83
83
  <table>
84
84
  <tr>
85
85
  <td width="50%" align="center">
86
- <img src="docs/screenshots/dashboard-light.png" alt="Dashboard, light theme"><br>
86
+ <img src="https://raw.githubusercontent.com/schubydoo/clauster/main/docs/screenshots/dashboard-light.png" alt="Dashboard, light theme"><br>
87
87
  <sub><b>Dark / light</b> — theme toggle persists across reloads</sub>
88
88
  </td>
89
89
  <td width="50%" align="center">
90
- <img src="docs/screenshots/new-project-clone.png" alt="Create or clone a project"><br>
90
+ <img src="https://raw.githubusercontent.com/schubydoo/clauster/main/docs/screenshots/new-project-clone.png" alt="Create or clone a project"><br>
91
91
  <sub><b>Create or clone</b> — SSRF-guarded, cloned code runs only on Start</sub>
92
92
  </td>
93
93
  </tr>
94
94
  <tr>
95
95
  <td width="50%" align="center">
96
- <img src="docs/screenshots/login-dark.png" alt="Password login"><br>
96
+ <img src="https://raw.githubusercontent.com/schubydoo/clauster/main/docs/screenshots/login-dark.png" alt="Password login"><br>
97
97
  <sub><b>Password login</b> — for non-loopback / networked deploys</sub>
98
98
  </td>
99
99
  <td width="50%" align="center" valign="middle">
@@ -107,7 +107,7 @@ a project, start a bridge, and attach to it from `claude.ai/code` or the mobile
107
107
 
108
108
  Everything below is implemented and shipping. Items marked **(opt-in)** are gated
109
109
  behind a config flag and off by default — the flag is named inline so you can find
110
- it in [`clauster.yml.example`](clauster.yml.example).
110
+ it in [`clauster.yml.example`](https://github.com/schubydoo/clauster/blob/main/clauster.yml.example).
111
111
 
112
112
  ### Projects & bridges
113
113
 
@@ -185,20 +185,33 @@ it; it isn't vendored).
185
185
 
186
186
  ## Docker
187
187
 
188
- Multi-arch images (`linux/amd64`, `linux/arm64`) are published to GHCR on each release:
188
+ Multi-arch images (`linux/amd64`, `linux/arm64`) are published to GHCR on each release.
189
+ The image binds `0.0.0.0`, so it **requires enforced auth** to start. First generate a
190
+ password hash — this runs `clauster` *inside* the image, so you don't need it on the host:
191
+
192
+ ```sh
193
+ docker run --rm -it ghcr.io/schubydoo/clauster:latest clauster hash-password
194
+ ```
195
+
196
+ Copy the printed `$argon2id$…` hash, then start the server with auth enabled:
189
197
 
190
198
  ```sh
191
199
  docker run -d --name clauster \
192
200
  -p 7621:7621 \
193
201
  -e PUID=1000 -e PGID=1000 \
194
- -e CLAUSTER_AUTH_PASSWORD_HASH="$(clauster hash-password)" \
202
+ -e CLAUSTER_AUTH_ENABLED=true \
203
+ -e CLAUSTER_AUTH_PASSWORD_REQUIRED=true \
204
+ -e 'CLAUSTER_AUTH_PASSWORD_HASH=$argon2id$v=19$...' \
195
205
  -v /path/to/config:/config \
196
206
  -v /path/to/projects:/projects \
197
207
  ghcr.io/schubydoo/clauster:latest
198
208
  ```
199
209
 
200
- - The image binds `0.0.0.0`, which **requires auth** — set `CLAUSTER_AUTH_PASSWORD_HASH`
201
- (or put it in `/config/clauster.yml`) or the container exits on start.
210
+ - The image binds `0.0.0.0`, so it won't start without **enforced** auth — set
211
+ `CLAUSTER_AUTH_ENABLED=true` **and** `CLAUSTER_AUTH_PASSWORD_REQUIRED=true` **and** a
212
+ `CLAUSTER_AUTH_PASSWORD_HASH` (or configure reverse-proxy trust in `/config/clauster.yml`),
213
+ or the container exits on start. Single-quote the hash env value — the argon2 hash
214
+ contains `$` that your shell would otherwise expand.
202
215
  - `/config` holds `clauster.yml` + state; `/projects` is your `projects_root`.
203
216
  `PUID`/`PGID` remap the runtime user to own bind-mounts.
204
217
  - `claude` is **not** baked in — tell Clauster where it is one of two ways: mount the
@@ -213,16 +226,17 @@ docker run -d --name clauster \
213
226
  ## Auth & networking
214
227
 
215
228
  Loopback (`127.0.0.1`) needs no auth. Binding to a non-loopback address is refused
216
- unless you enable one of: password login (`auth.password_required` + a hash from
217
- `clauster hash-password`), reverse-proxy trust (peer-IP allowlist + HMAC header), or
218
- an explicit `auth.allow_unauthenticated_network` opt-out for a trusted LAN. Sessions
229
+ unless authentication is actually enforced set `auth.enabled: true` (the master
230
+ switch) together with either password login (`auth.password_required` + a hash from
231
+ `clauster hash-password`) or reverse-proxy trust (peer-IP allowlist + HMAC header) —
232
+ or, to opt out on a trusted LAN, `auth.allow_unauthenticated_network`. Sessions
219
233
  are signed cookies with server-side revocation ("log out everywhere"); WebSocket
220
234
  connections are authenticated before accept and origin-checked.
221
235
 
222
236
  ## Configuration
223
237
 
224
238
  All settings live in `clauster.yml` — see
225
- [`clauster.yml.example`](clauster.yml.example) for the full, commented schema. Any
239
+ [`clauster.yml.example`](https://github.com/schubydoo/clauster/blob/main/clauster.yml.example) for the full, commented schema. Any
226
240
  scalar key is overridable by an environment variable of the form
227
241
  `CLAUSTER_<UPPER_SNAKE_PATH>`. The schema is additive-only — old configs always
228
242
  validate against newer versions.
@@ -231,6 +245,7 @@ validate against newer versions.
231
245
  | --- | --- | --- |
232
246
  | `host` / `port` | `127.0.0.1` / `7621` | bind address (non-loopback needs auth) |
233
247
  | `projects_root` | — | directory whose children become project cards |
248
+ | `auth.enabled` | `false` | master auth switch — must be on for password / proxy auth to apply |
234
249
  | `auth.password_required` | `false` | require login (`clauster hash-password` for the hash) |
235
250
  | `claude.resume_recap` | `false` | recap the prior transcript into a restarted bridge |
236
251
  | `claude.resume_mode` | `standard` | `pty` = native true-resume on Restart (POSIX) |
@@ -277,4 +292,4 @@ and CI-gated on Linux; macOS / Windows are in the test matrix. Apache-2.0 licens
277
292
 
278
293
  ## License
279
294
 
280
- [Apache License 2.0](LICENSE).
295
+ [Apache License 2.0](https://github.com/schubydoo/clauster/blob/main/LICENSE).
@@ -17,14 +17,14 @@
17
17
 
18
18
  <p align="center">
19
19
  <img alt="Python 3.11+" src="https://img.shields.io/badge/python-3.11%2B-blue.svg">
20
- <a href="LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg"></a>
20
+ <a href="https://github.com/schubydoo/clauster/blob/main/LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-blue.svg"></a>
21
21
  <a href="https://github.com/schubydoo/clauster/pkgs/container/clauster"><img alt="GHCR" src="https://img.shields.io/badge/ghcr.io-clauster-2496ED?logo=docker&logoColor=white"></a>
22
22
  <a href="https://github.com/astral-sh/ruff"><img alt="Ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json"></a>
23
23
  <a href="https://pre-commit.com/"><img alt="pre-commit" src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white"></a>
24
24
  </p>
25
25
 
26
26
  <p align="center">
27
- <img src="docs/screenshots/dashboard-dark.png" alt="Clauster dashboard" width="860">
27
+ <img src="https://raw.githubusercontent.com/schubydoo/clauster/main/docs/screenshots/dashboard-dark.png" alt="Clauster dashboard" width="860">
28
28
  </p>
29
29
 
30
30
  Anthropic's first-party tooling assumes terminal access on the host to spawn a
@@ -40,17 +40,17 @@ a project, start a bridge, and attach to it from `claude.ai/code` or the mobile
40
40
  <table>
41
41
  <tr>
42
42
  <td width="50%" align="center">
43
- <img src="docs/screenshots/dashboard-light.png" alt="Dashboard, light theme"><br>
43
+ <img src="https://raw.githubusercontent.com/schubydoo/clauster/main/docs/screenshots/dashboard-light.png" alt="Dashboard, light theme"><br>
44
44
  <sub><b>Dark / light</b> — theme toggle persists across reloads</sub>
45
45
  </td>
46
46
  <td width="50%" align="center">
47
- <img src="docs/screenshots/new-project-clone.png" alt="Create or clone a project"><br>
47
+ <img src="https://raw.githubusercontent.com/schubydoo/clauster/main/docs/screenshots/new-project-clone.png" alt="Create or clone a project"><br>
48
48
  <sub><b>Create or clone</b> — SSRF-guarded, cloned code runs only on Start</sub>
49
49
  </td>
50
50
  </tr>
51
51
  <tr>
52
52
  <td width="50%" align="center">
53
- <img src="docs/screenshots/login-dark.png" alt="Password login"><br>
53
+ <img src="https://raw.githubusercontent.com/schubydoo/clauster/main/docs/screenshots/login-dark.png" alt="Password login"><br>
54
54
  <sub><b>Password login</b> — for non-loopback / networked deploys</sub>
55
55
  </td>
56
56
  <td width="50%" align="center" valign="middle">
@@ -64,7 +64,7 @@ a project, start a bridge, and attach to it from `claude.ai/code` or the mobile
64
64
 
65
65
  Everything below is implemented and shipping. Items marked **(opt-in)** are gated
66
66
  behind a config flag and off by default — the flag is named inline so you can find
67
- it in [`clauster.yml.example`](clauster.yml.example).
67
+ it in [`clauster.yml.example`](https://github.com/schubydoo/clauster/blob/main/clauster.yml.example).
68
68
 
69
69
  ### Projects & bridges
70
70
 
@@ -142,20 +142,33 @@ it; it isn't vendored).
142
142
 
143
143
  ## Docker
144
144
 
145
- Multi-arch images (`linux/amd64`, `linux/arm64`) are published to GHCR on each release:
145
+ Multi-arch images (`linux/amd64`, `linux/arm64`) are published to GHCR on each release.
146
+ The image binds `0.0.0.0`, so it **requires enforced auth** to start. First generate a
147
+ password hash — this runs `clauster` *inside* the image, so you don't need it on the host:
148
+
149
+ ```sh
150
+ docker run --rm -it ghcr.io/schubydoo/clauster:latest clauster hash-password
151
+ ```
152
+
153
+ Copy the printed `$argon2id$…` hash, then start the server with auth enabled:
146
154
 
147
155
  ```sh
148
156
  docker run -d --name clauster \
149
157
  -p 7621:7621 \
150
158
  -e PUID=1000 -e PGID=1000 \
151
- -e CLAUSTER_AUTH_PASSWORD_HASH="$(clauster hash-password)" \
159
+ -e CLAUSTER_AUTH_ENABLED=true \
160
+ -e CLAUSTER_AUTH_PASSWORD_REQUIRED=true \
161
+ -e 'CLAUSTER_AUTH_PASSWORD_HASH=$argon2id$v=19$...' \
152
162
  -v /path/to/config:/config \
153
163
  -v /path/to/projects:/projects \
154
164
  ghcr.io/schubydoo/clauster:latest
155
165
  ```
156
166
 
157
- - The image binds `0.0.0.0`, which **requires auth** — set `CLAUSTER_AUTH_PASSWORD_HASH`
158
- (or put it in `/config/clauster.yml`) or the container exits on start.
167
+ - The image binds `0.0.0.0`, so it won't start without **enforced** auth — set
168
+ `CLAUSTER_AUTH_ENABLED=true` **and** `CLAUSTER_AUTH_PASSWORD_REQUIRED=true` **and** a
169
+ `CLAUSTER_AUTH_PASSWORD_HASH` (or configure reverse-proxy trust in `/config/clauster.yml`),
170
+ or the container exits on start. Single-quote the hash env value — the argon2 hash
171
+ contains `$` that your shell would otherwise expand.
159
172
  - `/config` holds `clauster.yml` + state; `/projects` is your `projects_root`.
160
173
  `PUID`/`PGID` remap the runtime user to own bind-mounts.
161
174
  - `claude` is **not** baked in — tell Clauster where it is one of two ways: mount the
@@ -170,16 +183,17 @@ docker run -d --name clauster \
170
183
  ## Auth & networking
171
184
 
172
185
  Loopback (`127.0.0.1`) needs no auth. Binding to a non-loopback address is refused
173
- unless you enable one of: password login (`auth.password_required` + a hash from
174
- `clauster hash-password`), reverse-proxy trust (peer-IP allowlist + HMAC header), or
175
- an explicit `auth.allow_unauthenticated_network` opt-out for a trusted LAN. Sessions
186
+ unless authentication is actually enforced set `auth.enabled: true` (the master
187
+ switch) together with either password login (`auth.password_required` + a hash from
188
+ `clauster hash-password`) or reverse-proxy trust (peer-IP allowlist + HMAC header) —
189
+ or, to opt out on a trusted LAN, `auth.allow_unauthenticated_network`. Sessions
176
190
  are signed cookies with server-side revocation ("log out everywhere"); WebSocket
177
191
  connections are authenticated before accept and origin-checked.
178
192
 
179
193
  ## Configuration
180
194
 
181
195
  All settings live in `clauster.yml` — see
182
- [`clauster.yml.example`](clauster.yml.example) for the full, commented schema. Any
196
+ [`clauster.yml.example`](https://github.com/schubydoo/clauster/blob/main/clauster.yml.example) for the full, commented schema. Any
183
197
  scalar key is overridable by an environment variable of the form
184
198
  `CLAUSTER_<UPPER_SNAKE_PATH>`. The schema is additive-only — old configs always
185
199
  validate against newer versions.
@@ -188,6 +202,7 @@ validate against newer versions.
188
202
  | --- | --- | --- |
189
203
  | `host` / `port` | `127.0.0.1` / `7621` | bind address (non-loopback needs auth) |
190
204
  | `projects_root` | — | directory whose children become project cards |
205
+ | `auth.enabled` | `false` | master auth switch — must be on for password / proxy auth to apply |
191
206
  | `auth.password_required` | `false` | require login (`clauster hash-password` for the hash) |
192
207
  | `claude.resume_recap` | `false` | recap the prior transcript into a restarted bridge |
193
208
  | `claude.resume_mode` | `standard` | `pty` = native true-resume on Restart (POSIX) |
@@ -234,4 +249,4 @@ and CI-gated on Linux; macOS / Windows are in the test matrix. Apache-2.0 licens
234
249
 
235
250
  ## License
236
251
 
237
- [Apache License 2.0](LICENSE).
252
+ [Apache License 2.0](https://github.com/schubydoo/clauster/blob/main/LICENSE).
@@ -39,7 +39,8 @@ projects: {}
39
39
  # allow_bypass_permissions: false
40
40
 
41
41
  auth:
42
- enabled: false # turn on to require login / gate non-loopback
42
+ enabled: false # MASTER switch must be true for password_required /
43
+ # reverse_proxy auth to actually be enforced
43
44
  password_required: false
44
45
  password_hash: null # argon2id; generate via `clauster hash-password`
45
46
  cookie_secure: auto # auto | always | never
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clauster"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Self-hosted web UI for spawning and managing Claude Code remote-control bridges on a remote host."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """Clauster — self-hosted dispatcher for Claude Code remote-control bridges."""
2
2
 
3
- __version__ = "0.2.0" # x-release-please-version
3
+ __version__ = "0.2.2" # x-release-please-version
@@ -122,6 +122,21 @@ class AuthConfig(BaseModel):
122
122
  ) # extra WS/CSRF origins (proxy domain)
123
123
 
124
124
 
125
+ def _missing_enforced_auth(host: str, auth: AuthConfig) -> bool:
126
+ """Return True when binding ``host`` would NOT actually enforce authentication.
127
+
128
+ The runtime guard gates on ``auth.enabled``, so a non-loopback bind only enforces
129
+ auth when ``auth.enabled`` is set together with a method (``password_required`` or
130
+ ``reverse_proxy.enabled``). Loopback never needs auth. The explicit
131
+ ``allow_unauthenticated_network`` opt-out is intentionally left to callers (the
132
+ config validator permits it; ``ops._check_auth`` downgrades it to a warning) so
133
+ both use one shared definition of "enforced auth" without conflating the opt-out.
134
+ """
135
+ if host in _LOOPBACK_HOSTS:
136
+ return False
137
+ return not (auth.enabled and (auth.password_required or auth.reverse_proxy.enabled))
138
+
139
+
125
140
  class CloneConfig(BaseModel):
126
141
  """Project clone/create guards (spec §11 clone+trust chain).
127
142
 
@@ -215,17 +230,21 @@ class ClausterConfig(BaseModel):
215
230
 
216
231
  @model_validator(mode="after")
217
232
  def _loopback_or_authed(self) -> ClausterConfig:
218
- # Non-loopback bind is only allowed once authentication can gate it.
219
- if self.host not in _LOOPBACK_HOSTS:
220
- a = self.auth
221
- if not (
222
- a.password_required or a.reverse_proxy.enabled or a.allow_unauthenticated_network
223
- ):
224
- raise ValueError(
225
- f"refusing non-loopback host={self.host!r} without auth. Set one of "
226
- "auth.password_required, auth.reverse_proxy.enabled, or (to opt out on a "
227
- "trusted LAN) auth.allow_unauthenticated_network."
228
- )
233
+ # Non-loopback bind is only allowed once authentication will ACTUALLY gate it.
234
+ # The runtime guard enforces auth only when `auth.enabled` is true; with it false
235
+ # every request passes through unauthenticated, so `password_required` /
236
+ # `reverse_proxy.enabled` *without* `auth.enabled` is a silent open door — the
237
+ # operator sets a password, the validator accepted it, yet the dashboard is served
238
+ # to anyone. Require enforcement to be real here (fail closed) instead. Shared with
239
+ # ops._check_auth via _missing_enforced_auth so validation and diagnostics agree.
240
+ a = self.auth
241
+ if _missing_enforced_auth(self.host, a) and not a.allow_unauthenticated_network:
242
+ raise ValueError(
243
+ f"refusing non-loopback host={self.host!r} without enforced auth. Set "
244
+ "auth.enabled: true together with auth.password_required (+ a hash from "
245
+ "`clauster hash-password`) or auth.reverse_proxy.enabled — or, to opt out "
246
+ "on a trusted LAN, auth.allow_unauthenticated_network."
247
+ )
229
248
  # Fail closed: password auth required but no hash configured would lock everyone out
230
249
  # (or, worse, be skipped) — refuse to start with a clear message.
231
250
  if self.auth.password_required and not self.auth.password_hash:
@@ -19,7 +19,7 @@ from datetime import UTC, datetime
19
19
  from pathlib import Path, PurePosixPath
20
20
 
21
21
  from . import claude_cli
22
- from .config import ClausterConfig, load_config
22
+ from .config import ClausterConfig, _missing_enforced_auth, load_config
23
23
  from .discovery import _load_trusted_paths, trust_state_for
24
24
  from .state import CURRENT_SCHEMA, StateStore
25
25
 
@@ -191,13 +191,12 @@ def _check_auth(config: ClausterConfig) -> Check:
191
191
  a = config.auth
192
192
  if a.password_required and not a.password_hash:
193
193
  return Check("auth", FAIL, "password_required but no password_hash set")
194
- loopback = config.host in {"127.0.0.1", "::1", "localhost"}
195
- if not loopback and not (
196
- a.password_required or a.reverse_proxy.enabled or a.allow_unauthenticated_network
197
- ):
198
- return Check("auth", FAIL, f"non-loopback host {config.host} without any auth")
199
- if not loopback and a.allow_unauthenticated_network:
200
- return Check("auth", WARN, "bound non-loopback with auth explicitly disabled")
194
+ # Same "is auth actually enforced?" rule the config validator uses, so doctor never
195
+ # calls a config consistent that the validator would refuse to start.
196
+ if _missing_enforced_auth(config.host, a):
197
+ if a.allow_unauthenticated_network:
198
+ return Check("auth", WARN, "bound non-loopback with auth explicitly disabled")
199
+ return Check("auth", FAIL, f"non-loopback host {config.host} without enforced auth")
201
200
  return Check("auth", OK, "configuration consistent")
202
201
 
203
202
 
@@ -93,7 +93,10 @@ _HASH = (
93
93
  @pytest.mark.parametrize(
94
94
  "extra",
95
95
  [
96
- "host: 0.0.0.0\nauth:\n reverse_proxy:\n enabled: true\n",
96
+ # reverse-proxy auth only counts when auth.enabled is set (it's the runtime gate).
97
+ "host: 0.0.0.0\nauth:\n enabled: true\n reverse_proxy:\n enabled: true\n",
98
+ "host: 0.0.0.0\nauth:\n enabled: true\n password_required: true\n"
99
+ f" password_hash: '{_HASH}'\n",
97
100
  "host: 0.0.0.0\nauth:\n allow_unauthenticated_network: true\n",
98
101
  ],
99
102
  )
@@ -102,6 +105,21 @@ def test_non_loopback_allowed_with_auth(write_config, extra):
102
105
  assert config.host == "0.0.0.0"
103
106
 
104
107
 
108
+ @pytest.mark.parametrize(
109
+ "extra",
110
+ [
111
+ # The footgun: a password (or proxy) is configured but auth.enabled is left at its
112
+ # false default, so the runtime guard would serve the dashboard unauthenticated.
113
+ # The validator must refuse rather than start a silently-open non-loopback bind.
114
+ f"host: 0.0.0.0\nauth:\n password_required: true\n password_hash: '{_HASH}'\n",
115
+ "host: 0.0.0.0\nauth:\n reverse_proxy:\n enabled: true\n",
116
+ ],
117
+ )
118
+ def test_non_loopback_rejected_when_auth_not_enabled(write_config, extra):
119
+ with pytest.raises(ValueError, match="without enforced auth"):
120
+ load_config(write_config(extra))
121
+
122
+
105
123
  def test_password_required_without_hash_fails_closed(write_config):
106
124
  cfg_path = write_config("auth:\n enabled: true\n password_required: true\n")
107
125
  with pytest.raises(ValueError, match="password_hash is empty"):
@@ -179,7 +179,7 @@ wheels = [
179
179
 
180
180
  [[package]]
181
181
  name = "clauster"
182
- version = "0.1.0"
182
+ version = "0.2.2"
183
183
  source = { editable = "." }
184
184
  dependencies = [
185
185
  { name = "argon2-cffi" },
@@ -1,60 +0,0 @@
1
- name: Release Please
2
-
3
- # Release Please reads Conventional Commits on main and opens a "release PR"
4
- # that bumps the version (src/clauster/__init__.py + pyproject.toml) and rewrites
5
- # CHANGELOG.md. Merging that PR tags the commit (e.g. `v0.2.0`), which fires
6
- # release.yml (`on: push: tags`).
7
-
8
- on:
9
- push:
10
- branches: [main]
11
-
12
- permissions:
13
- contents: read
14
-
15
- concurrency:
16
- group: release-please-${{ github.ref }}
17
- cancel-in-progress: false
18
-
19
- jobs:
20
- release-please:
21
- runs-on: ubuntu-latest
22
- timeout-minutes: 10
23
- # GITHUB_TOKEN stays read-only: the App token (minted below) does all the
24
- # writing, so the default token needs no write scope here.
25
- permissions:
26
- contents: read
27
- pull-requests: read
28
- steps:
29
- # Mint a short-lived installation token for the Clauster release GitHub
30
- # App so the release PR + version-bump commits are authored by the app's
31
- # bot identity, NOT a human account. Crucially this is *not* the default
32
- # GITHUB_TOKEN, so the tag pushed when the release PR merges still
33
- # *triggers* release.yml (GITHUB_TOKEN-pushed tags don't — anti-recursion).
34
- #
35
- # Requires (set up once, manually):
36
- # - a GitHub App owned by the repo owner, installed on this repo, with
37
- # repo permissions Contents: R/W + Pull requests: R/W;
38
- # - secrets RELEASE_PLEASE_APP_ID (numeric App ID) and
39
- # RELEASE_PLEASE_APP_PRIVATE_KEY (the App's .pem private key).
40
- # Until those exist this step 401s — merge this only after they're added.
41
- # Supersedes the old RELEASE_PLEASE_TOKEN PAT (which attributed PRs to a
42
- # real user); that secret can be deleted once this is live.
43
- - name: Mint GitHub App token
44
- id: app-token
45
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
46
- with:
47
- app-id: ${{ secrets.RELEASE_PLEASE_APP_ID }}
48
- private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
49
- # Scope the minted token to exactly what release-please needs, rather
50
- # than inheriting the installation's full permission set (zizmor
51
- # least-privilege). Add permission-issues: write only if you enable
52
- # release-please's label-on-issue features.
53
- permission-contents: write
54
- permission-pull-requests: write
55
-
56
- - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
57
- with:
58
- token: ${{ steps.app-token.outputs.token }}
59
- config-file: release-please-config.json
60
- manifest-file: .release-please-manifest.json
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.2.0"
3
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes