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.
- {clauster-0.2.0 → clauster-0.2.2}/.bestpractices.json +5 -5
- {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/lint.yml +2 -2
- clauster-0.2.2/.github/workflows/release-please.yml +139 -0
- clauster-0.2.2/.release-please-manifest.json +3 -0
- {clauster-0.2.0 → clauster-0.2.2}/CHANGELOG.md +19 -0
- {clauster-0.2.0 → clauster-0.2.2}/Dockerfile +3 -1
- {clauster-0.2.0 → clauster-0.2.2}/PKG-INFO +31 -16
- {clauster-0.2.0 → clauster-0.2.2}/README.md +30 -15
- {clauster-0.2.0 → clauster-0.2.2}/clauster.yml.example +2 -1
- {clauster-0.2.0 → clauster-0.2.2}/pyproject.toml +1 -1
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/__init__.py +1 -1
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/config.py +30 -11
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/ops.py +7 -8
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_config.py +19 -1
- {clauster-0.2.0 → clauster-0.2.2}/uv.lock +1 -1
- clauster-0.2.0/.github/workflows/release-please.yml +0 -60
- clauster-0.2.0/.release-please-manifest.json +0 -3
- {clauster-0.2.0 → clauster-0.2.2}/.coderabbit.yaml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.dockerignore +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.github/CODEOWNERS +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.github/settings.yml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/ci.yml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/pr-title.yml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/release.yml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/scorecard.yml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.github/workflows/security.yml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.gitignore +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.markdownlint-cli2.yaml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.pre-commit-config.yaml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/.yamllint.yaml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/CONTRIBUTING.md +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/LICENSE +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/SECURITY.md +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/THIRD_PARTY_NOTICES.md +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/UPGRADING.md +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/clauster.spec +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/codecov.yml +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/docker/entrypoint.sh +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/docs/screenshots/dashboard-dark.png +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/docs/screenshots/dashboard-light.png +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/docs/screenshots/login-dark.png +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/docs/screenshots/new-project-clone.png +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/package-lock.json +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/package.json +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/release-please-config.json +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/renovate.json +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/scripts/build-binary.sh +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/scripts/lint-docs.sh +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/__main__.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/app.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/auth.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/bridge_log.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/claude_cli.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/claude_md.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/clone_jobs.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/discovery.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/environments.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/hooks/__init__.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/hooks/resume_recap.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/inspector.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/logstream.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/models.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/pointers.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/procutil.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/provisioning.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/pty_keeper.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/recap.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/redact.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/runner.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/state.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/alpine.LICENSE +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/alpine.min.js +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/clauster.css +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/favicon.svg +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/iconoir/LICENSE +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/iconoir/README.md +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/tabler/LICENSE +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/tabler/README.md +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/tabler/css/tabler.min.css +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/tabler/js/tabler.min.js +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/static/vendor/versions.txt +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/templates/_iconoir_sprite.html +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/templates/_project_card.html +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/templates/dashboard.html +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/templates/login.html +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/trust.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/src/clauster/usage.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/E2E_CHECKLIST.md +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/conftest.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/bridge-logs/test1-bridge-debug.log +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/fake_claude/claude +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/fake_claude/claude.cmd +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/fake_git/git +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/fake_git/git.cmd +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/pointers/dockerize2.bridge-pointer.json +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/pointers/test1.bridge-pointer.json +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/pointers/test2.bridge-pointer.json +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/fixtures/transcripts/test1-session.jsonl +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_app.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_app_auth.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_app_instances.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_app_routes.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_auth.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_bridge_log.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_claude_md.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_clone_jobs.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_discovery.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_environments.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_fixtures.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_inspector.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_logstream.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_logtail.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_main.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_ops.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_pointers.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_procutil.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_provisioning.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_pty_keeper.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_recap.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_redact.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_runner.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_runner_pty.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_runner_recap.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_spawn_controls.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_state.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_trust.py +0 -0
- {clauster-0.2.0 → clauster-0.2.2}/tests/test_urls.py +0 -0
- {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": "
|
|
41
|
-
"version_tags_justification": "
|
|
42
|
-
"release_notes_status": "
|
|
43
|
-
"release_notes_justification": "
|
|
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
|
|
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
|
|
46
|
+
- name: Set up Node 24
|
|
47
47
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
48
48
|
with:
|
|
49
|
-
node-version: "
|
|
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."
|
|
@@ -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
|
|
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.
|
|
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
|
|
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`,
|
|
201
|
-
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
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`,
|
|
158
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
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 #
|
|
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
|
|
@@ -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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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"):
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|