Compare commits
43 Commits
9e649d3177
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
042e3bf05a | ||
|
|
663bf5dfa4 | ||
|
|
3f9c7f8dd6 | ||
|
|
55a07c8ba5 | ||
|
|
89d6952c46 | ||
|
|
58c3fd61b8 | ||
|
|
f10a566b4c | ||
|
|
0ead28a7bf | ||
|
|
5f9ecb7b05 | ||
|
|
99a55c2109 | ||
|
|
b2ada7e4a7 | ||
|
|
2e0db91c2b | ||
|
|
16931942b9 | ||
|
|
3b67bef373 | ||
|
|
278eeb931b | ||
|
|
83aa854206 | ||
|
|
885b055768 | ||
|
|
79fb5a93ef | ||
|
|
b1f238a024 | ||
|
|
a2a3583a1d | ||
|
|
37f73b124b | ||
|
|
497691007b | ||
|
|
f79ae66d73 | ||
|
|
ff4df36fba | ||
|
|
ae2c19074a | ||
|
|
c10801ae75 | ||
|
|
c203703ead | ||
|
|
eef927d485 | ||
|
|
0e431c8281 | ||
|
|
10d32afa69 | ||
|
|
041356e68b | ||
|
|
789f8813d3 | ||
|
|
85bc9c723d | ||
|
|
dd650156f3 | ||
|
|
6a27361cef | ||
|
|
26d922de3e | ||
|
|
6f017de2e0 | ||
|
|
5f330bb971 | ||
|
|
2cc76e19d4 | ||
|
|
8e864f032a | ||
|
|
278818970f | ||
|
|
b081e94323 | ||
|
|
5892a8f354 |
12
.github/stargazer_countries.csv
vendored
@@ -105,6 +105,7 @@ BigTwoFly,
|
||||
Bigdaddy1990,Germany
|
||||
BilboBagCoder,
|
||||
Bill-Dung,Germany
|
||||
Blueyzachary,
|
||||
BobMcCloy,
|
||||
Bobdugrand8,France
|
||||
Boilerplate4u,
|
||||
@@ -234,6 +235,7 @@ EricCorleone,
|
||||
Erreur32,
|
||||
Errox,Netherlands
|
||||
Espagona,
|
||||
EsteSama,
|
||||
EthanVCS,United Kingdom
|
||||
EtienneMD,
|
||||
Evel270,
|
||||
@@ -347,6 +349,7 @@ Jaber7,
|
||||
JackTyson,United Kingdom
|
||||
JackoKic,
|
||||
JaggedJax,United States
|
||||
JairCosta00,
|
||||
Jak12-3,
|
||||
Jango024,
|
||||
JaroSu,Poland
|
||||
@@ -738,6 +741,7 @@ TimInTech,Germany
|
||||
Timbo74,
|
||||
Tohtli,
|
||||
Tokahiro,Germany
|
||||
TokenLimitReached,
|
||||
TomCasavant,
|
||||
TomHuber1,
|
||||
TopsideWings,
|
||||
@@ -1105,6 +1109,7 @@ cln-io,
|
||||
cloogshizer,
|
||||
cloudlena,Switzerland
|
||||
clubxtc,
|
||||
clutchthrower,
|
||||
cndoit18,China
|
||||
cnrlmr,
|
||||
coaster3000,United States
|
||||
@@ -1162,6 +1167,7 @@ danctrl,Germany
|
||||
danez,United States
|
||||
danieldotnl,Netherlands
|
||||
danishru,
|
||||
danmulvey,United States
|
||||
dannybeeckman,
|
||||
dannybloomfield,United States
|
||||
danveitch76,
|
||||
@@ -1234,6 +1240,7 @@ dmanifold,
|
||||
dmostert,
|
||||
dnoggle,United States
|
||||
dobrjaha,
|
||||
docker-alex-ai,
|
||||
docker-master,
|
||||
doctorvanmartin,
|
||||
dollannn,Sweden
|
||||
@@ -1372,6 +1379,7 @@ frizzi42,
|
||||
froggy974,France
|
||||
frostworx,
|
||||
frsantos,Spain
|
||||
frunkad,
|
||||
fuatakgun,
|
||||
fullstackdelay,Germany
|
||||
funar,United States
|
||||
@@ -1584,6 +1592,7 @@ jhhbe,
|
||||
jhiegel,
|
||||
jhron,
|
||||
jiadongjiang,
|
||||
jiange1236,
|
||||
jimmyang1992,
|
||||
jine,Sweden
|
||||
jj-csg,
|
||||
@@ -2141,6 +2150,7 @@ quirbiefe,
|
||||
qun-media,
|
||||
r0rqual,United States
|
||||
rJUUSO,
|
||||
rafaelcruzmartins,
|
||||
ralong777,
|
||||
ranjitrajkumar,Canada
|
||||
raphael1688,
|
||||
@@ -2212,6 +2222,7 @@ rstruminski,
|
||||
rtizzy,United States
|
||||
rtmlp,
|
||||
rucas,United States
|
||||
rummik,United States
|
||||
ruok911,
|
||||
rvbg,Germany
|
||||
rwagnervm,Brazil
|
||||
@@ -2359,6 +2370,7 @@ sudo-shubham,India
|
||||
suiciety,
|
||||
sunshine-hass,
|
||||
superiuspi,France
|
||||
supersonic-jet,
|
||||
supersonical,
|
||||
superyass,
|
||||
sweetmeats83,United States
|
||||
|
||||
|
BIN
.github/stargazer_map.png
vendored
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 58 KiB |
BIN
.github/stats.png
vendored
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
.github/stats_addons.png
vendored
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 3.6 KiB |
6
.github/workflows/archived_lint-checks.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
container: ghcr.io/hadolint/hadolint:latest-alpine
|
||||
steps:
|
||||
- name: ↩️ Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
container: koalaman/shellcheck-alpine:latest
|
||||
steps:
|
||||
- name: ↩️ Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ↩️ Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
# Full git history is needed to get a proper list of changed files within `super-linter`
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/daily_README.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Install jq + yq (v4)
|
||||
run: |
|
||||
|
||||
18
.github/workflows/daily_ai_fix.yaml
vendored
@@ -50,7 +50,6 @@ permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
||||
|
||||
concurrency:
|
||||
group: ai-fix-sweep
|
||||
@@ -66,7 +65,7 @@ jobs:
|
||||
environment: CR_PAT
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.AI_PR_TOKEN }}
|
||||
@@ -126,12 +125,19 @@ jobs:
|
||||
|
||||
- name: Analyse and fix
|
||||
if: steps.batch.outputs.count != '0'
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# One sticky, auto-updating status comment per run instead of the
|
||||
# model narrating its own progress in scattered comments.
|
||||
track_progress: true
|
||||
# Skip the OIDC -> Claude App token exchange. The scheduled path
|
||||
# happens to pass it (github.actor is the maintainer), but
|
||||
# workflow_dispatch by anyone else would 401. AI_PR_TOKEN, not
|
||||
# GITHUB_TOKEN, so a PR Claude opens triggers CI.
|
||||
github_token: ${{ secrets.AI_PR_TOKEN }}
|
||||
# No track_progress here. It needs an issue or PR to hang its sticky
|
||||
# comment on, and the action hard-fails validation without one; this
|
||||
# workflow only ever runs on schedule/workflow_dispatch. Per-issue
|
||||
# progress still gets reported — issue-fix.md has Claude comment on
|
||||
# each issue directly via gh.
|
||||
prompt: |
|
||||
The batch of issues to work through is /tmp/ai-fix/batch.json.
|
||||
Follow .github/prompts/issue-fix.md exactly. Do not deviate from
|
||||
|
||||
2
.github/workflows/generate_stargazer_map.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
|
||||
2
.github/workflows/helper_stats_graphs.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
- name: Install apps
|
||||
run: |
|
||||
git pull --rebase origin master
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
4
.github/workflows/on_claude_mention.yml
vendored
@@ -59,12 +59,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.
|
||||
|
||||
14
.github/workflows/on_issue_approved.yaml
vendored
@@ -32,7 +32,6 @@ permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
||||
|
||||
concurrency:
|
||||
group: ai-approve-${{ github.event.issue.number || inputs.issue }}
|
||||
@@ -76,7 +75,7 @@ jobs:
|
||||
environment: CR_PAT
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.AI_PR_TOKEN }}
|
||||
@@ -136,10 +135,17 @@ jobs:
|
||||
|
||||
- name: Execute the plan
|
||||
if: steps.bundle.outputs.has_plan == 'true'
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
track_progress: true
|
||||
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
||||
# github.actor lacks write access. AI_PR_TOKEN, not GITHUB_TOKEN, so
|
||||
# a PR Claude opens triggers CI.
|
||||
github_token: ${{ secrets.AI_PR_TOKEN }}
|
||||
# Only the `issues` path has a comment thread to track progress in.
|
||||
# On workflow_dispatch there is none, and passing true there fails
|
||||
# the action's input validation outright.
|
||||
track_progress: ${{ github.event_name == 'issues' }}
|
||||
prompt: |
|
||||
The approved plan is /tmp/ai-exec/plan.md and the issue it belongs
|
||||
to is /tmp/ai-exec/issue.json. Follow .github/prompts/issue-execute-plan.md
|
||||
|
||||
2
.github/workflows/on_issues.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
- name: Assign issues
|
||||
run: |
|
||||
# Init
|
||||
|
||||
11
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -41,7 +41,6 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
||||
|
||||
concurrency:
|
||||
group: ai-triage-${{ github.event.issue.number || inputs.issue || github.run_id }}
|
||||
@@ -124,7 +123,7 @@ jobs:
|
||||
# (issues.opened, dispatch) never set claim, so they always proceed.
|
||||
- name: Checkout tooling
|
||||
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
||||
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
@@ -145,9 +144,15 @@ jobs:
|
||||
id: classify
|
||||
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
||||
continue-on-error: true
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Without this the action falls back to the OIDC -> Claude App token
|
||||
# exchange, which 401s ("User does not have write access on this
|
||||
# repository") whenever github.actor is the outside reporter who
|
||||
# opened the issue or replied to a needs-info request. Same token the
|
||||
# step already exports as GH_TOKEN; classify only reads.
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
show_full_output: true
|
||||
prompt: |
|
||||
Read /tmp/ai-triage/context.md, then follow the instructions in
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Ping mapped submitter when add-on is mentioned
|
||||
env:
|
||||
|
||||
10
.github/workflows/on_pr_coderabbit.yml
vendored
@@ -25,7 +25,6 @@ permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
||||
|
||||
concurrency:
|
||||
group: ai-coderabbit-${{ github.event.pull_request.number }}
|
||||
@@ -66,7 +65,7 @@ jobs:
|
||||
|
||||
- name: Checkout PR branch
|
||||
if: steps.claim.outputs.go == 'true'
|
||||
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
@@ -80,9 +79,14 @@ jobs:
|
||||
|
||||
- name: Address CodeRabbit comments
|
||||
if: steps.claim.outputs.go == 'true'
|
||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
||||
# github.actor lacks write access — here github.actor is
|
||||
# coderabbitai[bot], the review submitter. AI_PR_TOKEN, not
|
||||
# GITHUB_TOKEN, so the pushed fixes re-trigger CI on the PR.
|
||||
github_token: ${{ secrets.AI_PR_TOKEN }}
|
||||
prompt: |
|
||||
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
|
||||
on ${{ github.repository }}. You are on that PR's branch. Follow
|
||||
|
||||
6
.github/workflows/onpr_check-pr.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
changedChangelogFiles: ${{ steps.changed-files.outputs.changelogs_files }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
# Need the merge commit's parents resolvable (HEAD^1 below): a depth-1 shallow
|
||||
# checkout truncates parent refs entirely at the boundary commit.
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
||||
steps:
|
||||
- name: ↩️ Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: 🔎 Run Home Assistant Add-on Lint
|
||||
uses: frenck/action-addon-linter@v2
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
||||
steps:
|
||||
- name: ↩️ Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Copy templates into addon build context
|
||||
env:
|
||||
|
||||
12
.github/workflows/onpush_builder.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
changedAddons: ${{ steps.find_addons.outputs.changed_addons }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
sanitizeCommitted: ${{ steps.sanitize_commit.outputs.committed }}
|
||||
sanitizeCommitSha: ${{ steps.sanitize_commit.outputs.commit_long_sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
matrix:
|
||||
addon: ${{ fromJSON(needs.detect-changed-addons.outputs.changedAddons) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Run Home Assistant Add-on Lint
|
||||
uses: frenck/action-addon-linter@v2
|
||||
with:
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
- arch: aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -406,7 +406,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
4
.github/workflows/weekly_crlftolf.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository contents
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Use action to check for CRLF endings
|
||||
uses: erclu/check-crlf@v1
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest # Use a Linux runner
|
||||
steps:
|
||||
- name: Checkout repository contents
|
||||
uses: actions/checkout@v7 # Use the checkout action
|
||||
uses: actions/checkout@v7.0.1 # Use the checkout action
|
||||
- name: Find files with CRLF endings
|
||||
uses: erclu/check-crlf@v1.2.0 # Use the check-crlf action
|
||||
id: check-crlf # Assign an id to this step
|
||||
|
||||
2
.github/workflows/weekly_reduceimagesize.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: Compress Images
|
||||
id: calibre
|
||||
|
||||
2
.github/workflows/weekly_stats.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v7.0.1
|
||||
- name: Create stats
|
||||
run: |
|
||||
echo "Starting"
|
||||
|
||||
@@ -3,6 +3,160 @@
|
||||
|
||||
set -e
|
||||
|
||||
################################################################################
|
||||
# Block markers, temp helper, quoting and export-block builders
|
||||
#
|
||||
# Everything here is free of side effects and defined before the Supervisor
|
||||
# guard, so that the self-test below can exercise the whole value path outside a
|
||||
# container, where bashio is not available.
|
||||
################################################################################
|
||||
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
|
||||
BLOCK_END="# --- END ADDON ENV (generated) ---"
|
||||
|
||||
mktemp_safe() {
|
||||
local tmpdir="${TMPDIR:-/tmp}"
|
||||
mkdir -p "$tmpdir"
|
||||
mktemp "$tmpdir/tmp.XXXXXXXXXX"
|
||||
}
|
||||
|
||||
dotenv_quote() {
|
||||
# For /.env and /etc/environment: double quotes + minimal escaping.
|
||||
#
|
||||
# These files are read back by sourcing them from a shell, so every
|
||||
# character that is still special inside double quotes has to be escaped.
|
||||
# $ and ` used to be left alone, which meant a value was expanded instead of
|
||||
# being read literally: a password like pa$$w0rd came back with the shell
|
||||
# PID spliced into it, and a value containing backticks ran as a command.
|
||||
#
|
||||
# Backslash must be doubled first, so that the backslashes added below are
|
||||
# not doubled in turn.
|
||||
local v="$1"
|
||||
v="${v//\\/\\\\}"
|
||||
v="${v//\"/\\\"}"
|
||||
v="${v//\$/\\\$}"
|
||||
v="${v//\`/\\\`}"
|
||||
v="${v//$'\n'/\\n}"
|
||||
v="${v//$'\r'/\\r}"
|
||||
printf '"%s"' "$v"
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
# Single-quote for safe injection into shell code.
|
||||
#
|
||||
# Inside single quotes every character is literal, so the only thing a value
|
||||
# needs escaping for is the quote character itself: close the quote, emit an
|
||||
# escaped quote, reopen it. Backslashes must be left untouched.
|
||||
#
|
||||
# This used to double every backslash and to replace ' with '"'"' followed by
|
||||
# a stray space. The stray space corrupted every value containing a quote
|
||||
# (O'Brien pass arrived as O' Brien pass); the doubling was undone further
|
||||
# down the path by the "awk -v" in append_export, so backslashes survived by
|
||||
# accident. Both halves are fixed together -- see the note in append_export.
|
||||
local s="$1"
|
||||
printf "'%s'" "${s//\'/\'\\\'\'}"
|
||||
}
|
||||
|
||||
append_export() {
|
||||
# Plain append, deliberately not awk: "awk -v q=$value" runs the value
|
||||
# through awk's escape processing, which turns \t into a tab, \b into a
|
||||
# backspace and \\ into a single backslash. That used to be cancelled out by
|
||||
# shell_quote doubling every backslash, so the two bugs hid each other --
|
||||
# fixing only one of them corrupts the value.
|
||||
printf 'export %s=%s\n' "$1" "$(shell_quote "$2")" >> "$EXPORT_BODY"
|
||||
}
|
||||
|
||||
compose_export_block() {
|
||||
{
|
||||
echo "$BLOCK_BEGIN"
|
||||
echo "# Generated from $JSONSOURCE"
|
||||
cat "$EXPORT_BODY"
|
||||
echo "$BLOCK_END"
|
||||
} > "$EXPORT_BLOCK"
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Self-test: bash .templates/00-global_var.sh --self-test
|
||||
#
|
||||
# Builds a real export block and sources it, which is exactly what happens once
|
||||
# the block is injected at the top of a service run script, then checks that
|
||||
# every value came back byte for byte. Testing the whole path matters: the two
|
||||
# defects this guards against (shell_quote doubling backslashes and append_export
|
||||
# passing values through "awk -v") cancelled each other out, so a test of either
|
||||
# helper alone reported success while the pair was wrong.
|
||||
#
|
||||
# Runs before the Supervisor guard and exits, so it never affects startup.
|
||||
################################################################################
|
||||
if [[ "${1:-}" == "--self-test" ]]; then
|
||||
# Literal test data: the single quotes and metacharacters are the point.
|
||||
# shellcheck disable=SC2016
|
||||
self_test_values=(
|
||||
'plain.host'
|
||||
'next\.duckdns\.org' # regex, dots escaped once
|
||||
'next\\.duckdns\\.org' # regex, dots escaped twice by the user
|
||||
'C:\Users\bob\share' # windows path, \U and \b are awk escapes
|
||||
'\\server\share' # UNC path
|
||||
'col\tsep' # \t is an awk escape
|
||||
"O'Brien pass" # embedded quote
|
||||
"it's a 'quoted' word" # several embedded quotes
|
||||
"'leading"
|
||||
"trailing'"
|
||||
'a$b`c"d' # shell metacharacters
|
||||
'*.example.com|^foo\d+$'
|
||||
$'sp ace\ttab'
|
||||
''
|
||||
)
|
||||
|
||||
JSONSOURCE="self-test"
|
||||
EXPORT_BODY="$(mktemp_safe)"
|
||||
EXPORT_BLOCK="$(mktemp_safe)"
|
||||
self_test_env="$(mktemp_safe)"
|
||||
trap 'rm -f "$EXPORT_BODY" "$EXPORT_BLOCK" "$self_test_env"' EXIT
|
||||
self_test_rc=0
|
||||
|
||||
self_test_check() {
|
||||
# $1 name of the variable that was read back, $2 expected value, $3 how
|
||||
local self_test_got="${!1}"
|
||||
[[ "$self_test_got" == "$2" ]] && return 0
|
||||
printf 'FAIL (%s): <%s> came back as <%s>\n' "$3" "$2" "$self_test_got"
|
||||
self_test_rc=1
|
||||
}
|
||||
|
||||
# 1. The export block, sourced the way an injected run script would
|
||||
for self_test_i in "${!self_test_values[@]}"; do
|
||||
append_export "SELFTEST_${self_test_i}" "${self_test_values[$self_test_i]}"
|
||||
done
|
||||
compose_export_block
|
||||
# shellcheck source=/dev/null
|
||||
. "$EXPORT_BLOCK"
|
||||
for self_test_i in "${!self_test_values[@]}"; do
|
||||
self_test_check "SELFTEST_${self_test_i}" "${self_test_values[$self_test_i]}" "export block"
|
||||
done
|
||||
|
||||
# 2. /.env, sourced the way browserless_chrome and wger read it back.
|
||||
# Values holding a newline are out of scope: dotenv_quote writes them as a
|
||||
# literal \n, which a dotenv parser unescapes but a shell does not.
|
||||
for self_test_i in "${!self_test_values[@]}"; do
|
||||
printf 'DOTENVTEST_%s=%s\n' \
|
||||
"$self_test_i" "$(dotenv_quote "${self_test_values[$self_test_i]}")"
|
||||
done > "$self_test_env"
|
||||
if ! bash -n "$self_test_env"; then
|
||||
# An unescaped backtick or quote leaves the file unparseable, which would
|
||||
# abort the sourcing shell instead of just yielding a wrong value.
|
||||
echo "FAIL (dotenv): generated env file is not valid shell"
|
||||
self_test_rc=1
|
||||
else
|
||||
# shellcheck source=/dev/null
|
||||
. "$self_test_env"
|
||||
for self_test_i in "${!self_test_values[@]}"; do
|
||||
self_test_check "DOTENVTEST_${self_test_i}" "${self_test_values[$self_test_i]}" "dotenv"
|
||||
done
|
||||
fi
|
||||
|
||||
[[ "$self_test_rc" -eq 0 ]] &&
|
||||
echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
|
||||
exit "$self_test_rc"
|
||||
fi
|
||||
|
||||
################################################################################
|
||||
# Guard: only run inside Supervisor-managed add-ons
|
||||
################################################################################
|
||||
@@ -30,15 +184,6 @@ command -v jq >/dev/null || bashio::exit.nok "jq is required"
|
||||
mkdir -p /etc
|
||||
touch "$ETC_ENV_FILE"
|
||||
|
||||
################################################################################
|
||||
# Temp helper
|
||||
################################################################################
|
||||
mktemp_safe() {
|
||||
local tmpdir="${TMPDIR:-/tmp}"
|
||||
mkdir -p "$tmpdir"
|
||||
mktemp "$tmpdir/tmp.XXXXXXXXXX"
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Secrets support
|
||||
################################################################################
|
||||
@@ -68,54 +213,13 @@ resolve_secret() {
|
||||
printf '%s' "$line"
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Quoting
|
||||
################################################################################
|
||||
dotenv_quote() {
|
||||
# For /.env and /etc/environment: double quotes + minimal escaping
|
||||
local v="$1"
|
||||
v="${v//\\/\\\\}"
|
||||
v="${v//\"/\\\"}"
|
||||
v="${v//$'\n'/\\n}"
|
||||
v="${v//$'\r'/\\r}"
|
||||
printf '"%s"' "$v"
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
# Single-quote for safe injection in shell code
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\'/\'\"\'\"\' }"
|
||||
s="${s% }"
|
||||
printf "'%s'" "$s"
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# S6 + script injection block
|
||||
################################################################################
|
||||
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
|
||||
BLOCK_END="# --- END ADDON ENV (generated) ---"
|
||||
|
||||
EXPORT_BLOCK="$(mktemp_safe)"
|
||||
EXPORT_BODY="$(mktemp_safe)"
|
||||
KV_FILE="$(mktemp_safe)"
|
||||
trap 'rm -f "$EXPORT_BLOCK" "$KV_FILE"' EXIT
|
||||
|
||||
{
|
||||
echo "$BLOCK_BEGIN"
|
||||
echo "# Generated from $JSONSOURCE"
|
||||
echo "$BLOCK_END"
|
||||
} > "$EXPORT_BLOCK"
|
||||
|
||||
append_export() {
|
||||
local k="$1" v="$2" q
|
||||
q="$(shell_quote "$v")"
|
||||
|
||||
awk -v k="$k" -v q="$q" -v e="$BLOCK_END" '
|
||||
$0==e { print "export " k "=" q }
|
||||
{ print }
|
||||
' "$EXPORT_BLOCK" > "$EXPORT_BLOCK.tmp"
|
||||
mv "$EXPORT_BLOCK.tmp" "$EXPORT_BLOCK"
|
||||
}
|
||||
trap 'rm -f "$EXPORT_BLOCK" "$EXPORT_BODY" "$KV_FILE"' EXIT
|
||||
|
||||
inject_block() {
|
||||
local f="$1" tmp
|
||||
@@ -235,6 +339,8 @@ cp "$ENV_FILE" "$ETC_ENV_FILE"
|
||||
################################################################################
|
||||
# Inject into scripts and shells (best-effort)
|
||||
################################################################################
|
||||
compose_export_block
|
||||
|
||||
for f in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run /etc/cont-init.d/*.sh /entrypoint.sh /etc/bash.bashrc "${GLOBAL_VAR_FILES:-}"; do
|
||||
[[ -f "$f" ]] && inject_block "$f"
|
||||
done
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
aurral/stats.png
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
baikal/stats.png
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +1,13 @@
|
||||
|
||||
## 1.6.0.2 (2026-07-27)
|
||||
|
||||
- Fix base_url sed patterns rewriting *every* `base_url` key in Bazarr's config.yaml (radarr.base_url, sonarr.base_url, and any other configured integration), instead of only Bazarr's own under `general:`. This silently broke the Radarr/Sonarr connections inside Bazarr on every addon restart when ingress was enabled
|
||||
|
||||
## 1.6.0.1 (2026-07-27)
|
||||
|
||||
- Fix ingress: nginx rewrote Bazarr's redirects into an absolute `http://<host>:8099/...` URL, which the browser blocked as mixed content when Home Assistant is served over HTTPS. Redirects now stay relative and point at the ingress path
|
||||
- Fix fallback base_url in the nginx service script missing its leading `/`, which crashed Bazarr on startup
|
||||
|
||||
## 1.6.0 (2026-07-08)
|
||||
|
||||
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
|
||||
|
||||
@@ -112,4 +112,4 @@ schema:
|
||||
slug: bazarr_nas
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/bazarr
|
||||
version: "1.6.0"
|
||||
version: "1.6.0.2"
|
||||
|
||||
@@ -35,16 +35,20 @@ if [ -f "$CONFIG_LOCATION" ]; then
|
||||
ingress_noauth)
|
||||
bashio::log.green "Ingress is enabled, authentication is disabled"
|
||||
bashio::log.yellow "WARNING : Make sure that the port is not exposed externally by your router to avoid a security risk !"
|
||||
# Set base_url (must start with / for Flask blueprint registration)
|
||||
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
|
||||
# Set base_url (must start with / for Flask blueprint registration).
|
||||
# Scoped to the general: block only -- config.yaml also carries a
|
||||
# base_url under each configured *arr integration (radarr.base_url,
|
||||
# sonarr.base_url, ...) and those must not be touched.
|
||||
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
|
||||
# Disable auth
|
||||
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: null/ }' "$CONFIG_LOCATION"
|
||||
;;
|
||||
# Ingress mode, with authentication
|
||||
ingress_auth)
|
||||
bashio::log.green "Ingress is enabled, and external authentication is enabled"
|
||||
# Set base_url (must start with / for Flask blueprint registration)
|
||||
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
|
||||
# Set base_url (must start with / for Flask blueprint registration).
|
||||
# Scoped to the general: block only -- see note above.
|
||||
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
|
||||
# Enable Bazarr auth when leaving ingress_noauth
|
||||
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
|
||||
;;
|
||||
@@ -52,7 +56,8 @@ if [ -f "$CONFIG_LOCATION" ]; then
|
||||
noingress_auth)
|
||||
bashio::log.green "Disabling ingress and enabling authentication"
|
||||
bashio::log.yellow "WARNING : Ingress is disabled so the app won't be available from HA itself !"
|
||||
sed -i "s/ base_url:.*/ base_url: ''/" "$CONFIG_LOCATION"
|
||||
# Scoped to the general: block only -- see note above.
|
||||
sed -i "/^general:/,/^[^ ]/{ s/ base_url:.*/ base_url: ''/; }" "$CONFIG_LOCATION"
|
||||
# Enable Bazarr auth when leaving ingress_noauth
|
||||
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
|
||||
;;
|
||||
|
||||
@@ -19,6 +19,19 @@ server {
|
||||
proxy_set_header Connection $http_connection;
|
||||
#auth_basic off;
|
||||
|
||||
# Adjust Location headers in backend redirects
|
||||
# Bazarr is Flask-based and answers /bazarr with a redirect to /bazarr/,
|
||||
# made absolute against the Host nginx sends upstream, so it reads
|
||||
# http://127.0.0.1:6767/bazarr/. proxy_redirect strips that prefix, and
|
||||
# nginx then re-absolutises the result as $scheme://$host:$server_port/...
|
||||
# i.e. http://<ha_host>:8099/bazarr/ -- blocked by the browser as mixed
|
||||
# content inside the ingress iframe. absolute_redirect off keeps it
|
||||
# relative; the proxy_redirect rules re-prefix it with the ingress entry
|
||||
# (the second rule also covers a redirect that was relative already).
|
||||
absolute_redirect off; # Do not add port to redirect
|
||||
proxy_redirect http://127.0.0.1:6767/ %%ingress_entry%%/;
|
||||
proxy_redirect / %%ingress_entry%%/;
|
||||
|
||||
# Correct base_url
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once off;
|
||||
|
||||
@@ -15,9 +15,13 @@ bashio::net.wait_for "$port" localhost 900
|
||||
if [ -f "$CONFIG_LOCATION" ]; then
|
||||
if ! bashio::config.true "ingress_disabled"; then
|
||||
if ! bashio::config.has_value "connection_mode" || [ "$(bashio::config 'connection_mode')" != "noingress_auth" ]; then
|
||||
if ! grep -q "base_url.*$slug" "$CONFIG_LOCATION"; then
|
||||
if ! sed -n "/^general:/,/^[^ ]/ { /^ base_url: \/$slug$/p; }" "$CONFIG_LOCATION" | grep -q .; then
|
||||
bashio::log.warning "BaseUrl not set properly, restarting"
|
||||
sed -i "s/ base_url:.*/ base_url: $slug/" "$CONFIG_LOCATION"
|
||||
# Must start with / for Flask blueprint registration. Scoped to
|
||||
# the general: block only -- config.yaml also carries a base_url
|
||||
# under each configured *arr integration (radarr.base_url,
|
||||
# sonarr.base_url, ...) and those must not be touched.
|
||||
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
|
||||
bashio::addon.restart
|
||||
fi
|
||||
fi
|
||||
|
||||
BIN
bazarr/stats.png
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,4 +1,19 @@
|
||||
## 1.36.2 (28-07-2026)
|
||||
- Minor bugs fixed
|
||||
|
||||
## 1.36.1 (27-07-2026)
|
||||
|
||||
- Fix the Codex CLI install failing on every boot with `Verified Codex <version> installation failed; Codex is unavailable this boot`, leaving `install_codex_cli` permanently non-functional. The download, its SHA-256 verification, and the extraction all succeeded; the chain broke at the final step, which validates the candidate binary by running `--version` as the `abc` runtime user. `mktemp -d` creates its directory `0700 root:root`, and `abc` cannot traverse a root-only directory, so executing the staged binary failed with `unable to exec: Permission denied` (exit 126) before it could be moved into place. Reproduced and fixed by making the staging directory traversable (`chmod 0755`) immediately after `mktemp`; verified on a live add-on container, where the same probe goes from exit 126 to success once the mode is widened. Nothing secret is staged there — the public release archive and the extracted binary, both world-readable upstream artifacts — and the existing `cleanup()` trap still removes the directory on exit. The validation deliberately keeps running as `abc` rather than root, so the binary is exercised as the identity that will actually run it.
|
||||
|
||||
## 1.36 (27-07-2026)
|
||||
|
||||
- Add optional OpenAI Codex CLI support, so a Claude session in this add-on can delegate work to ChatGPT Codex. Three parts: an `install_codex_cli` switch, a browserless way to activate a ChatGPT subscription on it, and an MCP registration that makes Codex callable as a tool from Claude.
|
||||
- **Install (`install_codex_cli`, default off).** Codex is deliberately *not* baked into the image: the Linux release binary is ~310 MB extracted, which is not worth carrying in every installation for an off-by-default option, and updating it would then require an add-on rebuild. The new `81-codex_cli.sh` cont-init script downloads the pinned static-musl release (`ARG`/`ENV CODEX_VERSION`, currently `0.145.0`) into `/data/codex/bin` and symlinks it onto `PATH`. `/data` is the add-on's own persistent volume — independent of the configurable `data_location` — so the ~113 MB download happens once, not per boot and not again after an add-on update unless the pinned release changes. The install prefix is outside `$HOME` on purpose: the managed-MCP merge in `82-claude_tools.sh` treats any server `command` under `$HOME` as user-installed and refuses to manage it, so an add-on-owned binary living there could never be updated or removed by the add-on. The download fails open like the Claude Desktop update check — an offline boot, a GitHub outage or a bad pin logs a warning and leaves any existing binary in place instead of aborting startup, and the new binary is validated by actually running `--version` from the staging directory before it replaces the current one, so a truncated download can never overwrite a working install. Staging deliberately happens under `/data` rather than the default `/tmp`: `/tmp` in this image is a RAM-backed tmpfs, so holding the 110 MB archive plus the 310 MB binary there during boot is a real risk on a small Home Assistant host, and it is mounted `noexec`, which would make the validation step impossible anyway. Staging on the destination filesystem also turns the final move into an atomic rename instead of a second 310 MB copy. Disabling the option is non-destructive: the binary and the completed sign-in are kept, so a disable/re-enable cycle needs neither another download nor another login.
|
||||
- **Subscription activation (`codex-login`).** Codex's default sign-in serves an OAuth callback on `localhost:1455` and expects a local browser, which cannot work here — the image ships no browser, and the add-on is reached through Home Assistant ingress. The new `/usr/local/bin/codex-login` helper runs `codex login --device-auth` instead, the flow OpenAI documents for headless machines: it prints a verification URL and a short-lived one-time code to approve on any other device, then polls until done. It can be run from the desktop's xterm (openbox right-click menu), from a Claude Code session, or from the container console, and it drops to the `abc` runtime user first so `~/.codex/auth.json` is not created root-owned — the same failure mode `83-github_cli.sh` documents for `~/.gitconfig`. Output is line-buffered so the URL and code appear immediately even without a TTY. If a session is already authenticated the helper reports the account and exits rather than starting a second flow. Credentials live in the persistent home, so the sign-in survives restarts and updates.
|
||||
- **MCP bridge.** `codex mcp-server` exposes Codex itself as a stdio MCP server (tools `codex` and `codex-reply`, verified by driving the 0.145.0 binary over stdio), which is registered through the *existing* managed-MCP merge in `82-claude_tools.sh` rather than a second copy of that logic — so it inherits the merge's idempotence, its refusal to clobber a user-customized entry, its removal-when-disabled behaviour, and the `0600` re-tightening of the token-bearing config files. Registration covers both Claude Code (`~/.claude.json`) and Claude Desktop, so every session type gets the tool. A managed `CLAUDE.md` block tells Claude what Codex is and when a second agent is worth the round-trip, since registering a server without guidance rarely gets it used — the same gap the Headroom block exists to close.
|
||||
- **Sandbox (`codex_sandbox_mode`, default `danger-full-access`).** Codex normally confines itself with a Linux OS sandbox (Landlock, or its bundled bubblewrap), which is unreliable inside a Home Assistant add-on container — and the container is already the security boundary. The default therefore bypasses Codex's own sandbox; `read-only` and `workspace-write` remain selectable. The chosen mode is applied in two places that are kept in sync: as root-level `-c` config overrides in the MCP registration (Codex forwards those to the MCP server, and they must precede the subcommand), and as a managed block at the top of `~/.codex/config.toml` so plain terminal `codex`/`codex exec` runs behave the same. That block is written at position 0 rather than appended, because a bare TOML key placed after a `[table]` header would silently become a key *of that table*. `approval_policy` is always `never` — an MCP- or cron-driven run has nobody to answer a prompt. Claude Code's own permission prompts still gate every `mcp__codex__*` call unless `permission_mode` is `bypass`.
|
||||
- `84-claude_runtime_ownership.sh` now also reconciles `~/.codex`, and `claude-tools-doctor.sh` reports the Codex binary, the installed-versus-pinned version, the two new switches, and the login status (which prints the auth mode, never the token).
|
||||
|
||||
## 1.35 (23-07-2026)
|
||||
|
||||
- Fix recurring "For your security, sign in again to keep using Claude." and the Claude app's dispatch tab showing this desktop as offline when opened from mobile first. Root cause (confirmed from `~/.config/Claude/logs/main.log` on a live install): the app launches with `--password-store=gnome-libsecret`, forcing Electron's libsecret/Secret-Service backend, but `gnome-keyring` was removed from the image in a previous commit because it prompted for a keyring password on first boot and blocked the app from launching. With the flag still forcing libsecret and no keyring daemon running, `safeStorage.isEncryptionAvailable()` is `false` — logs showed `session will not persist; app secrets fall back to plaintext` and `cannot store allowlist cache`. The un-persisted session then goes stale, failing the elevated-access OAuth check (`session_stale_relogin`) that the cowork/dispatch bridge needs, so the bridge is "parked until re-login" — which is what the Claude app surfaces as the desktop being offline, until a fresh sign-in (only completable from a computer, see `SIGN_IN.md` Problem A) un-parks it. `rootfs/defaults/autostart` now launches with `--password-store=basic` instead: Electron's built-in fixed-key store needs no daemon and never prompts, and persists under `$HOME/.config/Claude` (`/data/data`, persistent), so the session survives restarts and dispatch stays online regardless of which device connects first. A passwordless keyring was considered and rejected — it would live in the same persistent volume as the ciphertext, adding no real protection in this single-user self-hosted setup. `Dockerfile`'s stale comment (still describing gnome-keyring as installed) is corrected; the package stays removed.
|
||||
|
||||
@@ -94,7 +94,7 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
||||
# cannot alter executables elsewhere in the image.
|
||||
COPY rootfs/ /
|
||||
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" \) -print -exec chmod +x {} \; && \
|
||||
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" -o -name "codex-login" \) -print -exec chmod +x {} \; && \
|
||||
chmod +x /usr/local/bin/claude
|
||||
|
||||
# Uses /bin for compatibility purposes
|
||||
|
||||
@@ -78,6 +78,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
- Custom script support through the repository standard `claude_desktop.sh`.
|
||||
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
|
||||
available as an opt-in plugin.
|
||||
- Optional OpenAI Codex CLI, authenticated exclusively with a ChatGPT
|
||||
subscription and reachable from Claude through the native Codex MCP server.
|
||||
- Optional Home Assistant MCP bridge so Claude can query and control Home
|
||||
Assistant.
|
||||
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
|
||||
@@ -105,6 +107,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
|
||||
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
|
||||
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
|
||||
| `install_codex_cli` | `false` | Install the latest stable OpenAI Codex CLI at startup and register its native MCP server so Claude can delegate work to ChatGPT Codex. |
|
||||
| `codex_sandbox_mode` | `workspace-write` | Filesystem scope Codex runs with: `read-only`, `workspace-write`, or `danger-full-access`. |
|
||||
| `enable_tools_health_report` | `true` | Write independent Headroom, RTK, and TokenSave gains to the add-on log hourly. |
|
||||
| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. |
|
||||
| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
|
||||
@@ -195,6 +199,77 @@ The dashboard is disabled externally by default. To expose it:
|
||||
The dashboard is unauthenticated. Do not publish this port to the public
|
||||
internet.
|
||||
|
||||
## Codex CLI
|
||||
|
||||
Setting `install_codex_cli: true` adds OpenAI's Codex CLI alongside Claude and
|
||||
registers `codex mcp-server` in both Claude Code and Claude Desktop. A Claude
|
||||
session can therefore delegate a task to ChatGPT Codex and read its result back
|
||||
through MCP.
|
||||
|
||||
Codex is not baked into the image because its Linux binary is large and the
|
||||
feature is off by default. At each startup, the add-on resolves the latest
|
||||
stable upstream release. It downloads the architecture-specific binary into
|
||||
persistent `/data/codex/bin` only when the installed release is missing or
|
||||
outdated, verifies the GitHub-published SHA-256 digest before extraction or
|
||||
execution, validates the staged binary with `--version`, and replaces the
|
||||
existing binary atomically. If release metadata or the download is unavailable,
|
||||
startup continues and a previously working installation is retained.
|
||||
|
||||
### Signing in with a ChatGPT subscription
|
||||
|
||||
The add-on has no browser, so use the bundled device-code helper:
|
||||
|
||||
```bash
|
||||
codex-login
|
||||
```
|
||||
|
||||
Run it from the desktop's xterm, a Claude Code session, or the container
|
||||
console. It prints a verification URL and one-time code that you approve on
|
||||
another device. Credentials are stored in the runtime user's persistent
|
||||
`~/.codex/auth.json`, so the sign-in survives restarts and add-on updates.
|
||||
|
||||
This integration is deliberately **subscription-only**. The managed launcher
|
||||
removes any inherited `OPENAI_API_KEY` and starts every Codex command—including
|
||||
`codex mcp-server`—with:
|
||||
|
||||
```toml
|
||||
forced_login_method = "chatgpt"
|
||||
cli_auth_credentials_store = "file"
|
||||
```
|
||||
|
||||
The launcher also removes caller-provided overrides for those two keys before
|
||||
starting Codex. The same values are maintained in `~/.codex/config.toml`.
|
||||
Consequently, the MCP server uses the ChatGPT Codex entitlement and cannot
|
||||
silently fall back to usage-based OpenAI API-key billing.
|
||||
|
||||
### Using Codex from Claude
|
||||
|
||||
Claude receives two native MCP tools:
|
||||
|
||||
- `mcp__codex__codex` starts a task. Pass a self-contained `prompt` and set
|
||||
`cwd` to the repository Codex should inspect. The result includes a
|
||||
`threadId`.
|
||||
- `mcp__codex__codex-reply` continues the same Codex thread with its
|
||||
`threadId`.
|
||||
|
||||
The add-on also installs managed Claude guidance recommending Codex for
|
||||
independent review, a second diagnosis, or a competing implementation rather
|
||||
than routine lookups. Codex consumption counts against the signed-in ChatGPT
|
||||
plan's Codex allowance.
|
||||
|
||||
### Sandbox scope
|
||||
|
||||
`codex_sandbox_mode` defaults to `workspace-write`, allowing implementation
|
||||
inside the supplied repository without granting unrestricted access to every
|
||||
mounted path. Select `read-only` for review-only delegation. Use
|
||||
`danger-full-access` only as an explicit fallback when Codex's nested Linux
|
||||
sandbox is unavailable in the Home Assistant add-on container and the mounted
|
||||
paths are trusted.
|
||||
|
||||
`approval_policy` is always `never`, because an MCP-driven Codex process has no
|
||||
interactive operator to answer a prompt. Claude Code's own permissions still
|
||||
gate the `mcp__codex__*` call unless `permission_mode` is `bypass`.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
Run the following inside the add-on through a custom script or container console:
|
||||
@@ -206,8 +281,8 @@ claude-tools-doctor.sh
|
||||
The report checks the tool binaries, configuration switches, configured and
|
||||
effective runtime identities, redacted MCP registrations, Claude hooks,
|
||||
permission mode, Headroom health, TokenSave indexes, routing, and recorded
|
||||
savings. It never prints MCP environment values because the Home Assistant MCP
|
||||
entry can contain a long-lived token.
|
||||
savings. It never prints MCP environment values or raw Codex authentication
|
||||
status because either can contain credentials or masked credential fragments.
|
||||
|
||||
The hourly report can also be invoked manually:
|
||||
|
||||
@@ -288,6 +363,8 @@ Persistent state is stored in the configured `data_location` (default
|
||||
shared home
|
||||
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
|
||||
project
|
||||
- Codex authentication and configuration: `~/.codex`; the verified executable
|
||||
and subscription-only launcher live in persistent `/data/codex/bin`
|
||||
|
||||
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
|
||||
`$HOME/.cache`.
|
||||
|
||||
@@ -59,6 +59,8 @@ options:
|
||||
headroom_auto_compress: true
|
||||
headroom_wrap_claude_code: true
|
||||
install_caveman: false
|
||||
install_codex_cli: false
|
||||
codex_sandbox_mode: workspace-write
|
||||
install_github_cli: true
|
||||
install_headroom: true
|
||||
install_rtk: true
|
||||
@@ -107,6 +109,8 @@ schema:
|
||||
headroom_auto_compress: bool?
|
||||
headroom_wrap_claude_code: bool
|
||||
install_caveman: bool
|
||||
install_codex_cli: bool
|
||||
codex_sandbox_mode: list(read-only|workspace-write|danger-full-access)
|
||||
install_github_cli: bool
|
||||
install_headroom: bool
|
||||
install_rtk: bool
|
||||
@@ -118,5 +122,5 @@ slug: claude_desktop
|
||||
tmpfs: true
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.35"
|
||||
version: "1.36.2"
|
||||
video: true
|
||||
|
||||
328
claude_desktop/rootfs/etc/cont-init.d/81-codex_cli.sh
Executable file
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# OpenAI Codex CLI, installed on demand rather than baked into the image: the Linux release
|
||||
# binary is large and the option is off by default. Runs before 82-claude_tools.sh so the binary
|
||||
# exists when that script registers the `codex` MCP server.
|
||||
#
|
||||
# The install prefix is /data/codex, NOT $HOME/.codex/bin: /data is persistent regardless of the
|
||||
# configurable data_location, and the managed MCP merge treats commands under $HOME as
|
||||
# user-installed. Codex state (auth.json, config.toml) remains in the runtime user's home.
|
||||
CODEX_ROOT="/data/codex"
|
||||
CODEX_PREFIX="${CODEX_ROOT}/bin"
|
||||
CODEX_BIN="${CODEX_PREFIX}/codex"
|
||||
CODEX_REAL="${CODEX_PREFIX}/codex-real"
|
||||
CODEX_STAMP="${CODEX_PREFIX}/.version"
|
||||
CODEX_LINK="/usr/local/bin/codex"
|
||||
CODEX_RELEASE_API="https://api.github.com/repos/openai/codex/releases/latest"
|
||||
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
bashio::log.warning "Unable to resolve the abc runtime home; using /data/data"
|
||||
RUNTIME_HOME="/data/data"
|
||||
fi
|
||||
|
||||
run_as_runtime_user() {
|
||||
s6-setuidgid abc env HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$@"
|
||||
}
|
||||
|
||||
if ! bashio::config.true 'install_codex_cli'; then
|
||||
# Non-destructive: preserve the binary and completed ChatGPT sign-in for a later re-enable.
|
||||
# 82-claude_tools.sh removes only the MCP registration and managed guidance.
|
||||
bashio::log.info "Codex CLI disabled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64) CODEX_TARGET="x86_64-unknown-linux-musl" ;;
|
||||
aarch64 | arm64) CODEX_TARGET="aarch64-unknown-linux-musl" ;;
|
||||
*)
|
||||
bashio::log.warning "Codex CLI has no release binary for $(uname -m); skipping"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
CODEX_ASSET="codex-${CODEX_TARGET}.tar.gz"
|
||||
mkdir -p "$CODEX_PREFIX"
|
||||
|
||||
# Migrate the PR's earlier direct-binary layout to the enforced wrapper layout without another
|
||||
# download. The real binary is kept separately; `codex` becomes a small launcher that always
|
||||
# forces ChatGPT subscription authentication and removes any inherited API key.
|
||||
if [ ! -x "$CODEX_REAL" ] \
|
||||
&& [ -x "$CODEX_BIN" ] \
|
||||
&& run_as_runtime_user "$CODEX_BIN" --version > /dev/null 2>&1; then
|
||||
mv -f "$CODEX_BIN" "$CODEX_REAL"
|
||||
fi
|
||||
|
||||
# Resolve the latest stable release and its GitHub-published SHA-256 digest on every boot. This
|
||||
# follows upstream updates without pinning a version, while downloading the large asset only when
|
||||
# the installed version changes. A metadata outage never replaces or removes a working binary.
|
||||
codex_tmp="$(mktemp -d -p "$CODEX_ROOT")"
|
||||
# mktemp always creates 0700 root:root here, but the candidate binary is validated by running it
|
||||
# as the abc runtime user, which cannot traverse a root-only directory — that made every install
|
||||
# fail at the --version step with "unable to exec: Permission denied" (exit 126) and report
|
||||
# "Codex is unavailable this boot". Make the staging directory traversable. Nothing secret is
|
||||
# staged here: it holds the public release archive and the extracted binary, both of which are
|
||||
# world-readable upstream artifacts, and cleanup() removes the directory on exit.
|
||||
chmod 0755 "$codex_tmp"
|
||||
cleanup() {
|
||||
rm -rf "$codex_tmp"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
release_metadata="${codex_tmp}/release.json"
|
||||
release_info=""
|
||||
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 30 \
|
||||
-o "$release_metadata" "$CODEX_RELEASE_API"; then
|
||||
release_info="$(
|
||||
CODEX_ASSET="$CODEX_ASSET" python3 - "$release_metadata" <<'PY' 2> /dev/null || true
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
metadata = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
tag = metadata.get("tag_name", "")
|
||||
if not isinstance(tag, str) or not tag.startswith("rust-v"):
|
||||
raise SystemExit("unexpected release tag")
|
||||
version = tag.removeprefix("rust-v")
|
||||
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)(?:\.[0-9]+){0,2})?", version):
|
||||
raise SystemExit("unexpected release version")
|
||||
|
||||
asset_name = os.environ["CODEX_ASSET"]
|
||||
asset = next(
|
||||
(
|
||||
item
|
||||
for item in metadata.get("assets", [])
|
||||
if isinstance(item, dict) and item.get("name") == asset_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if asset is None:
|
||||
raise SystemExit("release asset missing")
|
||||
digest = asset.get("digest", "")
|
||||
if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-fA-F]{64}", digest):
|
||||
raise SystemExit("release asset has no valid SHA-256 digest")
|
||||
url = asset.get("browser_download_url", "")
|
||||
if not isinstance(url, str) or not url.startswith("https://github.com/openai/codex/releases/download/"):
|
||||
raise SystemExit("unexpected release asset URL")
|
||||
|
||||
print(f"{version}\t{digest.removeprefix('sha256:').lower()}\t{url}")
|
||||
PY
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ -z "$release_info" ]; then
|
||||
if [ -x "$CODEX_REAL" ] && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
|
||||
bashio::log.warning "Unable to resolve the latest verified Codex release; keeping the existing install"
|
||||
else
|
||||
bashio::log.warning "Unable to resolve the latest verified Codex release; Codex is unavailable this boot"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
IFS=$'\t' read -r CODEX_WANTED CODEX_SHA256 CODEX_URL <<< "$release_info"
|
||||
|
||||
if [ -x "$CODEX_REAL" ] \
|
||||
&& [ "$(cat "$CODEX_STAMP" 2> /dev/null || true)" = "$CODEX_WANTED" ] \
|
||||
&& run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then
|
||||
bashio::log.info "Codex CLI ${CODEX_WANTED} already installed (latest stable)"
|
||||
else
|
||||
bashio::log.info "Installing latest stable Codex CLI ${CODEX_WANTED} (${CODEX_TARGET}); this is a large one-time download"
|
||||
archive="${codex_tmp}/${CODEX_ASSET}"
|
||||
extracted="${codex_tmp}/codex-${CODEX_TARGET}"
|
||||
|
||||
# Fail open for add-on startup but fail closed for the candidate binary: its official
|
||||
# release digest must match before extraction or execution, and replacement happens only
|
||||
# after the staged binary successfully runs.
|
||||
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 600 \
|
||||
-o "$archive" "$CODEX_URL" \
|
||||
&& printf '%s %s\n' "$CODEX_SHA256" "$archive" | sha256sum -c - > /dev/null \
|
||||
&& tar -xzf "$archive" -C "$codex_tmp" \
|
||||
&& [ -f "$extracted" ] \
|
||||
&& chmod 0755 "$extracted" \
|
||||
&& run_as_runtime_user "$extracted" --version > /dev/null 2>&1 \
|
||||
&& mv -f "$extracted" "$CODEX_REAL"; then
|
||||
printf '%s' "$CODEX_WANTED" > "$CODEX_STAMP"
|
||||
bashio::log.info "Codex CLI installed: $("$CODEX_REAL" --version 2> /dev/null || echo unknown)"
|
||||
elif [ -x "$CODEX_REAL" ]; then
|
||||
bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; keeping the existing install"
|
||||
else
|
||||
bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; Codex is unavailable this boot"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$CODEX_REAL" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Every Codex entry point, including the MCP server launched by Claude, goes through this wrapper.
|
||||
# This is an execution-time guarantee in addition to the managed config below: API-key billing
|
||||
# cannot be selected even if an API key is present in the surrounding environment. Caller-provided
|
||||
# overrides for the two authentication guards are stripped before the forced root-level overrides
|
||||
# are inserted; root-level -c flags must precede Codex subcommands such as `mcp-server`.
|
||||
{
|
||||
printf '#!/usr/bin/env bash\n'
|
||||
printf 'CODEX_REAL=%q\n' "$CODEX_REAL"
|
||||
cat <<'SH'
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
echo "codex: unable to resolve the abc runtime home" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
is_managed_override() {
|
||||
local assignment="$1"
|
||||
local key="${assignment%%=*}"
|
||||
key="${key//[[:space:]]/}"
|
||||
case "$key" in
|
||||
forced_login_method | cli_auth_credentials_store) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
filtered_args=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-c | --config)
|
||||
if [ "$#" -lt 2 ]; then
|
||||
filtered_args+=("$1")
|
||||
shift
|
||||
continue
|
||||
fi
|
||||
if is_managed_override "$2"; then
|
||||
shift 2
|
||||
continue
|
||||
fi
|
||||
filtered_args+=("$1" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--config=*)
|
||||
assignment="${1#--config=}"
|
||||
if ! is_managed_override "$assignment"; then
|
||||
filtered_args+=("$1")
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-c*)
|
||||
assignment="${1#-c}"
|
||||
if ! is_managed_override "$assignment"; then
|
||||
filtered_args+=("$1")
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
filtered_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
forced_args=(
|
||||
-c 'forced_login_method="chatgpt"'
|
||||
-c 'cli_auth_credentials_store="file"'
|
||||
)
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
exec s6-setuidgid abc env -u OPENAI_API_KEY \
|
||||
HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" \
|
||||
"$CODEX_REAL" "${forced_args[@]}" "${filtered_args[@]}"
|
||||
fi
|
||||
|
||||
unset OPENAI_API_KEY
|
||||
export HOME="$RUNTIME_HOME"
|
||||
export CODEX_HOME="$RUNTIME_HOME/.codex"
|
||||
exec "$CODEX_REAL" "${forced_args[@]}" "${filtered_args[@]}"
|
||||
SH
|
||||
} > "$CODEX_BIN"
|
||||
chmod 0755 "$CODEX_BIN"
|
||||
|
||||
chown -R -- "$(id -u abc):$(id -g abc)" "$CODEX_ROOT" \
|
||||
|| bashio::log.warning "Unable to set ownership on ${CODEX_ROOT}"
|
||||
ln -sfn "$CODEX_BIN" "$CODEX_LINK"
|
||||
|
||||
# Manage the root-level defaults used by terminal Codex and by `codex mcp-server`.
|
||||
#
|
||||
# `forced_login_method = "chatgpt"` makes subscription authentication the only permitted login
|
||||
# method, so an inherited OPENAI_API_KEY cannot silently switch this integration to API billing.
|
||||
# File storage is explicit because the container has no supported OS keyring.
|
||||
#
|
||||
# The managed block must be first: a bare TOML key after a [table] header belongs to that table.
|
||||
# Existing top-level definitions of the managed keys are removed before insertion; retaining them
|
||||
# would create duplicate keys and make the entire Codex configuration invalid.
|
||||
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'danger-full-access')"
|
||||
run_as_runtime_user mkdir -p "$RUNTIME_HOME/.codex"
|
||||
CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" RUNTIME_HOME="$RUNTIME_HOME" \
|
||||
run_as_runtime_user python3 - <<'PY' \
|
||||
|| bashio::log.warning "Unable to update the managed Codex configuration block"
|
||||
import os
|
||||
import re
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
BEGIN = "# BEGIN managed by claude_desktop addon"
|
||||
END = "# END managed by claude_desktop addon"
|
||||
MANAGED_KEYS = {
|
||||
"sandbox_mode",
|
||||
"approval_policy",
|
||||
"forced_login_method",
|
||||
"cli_auth_credentials_store",
|
||||
}
|
||||
|
||||
block = "\n".join(
|
||||
[
|
||||
BEGIN,
|
||||
"# Managed defaults for terminal and MCP-driven Codex runs.",
|
||||
f'sandbox_mode = "{os.environ["CODEX_SANDBOX_MODE"]}"',
|
||||
'approval_policy = "never"',
|
||||
"# Require ChatGPT subscription OAuth; do not fall back to API-key billing.",
|
||||
'forced_login_method = "chatgpt"',
|
||||
"# This container has no supported OS keyring; keep OAuth credentials in auth.json.",
|
||||
'cli_auth_credentials_store = "file"',
|
||||
END,
|
||||
]
|
||||
)
|
||||
|
||||
path = Path(os.environ["RUNTIME_HOME"]) / ".codex" / "config.toml"
|
||||
original = path.read_text(encoding="utf-8") if path.exists() else ""
|
||||
rest = re.sub(
|
||||
rf"{re.escape(BEGIN)}.*?{re.escape(END)}\n?",
|
||||
"",
|
||||
original,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
table_header = re.compile(r"^\s*\[\[?.+?\]\]?\s*(?:#.*)?$")
|
||||
assignment = re.compile(
|
||||
r'''^\s*(?P<key>[A-Za-z0-9_-]+|"[^"]+"|'[^']+')\s*='''
|
||||
)
|
||||
kept = []
|
||||
at_top_level = True
|
||||
for line in rest.splitlines(keepends=True):
|
||||
if at_top_level and table_header.match(line):
|
||||
at_top_level = False
|
||||
match = assignment.match(line) if at_top_level else None
|
||||
if match:
|
||||
key = match.group("key")
|
||||
if key[:1] in {'"', "'"}:
|
||||
key = key[1:-1]
|
||||
if key in MANAGED_KEYS:
|
||||
continue
|
||||
kept.append(line)
|
||||
|
||||
remainder = "".join(kept).lstrip("\n")
|
||||
new = block + "\n" + (("\n" + remainder) if remainder else "")
|
||||
tomllib.loads(new)
|
||||
if new != original:
|
||||
path.write_text(new, encoding="utf-8")
|
||||
path.chmod(0o600)
|
||||
PY
|
||||
|
||||
if [ -f "$RUNTIME_HOME/.codex/auth.json" ]; then
|
||||
bashio::log.info "Codex CLI is signed in with stored ChatGPT credentials"
|
||||
else
|
||||
bashio::log.info "Codex CLI is not signed in yet; run 'codex-login' to activate your ChatGPT subscription"
|
||||
fi
|
||||
@@ -293,6 +293,20 @@ if $TOKENSAVE_ENABLED; then
|
||||
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||
fi
|
||||
|
||||
# Codex CLI is installed by 81-codex_cli.sh into /data/codex/bin — deliberately outside $HOME,
|
||||
# because is_managed() below treats any command under $HOME as user-installed.
|
||||
CODEX_BIN="/data/codex/bin/codex"
|
||||
CODEX_ENABLED=false
|
||||
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'workspace-write')"
|
||||
if bashio::config.true 'install_codex_cli'; then
|
||||
if [ -x "$CODEX_BIN" ]; then
|
||||
CODEX_ENABLED=true
|
||||
bashio::log.info "codex $("$CODEX_BIN" --version 2> /dev/null || true) available; registering the codex MCP server (sandbox: ${CODEX_SANDBOX_MODE})"
|
||||
else
|
||||
bashio::log.warning "codex is not available"
|
||||
fi
|
||||
fi
|
||||
|
||||
HA_MCP_ENABLED=false
|
||||
HA_MCP_URL=""
|
||||
HA_MCP_TOKEN=""
|
||||
@@ -314,6 +328,7 @@ fi
|
||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
||||
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
|
||||
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
|
||||
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||
@@ -326,6 +341,7 @@ MANAGED_BASENAMES = {
|
||||
"headroom": "headroom",
|
||||
"tokensave": "tokensave",
|
||||
"homeassistant": "mcp-proxy",
|
||||
"codex": "codex",
|
||||
}
|
||||
|
||||
desired = {}
|
||||
@@ -341,6 +357,25 @@ if os.environ["HEADROOM_ENABLED"] == "true":
|
||||
}
|
||||
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
||||
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
|
||||
if os.environ["CODEX_ENABLED"] == "true":
|
||||
# `codex mcp-server` exposes Codex itself as an stdio MCP server (tools: codex, codex-reply),
|
||||
# which is what lets a Claude session hand a task to ChatGPT Codex. The sandbox/approval
|
||||
# policy is pinned with root-level `-c` overrides, which Codex forwards to the MCP server;
|
||||
# they must precede the subcommand. approval_policy is always "never" because an MCP-driven
|
||||
# run has nobody to answer a prompt. The sandbox defaults to workspace-write; users can opt
|
||||
# into danger-full-access explicitly if the nested sandbox is unavailable in their container.
|
||||
# 81-codex_cli.sh writes the same values into ~/.codex/config.toml so plain terminal `codex`
|
||||
# runs behave identically.
|
||||
desired["codex"] = {
|
||||
"command": os.environ["CODEX_BIN"],
|
||||
"args": [
|
||||
"-c",
|
||||
f'sandbox_mode="{os.environ["CODEX_SANDBOX_MODE"]}"',
|
||||
"-c",
|
||||
'approval_policy="never"',
|
||||
"mcp-server",
|
||||
],
|
||||
}
|
||||
if os.environ["HA_MCP_ENABLED"] == "true":
|
||||
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
|
||||
# mcp-proxy defaults to SSE, so the transport flags are required.
|
||||
@@ -531,6 +566,32 @@ else
|
||||
manage_claude_md_block ha-api-helper remove
|
||||
fi
|
||||
|
||||
# Registering the MCP server is not enough on its own: without guidance the model rarely reaches
|
||||
# for a second agent, the same gap the Headroom block above exists to close.
|
||||
if $CODEX_ENABLED; then
|
||||
manage_claude_md_block codex add <<'MD'
|
||||
## Delegating to ChatGPT Codex
|
||||
|
||||
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT
|
||||
subscription. It is a genuinely independent second agent — a different model family, reading the
|
||||
files itself — not a search tool. It is slow and costs the user's ChatGPT quota, so use it when a
|
||||
second opinion is worth minutes, not for routine lookups.
|
||||
|
||||
Good uses: an independent review of a design or a risky change before it lands; a second
|
||||
diagnosis of a bug you have a theory about but cannot confirm; a competing implementation of a
|
||||
self-contained piece you can then compare against your own.
|
||||
|
||||
Call `mcp__codex__codex` with `prompt` and always set `cwd` to the repository being discussed —
|
||||
Codex reads the files itself, so it needs the right working directory and enough context in the
|
||||
prompt to act without seeing this conversation. Continue an exchange with
|
||||
`mcp__codex__codex-reply` (note the hyphen) using the `threadId` it returned, rather than
|
||||
starting a fresh `codex` call. Treat its answers as a peer's opinion: verify claims about this
|
||||
codebase before acting on them.
|
||||
MD
|
||||
else
|
||||
manage_claude_md_block codex remove
|
||||
fi
|
||||
|
||||
if bashio::config.true 'install_rtk'; then
|
||||
if command -v rtk &> /dev/null; then
|
||||
bashio::log.info "Configuring rtk Claude Code integration"
|
||||
|
||||
@@ -2,14 +2,22 @@
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
|
||||
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
|
||||
# root). Reconcile ownership with that identity after all Claude configuration writes are
|
||||
# complete, as a safety net in case any intermediate step re-owned a managed path.
|
||||
# Earlier configuration scripts intentionally run as root. Reconcile the paths written by those
|
||||
# scripts with the final abc runtime identity and its configured persistent home.
|
||||
RUNTIME_UID="$(id -u abc)"
|
||||
RUNTIME_GID="$(id -g abc)"
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
|
||||
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
bashio::log.warning "Unable to resolve the abc runtime home; skipping runtime ownership reconciliation"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for managed_path in \
|
||||
"$RUNTIME_HOME/.claude" \
|
||||
"$RUNTIME_HOME/.claude.json" \
|
||||
"$RUNTIME_HOME/.config/Claude" \
|
||||
"$RUNTIME_HOME/.codex"; do
|
||||
if [ -e "$managed_path" ]; then
|
||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|
||||
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
|
||||
# printing MCP environment values (which may contain the Home Assistant access token).
|
||||
# printing MCP environment values or authentication material.
|
||||
# shellcheck shell=bash
|
||||
set +e
|
||||
set -o pipefail
|
||||
export NO_COLOR=1
|
||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
||||
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
RUNTIME_HOME="/data/data"
|
||||
fi
|
||||
|
||||
section() {
|
||||
printf '\n=== %s ===\n' "$1"
|
||||
}
|
||||
|
||||
section "Installed binaries"
|
||||
for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do
|
||||
for tool in claude claude-desktop headroom rtk tokensave codex git gh rg jq shellcheck yamllint hadolint actionlint; do
|
||||
resolved="$(command -v "$tool" 2> /dev/null || true)"
|
||||
if [ -n "$resolved" ]; then
|
||||
printf '%-16s %s\n' "$tool" "$resolved"
|
||||
@@ -22,13 +27,14 @@ for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck
|
||||
done
|
||||
|
||||
section "Configured switches"
|
||||
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
|
||||
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_codex_cli codex_sandbox_mode install_caveman enable_tools_health_report; do
|
||||
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
|
||||
done
|
||||
|
||||
section "Runtime identity"
|
||||
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
|
||||
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
|
||||
printf '%-30s %s\n' "abc runtime home" "$RUNTIME_HOME"
|
||||
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
|
||||
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||
if [ "$(id -u abc)" -eq 0 ]; then
|
||||
@@ -39,11 +45,12 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||
fi
|
||||
|
||||
section "Claude Code permission state"
|
||||
python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
@@ -59,13 +66,15 @@ else:
|
||||
PY
|
||||
|
||||
section "MCP registrations (environment values redacted)"
|
||||
python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
home = Path(os.environ["RUNTIME_HOME"])
|
||||
paths = [
|
||||
Path.home() / ".claude.json",
|
||||
Path.home() / ".config/Claude/claude_desktop_config.json",
|
||||
home / ".claude.json",
|
||||
home / ".config/Claude/claude_desktop_config.json",
|
||||
]
|
||||
for path in paths:
|
||||
print(path)
|
||||
@@ -95,11 +104,12 @@ for path in paths:
|
||||
PY
|
||||
|
||||
section "Claude Code hooks"
|
||||
python3 - <<'PY'
|
||||
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".claude/settings.json"
|
||||
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except FileNotFoundError:
|
||||
@@ -148,18 +158,17 @@ section "TokenSave"
|
||||
if bashio::config.true 'install_tokensave'; then
|
||||
tokensave doctor --agent claude || true
|
||||
tokensave gain --all --range 30d || true
|
||||
# Capture before looping — see the matching comment in 82-claude_tools.sh: feeding the
|
||||
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
|
||||
# Capture before looping — see the matching comment in 82-claude_tools.sh.
|
||||
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||
continue
|
||||
fi
|
||||
repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
repo_root="$(s6-setuidgid abc env HOME="$RUNTIME_HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
|
||||
if [ -z "$repo_root" ]; then
|
||||
echo "${configured_path}: not a Git repository"
|
||||
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
||||
s6-setuidgid abc env HOME="$HOME" tokensave status "$repo_root" --short || true
|
||||
s6-setuidgid abc env HOME="$RUNTIME_HOME" tokensave status "$repo_root" --short || true
|
||||
else
|
||||
echo "${repo_root}: NOT INITIALIZED"
|
||||
fi
|
||||
@@ -168,6 +177,45 @@ else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "Codex"
|
||||
if bashio::config.true 'install_codex_cli'; then
|
||||
codex_bin="/data/codex/bin/codex"
|
||||
if [ -x "$codex_bin" ]; then
|
||||
printf '%-30s %s\n' "installed" "$("$codex_bin" --version 2> /dev/null || echo 'FAILED TO RUN')"
|
||||
printf '%-30s %s\n' "installed version stamp" "$(cat /data/codex/bin/.version 2> /dev/null || echo 'MISSING')"
|
||||
printf '%-30s %s\n' "release policy" "latest stable, SHA-256 verified"
|
||||
printf '%-30s %s\n' "authentication policy" "ChatGPT subscription only"
|
||||
|
||||
# Never forward raw `login status` output: non-ChatGPT modes can include masked secret
|
||||
# fragments. Only print explicitly allow-listed states.
|
||||
codex_status="$(
|
||||
s6-setuidgid abc env -u OPENAI_API_KEY \
|
||||
HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" \
|
||||
"$codex_bin" login status 2>&1
|
||||
)"
|
||||
codex_status_rc=$?
|
||||
case "$codex_status" in
|
||||
*"Logged in using ChatGPT"*)
|
||||
echo "Logged in using ChatGPT"
|
||||
;;
|
||||
*"Not logged in"*)
|
||||
echo "Not logged in; run 'codex-login' to activate a ChatGPT subscription"
|
||||
;;
|
||||
*)
|
||||
if [ "$codex_status_rc" -eq 0 ]; then
|
||||
echo "Authenticated with a non-ChatGPT method; run 'codex-login' to enforce subscription authentication"
|
||||
else
|
||||
echo "Unable to determine Codex login status safely; run 'codex-login'"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "enabled but ${codex_bin} is MISSING (download failed or add-on not yet restarted)"
|
||||
fi
|
||||
else
|
||||
echo "disabled"
|
||||
fi
|
||||
|
||||
section "Claude routing"
|
||||
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
|
||||
printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)"
|
||||
|
||||
48
claude_desktop/rootfs/usr/local/bin/codex-login
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# Activate a ChatGPT subscription on the Codex CLI from inside the add-on.
|
||||
#
|
||||
# The add-on ships no browser, so the normal `codex login` flow cannot complete here.
|
||||
# `codex login --device-auth` prints a verification URL and one-time code that can be
|
||||
# approved on another device. Credentials persist in the abc runtime user's home.
|
||||
# shellcheck shell=bash
|
||||
set -o pipefail
|
||||
|
||||
CODEX_BIN="/data/codex/bin/codex"
|
||||
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
|
||||
if [ -z "$RUNTIME_HOME" ]; then
|
||||
echo "codex-login: unable to resolve the abc runtime home" >&2
|
||||
exit 1
|
||||
fi
|
||||
export HOME="$RUNTIME_HOME"
|
||||
export CODEX_HOME="$RUNTIME_HOME/.codex"
|
||||
|
||||
if ! bashio::config.true 'install_codex_cli'; then
|
||||
echo "codex-login: the install_codex_cli add-on option is disabled" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$CODEX_BIN" ]; then
|
||||
echo "codex-login: ${CODEX_BIN} is not installed; enable install_codex_cli and restart the add-on" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Everything runs as abc so auth.json is readable by the same account that launches the MCP
|
||||
# server. Explicit HOME handling also makes container-console calls safe when the caller is root.
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
exec s6-setuidgid abc env -u OPENAI_API_KEY HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$0" "$@"
|
||||
fi
|
||||
|
||||
# The managed Codex config forces `chatgpt` authentication and file credential storage. An
|
||||
# inherited API key is removed as defense in depth so this helper cannot activate API billing.
|
||||
unset OPENAI_API_KEY
|
||||
|
||||
# Branch on the exit code, not the text: "Not logged in" also contains "logged in".
|
||||
if "$CODEX_BIN" login status; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "codex-login: starting ChatGPT subscription device-code sign-in."
|
||||
echo "codex-login: open the URL below on any device and enter the displayed code."
|
||||
|
||||
# Line-buffered so the URL and code appear while Codex is still polling in non-TTY callers.
|
||||
exec stdbuf -oL -eL "$CODEX_BIN" login --device-auth "$@"
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
codex/stats.png
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,4 +1,19 @@
|
||||
|
||||
## 26.04.2.4.1 (2026-07-26)
|
||||
- Rebuild on a Debian base: upstream turned collabora/code into a distroless image with no shell, which broke the addon build entirely. collabora/code stays the tracked upstream image in build.json, but is now a build stage whose payload is copied onto ghcr.io/hassio-addons/debian-base, and the addon ships its own launcher in place of the removed /start-collabora-online.sh
|
||||
- build.json names the architecture explicitly again (`collabora/code:latest-amd64` and `collabora/code:latest-arm64`). The builder never passes `--platform`, so the tag is the only thing that decides which binaries land in the addon
|
||||
- Restore the file capabilities on `coolforkit-caps` and `coolmount`. The official image carries them as extended attributes, which `COPY --from` does not transfer, and without them Collabora starts but cannot open any document
|
||||
- The certificates for `ssl: true` are read from the copies in /etc/coolwsd rather than from /ssl directly, which Collabora could not read as uid 1001 when the private key is root-only
|
||||
- Make the `ssl` option authoritative even when `extra_params` is empty or customized. `ssl: false` now always disables Collabora's internal HTTPS instead of silently falling back to its default self-signed TLS
|
||||
- Fix version numbering: releases on CollaboraOnline/online are now Helm charts only, which had renumbered the addon from 25.4.9.2 down to 1.3.0 and hid updates. The version is tracked from the collabora/code Docker Hub tags again
|
||||
- `server_name` is now passed to Collabora, fixing `Your browser has been unable to connect to the Collabora server` behind a reverse proxy
|
||||
- `domain1` was never passed to Collabora at all (the script read a `domain` option that does not exist, and recent Collabora releases dropped that variable). It is now deprecated and applied as `server_name`
|
||||
- `aliasgroup*` values are normalised: unescaped, escaped and double-escaped dots all produce the correct regex, and the value handed to Collabora is printed in the log
|
||||
- Added `ssl_termination`, needed when `ssl` is false but Collabora is reached over https through a reverse proxy
|
||||
- Added `aliasgroup2` and `aliasgroup3` for additional Nextcloud servers
|
||||
- `cert_domain` is now a string (it is a certificate common name) and is passed to Collabora
|
||||
- Documented the above, and corrected the README which asked for two backslashes where Collabora expects one
|
||||
|
||||
## 1.3.0 (2026-07-16)
|
||||
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
||||
|
||||
|
||||
@@ -16,7 +16,45 @@
|
||||
|
||||
ARG BUILD_FROM
|
||||
ARG BUILD_VERSION
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
###############################################################################
|
||||
# Get Collabora Online from the official image (BUILD_FROM, see build.json)
|
||||
#
|
||||
# build.json pins the architecture explicitly, collabora/code:latest-amd64 and
|
||||
# collabora/code:latest-arm64, rather than the multi-arch collabora/code:latest.
|
||||
# The builder never passes --platform: it runs the amd64 build on a native amd64
|
||||
# runner and the aarch64 build on a native arm runner, so the only thing that
|
||||
# decides which Collabora binaries end up in the add-on is this tag. With the
|
||||
# multi-arch tag that happens to resolve correctly, but only for as long as the
|
||||
# runner architecture keeps matching the target, and a mismatch would silently
|
||||
# produce an image full of foreign-architecture binaries. The per-arch tags are
|
||||
# published in lockstep with latest, so nothing is lost by naming them.
|
||||
#
|
||||
# Upstream rebuilt collabora/code as a Nix-based distroless image: /bin and
|
||||
# /sbin are empty, so it can no longer be the base of the add-on itself, as s6,
|
||||
# bashio and every RUN need a shell. It stays the tracked upstream image, and
|
||||
# only the Collabora payload is copied out of it onto a Debian runtime.
|
||||
#
|
||||
# Copy only what belongs to Collabora. Do NOT copy /etc or /nix: in that image
|
||||
# /etc/resolv.conf, /etc/hosts, /etc/passwd, /etc/group and /etc/nsswitch.conf
|
||||
# are symlinks into /nix/store, and importing them breaks DNS resolution and
|
||||
# wipes the base image users.
|
||||
###############################################################################
|
||||
# hadolint ignore=DL3006
|
||||
FROM ${BUILD_FROM} AS collabora
|
||||
|
||||
###############################################################################
|
||||
# Build the actual add-on on a base that has a shell
|
||||
###############################################################################
|
||||
FROM ghcr.io/hassio-addons/debian-base:9.3.0
|
||||
|
||||
# Inherited from the base, declared here so it is visible to hadolint and to
|
||||
# anyone adding a pipe below. Note that the linkage check does NOT pipe into
|
||||
# grep: with pipefail an ldd that exits non-zero (a binary it cannot handle at
|
||||
# all) would make the pipeline fail even though grep matched, and "if" would
|
||||
# then read that as "no unresolved libraries" -- the one case worth catching.
|
||||
# Capturing the output and matching it with case avoids the question entirely.
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
##################
|
||||
# 2 Modify Image #
|
||||
@@ -55,11 +93,88 @@ COPY ha_automodules.sh /ha_automodules.sh
|
||||
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
|
||||
|
||||
# Manual apps
|
||||
ENV PACKAGES=""
|
||||
# coolwsd itself only needs glibc/libstdc++ (max GLIBCXX_3.4.22); the office
|
||||
# engine bundles its own cairo, fontconfig, curl, icu and fonts under
|
||||
# /opt/collaboraoffice/program. openssl is used to generate the self-signed
|
||||
# certificate when the ssl option is off, cpio and findutils by the jail setup.
|
||||
ENV PACKAGES="ca-certificates cpio findutils fontconfig libcap2-bin libstdc++6 openssl tzdata"
|
||||
|
||||
# Automatic apps & bashio
|
||||
COPY ha_autoapps.sh /ha_autoapps.sh
|
||||
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" || true && rm /ha_autoapps.sh
|
||||
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
|
||||
|
||||
# Collabora Online payload, taken from the official image. COPY --from keeps the
|
||||
# numeric ownership, so /opt/cool and /etc/coolwsd arrive already owned by 1001.
|
||||
COPY --from=collabora /usr/bin/coolwsd /usr/bin/coolforkit-caps /usr/bin/coolforkit-ns /usr/bin/coolmount /usr/bin/
|
||||
COPY --from=collabora /usr/share/coolwsd /usr/share/coolwsd
|
||||
COPY --from=collabora /etc/coolwsd /etc/coolwsd
|
||||
COPY --from=collabora /opt/collaboraoffice /opt/collaboraoffice
|
||||
COPY --from=collabora /opt/cool /opt/cool
|
||||
|
||||
# Recreate the runtime user the official image declares (uid/gid 1001), and the
|
||||
# per-container state upstream sets up in its own final build stage.
|
||||
RUN \
|
||||
groupadd --gid 1001 cool && \
|
||||
useradd --uid 1001 --gid 1001 --no-create-home --home-dir /opt/cool --shell /usr/sbin/nologin cool && \
|
||||
mkdir -p /opt/cool/child-roots /opt/cool/cache && \
|
||||
chown -R 1001:1001 /opt/cool /etc/coolwsd && \
|
||||
chmod 640 /etc/coolwsd/coolwsd.xml && \
|
||||
touch /var/log/coolwsd.log && \
|
||||
chown 1001:1001 /var/log/coolwsd.log && \
|
||||
# the WOPI proof key must be unique per container, not baked into the image
|
||||
rm -rf /etc/coolwsd/proof_key* && \
|
||||
(fc-cache /opt/collaboraoffice/share/fonts/truetype > /dev/null 2>&1 || true)
|
||||
|
||||
# Restore the file capabilities. The official image carries them as extended
|
||||
# attributes on two binaries:
|
||||
# coolforkit-caps cap_chown,cap_fowner,cap_sys_chroot=ep
|
||||
# coolmount cap_sys_admin=ep
|
||||
# COPY --from does not transfer extended attributes, so both arrive stripped.
|
||||
# Nothing about the build notices: coolwsd starts and serves the admin console,
|
||||
# but every document fails to open because it cannot chroot a kit process. Set
|
||||
# them again and check they stuck, so a builder without xattr support fails here
|
||||
# instead of shipping an add-on that only looks like it works.
|
||||
#
|
||||
# cap_sys_admin on coolmount only takes effect if the container is given
|
||||
# SYS_ADMIN, which the add-on does not request; without it Collabora copies its
|
||||
# child roots instead of bind-mounting them, which is slower but works.
|
||||
RUN \
|
||||
setcap "cap_chown,cap_fowner,cap_sys_chroot=ep" /usr/bin/coolforkit-caps && \
|
||||
setcap "cap_sys_admin=ep" /usr/bin/coolmount && \
|
||||
caps="$(getcap /usr/bin/coolforkit-caps /usr/bin/coolmount)" && \
|
||||
case "$caps" in \
|
||||
*cap_sys_chroot*) ;; \
|
||||
*) echo "coolforkit-caps lost its capabilities: ${caps}"; exit 1 ;; \
|
||||
esac && \
|
||||
case "$caps" in \
|
||||
*cap_sys_admin*) ;; \
|
||||
*) echo "coolmount lost its capabilities: ${caps}"; exit 1 ;; \
|
||||
esac
|
||||
|
||||
# Fail the build rather than ship an image with unresolved runtime dependencies.
|
||||
# The payload was linked against the libraries of the distroless image, so each
|
||||
# executable and shared library is checked against the Debian runtime.
|
||||
#
|
||||
# coolwsd itself cannot be executed as a smoke test. It refuses to run as root
|
||||
# ("Do not run as root. Please run as cool user.", exit 78), and --version does
|
||||
# not exit either -- the official entrypoint passes it to the long-running
|
||||
# server to get the version into the log. Checking that every binary resolves
|
||||
# its libraries proves the same thing and terminates.
|
||||
RUN \
|
||||
command -v openssl > /dev/null && \
|
||||
command -v su > /dev/null && \
|
||||
for binary in \
|
||||
/usr/bin/coolwsd \
|
||||
/usr/bin/coolforkit-caps \
|
||||
/usr/bin/coolforkit-ns \
|
||||
/usr/bin/coolmount \
|
||||
/opt/collaboraoffice/program/soffice.bin \
|
||||
/opt/collaboraoffice/program/libmergedlo.so; do \
|
||||
libs="$(ldd "$binary" 2>&1)"; \
|
||||
case "$libs" in \
|
||||
*"not found"*) echo "Unresolved libraries in ${binary}:"; echo "$libs"; exit 1 ;; \
|
||||
esac; \
|
||||
done
|
||||
|
||||
################
|
||||
# 4 Entrypoint #
|
||||
|
||||
@@ -52,22 +52,63 @@ Webui can be found at `https://homeassistant:9980/browser/dist/admin/admin.html`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `aliasgroup1` | str | | Nextcloud external domain with escaped dots using two \ (e.g. `nextcloud_domain\\.com`) |
|
||||
| `domain1` | str | | Collabora external domain with escaped dots using two \ (e.g. `code_domain\\.com`) |
|
||||
| `aliasgroup1` | str | | External address of the **Nextcloud** server allowed to use this Collabora (e.g. `https://nextcloud_domain\.com:443`) |
|
||||
| `aliasgroup2` | str | | A second Nextcloud server, same format as `aliasgroup1` |
|
||||
| `aliasgroup3` | str | | A third Nextcloud server, same format as `aliasgroup1` |
|
||||
| `server_name` | str | | External hostname (and port) of **this Collabora** server, as the browser reaches it (e.g. `code_domain.com:9980`). Set it when Collabora sits behind a reverse proxy |
|
||||
| `ssl_termination` | bool | `false` | Set to `true` when `ssl` is `false` but the browser reaches Collabora over `https` through a reverse proxy |
|
||||
| `extra_params` | str | | Extra parameters passed to the Collabora start script |
|
||||
| `ssl` | bool | `false` | Enable SSL using certificates from /ssl |
|
||||
| `certfile` | str | `fullchain.pem` | Certificate file name located in /ssl |
|
||||
| `keyfile` | str | `privkey.pem` | Private key file name located in /ssl |
|
||||
| `cert_domain` | str | | Common name of the self-signed certificate generated when `ssl` is `false` |
|
||||
| `username` | str | | Username for the Collabora admin console |
|
||||
| `password` | str | | Password for the Collabora admin console |
|
||||
| `dictionaries` | str | | Space-separated list of dictionary languages to install |
|
||||
| `domain1` | str | | **Deprecated**, use `server_name` instead |
|
||||
|
||||
#### About the escaped dots in `aliasgroup*`
|
||||
|
||||
Collabora matches the `aliasgroup*` addresses as **regular expressions**, so a dot
|
||||
has to be escaped with a **single** backslash: `next\.duckdns\.org`, not
|
||||
`next\\.duckdns\\.org`. A doubled backslash means "a literal backslash followed by
|
||||
any character", which never matches a real hostname, and Collabora then rejects the
|
||||
Nextcloud server.
|
||||
|
||||
Earlier versions of this page asked for two backslashes, which was wrong. The add-on
|
||||
now normalises whatever you type, so `next.duckdns.org`, `next\.duckdns\.org` and
|
||||
`next\\.duckdns\\.org` all end up as the same correct pattern. The value that is
|
||||
really handed to Collabora is printed in the add-on log at startup:
|
||||
|
||||
```text
|
||||
Allowed Nextcloud host aliasgroup1: https://next\.duckdns\.org:443
|
||||
```
|
||||
|
||||
Values containing other regex characters (`*`, `|`, `(`, `[`, …) are left untouched,
|
||||
so hand-written patterns keep working.
|
||||
|
||||
`server_name` is **not** a regular expression: write it as a plain hostname, without
|
||||
backslashes.
|
||||
|
||||
### Example configuration
|
||||
|
||||
Nextcloud on `https://next.duckdns.org` and Collabora reachable on
|
||||
`https://code.duckdns.org:9980`, with a reverse proxy handling the certificates:
|
||||
|
||||
```yaml
|
||||
aliasgroup1: nextcloud_domain\\.com
|
||||
domain1: code_domain\\.com
|
||||
extra_params: ""
|
||||
aliasgroup1: https://next\.duckdns\.org:443
|
||||
server_name: code.duckdns.org:9980
|
||||
ssl_termination: true
|
||||
ssl: false
|
||||
username: admin
|
||||
password: changeme
|
||||
```
|
||||
|
||||
Same setup, but letting the add-on serve the certificates itself from `/ssl`:
|
||||
|
||||
```yaml
|
||||
aliasgroup1: https://next\.duckdns\.org:443
|
||||
server_name: code.duckdns.org:9980
|
||||
ssl: true
|
||||
certfile: fullchain.pem
|
||||
keyfile: privkey.pem
|
||||
@@ -81,7 +122,19 @@ password: changeme
|
||||
1. Start the add-on and expose the Collabora server to an external domain.
|
||||
1. Install and configure the Nextcloud add-on.
|
||||
1. Inside Nextcloud, install the **Nextcloud Office** app.
|
||||
1. In Nextcloud **Administration Settings → Office**, set the Collabora server URL to `https://yourdomain:9980` and enable **Disable certificate validation**.
|
||||
1. In Nextcloud **Administration Settings → Office**, set the Collabora server URL to
|
||||
the **Collabora** address, not the Nextcloud one — with the example above that is
|
||||
`https://code.duckdns.org:9980` — and enable **Disable certificate validation** if
|
||||
the add-on serves a self-signed certificate.
|
||||
1. Add both hostnames to the Nextcloud `trusted_domains`.
|
||||
|
||||
The two hostnames have different roles, and swapping them is the most common cause of
|
||||
`Could not establish connection to the Collabora Online server`:
|
||||
|
||||
- `aliasgroup1` is the **Nextcloud** address, it tells Collabora which server is
|
||||
allowed to ask it to open documents.
|
||||
- `server_name` is the **Collabora** address, it tells Collabora which URL to hand
|
||||
back to the browser.
|
||||
|
||||
### Custom Scripts and Environment Variables
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ options:
|
||||
env_vars: []
|
||||
aliasgroup1: ""
|
||||
certfile: fullchain.pem
|
||||
domain1: ""
|
||||
server_name: ""
|
||||
extra_params:
|
||||
--o:ssl.enable=false --o:user_interface.use_integration_theme=false
|
||||
--o:net.proto=IPv4
|
||||
@@ -33,7 +33,9 @@ schema:
|
||||
value: str?
|
||||
TZ: str?
|
||||
aliasgroup1: str
|
||||
cert_domain: bool?
|
||||
aliasgroup2: str?
|
||||
aliasgroup3: str?
|
||||
cert_domain: str?
|
||||
certfile: str
|
||||
dictionaries: str?
|
||||
domain1: str?
|
||||
@@ -42,8 +44,9 @@ schema:
|
||||
password: password
|
||||
server_name: str?
|
||||
ssl: bool
|
||||
ssl_termination: bool?
|
||||
username: str
|
||||
slug: collabora
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "1.3.0"
|
||||
version: "26.04.2.4.1"
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:9980]/browser/dist/admin/admin.html"
|
||||
|
||||
@@ -2,9 +2,58 @@
|
||||
# shellcheck shell=bash
|
||||
set -e
|
||||
|
||||
if bashio::config.has_value 'domain'; then
|
||||
domain="$(bashio::config 'domain')"
|
||||
export domain
|
||||
# coolwsd matches storage.wopi.alias_groups host/alias entries as regular
|
||||
# expressions, so every dot has to be escaped with a single backslash. The value
|
||||
# is typed by hand in the add-on options, where it is easy to end up with no
|
||||
# escaping at all or with doubled backslashes, and a wrong pattern silently
|
||||
# never matches: Collabora then refuses the Nextcloud host. Accept all three
|
||||
# spellings and always hand coolwsd the canonical single-escaped form.
|
||||
REGEX_METACHARACTERS='][(){}|*+?^$'
|
||||
normalise_wopi_host() {
|
||||
local value="$1"
|
||||
|
||||
# A value containing regex metacharacters was written by someone who knows
|
||||
# what they are doing, leave it exactly as-is.
|
||||
if [[ "$value" == *["$REGEX_METACHARACTERS"]* ]]; then
|
||||
printf '%s' "$value"
|
||||
return
|
||||
fi
|
||||
|
||||
value="${value//\\/}" # drop whatever escaping was typed, at any depth
|
||||
value="${value//./\\.}" # re-escape every dot exactly once
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
# server_name is a literal "hostname[:port]", not a regex and not a URL
|
||||
normalise_server_name() {
|
||||
local value="$1"
|
||||
value="${value//\\/}" # never escaped, drop backslashes if any were copied over
|
||||
value="${value#*://}" # strip the scheme
|
||||
value="${value%%/*}" # strip any path
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
for index in 1 2 3; do
|
||||
if bashio::config.has_value "aliasgroup${index}"; then
|
||||
aliasgroup="$(normalise_wopi_host "$(bashio::config "aliasgroup${index}")")"
|
||||
export "aliasgroup${index}=${aliasgroup}"
|
||||
bashio::log.info "Allowed Nextcloud host aliasgroup${index}: ${aliasgroup}"
|
||||
fi
|
||||
done
|
||||
|
||||
if bashio::config.has_value 'server_name'; then
|
||||
server_name="$(normalise_server_name "$(bashio::config 'server_name')")"
|
||||
export server_name
|
||||
elif bashio::config.has_value 'domain1'; then
|
||||
# domain1 predates server_name and was documented as "the Collabora external
|
||||
# domain", which is what server_name means to coolwsd. It was never actually
|
||||
# passed to Collabora, so honour it here rather than keep ignoring it.
|
||||
server_name="$(normalise_server_name "$(bashio::config 'domain1')")"
|
||||
export server_name
|
||||
bashio::log.warning "domain1 is deprecated, please use server_name instead"
|
||||
fi
|
||||
if [ -n "${server_name:-}" ]; then
|
||||
bashio::log.info "Collabora public hostname (server_name): ${server_name}"
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'username'; then
|
||||
@@ -17,9 +66,9 @@ if bashio::config.has_value 'password'; then
|
||||
export password
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'aliasgroup1'; then
|
||||
aliasgroup1="$(bashio::config 'aliasgroup1')"
|
||||
export aliasgroup1
|
||||
if bashio::config.has_value 'cert_domain'; then
|
||||
cert_domain="$(bashio::config 'cert_domain')"
|
||||
export cert_domain
|
||||
fi
|
||||
|
||||
if bashio::config.has_value 'dictionaries'; then
|
||||
@@ -32,6 +81,12 @@ if bashio::config.has_value 'extra_params'; then
|
||||
extra_params="$(bashio::config 'extra_params')"
|
||||
fi
|
||||
|
||||
# The add-on ssl option is authoritative. coolwsd defaults ssl.enable to true,
|
||||
# so merely clearing extra_params used to re-enable its self-signed HTTPS even
|
||||
# when ssl was false, which breaks reverse proxies expecting plain HTTP.
|
||||
extra_params="${extra_params//--o:ssl.enable=false/}"
|
||||
extra_params="${extra_params//--o:ssl.enable=true/}"
|
||||
|
||||
if bashio::config.true 'ssl'; then
|
||||
export DONT_GEN_SSL_CERT=true
|
||||
bashio::config.require.ssl
|
||||
@@ -45,16 +100,31 @@ if bashio::config.true 'ssl'; then
|
||||
bashio::log.error "Key file /ssl/${keyfile} not found"
|
||||
exit 1
|
||||
fi
|
||||
cp -f /ssl/${keyfile} /etc/coolwsd/key.pem
|
||||
cp -f /ssl/${certfile} /etc/coolwsd/cert.pem
|
||||
cp -f /ssl/${certfile} /etc/coolwsd/ca-chain.cert.pem
|
||||
extra_params="${extra_params/--o:ssl.enable=false/}"
|
||||
# Point Collabora at the copies rather than at /ssl. coolwsd runs as uid
|
||||
# 1001 and /ssl is mounted read-only with whatever ownership the certificate
|
||||
# tooling left behind, which for a private key is commonly root-only. These
|
||||
# copies are picked up by the chown below, so they are readable regardless.
|
||||
cp -f "/ssl/${keyfile}" /etc/coolwsd/key.pem
|
||||
cp -f "/ssl/${certfile}" /etc/coolwsd/cert.pem
|
||||
cp -f "/ssl/${certfile}" /etc/coolwsd/ca-chain.cert.pem
|
||||
chmod 600 /etc/coolwsd/key.pem
|
||||
extra_params="${extra_params} \
|
||||
--o:ssl.enable=true
|
||||
--o:ssl.enable=true \
|
||||
--o:ssl.termination=false \
|
||||
--o:ssl.cert_file_path=/ssl/${certfile} \
|
||||
--o:ssl.key_file_path=/ssl/${keyfile} \
|
||||
--o:ssl.ca_file_path=/ssl/${certfile}"
|
||||
--o:ssl.cert_file_path=/etc/coolwsd/cert.pem \
|
||||
--o:ssl.key_file_path=/etc/coolwsd/key.pem \
|
||||
--o:ssl.ca_file_path=/etc/coolwsd/ca-chain.cert.pem"
|
||||
else
|
||||
extra_params="${extra_params} --o:ssl.enable=false"
|
||||
if [[ "$extra_params" != *ssl.termination* ]]; then
|
||||
# With SSL disabled, termination must be enabled when a reverse proxy
|
||||
# exposes Collabora over https, otherwise it advertises http/ws URLs.
|
||||
if bashio::config.true 'ssl_termination'; then
|
||||
extra_params="${extra_params} --o:ssl.termination=true"
|
||||
elif ! bashio::config.has_value 'ssl_termination'; then
|
||||
bashio::log.notice "If Collabora is reached over https through a reverse proxy, set ssl_termination to true"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
export extra_params
|
||||
@@ -83,4 +153,9 @@ chown -R 1001 /etc/coolwsd
|
||||
chmod -R 755 /opt/cool/systemplate
|
||||
|
||||
bashio::log.info "Starting Collabora Online..."
|
||||
su -p -s /bin/bash "$(getent passwd 1001 | cut -d: -f1)" -c "/start-collabora-online.sh"
|
||||
# coolwsd refuses to run as root. The official image used to ship
|
||||
# /start-collabora-online.sh, which is gone since it became distroless, so the
|
||||
# add-on provides its own launcher. It reads everything from the environment,
|
||||
# which su -p preserves.
|
||||
export HOME=/opt/cool
|
||||
su -p -s /bin/bash cool -c /usr/local/bin/collabora-run.sh
|
||||
|
||||
55
collabora/rootfs/usr/local/bin/collabora-run.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Launch coolwsd.
|
||||
#
|
||||
# The official image used to ship /start-collabora-online.sh and set it as its
|
||||
# entrypoint. Since the move to a distroless image that script is gone, so the
|
||||
# add-on provides its own equivalent. It is invoked as uid 1001 by
|
||||
# /etc/cont-init.d/99-run.sh and takes everything from the environment, which
|
||||
# avoids re-quoting extra_params through su.
|
||||
set -e
|
||||
|
||||
# Collabora serves https itself unless the add-on already installed real
|
||||
# certificates, in which case 99-run.sh exports DONT_GEN_SSL_CERT.
|
||||
cert_params=""
|
||||
if [ -z "${DONT_GEN_SSL_CERT:-}" ]; then
|
||||
SSL_DIR="/tmp/ssl"
|
||||
mkdir -p "${SSL_DIR}/certs/ca" "${SSL_DIR}/certs/servers/localhost" "${SSL_DIR}/certs/tmp"
|
||||
|
||||
openssl genrsa -out "${SSL_DIR}/certs/ca/root.key.pem" 2048
|
||||
openssl req -x509 -new -nodes \
|
||||
-key "${SSL_DIR}/certs/ca/root.key.pem" -days 9131 \
|
||||
-out "${SSL_DIR}/certs/ca/root.crt.pem" \
|
||||
-subj "/C=DE/ST=BW/L=Stuttgart/O=Dummy Authority/CN=Dummy Authority"
|
||||
|
||||
openssl genrsa -out "${SSL_DIR}/certs/servers/localhost/privkey.pem" 2048
|
||||
openssl req -new -sha256 \
|
||||
-key "${SSL_DIR}/certs/servers/localhost/privkey.pem" \
|
||||
-out "${SSL_DIR}/certs/tmp/localhost.csr.pem" \
|
||||
-subj "/C=DE/ST=BW/L=Stuttgart/O=Dummy Authority/CN=${cert_domain:-localhost}"
|
||||
openssl x509 -req -days 9131 \
|
||||
-in "${SSL_DIR}/certs/tmp/localhost.csr.pem" \
|
||||
-CA "${SSL_DIR}/certs/ca/root.crt.pem" \
|
||||
-CAkey "${SSL_DIR}/certs/ca/root.key.pem" -CAcreateserial \
|
||||
-out "${SSL_DIR}/certs/servers/localhost/cert.pem"
|
||||
|
||||
cert_params="--o:ssl.cert_file_path=${SSL_DIR}/certs/servers/localhost/cert.pem \
|
||||
--o:ssl.key_file_path=${SSL_DIR}/certs/servers/localhost/privkey.pem \
|
||||
--o:ssl.ca_file_path=${SSL_DIR}/certs/ca/root.crt.pem"
|
||||
fi
|
||||
|
||||
# Flags mirror the entrypoint of the official image. extra_params is expanded
|
||||
# last so that add-on options and user overrides win.
|
||||
# shellcheck disable=SC2086
|
||||
exec /usr/bin/coolwsd \
|
||||
--version \
|
||||
--use-env-vars \
|
||||
${cert_params} \
|
||||
--o:sys_template_path=/opt/cool/systemplate \
|
||||
--o:child_root_path=/opt/cool/child-roots \
|
||||
--o:file_server_root_path=/usr/share/coolwsd \
|
||||
--o:cache_files.path=/opt/cool/cache \
|
||||
--o:logging.color=false \
|
||||
--o:stop_on_config_change=true \
|
||||
${extra_params:-}
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"last_update": "2026-07-16",
|
||||
"github_exclude": "sha256",
|
||||
"last_update": "2026-07-26",
|
||||
"repository": "alexbelgium/hassio-addons",
|
||||
"slug": "collabora",
|
||||
"source": "github",
|
||||
"upstream_repo": "CollaboraOnline/online",
|
||||
"upstream_version": "1.3.0"
|
||||
"source": "dockerhub",
|
||||
"upstream_repo": "collabora/code",
|
||||
"upstream_version": "26.04.2.4.1"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
emby/stats.png
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
ente/stats.png
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +1,5 @@
|
||||
## 2.63.18.5 (26-07-2026)
|
||||
- Fixed healthcheck log spam when ssl is enabled (#2881)
|
||||
## 2.63.18.4 (22-07-2026)
|
||||
- Minor bugs fixed
|
||||
## 2.63.18.3 (22-07-2026)
|
||||
|
||||
@@ -134,4 +134,4 @@ HEALTHCHECK \
|
||||
--retries=5 \
|
||||
--start-period=30s \
|
||||
--timeout=25s \
|
||||
CMD if [ "$ssl" = "true" ]; then curl -f -k https://127.0.01:${HEALTH_PORT}${HEALTH_URL}; else curl -f http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}/; fi
|
||||
CMD curl -A "HealthCheck: Docker/1.0" -s -f -k "$(cat /run/health_protocol 2>/dev/null || echo http)://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}/" >/dev/null 2>&1 || exit 1
|
||||
|
||||
@@ -126,4 +126,4 @@ schema:
|
||||
slug: filebrowser
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: "2.63.18.4"
|
||||
version: "2.63.18.5"
|
||||
|
||||
@@ -51,6 +51,10 @@ if bashio::config.true 'ssl'; then
|
||||
ADDON_PROTOCOL=https
|
||||
fi
|
||||
|
||||
# Expose the protocol to the docker HEALTHCHECK, which runs outside this
|
||||
# shell and therefore cannot read the addon options
|
||||
echo -n "${ADDON_PROTOCOL}" > /run/health_protocol
|
||||
|
||||
#port=$(bashio::addon.port 80)
|
||||
ingress_port=$(bashio::addon.ingress_port)
|
||||
ingress_interface=$(bashio::addon.ip_address)
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
gitea/stats.png
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
grav/stats.png
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
immich/stats.png
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
inadyn/stats.png
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.6 KiB |