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,3 +1,18 @@
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -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 |