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
|
Bigdaddy1990,Germany
|
||||||
BilboBagCoder,
|
BilboBagCoder,
|
||||||
Bill-Dung,Germany
|
Bill-Dung,Germany
|
||||||
|
Blueyzachary,
|
||||||
BobMcCloy,
|
BobMcCloy,
|
||||||
Bobdugrand8,France
|
Bobdugrand8,France
|
||||||
Boilerplate4u,
|
Boilerplate4u,
|
||||||
@@ -234,6 +235,7 @@ EricCorleone,
|
|||||||
Erreur32,
|
Erreur32,
|
||||||
Errox,Netherlands
|
Errox,Netherlands
|
||||||
Espagona,
|
Espagona,
|
||||||
|
EsteSama,
|
||||||
EthanVCS,United Kingdom
|
EthanVCS,United Kingdom
|
||||||
EtienneMD,
|
EtienneMD,
|
||||||
Evel270,
|
Evel270,
|
||||||
@@ -347,6 +349,7 @@ Jaber7,
|
|||||||
JackTyson,United Kingdom
|
JackTyson,United Kingdom
|
||||||
JackoKic,
|
JackoKic,
|
||||||
JaggedJax,United States
|
JaggedJax,United States
|
||||||
|
JairCosta00,
|
||||||
Jak12-3,
|
Jak12-3,
|
||||||
Jango024,
|
Jango024,
|
||||||
JaroSu,Poland
|
JaroSu,Poland
|
||||||
@@ -738,6 +741,7 @@ TimInTech,Germany
|
|||||||
Timbo74,
|
Timbo74,
|
||||||
Tohtli,
|
Tohtli,
|
||||||
Tokahiro,Germany
|
Tokahiro,Germany
|
||||||
|
TokenLimitReached,
|
||||||
TomCasavant,
|
TomCasavant,
|
||||||
TomHuber1,
|
TomHuber1,
|
||||||
TopsideWings,
|
TopsideWings,
|
||||||
@@ -1105,6 +1109,7 @@ cln-io,
|
|||||||
cloogshizer,
|
cloogshizer,
|
||||||
cloudlena,Switzerland
|
cloudlena,Switzerland
|
||||||
clubxtc,
|
clubxtc,
|
||||||
|
clutchthrower,
|
||||||
cndoit18,China
|
cndoit18,China
|
||||||
cnrlmr,
|
cnrlmr,
|
||||||
coaster3000,United States
|
coaster3000,United States
|
||||||
@@ -1162,6 +1167,7 @@ danctrl,Germany
|
|||||||
danez,United States
|
danez,United States
|
||||||
danieldotnl,Netherlands
|
danieldotnl,Netherlands
|
||||||
danishru,
|
danishru,
|
||||||
|
danmulvey,United States
|
||||||
dannybeeckman,
|
dannybeeckman,
|
||||||
dannybloomfield,United States
|
dannybloomfield,United States
|
||||||
danveitch76,
|
danveitch76,
|
||||||
@@ -1234,6 +1240,7 @@ dmanifold,
|
|||||||
dmostert,
|
dmostert,
|
||||||
dnoggle,United States
|
dnoggle,United States
|
||||||
dobrjaha,
|
dobrjaha,
|
||||||
|
docker-alex-ai,
|
||||||
docker-master,
|
docker-master,
|
||||||
doctorvanmartin,
|
doctorvanmartin,
|
||||||
dollannn,Sweden
|
dollannn,Sweden
|
||||||
@@ -1372,6 +1379,7 @@ frizzi42,
|
|||||||
froggy974,France
|
froggy974,France
|
||||||
frostworx,
|
frostworx,
|
||||||
frsantos,Spain
|
frsantos,Spain
|
||||||
|
frunkad,
|
||||||
fuatakgun,
|
fuatakgun,
|
||||||
fullstackdelay,Germany
|
fullstackdelay,Germany
|
||||||
funar,United States
|
funar,United States
|
||||||
@@ -1584,6 +1592,7 @@ jhhbe,
|
|||||||
jhiegel,
|
jhiegel,
|
||||||
jhron,
|
jhron,
|
||||||
jiadongjiang,
|
jiadongjiang,
|
||||||
|
jiange1236,
|
||||||
jimmyang1992,
|
jimmyang1992,
|
||||||
jine,Sweden
|
jine,Sweden
|
||||||
jj-csg,
|
jj-csg,
|
||||||
@@ -2141,6 +2150,7 @@ quirbiefe,
|
|||||||
qun-media,
|
qun-media,
|
||||||
r0rqual,United States
|
r0rqual,United States
|
||||||
rJUUSO,
|
rJUUSO,
|
||||||
|
rafaelcruzmartins,
|
||||||
ralong777,
|
ralong777,
|
||||||
ranjitrajkumar,Canada
|
ranjitrajkumar,Canada
|
||||||
raphael1688,
|
raphael1688,
|
||||||
@@ -2212,6 +2222,7 @@ rstruminski,
|
|||||||
rtizzy,United States
|
rtizzy,United States
|
||||||
rtmlp,
|
rtmlp,
|
||||||
rucas,United States
|
rucas,United States
|
||||||
|
rummik,United States
|
||||||
ruok911,
|
ruok911,
|
||||||
rvbg,Germany
|
rvbg,Germany
|
||||||
rwagnervm,Brazil
|
rwagnervm,Brazil
|
||||||
@@ -2359,6 +2370,7 @@ sudo-shubham,India
|
|||||||
suiciety,
|
suiciety,
|
||||||
sunshine-hass,
|
sunshine-hass,
|
||||||
superiuspi,France
|
superiuspi,France
|
||||||
|
supersonic-jet,
|
||||||
supersonical,
|
supersonical,
|
||||||
superyass,
|
superyass,
|
||||||
sweetmeats83,United States
|
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
|
container: ghcr.io/hadolint/hadolint:latest-alpine
|
||||||
steps:
|
steps:
|
||||||
- name: ↩️ Checkout
|
- name: ↩️ Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed_files
|
id: changed_files
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
container: koalaman/shellcheck-alpine:latest
|
container: koalaman/shellcheck-alpine:latest
|
||||||
steps:
|
steps:
|
||||||
- name: ↩️ Checkout
|
- name: ↩️ Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: Get changed files
|
- name: Get changed files
|
||||||
id: changed_files
|
id: changed_files
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: ↩️ Checkout
|
- name: ↩️ Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
# Full git history is needed to get a proper list of changed files within `super-linter`
|
# Full git history is needed to get a proper list of changed files within `super-linter`
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
2
.github/workflows/daily_README.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: Install jq + yq (v4)
|
- name: Install jq + yq (v4)
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
18
.github/workflows/daily_ai_fix.yaml
vendored
@@ -50,7 +50,6 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ai-fix-sweep
|
group: ai-fix-sweep
|
||||||
@@ -66,7 +65,7 @@ jobs:
|
|||||||
environment: CR_PAT
|
environment: CR_PAT
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.AI_PR_TOKEN }}
|
token: ${{ secrets.AI_PR_TOKEN }}
|
||||||
@@ -126,12 +125,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Analyse and fix
|
- name: Analyse and fix
|
||||||
if: steps.batch.outputs.count != '0'
|
if: steps.batch.outputs.count != '0'
|
||||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
# One sticky, auto-updating status comment per run instead of the
|
# Skip the OIDC -> Claude App token exchange. The scheduled path
|
||||||
# model narrating its own progress in scattered comments.
|
# happens to pass it (github.actor is the maintainer), but
|
||||||
track_progress: true
|
# 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: |
|
prompt: |
|
||||||
The batch of issues to work through is /tmp/ai-fix/batch.json.
|
The batch of issues to work through is /tmp/ai-fix/batch.json.
|
||||||
Follow .github/prompts/issue-fix.md exactly. Do not deviate from
|
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:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v7
|
uses: actions/setup-python@v7
|
||||||
|
|||||||
2
.github/workflows/helper_stats_graphs.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
- name: Install apps
|
- name: Install apps
|
||||||
run: |
|
run: |
|
||||||
git pull --rebase origin master
|
git pull --rebase origin master
|
||||||
|
|||||||
4
.github/workflows/lint.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/on_claude_mention.yml
vendored
@@ -59,12 +59,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.
|
# 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
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ai-approve-${{ github.event.issue.number || inputs.issue }}
|
group: ai-approve-${{ github.event.issue.number || inputs.issue }}
|
||||||
@@ -76,7 +75,7 @@ jobs:
|
|||||||
environment: CR_PAT
|
environment: CR_PAT
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.AI_PR_TOKEN }}
|
token: ${{ secrets.AI_PR_TOKEN }}
|
||||||
@@ -136,10 +135,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Execute the plan
|
- name: Execute the plan
|
||||||
if: steps.bundle.outputs.has_plan == 'true'
|
if: steps.bundle.outputs.has_plan == 'true'
|
||||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
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: |
|
prompt: |
|
||||||
The approved plan is /tmp/ai-exec/plan.md and the issue it belongs
|
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
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
- name: Assign issues
|
- name: Assign issues
|
||||||
run: |
|
run: |
|
||||||
# Init
|
# Init
|
||||||
|
|||||||
11
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -41,7 +41,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ai-triage-${{ github.event.issue.number || inputs.issue || github.run_id }}
|
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.
|
# (issues.opened, dispatch) never set claim, so they always proceed.
|
||||||
- name: Checkout tooling
|
- name: Checkout tooling
|
||||||
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
||||||
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
@@ -145,9 +144,15 @@ jobs:
|
|||||||
id: classify
|
id: classify
|
||||||
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
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
|
show_full_output: true
|
||||||
prompt: |
|
prompt: |
|
||||||
Read /tmp/ai-triage/context.md, then follow the instructions in
|
Read /tmp/ai-triage/context.md, then follow the instructions in
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: Ping mapped submitter when add-on is mentioned
|
- name: Ping mapped submitter when add-on is mentioned
|
||||||
env:
|
env:
|
||||||
|
|||||||
10
.github/workflows/on_pr_coderabbit.yml
vendored
@@ -25,7 +25,6 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
issues: write
|
issues: write
|
||||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ai-coderabbit-${{ github.event.pull_request.number }}
|
group: ai-coderabbit-${{ github.event.pull_request.number }}
|
||||||
@@ -66,7 +65,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Checkout PR branch
|
- name: Checkout PR branch
|
||||||
if: steps.claim.outputs.go == 'true'
|
if: steps.claim.outputs.go == 'true'
|
||||||
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -80,9 +79,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Address CodeRabbit comments
|
- name: Address CodeRabbit comments
|
||||||
if: steps.claim.outputs.go == 'true'
|
if: steps.claim.outputs.go == 'true'
|
||||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
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: |
|
prompt: |
|
||||||
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
|
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
|
||||||
on ${{ github.repository }}. You are on that PR's branch. Follow
|
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 }}
|
changedChangelogFiles: ${{ steps.changed-files.outputs.changelogs_files }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
# Need the merge commit's parents resolvable (HEAD^1 below): a depth-1 shallow
|
# Need the merge commit's parents resolvable (HEAD^1 below): a depth-1 shallow
|
||||||
# checkout truncates parent refs entirely at the boundary commit.
|
# checkout truncates parent refs entirely at the boundary commit.
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
||||||
steps:
|
steps:
|
||||||
- name: ↩️ Checkout
|
- name: ↩️ Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: 🔎 Run Home Assistant Add-on Lint
|
- name: 🔎 Run Home Assistant Add-on Lint
|
||||||
uses: frenck/action-addon-linter@v2
|
uses: frenck/action-addon-linter@v2
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
|
||||||
steps:
|
steps:
|
||||||
- name: ↩️ Checkout
|
- name: ↩️ Checkout
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: Copy templates into addon build context
|
- name: Copy templates into addon build context
|
||||||
env:
|
env:
|
||||||
|
|||||||
12
.github/workflows/onpush_builder.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
changedAddons: ${{ steps.find_addons.outputs.changed_addons }}
|
changedAddons: ${{ steps.find_addons.outputs.changed_addons }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
sanitizeCommitted: ${{ steps.sanitize_commit.outputs.committed }}
|
sanitizeCommitted: ${{ steps.sanitize_commit.outputs.committed }}
|
||||||
sanitizeCommitSha: ${{ steps.sanitize_commit.outputs.commit_long_sha }}
|
sanitizeCommitSha: ${{ steps.sanitize_commit.outputs.commit_long_sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
addon: ${{ fromJSON(needs.detect-changed-addons.outputs.changedAddons) }}
|
addon: ${{ fromJSON(needs.detect-changed-addons.outputs.changedAddons) }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7.0.1
|
||||||
- name: Run Home Assistant Add-on Lint
|
- name: Run Home Assistant Add-on Lint
|
||||||
uses: frenck/action-addon-linter@v2
|
uses: frenck/action-addon-linter@v2
|
||||||
with:
|
with:
|
||||||
@@ -137,7 +137,7 @@ jobs:
|
|||||||
- arch: aarch64
|
- arch: aarch64
|
||||||
runner: ubuntu-24.04-arm
|
runner: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -406,7 +406,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/weekly_crlftolf.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository contents
|
- name: Checkout repository contents
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: Use action to check for CRLF endings
|
- name: Use action to check for CRLF endings
|
||||||
uses: erclu/check-crlf@v1
|
uses: erclu/check-crlf@v1
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest # Use a Linux runner
|
runs-on: ubuntu-latest # Use a Linux runner
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository contents
|
- 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
|
- name: Find files with CRLF endings
|
||||||
uses: erclu/check-crlf@v1.2.0 # Use the check-crlf action
|
uses: erclu/check-crlf@v1.2.0 # Use the check-crlf action
|
||||||
id: check-crlf # Assign an id to this step
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
|
|
||||||
- name: Compress Images
|
- name: Compress Images
|
||||||
id: calibre
|
id: calibre
|
||||||
|
|||||||
2
.github/workflows/weekly_stats.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7.0.1
|
||||||
- name: Create stats
|
- name: Create stats
|
||||||
run: |
|
run: |
|
||||||
echo "Starting"
|
echo "Starting"
|
||||||
|
|||||||
@@ -3,6 +3,160 @@
|
|||||||
|
|
||||||
set -e
|
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
|
# 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
|
mkdir -p /etc
|
||||||
touch "$ETC_ENV_FILE"
|
touch "$ETC_ENV_FILE"
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Temp helper
|
|
||||||
################################################################################
|
|
||||||
mktemp_safe() {
|
|
||||||
local tmpdir="${TMPDIR:-/tmp}"
|
|
||||||
mkdir -p "$tmpdir"
|
|
||||||
mktemp "$tmpdir/tmp.XXXXXXXXXX"
|
|
||||||
}
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Secrets support
|
# Secrets support
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -68,54 +213,13 @@ resolve_secret() {
|
|||||||
printf '%s' "$line"
|
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
|
# S6 + script injection block
|
||||||
################################################################################
|
################################################################################
|
||||||
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
|
|
||||||
BLOCK_END="# --- END ADDON ENV (generated) ---"
|
|
||||||
|
|
||||||
EXPORT_BLOCK="$(mktemp_safe)"
|
EXPORT_BLOCK="$(mktemp_safe)"
|
||||||
|
EXPORT_BODY="$(mktemp_safe)"
|
||||||
KV_FILE="$(mktemp_safe)"
|
KV_FILE="$(mktemp_safe)"
|
||||||
trap 'rm -f "$EXPORT_BLOCK" "$KV_FILE"' EXIT
|
trap 'rm -f "$EXPORT_BLOCK" "$EXPORT_BODY" "$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"
|
|
||||||
}
|
|
||||||
|
|
||||||
inject_block() {
|
inject_block() {
|
||||||
local f="$1" tmp
|
local f="$1" tmp
|
||||||
@@ -235,6 +339,8 @@ cp "$ENV_FILE" "$ETC_ENV_FILE"
|
|||||||
################################################################################
|
################################################################################
|
||||||
# Inject into scripts and shells (best-effort)
|
# 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
|
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"
|
[[ -f "$f" ]] && inject_block "$f"
|
||||||
done
|
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)
|
## 1.6.0 (2026-07-08)
|
||||||
|
|
||||||
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
|
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)
|
||||||
|
|||||||
@@ -112,4 +112,4 @@ schema:
|
|||||||
slug: bazarr_nas
|
slug: bazarr_nas
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons/tree/master/bazarr
|
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)
|
ingress_noauth)
|
||||||
bashio::log.green "Ingress is enabled, authentication is disabled"
|
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 !"
|
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)
|
# Set base_url (must start with / for Flask blueprint registration).
|
||||||
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
|
# 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
|
# Disable auth
|
||||||
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: null/ }' "$CONFIG_LOCATION"
|
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: null/ }' "$CONFIG_LOCATION"
|
||||||
;;
|
;;
|
||||||
# Ingress mode, with authentication
|
# Ingress mode, with authentication
|
||||||
ingress_auth)
|
ingress_auth)
|
||||||
bashio::log.green "Ingress is enabled, and external authentication is enabled"
|
bashio::log.green "Ingress is enabled, and external authentication is enabled"
|
||||||
# Set base_url (must start with / for Flask blueprint registration)
|
# Set base_url (must start with / for Flask blueprint registration).
|
||||||
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
|
# 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
|
# Enable Bazarr auth when leaving ingress_noauth
|
||||||
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
|
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
|
||||||
;;
|
;;
|
||||||
@@ -52,7 +56,8 @@ if [ -f "$CONFIG_LOCATION" ]; then
|
|||||||
noingress_auth)
|
noingress_auth)
|
||||||
bashio::log.green "Disabling ingress and enabling authentication"
|
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 !"
|
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
|
# Enable Bazarr auth when leaving ingress_noauth
|
||||||
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
|
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -19,6 +19,19 @@ server {
|
|||||||
proxy_set_header Connection $http_connection;
|
proxy_set_header Connection $http_connection;
|
||||||
#auth_basic off;
|
#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
|
# Correct base_url
|
||||||
proxy_set_header Accept-Encoding "";
|
proxy_set_header Accept-Encoding "";
|
||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ bashio::net.wait_for "$port" localhost 900
|
|||||||
if [ -f "$CONFIG_LOCATION" ]; then
|
if [ -f "$CONFIG_LOCATION" ]; then
|
||||||
if ! bashio::config.true "ingress_disabled"; then
|
if ! bashio::config.true "ingress_disabled"; then
|
||||||
if ! bashio::config.has_value "connection_mode" || [ "$(bashio::config 'connection_mode')" != "noingress_auth" ]; 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"
|
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
|
bashio::addon.restart
|
||||||
fi
|
fi
|
||||||
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)
|
## 1.35 (23-07-2026)
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
|||||||
# cannot alter executables elsewhere in the image.
|
# cannot alter executables elsewhere in the image.
|
||||||
COPY rootfs/ /
|
COPY rootfs/ /
|
||||||
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
|
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
|
chmod +x /usr/local/bin/claude
|
||||||
|
|
||||||
# Uses /bin for compatibility purposes
|
# 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`.
|
- Custom script support through the repository standard `claude_desktop.sh`.
|
||||||
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
|
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
|
||||||
available as an opt-in plugin.
|
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
|
- Optional Home Assistant MCP bridge so Claude can query and control Home
|
||||||
Assistant.
|
Assistant.
|
||||||
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
|
- 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. |
|
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
|
||||||
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
|
| `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_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. |
|
| `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. |
|
| `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. |
|
| `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
|
The dashboard is unauthenticated. Do not publish this port to the public
|
||||||
internet.
|
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
|
## Diagnostics
|
||||||
|
|
||||||
Run the following inside the add-on through a custom script or container console:
|
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
|
The report checks the tool binaries, configuration switches, configured and
|
||||||
effective runtime identities, redacted MCP registrations, Claude hooks,
|
effective runtime identities, redacted MCP registrations, Claude hooks,
|
||||||
permission mode, Headroom health, TokenSave indexes, routing, and recorded
|
permission mode, Headroom health, TokenSave indexes, routing, and recorded
|
||||||
savings. It never prints MCP environment values because the Home Assistant MCP
|
savings. It never prints MCP environment values or raw Codex authentication
|
||||||
entry can contain a long-lived token.
|
status because either can contain credentials or masked credential fragments.
|
||||||
|
|
||||||
The hourly report can also be invoked manually:
|
The hourly report can also be invoked manually:
|
||||||
|
|
||||||
@@ -288,6 +363,8 @@ Persistent state is stored in the configured `data_location` (default
|
|||||||
shared home
|
shared home
|
||||||
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
|
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
|
||||||
project
|
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
|
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
|
||||||
`$HOME/.cache`.
|
`$HOME/.cache`.
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ options:
|
|||||||
headroom_auto_compress: true
|
headroom_auto_compress: true
|
||||||
headroom_wrap_claude_code: true
|
headroom_wrap_claude_code: true
|
||||||
install_caveman: false
|
install_caveman: false
|
||||||
|
install_codex_cli: false
|
||||||
|
codex_sandbox_mode: workspace-write
|
||||||
install_github_cli: true
|
install_github_cli: true
|
||||||
install_headroom: true
|
install_headroom: true
|
||||||
install_rtk: true
|
install_rtk: true
|
||||||
@@ -107,6 +109,8 @@ schema:
|
|||||||
headroom_auto_compress: bool?
|
headroom_auto_compress: bool?
|
||||||
headroom_wrap_claude_code: bool
|
headroom_wrap_claude_code: bool
|
||||||
install_caveman: bool
|
install_caveman: bool
|
||||||
|
install_codex_cli: bool
|
||||||
|
codex_sandbox_mode: list(read-only|workspace-write|danger-full-access)
|
||||||
install_github_cli: bool
|
install_github_cli: bool
|
||||||
install_headroom: bool
|
install_headroom: bool
|
||||||
install_rtk: bool
|
install_rtk: bool
|
||||||
@@ -118,5 +122,5 @@ slug: claude_desktop
|
|||||||
tmpfs: true
|
tmpfs: true
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "1.35"
|
version: "1.36.2"
|
||||||
video: true
|
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"
|
done <<< "$TOKENSAVE_PROJECT_PATHS"
|
||||||
fi
|
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_ENABLED=false
|
||||||
HA_MCP_URL=""
|
HA_MCP_URL=""
|
||||||
HA_MCP_TOKEN=""
|
HA_MCP_TOKEN=""
|
||||||
@@ -314,6 +328,7 @@ fi
|
|||||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
||||||
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
|
||||||
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
|
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" \
|
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)" \
|
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||||
@@ -326,6 +341,7 @@ MANAGED_BASENAMES = {
|
|||||||
"headroom": "headroom",
|
"headroom": "headroom",
|
||||||
"tokensave": "tokensave",
|
"tokensave": "tokensave",
|
||||||
"homeassistant": "mcp-proxy",
|
"homeassistant": "mcp-proxy",
|
||||||
|
"codex": "codex",
|
||||||
}
|
}
|
||||||
|
|
||||||
desired = {}
|
desired = {}
|
||||||
@@ -341,6 +357,25 @@ if os.environ["HEADROOM_ENABLED"] == "true":
|
|||||||
}
|
}
|
||||||
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
||||||
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
|
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":
|
if os.environ["HA_MCP_ENABLED"] == "true":
|
||||||
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
|
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
|
||||||
# mcp-proxy defaults to SSE, so the transport flags are required.
|
# mcp-proxy defaults to SSE, so the transport flags are required.
|
||||||
@@ -531,6 +566,32 @@ else
|
|||||||
manage_claude_md_block ha-api-helper remove
|
manage_claude_md_block ha-api-helper remove
|
||||||
fi
|
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 bashio::config.true 'install_rtk'; then
|
||||||
if command -v rtk &> /dev/null; then
|
if command -v rtk &> /dev/null; then
|
||||||
bashio::log.info "Configuring rtk Claude Code integration"
|
bashio::log.info "Configuring rtk Claude Code integration"
|
||||||
|
|||||||
@@ -2,14 +2,22 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
|
# Earlier configuration scripts intentionally run as root. Reconcile the paths written by those
|
||||||
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
|
# scripts with the final abc runtime identity and its configured persistent home.
|
||||||
# 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.
|
|
||||||
RUNTIME_UID="$(id -u abc)"
|
RUNTIME_UID="$(id -u abc)"
|
||||||
RUNTIME_GID="$(id -g 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
|
if [ -e "$managed_path" ]; then
|
||||||
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|
||||||
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"
|
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
#!/usr/bin/with-contenv bashio
|
#!/usr/bin/with-contenv bashio
|
||||||
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
|
# 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
|
# shellcheck shell=bash
|
||||||
set +e
|
set +e
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
export NO_COLOR=1
|
export NO_COLOR=1
|
||||||
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
|
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() {
|
section() {
|
||||||
printf '\n=== %s ===\n' "$1"
|
printf '\n=== %s ===\n' "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
section "Installed binaries"
|
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)"
|
resolved="$(command -v "$tool" 2> /dev/null || true)"
|
||||||
if [ -n "$resolved" ]; then
|
if [ -n "$resolved" ]; then
|
||||||
printf '%-16s %s\n' "$tool" "$resolved"
|
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
|
done
|
||||||
|
|
||||||
section "Configured switches"
|
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")"
|
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
|
||||||
done
|
done
|
||||||
|
|
||||||
section "Runtime identity"
|
section "Runtime identity"
|
||||||
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
|
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' "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)"
|
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
|
||||||
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
||||||
if [ "$(id -u abc)" -eq 0 ]; then
|
if [ "$(id -u abc)" -eq 0 ]; then
|
||||||
@@ -39,11 +45,12 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
section "Claude Code permission state"
|
section "Claude Code permission state"
|
||||||
python3 - <<'PY'
|
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
path = Path.home() / ".claude/settings.json"
|
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
|
||||||
try:
|
try:
|
||||||
data = json.loads(path.read_text())
|
data = json.loads(path.read_text())
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -59,13 +66,15 @@ else:
|
|||||||
PY
|
PY
|
||||||
|
|
||||||
section "MCP registrations (environment values redacted)"
|
section "MCP registrations (environment values redacted)"
|
||||||
python3 - <<'PY'
|
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
home = Path(os.environ["RUNTIME_HOME"])
|
||||||
paths = [
|
paths = [
|
||||||
Path.home() / ".claude.json",
|
home / ".claude.json",
|
||||||
Path.home() / ".config/Claude/claude_desktop_config.json",
|
home / ".config/Claude/claude_desktop_config.json",
|
||||||
]
|
]
|
||||||
for path in paths:
|
for path in paths:
|
||||||
print(path)
|
print(path)
|
||||||
@@ -95,11 +104,12 @@ for path in paths:
|
|||||||
PY
|
PY
|
||||||
|
|
||||||
section "Claude Code hooks"
|
section "Claude Code hooks"
|
||||||
python3 - <<'PY'
|
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
path = Path.home() / ".claude/settings.json"
|
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
|
||||||
try:
|
try:
|
||||||
data = json.loads(path.read_text())
|
data = json.loads(path.read_text())
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -148,18 +158,17 @@ section "TokenSave"
|
|||||||
if bashio::config.true 'install_tokensave'; then
|
if bashio::config.true 'install_tokensave'; then
|
||||||
tokensave doctor --agent claude || true
|
tokensave doctor --agent claude || true
|
||||||
tokensave gain --all --range 30d || true
|
tokensave gain --all --range 30d || true
|
||||||
# Capture before looping — see the matching comment in 82-claude_tools.sh: feeding the
|
# Capture before looping — see the matching comment in 82-claude_tools.sh.
|
||||||
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
|
|
||||||
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
|
||||||
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
|
||||||
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
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
|
if [ -z "$repo_root" ]; then
|
||||||
echo "${configured_path}: not a Git repository"
|
echo "${configured_path}: not a Git repository"
|
||||||
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
|
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
|
else
|
||||||
echo "${repo_root}: NOT INITIALIZED"
|
echo "${repo_root}: NOT INITIALIZED"
|
||||||
fi
|
fi
|
||||||
@@ -168,6 +177,45 @@ else
|
|||||||
echo "disabled"
|
echo "disabled"
|
||||||
fi
|
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"
|
section "Claude routing"
|
||||||
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
|
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)"
|
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)
|
## 1.3.0 (2026-07-16)
|
||||||
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
- Update to latest version from CollaboraOnline/online (changelog : https://github.com/CollaboraOnline/online/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,45 @@
|
|||||||
|
|
||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
ARG BUILD_VERSION
|
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 #
|
# 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
|
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
|
||||||
|
|
||||||
# Manual apps
|
# 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
|
# Automatic apps & bashio
|
||||||
COPY ha_autoapps.sh /ha_autoapps.sh
|
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 #
|
# 4 Entrypoint #
|
||||||
|
|||||||
@@ -52,22 +52,63 @@ Webui can be found at `https://homeassistant:9980/browser/dist/admin/admin.html`
|
|||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|--------|------|---------|-------------|
|
|--------|------|---------|-------------|
|
||||||
| `aliasgroup1` | str | | Nextcloud external domain with escaped dots using two \ (e.g. `nextcloud_domain\\.com`) |
|
| `aliasgroup1` | str | | External address of the **Nextcloud** server allowed to use this Collabora (e.g. `https://nextcloud_domain\.com:443`) |
|
||||||
| `domain1` | str | | Collabora external domain with escaped dots using two \ (e.g. `code_domain\\.com`) |
|
| `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 |
|
| `extra_params` | str | | Extra parameters passed to the Collabora start script |
|
||||||
| `ssl` | bool | `false` | Enable SSL using certificates from /ssl |
|
| `ssl` | bool | `false` | Enable SSL using certificates from /ssl |
|
||||||
| `certfile` | str | `fullchain.pem` | Certificate file name located in /ssl |
|
| `certfile` | str | `fullchain.pem` | Certificate file name located in /ssl |
|
||||||
| `keyfile` | str | `privkey.pem` | Private key 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 |
|
| `username` | str | | Username for the Collabora admin console |
|
||||||
| `password` | str | | Password for the Collabora admin console |
|
| `password` | str | | Password for the Collabora admin console |
|
||||||
| `dictionaries` | str | | Space-separated list of dictionary languages to install |
|
| `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
|
### 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
|
```yaml
|
||||||
aliasgroup1: nextcloud_domain\\.com
|
aliasgroup1: https://next\.duckdns\.org:443
|
||||||
domain1: code_domain\\.com
|
server_name: code.duckdns.org:9980
|
||||||
extra_params: ""
|
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
|
ssl: true
|
||||||
certfile: fullchain.pem
|
certfile: fullchain.pem
|
||||||
keyfile: privkey.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. Start the add-on and expose the Collabora server to an external domain.
|
||||||
1. Install and configure the Nextcloud add-on.
|
1. Install and configure the Nextcloud add-on.
|
||||||
1. Inside Nextcloud, install the **Nextcloud Office** app.
|
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
|
### Custom Scripts and Environment Variables
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ options:
|
|||||||
env_vars: []
|
env_vars: []
|
||||||
aliasgroup1: ""
|
aliasgroup1: ""
|
||||||
certfile: fullchain.pem
|
certfile: fullchain.pem
|
||||||
domain1: ""
|
server_name: ""
|
||||||
extra_params:
|
extra_params:
|
||||||
--o:ssl.enable=false --o:user_interface.use_integration_theme=false
|
--o:ssl.enable=false --o:user_interface.use_integration_theme=false
|
||||||
--o:net.proto=IPv4
|
--o:net.proto=IPv4
|
||||||
@@ -33,7 +33,9 @@ schema:
|
|||||||
value: str?
|
value: str?
|
||||||
TZ: str?
|
TZ: str?
|
||||||
aliasgroup1: str
|
aliasgroup1: str
|
||||||
cert_domain: bool?
|
aliasgroup2: str?
|
||||||
|
aliasgroup3: str?
|
||||||
|
cert_domain: str?
|
||||||
certfile: str
|
certfile: str
|
||||||
dictionaries: str?
|
dictionaries: str?
|
||||||
domain1: str?
|
domain1: str?
|
||||||
@@ -42,8 +44,9 @@ schema:
|
|||||||
password: password
|
password: password
|
||||||
server_name: str?
|
server_name: str?
|
||||||
ssl: bool
|
ssl: bool
|
||||||
|
ssl_termination: bool?
|
||||||
username: str
|
username: str
|
||||||
slug: collabora
|
slug: collabora
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
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"
|
webui: "[PROTO:ssl]://[HOST]:[PORT:9980]/browser/dist/admin/admin.html"
|
||||||
|
|||||||
@@ -2,9 +2,58 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if bashio::config.has_value 'domain'; then
|
# coolwsd matches storage.wopi.alias_groups host/alias entries as regular
|
||||||
domain="$(bashio::config 'domain')"
|
# expressions, so every dot has to be escaped with a single backslash. The value
|
||||||
export domain
|
# 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
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'username'; then
|
if bashio::config.has_value 'username'; then
|
||||||
@@ -17,9 +66,9 @@ if bashio::config.has_value 'password'; then
|
|||||||
export password
|
export password
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'aliasgroup1'; then
|
if bashio::config.has_value 'cert_domain'; then
|
||||||
aliasgroup1="$(bashio::config 'aliasgroup1')"
|
cert_domain="$(bashio::config 'cert_domain')"
|
||||||
export aliasgroup1
|
export cert_domain
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'dictionaries'; then
|
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')"
|
extra_params="$(bashio::config 'extra_params')"
|
||||||
fi
|
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
|
if bashio::config.true 'ssl'; then
|
||||||
export DONT_GEN_SSL_CERT=true
|
export DONT_GEN_SSL_CERT=true
|
||||||
bashio::config.require.ssl
|
bashio::config.require.ssl
|
||||||
@@ -45,16 +100,31 @@ if bashio::config.true 'ssl'; then
|
|||||||
bashio::log.error "Key file /ssl/${keyfile} not found"
|
bashio::log.error "Key file /ssl/${keyfile} not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
cp -f /ssl/${keyfile} /etc/coolwsd/key.pem
|
# Point Collabora at the copies rather than at /ssl. coolwsd runs as uid
|
||||||
cp -f /ssl/${certfile} /etc/coolwsd/cert.pem
|
# 1001 and /ssl is mounted read-only with whatever ownership the certificate
|
||||||
cp -f /ssl/${certfile} /etc/coolwsd/ca-chain.cert.pem
|
# tooling left behind, which for a private key is commonly root-only. These
|
||||||
extra_params="${extra_params/--o:ssl.enable=false/}"
|
# 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} \
|
extra_params="${extra_params} \
|
||||||
--o:ssl.enable=true
|
--o:ssl.enable=true \
|
||||||
--o:ssl.termination=false \
|
--o:ssl.termination=false \
|
||||||
--o:ssl.cert_file_path=/ssl/${certfile} \
|
--o:ssl.cert_file_path=/etc/coolwsd/cert.pem \
|
||||||
--o:ssl.key_file_path=/ssl/${keyfile} \
|
--o:ssl.key_file_path=/etc/coolwsd/key.pem \
|
||||||
--o:ssl.ca_file_path=/ssl/${certfile}"
|
--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
|
fi
|
||||||
|
|
||||||
export extra_params
|
export extra_params
|
||||||
@@ -83,4 +153,9 @@ chown -R 1001 /etc/coolwsd
|
|||||||
chmod -R 755 /opt/cool/systemplate
|
chmod -R 755 /opt/cool/systemplate
|
||||||
|
|
||||||
bashio::log.info "Starting Collabora Online..."
|
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",
|
"repository": "alexbelgium/hassio-addons",
|
||||||
"slug": "collabora",
|
"slug": "collabora",
|
||||||
"source": "github",
|
"source": "dockerhub",
|
||||||
"upstream_repo": "CollaboraOnline/online",
|
"upstream_repo": "collabora/code",
|
||||||
"upstream_version": "1.3.0"
|
"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)
|
## 2.63.18.4 (22-07-2026)
|
||||||
- Minor bugs fixed
|
- Minor bugs fixed
|
||||||
## 2.63.18.3 (22-07-2026)
|
## 2.63.18.3 (22-07-2026)
|
||||||
|
|||||||
@@ -134,4 +134,4 @@ HEALTHCHECK \
|
|||||||
--retries=5 \
|
--retries=5 \
|
||||||
--start-period=30s \
|
--start-period=30s \
|
||||||
--timeout=25s \
|
--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
|
slug: filebrowser
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
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
|
ADDON_PROTOCOL=https
|
||||||
fi
|
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)
|
#port=$(bashio::addon.port 80)
|
||||||
ingress_port=$(bashio::addon.ingress_port)
|
ingress_port=$(bashio::addon.ingress_port)
|
||||||
ingress_interface=$(bashio::addon.ip_address)
|
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 |