Compare commits

...

10 Commits

Author SHA1 Message Date
GitHub Actions
89d6952c46 Revert "Update config.yaml"
This reverts commit 58c3fd61b8.
2026-07-27 20:30:11 +00:00
Alexandre
58c3fd61b8 Update config.yaml 2026-07-27 22:20:35 +02:00
Alexandre
f10a566b4c claude_desktop: fix Codex install failing on every boot (#2915)
install_codex_cli was non-functional: every boot logged "Verified Codex
<version> installation failed; Codex is unavailable this boot" and no binary
was ever installed.

The download, its SHA-256 verification against the GitHub-published digest, 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 exec failed with "unable to exec: Permission denied" (exit 126)
before the binary could be moved into place. Because the whole chain is a single
&&-list, that surfaced only as the generic failure warning.

Fixed by making the staging directory traversable immediately after mktemp.
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.

Reproduced and verified on a live add-on container: the same probe goes from
exit 126 to success once the mode is widened, and the fixed script now completes
the install (codex-real 0.145.0 in place, wrapper on PATH, managed config
written, no staging leftovers).

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 22:09:21 +02:00
Alexandre
0ead28a7bf feat: add Portainer Business Edition add-on (#873) (#2916)
* feat(portainer_be): add Portainer Business Edition add-on

Adds a new `portainer_be` add-on based on the existing Portainer (CE)
add-on, requested in #873.

Business Edition has no public GitHub release tarball like CE, so the
binary and web assets are pulled from the official multi-arch
`portainer/portainer-ee` image via a multi-stage build and placed under
/opt/portainer, mirroring CE's layout exactly. All runtime scripts,
nginx/ingress config, options schema, SSL and password handling are
unchanged from CE, so behaviour is identical apart from the edition.

Users obtain a free (up to 3 nodes) Business Edition license key by
registering with Portainer and enter it in the web UI on first launch.

- config.yaml: slug portainer_be, BE image name, BE description/name
- Dockerfile: multi-stage COPY from portainer/portainer-ee (no CE tarball)
- updater.json: dockerhub source tracking portainer/portainer-ee
- apparmor.txt: unique profile name (portainer_be_addon)
- CHANGELOG/README/DOCS: BE-specific, documents the license-key step

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(portainer_be): address PR review feedback

- nginx finish: move shebang to byte 0 (leading blank line prevented S6 from
  recognising the interpreter, so the finish hook could fail to tear down the
  supervision tree) [Codex P2]
- ingress: tighten CSP to `frame-ancestors 'self'` to match the adjacent
  X-Frame-Options SAMEORIGIN; HA ingress embeds same-origin so the panel keeps
  working [CodeRabbit]
- README: correct login note (password is the configured option value, never
  printed to logs); drop MD012 consecutive blank lines [CodeRabbit]
- DOCS: fix "environement" -> "environment" typo [CodeRabbit]

Skipped: nginx SSL "idempotency" finding — /etc/nginx lives in the read-only
image layer and cont-init re-renders from the pristine template on every
container start, so in-place sed edits never accumulate or need restoring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert(portainer_be): keep CSP frame-ancestors * for ingress compatibility

Reverts the frame-ancestors 'self' change from the previous commit. The
wildcard is required for the Home Assistant ingress iframe to embed the
Portainer UI; tightening it breaks the ingress panel. Matches the CE add-on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 22:08:17 +02:00
dependabot[bot]
5f9ecb7b05 chore(deps): bump anthropics/claude-code-action from 1.0.181 to 1.0.183 (#2914)
Bumps [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) from 1.0.181 to 1.0.183.
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](44423bdec7...be7b93b190)

---
updated-dependencies:
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.183
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 21:35:07 +02:00
dependabot[bot]
99a55c2109 chore(deps): bump actions/checkout from 5.1.0 to 7.0.1 (#2913)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5.1.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v5.1.0...v7.0.1)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 21:34:56 +02:00
Alexandre
b2ada7e4a7 claude_desktop: add subscription-only Codex MCP delegation (#2911)
* claude_desktop: add optional Codex CLI with device-code login and MCP bridge

Adds OpenAI's Codex CLI to the add-on as an opt-in fourth tool, so a Claude
session can delegate work to ChatGPT Codex as an independent second agent.

Install (install_codex_cli, default off): Codex is deliberately not baked into
the image -- its Linux binary is ~310 MB extracted, which is not worth carrying
in every installation for an off-by-default option, and updating it would then
need an add-on rebuild. A new 81-codex_cli.sh downloads the pinned static-musl
release (ENV CODEX_VERSION) into /data/codex/bin instead. That prefix is outside
$HOME on purpose: the managed-MCP merge treats any command under $HOME as
user-installed and refuses to manage it. Staging happens under /data rather than
the default /tmp, which here is a RAM-backed tmpfs mounted noexec -- holding
420 MB there during boot is a risk on a small host, and the binary could not be
verified there at all. The download fails open like the Claude Desktop update
check and validates the new binary by running it before replacing the old one.

Login (codex-login): Codex's default sign-in serves an OAuth callback on
localhost:1455 and expects a local browser, which cannot work in this add-on.
The helper runs `codex login --device-auth` instead -- the flow OpenAI documents
for headless machines -- printing a URL and one-time code to approve elsewhere.
It drops to the abc runtime user first so auth.json is not created root-owned.

MCP (codex mcp-server): registered through the existing managed-MCP merge rather
than a second copy of it, so it inherits that code's idempotence, no-clobber and
removal-when-disabled behaviour. A managed CLAUDE.md block explains when a second
agent is worth the round-trip.

New codex_sandbox_mode (default danger-full-access) is applied both as -c
overrides on the MCP command and as a managed block at the top of
~/.codex/config.toml; Codex's own Landlock/bubblewrap sandbox is unreliable
inside the container, which is already the security boundary.

Verified against the real 0.145.0 binary: tools/list returns `codex` and
`codex-reply` (hyphen, not the underscore upstream docs report), an invalid
-c sandbox_mode is rejected by name, the installer lifecycle behaves correctly
on re-run and on a bad pin, and the device code is flushed within seconds while
still polling, which is the non-TTY case that matters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* claude_desktop: harden Codex subscription MCP setup

* claude_desktop: use runtime home for Codex login

* claude_desktop: reconcile runtime user home ownership

* claude_desktop: report verified Codex subscription setup

* claude_desktop: track latest Codex at runtime

* claude_desktop: document subscription-only Codex MCP

* claude_desktop: enforce Codex runtime identity

* claude_desktop: persist Codex in runtime home

* claude_desktop: prevent Codex auth override bypass

* claude_desktop: default Codex to workspace write

* claude_desktop: redact Codex authentication diagnostics

* claude_desktop: document safer Codex MCP defaults

* claude_desktop: validate Codex candidate as runtime user

* claude_desktop: align Codex sandbox fallback

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:49:11 +02:00
Alexandre
2e0db91c2b Merge pull request #2912 from alexbelgium/ai-fix/bazarr-base-url-scope
Fix Bazarr base_url sed clobbering Radarr/Sonarr's own base_url
2026-07-27 19:42:42 +02:00
Alexandre
16931942b9 Fix Bazarr base_url guard scope 2026-07-27 19:41:16 +02:00
Claude
3b67bef373 Fix Bazarr base_url sed clobbering Radarr/Sonarr's own base_url
Bazarr's config.yaml carries a base_url key under general: (Bazarr's own
ingress path) AND a separate base_url under each configured *arr integration
-- radarr.base_url, sonarr.base_url, etc. -- which is how Bazarr reaches
those services at their own ingress-prefixed URL.

Every base_url sed in this addon was unscoped:

    sed -i "s|  base_url:.*|  base_url: /$slug|" "$CONFIG_LOCATION"

sed applies s/// to every matching line in the file, not just the first, and
"  base_url:.*" matches any 2-space-indented base_url line regardless of
which top-level section it's under. Since general.base_url, radarr.base_url,
sonarr.base_url etc. all sit at that same indent, this rewrote all of them to
Bazarr's own value on every container start (32-nginx_ingress.sh) and again
in the run script's fallback -- silently breaking Bazarr's configured
connections to Radarr and Sonarr.

Scope each sed to the general: block only, reusing the range idiom this file
already uses to scope the auth: block's type: substitution:

    sed -i "/^general:/,/^[^ ]/{ s|  base_url:.*|  base_url: /$slug|; }" ...

Verified against a representative config.yaml (general/radarr/sonarr/subsarr
sections, including general:'s list-style provider entries) for all three
connection_mode branches plus the run script's fallback: general.base_url is
the only line touched in every case; radarr.base_url and sonarr.base_url
survive with their original values.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:46:30 +02:00
55 changed files with 1482 additions and 65 deletions

View File

@@ -13,7 +13,7 @@ jobs:
container: ghcr.io/hadolint/hadolint:latest-alpine
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Get changed files
id: changed_files
@@ -34,7 +34,7 @@ jobs:
container: koalaman/shellcheck-alpine:latest
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Get changed files
id: changed_files
@@ -54,7 +54,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
# Full git history is needed to get a proper list of changed files within `super-linter`
fetch-depth: 0

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Install jq + yq (v4)
run: |

View File

@@ -65,7 +65,7 @@ jobs:
environment: CR_PAT
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.AI_PR_TOKEN }}
@@ -125,7 +125,7 @@ jobs:
- name: Analyse and fix
if: steps.batch.outputs.count != '0'
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange. The scheduled path

View File

@@ -9,7 +9,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@v7

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Install apps
run: |
git pull --rebase origin master

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
@@ -82,7 +82,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0

View File

@@ -59,12 +59,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
- name: Run Claude Code
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.

View File

@@ -75,7 +75,7 @@ jobs:
environment: CR_PAT
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.AI_PR_TOKEN }}
@@ -135,7 +135,7 @@ jobs:
- name: Execute the plan
if: steps.bundle.outputs.has_plan == 'true'
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Assign issues
run: |
# Init

View File

@@ -123,7 +123,7 @@ jobs:
# (issues.opened, dispatch) never set claim, so they always proceed.
- name: Checkout tooling
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
persist-credentials: false
@@ -144,7 +144,7 @@ jobs:
id: classify
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
continue-on-error: true
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Without this the action falls back to the OIDC -> Claude App token

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Ping mapped submitter when add-on is mentioned
env:

View File

@@ -65,7 +65,7 @@ jobs:
- name: Checkout PR branch
if: steps.claim.outputs.go == 'true'
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0
@@ -79,7 +79,7 @@ jobs:
- name: Address CodeRabbit comments
if: steps.claim.outputs.go == 'true'
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever

View File

@@ -18,7 +18,7 @@ jobs:
changedChangelogFiles: ${{ steps.changed-files.outputs.changelogs_files }}
steps:
- name: Checkout repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
# Need the merge commit's parents resolvable (HEAD^1 below): a depth-1 shallow
# checkout truncates parent refs entirely at the boundary commit.
@@ -88,7 +88,7 @@ jobs:
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: 🔎 Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@v2
@@ -106,7 +106,7 @@ jobs:
addon: ${{ fromJSON(needs.check-addon-changes.outputs.changedAddons) }}
steps:
- name: ↩️ Checkout
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Copy templates into addon build context
env:

View File

@@ -22,7 +22,7 @@ jobs:
changedAddons: ${{ steps.find_addons.outputs.changed_addons }}
steps:
- name: Checkout repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
@@ -59,7 +59,7 @@ jobs:
sanitizeCommitted: ${{ steps.sanitize_commit.outputs.committed }}
sanitizeCommitSha: ${{ steps.sanitize_commit.outputs.commit_long_sha }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
@@ -112,7 +112,7 @@ jobs:
matrix:
addon: ${{ fromJSON(needs.detect-changed-addons.outputs.changedAddons) }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
- name: Run Home Assistant Add-on Lint
uses: frenck/action-addon-linter@v2
with:
@@ -137,7 +137,7 @@ jobs:
- arch: aarch64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
with:
persist-credentials: false
@@ -335,7 +335,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
@@ -406,7 +406,7 @@ jobs:
contents: write
steps:
- name: Checkout repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
with:
fetch-depth: 0

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository contents
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Use action to check for CRLF endings
uses: erclu/check-crlf@v1
@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest # Use a Linux runner
steps:
- name: Checkout repository contents
uses: actions/checkout@v7 # Use the checkout action
uses: actions/checkout@v7.0.1 # Use the checkout action
- name: Find files with CRLF endings
uses: erclu/check-crlf@v1.2.0 # Use the check-crlf action
id: check-crlf # Assign an id to this step

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Compress Images
id: calibre

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v7
uses: actions/checkout@v7.0.1
- name: Create stats
run: |
echo "Starting"

View File

@@ -1,4 +1,8 @@
## 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

View File

@@ -112,4 +112,4 @@ schema:
slug: bazarr_nas
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/bazarr
version: "1.6.0.1"
version: "1.6.0.2"

View File

@@ -35,16 +35,20 @@ if [ -f "$CONFIG_LOCATION" ]; then
ingress_noauth)
bashio::log.green "Ingress is enabled, authentication is disabled"
bashio::log.yellow "WARNING : Make sure that the port is not exposed externally by your router to avoid a security risk !"
# Set base_url (must start with / for Flask blueprint registration)
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
# Set base_url (must start with / for Flask blueprint registration).
# Scoped to the general: block only -- config.yaml also carries a
# base_url under each configured *arr integration (radarr.base_url,
# sonarr.base_url, ...) and those must not be touched.
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
# Disable auth
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: null/ }' "$CONFIG_LOCATION"
;;
# Ingress mode, with authentication
ingress_auth)
bashio::log.green "Ingress is enabled, and external authentication is enabled"
# Set base_url (must start with / for Flask blueprint registration)
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
# Set base_url (must start with / for Flask blueprint registration).
# Scoped to the general: block only -- see note above.
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
# Enable Bazarr auth when leaving ingress_noauth
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
;;
@@ -52,7 +56,8 @@ if [ -f "$CONFIG_LOCATION" ]; then
noingress_auth)
bashio::log.green "Disabling ingress and enabling authentication"
bashio::log.yellow "WARNING : Ingress is disabled so the app won't be available from HA itself !"
sed -i "s/ base_url:.*/ base_url: ''/" "$CONFIG_LOCATION"
# Scoped to the general: block only -- see note above.
sed -i "/^general:/,/^[^ ]/{ s/ base_url:.*/ base_url: ''/; }" "$CONFIG_LOCATION"
# Enable Bazarr auth when leaving ingress_noauth
sed -i '/^auth:/,/^[^ ]/{ s/ type:.*/ type: form/ }' "$CONFIG_LOCATION"
;;

View File

@@ -15,10 +15,13 @@ bashio::net.wait_for "$port" localhost 900
if [ -f "$CONFIG_LOCATION" ]; then
if ! bashio::config.true "ingress_disabled"; then
if ! bashio::config.has_value "connection_mode" || [ "$(bashio::config 'connection_mode')" != "noingress_auth" ]; then
if ! grep -q "base_url: /$slug" "$CONFIG_LOCATION"; then
if ! sed -n "/^general:/,/^[^ ]/ { /^ base_url: \/$slug$/p; }" "$CONFIG_LOCATION" | grep -q .; then
bashio::log.warning "BaseUrl not set properly, restarting"
# Must start with / for Flask blueprint registration
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
# Must start with / for Flask blueprint registration. Scoped to
# the general: block only -- config.yaml also carries a base_url
# under each configured *arr integration (radarr.base_url,
# sonarr.base_url, ...) and those must not be touched.
sed -i "/^general:/,/^[^ ]/{ s| base_url:.*| base_url: /$slug|; }" "$CONFIG_LOCATION"
bashio::addon.restart
fi
fi

View File

@@ -1,4 +1,17 @@
## 1.36.1 (27-07-2026)
- Fix the Codex CLI install failing on every boot with `Verified Codex <version> installation failed; Codex is unavailable this boot`, leaving `install_codex_cli` permanently non-functional. The download, its SHA-256 verification, and the extraction all succeeded; the chain broke at the final step, which validates the candidate binary by running `--version` as the `abc` runtime user. `mktemp -d` creates its directory `0700 root:root`, and `abc` cannot traverse a root-only directory, so executing the staged binary failed with `unable to exec: Permission denied` (exit 126) before it could be moved into place. Reproduced and fixed by making the staging directory traversable (`chmod 0755`) immediately after `mktemp`; verified on a live add-on container, where the same probe goes from exit 126 to success once the mode is widened. Nothing secret is staged there — the public release archive and the extracted binary, both world-readable upstream artifacts — and the existing `cleanup()` trap still removes the directory on exit. The validation deliberately keeps running as `abc` rather than root, so the binary is exercised as the identity that will actually run it.
## 1.36 (27-07-2026)
- Add optional OpenAI Codex CLI support, so a Claude session in this add-on can delegate work to ChatGPT Codex. Three parts: an `install_codex_cli` switch, a browserless way to activate a ChatGPT subscription on it, and an MCP registration that makes Codex callable as a tool from Claude.
- **Install (`install_codex_cli`, default off).** Codex is deliberately *not* baked into the image: the Linux release binary is ~310 MB extracted, which is not worth carrying in every installation for an off-by-default option, and updating it would then require an add-on rebuild. The new `81-codex_cli.sh` cont-init script downloads the pinned static-musl release (`ARG`/`ENV CODEX_VERSION`, currently `0.145.0`) into `/data/codex/bin` and symlinks it onto `PATH`. `/data` is the add-on's own persistent volume — independent of the configurable `data_location` — so the ~113 MB download happens once, not per boot and not again after an add-on update unless the pinned release changes. The install prefix is outside `$HOME` on purpose: the managed-MCP merge in `82-claude_tools.sh` treats any server `command` under `$HOME` as user-installed and refuses to manage it, so an add-on-owned binary living there could never be updated or removed by the add-on. The download fails open like the Claude Desktop update check — an offline boot, a GitHub outage or a bad pin logs a warning and leaves any existing binary in place instead of aborting startup, and the new binary is validated by actually running `--version` from the staging directory before it replaces the current one, so a truncated download can never overwrite a working install. Staging deliberately happens under `/data` rather than the default `/tmp`: `/tmp` in this image is a RAM-backed tmpfs, so holding the 110 MB archive plus the 310 MB binary there during boot is a real risk on a small Home Assistant host, and it is mounted `noexec`, which would make the validation step impossible anyway. Staging on the destination filesystem also turns the final move into an atomic rename instead of a second 310 MB copy. Disabling the option is non-destructive: the binary and the completed sign-in are kept, so a disable/re-enable cycle needs neither another download nor another login.
- **Subscription activation (`codex-login`).** Codex's default sign-in serves an OAuth callback on `localhost:1455` and expects a local browser, which cannot work here — the image ships no browser, and the add-on is reached through Home Assistant ingress. The new `/usr/local/bin/codex-login` helper runs `codex login --device-auth` instead, the flow OpenAI documents for headless machines: it prints a verification URL and a short-lived one-time code to approve on any other device, then polls until done. It can be run from the desktop's xterm (openbox right-click menu), from a Claude Code session, or from the container console, and it drops to the `abc` runtime user first so `~/.codex/auth.json` is not created root-owned — the same failure mode `83-github_cli.sh` documents for `~/.gitconfig`. Output is line-buffered so the URL and code appear immediately even without a TTY. If a session is already authenticated the helper reports the account and exits rather than starting a second flow. Credentials live in the persistent home, so the sign-in survives restarts and updates.
- **MCP bridge.** `codex mcp-server` exposes Codex itself as a stdio MCP server (tools `codex` and `codex-reply`, verified by driving the 0.145.0 binary over stdio), which is registered through the *existing* managed-MCP merge in `82-claude_tools.sh` rather than a second copy of that logic — so it inherits the merge's idempotence, its refusal to clobber a user-customized entry, its removal-when-disabled behaviour, and the `0600` re-tightening of the token-bearing config files. Registration covers both Claude Code (`~/.claude.json`) and Claude Desktop, so every session type gets the tool. A managed `CLAUDE.md` block tells Claude what Codex is and when a second agent is worth the round-trip, since registering a server without guidance rarely gets it used — the same gap the Headroom block exists to close.
- **Sandbox (`codex_sandbox_mode`, default `danger-full-access`).** Codex normally confines itself with a Linux OS sandbox (Landlock, or its bundled bubblewrap), which is unreliable inside a Home Assistant add-on container — and the container is already the security boundary. The default therefore bypasses Codex's own sandbox; `read-only` and `workspace-write` remain selectable. The chosen mode is applied in two places that are kept in sync: as root-level `-c` config overrides in the MCP registration (Codex forwards those to the MCP server, and they must precede the subcommand), and as a managed block at the top of `~/.codex/config.toml` so plain terminal `codex`/`codex exec` runs behave the same. That block is written at position 0 rather than appended, because a bare TOML key placed after a `[table]` header would silently become a key *of that table*. `approval_policy` is always `never` — an MCP- or cron-driven run has nobody to answer a prompt. Claude Code's own permission prompts still gate every `mcp__codex__*` call unless `permission_mode` is `bypass`.
- `84-claude_runtime_ownership.sh` now also reconciles `~/.codex`, and `claude-tools-doctor.sh` reports the Codex binary, the installed-versus-pinned version, the two new switches, and the login status (which prints the auth mode, never the token).
## 1.35 (23-07-2026)
- Fix recurring "For your security, sign in again to keep using Claude." and the Claude app's dispatch tab showing this desktop as offline when opened from mobile first. Root cause (confirmed from `~/.config/Claude/logs/main.log` on a live install): the app launches with `--password-store=gnome-libsecret`, forcing Electron's libsecret/Secret-Service backend, but `gnome-keyring` was removed from the image in a previous commit because it prompted for a keyring password on first boot and blocked the app from launching. With the flag still forcing libsecret and no keyring daemon running, `safeStorage.isEncryptionAvailable()` is `false` — logs showed `session will not persist; app secrets fall back to plaintext` and `cannot store allowlist cache`. The un-persisted session then goes stale, failing the elevated-access OAuth check (`session_stale_relogin`) that the cowork/dispatch bridge needs, so the bridge is "parked until re-login" — which is what the Claude app surfaces as the desktop being offline, until a fresh sign-in (only completable from a computer, see `SIGN_IN.md` Problem A) un-parks it. `rootfs/defaults/autostart` now launches with `--password-store=basic` instead: Electron's built-in fixed-key store needs no daemon and never prompts, and persists under `$HOME/.config/Claude` (`/data/data`, persistent), so the session survives restarts and dispatch stays online regardless of which device connects first. A passwordless keyring was considered and rejected — it would live in the same persistent volume as the ciphertext, adding no real protection in this single-user self-hosted setup. `Dockerfile`'s stale comment (still describing gnome-keyring as installed) is corrected; the package stays removed.

View File

@@ -94,7 +94,7 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
# cannot alter executables elsewhere in the image.
COPY rootfs/ /
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" \) -print -exec chmod +x {} \; && \
\( -name "*.sh" -o -name "run" -o -name "finish" -o -name "ha-cli" -o -name "codex-login" \) -print -exec chmod +x {} \; && \
chmod +x /usr/local/bin/claude
# Uses /bin for compatibility purposes

View File

@@ -78,6 +78,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
- Custom script support through the repository standard `claude_desktop.sh`.
- Bundled optimization tools: Headroom, RTK, and TokenSave; Caveman remains
available as an opt-in plugin.
- Optional OpenAI Codex CLI, authenticated exclusively with a ChatGPT
subscription and reachable from Claude through the native Codex MCP server.
- Optional Home Assistant MCP bridge so Claude can query and control Home
Assistant.
- Independent hourly savings reports for Headroom, RTK, and TokenSave.
@@ -105,6 +107,8 @@ Git synchronization hooks. A repository is indexed only when it is listed in
| `install_tokensave` | `true` | Install TokenSave's complete global Claude integration. |
| `tokensave_project_paths` | `[]` | Explicit absolute Git repository paths to initialize or sync at startup. |
| `install_caveman` | `false` | Install the third-party Caveman Claude Code plugin at startup. |
| `install_codex_cli` | `false` | Install the latest stable OpenAI Codex CLI at startup and register its native MCP server so Claude can delegate work to ChatGPT Codex. |
| `codex_sandbox_mode` | `workspace-write` | Filesystem scope Codex runs with: `read-only`, `workspace-write`, or `danger-full-access`. |
| `enable_tools_health_report` | `true` | Write independent Headroom, RTK, and TokenSave gains to the add-on log hourly. |
| `install_github_cli` | `true` | Enable setup checks for the baked-in `git` and `gh` commands. |
| `github_token` | | Optional GitHub token used to authenticate `gh` and Git operations. |
@@ -195,6 +199,77 @@ The dashboard is disabled externally by default. To expose it:
The dashboard is unauthenticated. Do not publish this port to the public
internet.
## Codex CLI
Setting `install_codex_cli: true` adds OpenAI's Codex CLI alongside Claude and
registers `codex mcp-server` in both Claude Code and Claude Desktop. A Claude
session can therefore delegate a task to ChatGPT Codex and read its result back
through MCP.
Codex is not baked into the image because its Linux binary is large and the
feature is off by default. At each startup, the add-on resolves the latest
stable upstream release. It downloads the architecture-specific binary into
persistent `/data/codex/bin` only when the installed release is missing or
outdated, verifies the GitHub-published SHA-256 digest before extraction or
execution, validates the staged binary with `--version`, and replaces the
existing binary atomically. If release metadata or the download is unavailable,
startup continues and a previously working installation is retained.
### Signing in with a ChatGPT subscription
The add-on has no browser, so use the bundled device-code helper:
```bash
codex-login
```
Run it from the desktop's xterm, a Claude Code session, or the container
console. It prints a verification URL and one-time code that you approve on
another device. Credentials are stored in the runtime user's persistent
`~/.codex/auth.json`, so the sign-in survives restarts and add-on updates.
This integration is deliberately **subscription-only**. The managed launcher
removes any inherited `OPENAI_API_KEY` and starts every Codex command—including
`codex mcp-server`—with:
```toml
forced_login_method = "chatgpt"
cli_auth_credentials_store = "file"
```
The launcher also removes caller-provided overrides for those two keys before
starting Codex. The same values are maintained in `~/.codex/config.toml`.
Consequently, the MCP server uses the ChatGPT Codex entitlement and cannot
silently fall back to usage-based OpenAI API-key billing.
### Using Codex from Claude
Claude receives two native MCP tools:
- `mcp__codex__codex` starts a task. Pass a self-contained `prompt` and set
`cwd` to the repository Codex should inspect. The result includes a
`threadId`.
- `mcp__codex__codex-reply` continues the same Codex thread with its
`threadId`.
The add-on also installs managed Claude guidance recommending Codex for
independent review, a second diagnosis, or a competing implementation rather
than routine lookups. Codex consumption counts against the signed-in ChatGPT
plan's Codex allowance.
### Sandbox scope
`codex_sandbox_mode` defaults to `workspace-write`, allowing implementation
inside the supplied repository without granting unrestricted access to every
mounted path. Select `read-only` for review-only delegation. Use
`danger-full-access` only as an explicit fallback when Codex's nested Linux
sandbox is unavailable in the Home Assistant add-on container and the mounted
paths are trusted.
`approval_policy` is always `never`, because an MCP-driven Codex process has no
interactive operator to answer a prompt. Claude Code's own permissions still
gate the `mcp__codex__*` call unless `permission_mode` is `bypass`.
## Diagnostics
Run the following inside the add-on through a custom script or container console:
@@ -206,8 +281,8 @@ claude-tools-doctor.sh
The report checks the tool binaries, configuration switches, configured and
effective runtime identities, redacted MCP registrations, Claude hooks,
permission mode, Headroom health, TokenSave indexes, routing, and recorded
savings. It never prints MCP environment values because the Home Assistant MCP
entry can contain a long-lived token.
savings. It never prints MCP environment values or raw Codex authentication
status because either can contain credentials or masked credential fragments.
The hourly report can also be invoked manually:
@@ -288,6 +363,8 @@ Persistent state is stored in the configured `data_location` (default
shared home
- TokenSave repository indexes: `.tokensave/` inside each explicitly configured
project
- Codex authentication and configuration: `~/.codex`; the verified executable
and subscription-only launcher live in persistent `/data/codex/bin`
Volatile cache data is redirected to `/tmp/cache` through `$XDG_CACHE_HOME` and
`$HOME/.cache`.

View File

@@ -59,6 +59,8 @@ options:
headroom_auto_compress: true
headroom_wrap_claude_code: true
install_caveman: false
install_codex_cli: false
codex_sandbox_mode: workspace-write
install_github_cli: true
install_headroom: true
install_rtk: true
@@ -107,6 +109,8 @@ schema:
headroom_auto_compress: bool?
headroom_wrap_claude_code: bool
install_caveman: bool
install_codex_cli: bool
codex_sandbox_mode: list(read-only|workspace-write|danger-full-access)
install_github_cli: bool
install_headroom: bool
install_rtk: bool
@@ -118,5 +122,5 @@ slug: claude_desktop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.35"
version: "1.36.1"
video: true

View 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' 'workspace-write')"
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

View File

@@ -293,6 +293,20 @@ if $TOKENSAVE_ENABLED; then
done <<< "$TOKENSAVE_PROJECT_PATHS"
fi
# Codex CLI is installed by 81-codex_cli.sh into /data/codex/bin — deliberately outside $HOME,
# because is_managed() below treats any command under $HOME as user-installed.
CODEX_BIN="/data/codex/bin/codex"
CODEX_ENABLED=false
CODEX_SANDBOX_MODE="$(bashio::config 'codex_sandbox_mode' 'workspace-write')"
if bashio::config.true 'install_codex_cli'; then
if [ -x "$CODEX_BIN" ]; then
CODEX_ENABLED=true
bashio::log.info "codex $("$CODEX_BIN" --version 2> /dev/null || true) available; registering the codex MCP server (sandbox: ${CODEX_SANDBOX_MODE})"
else
bashio::log.warning "codex is not available"
fi
fi
HA_MCP_ENABLED=false
HA_MCP_URL=""
HA_MCP_TOKEN=""
@@ -314,6 +328,7 @@ fi
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
HEADROOM_HF_HOME="${HOME}/.headroom/hf" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
CODEX_ENABLED="$CODEX_ENABLED" CODEX_BIN="$CODEX_BIN" CODEX_SANDBOX_MODE="$CODEX_SANDBOX_MODE" \
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
@@ -326,6 +341,7 @@ MANAGED_BASENAMES = {
"headroom": "headroom",
"tokensave": "tokensave",
"homeassistant": "mcp-proxy",
"codex": "codex",
}
desired = {}
@@ -341,6 +357,25 @@ if os.environ["HEADROOM_ENABLED"] == "true":
}
if os.environ["TOKENSAVE_ENABLED"] == "true":
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
if os.environ["CODEX_ENABLED"] == "true":
# `codex mcp-server` exposes Codex itself as an stdio MCP server (tools: codex, codex-reply),
# which is what lets a Claude session hand a task to ChatGPT Codex. The sandbox/approval
# policy is pinned with root-level `-c` overrides, which Codex forwards to the MCP server;
# they must precede the subcommand. approval_policy is always "never" because an MCP-driven
# run has nobody to answer a prompt. The sandbox defaults to workspace-write; users can opt
# into danger-full-access explicitly if the nested sandbox is unavailable in their container.
# 81-codex_cli.sh writes the same values into ~/.codex/config.toml so plain terminal `codex`
# runs behave identically.
desired["codex"] = {
"command": os.environ["CODEX_BIN"],
"args": [
"-c",
f'sandbox_mode="{os.environ["CODEX_SANDBOX_MODE"]}"',
"-c",
'approval_policy="never"',
"mcp-server",
],
}
if os.environ["HA_MCP_ENABLED"] == "true":
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
# mcp-proxy defaults to SSE, so the transport flags are required.
@@ -531,6 +566,32 @@ else
manage_claude_md_block ha-api-helper remove
fi
# Registering the MCP server is not enough on its own: without guidance the model rarely reaches
# for a second agent, the same gap the Headroom block above exists to close.
if $CODEX_ENABLED; then
manage_claude_md_block codex add <<'MD'
## Delegating to ChatGPT Codex
The `codex` MCP server runs OpenAI's Codex agent locally, signed in with the user's ChatGPT
subscription. It is a genuinely independent second agent — a different model family, reading the
files itself — not a search tool. It is slow and costs the user's ChatGPT quota, so use it when a
second opinion is worth minutes, not for routine lookups.
Good uses: an independent review of a design or a risky change before it lands; a second
diagnosis of a bug you have a theory about but cannot confirm; a competing implementation of a
self-contained piece you can then compare against your own.
Call `mcp__codex__codex` with `prompt` and always set `cwd` to the repository being discussed —
Codex reads the files itself, so it needs the right working directory and enough context in the
prompt to act without seeing this conversation. Continue an exchange with
`mcp__codex__codex-reply` (note the hyphen) using the `threadId` it returned, rather than
starting a fresh `codex` call. Treat its answers as a peer's opinion: verify claims about this
codebase before acting on them.
MD
else
manage_claude_md_block codex remove
fi
if bashio::config.true 'install_rtk'; then
if command -v rtk &> /dev/null; then
bashio::log.info "Configuring rtk Claude Code integration"

View File

@@ -2,14 +2,22 @@
# shellcheck shell=bash
set -e
# Earlier configuration scripts intentionally run as root. 20-folders.sh remapped abc to the
# effective runtime identity (never root in bypass mode, where Claude Code refuses to run as
# root). Reconcile ownership with that identity after all Claude configuration writes are
# complete, as a safety net in case any intermediate step re-owned a managed path.
# Earlier configuration scripts intentionally run as root. Reconcile the paths written by those
# scripts with the final abc runtime identity and its configured persistent home.
RUNTIME_UID="$(id -u abc)"
RUNTIME_GID="$(id -g abc)"
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
for managed_path in "$HOME/.claude" "$HOME/.claude.json" "$HOME/.config/Claude"; do
if [ -z "$RUNTIME_HOME" ]; then
bashio::log.warning "Unable to resolve the abc runtime home; skipping runtime ownership reconciliation"
exit 0
fi
for managed_path in \
"$RUNTIME_HOME/.claude" \
"$RUNTIME_HOME/.claude.json" \
"$RUNTIME_HOME/.config/Claude" \
"$RUNTIME_HOME/.codex"; do
if [ -e "$managed_path" ]; then
chown -R -- "${RUNTIME_UID}:${RUNTIME_GID}" "$managed_path" \
|| bashio::log.warning "Unable to set effective runtime ownership on $managed_path"

View File

@@ -1,18 +1,23 @@
#!/usr/bin/with-contenv bashio
# Diagnose installation, registration, routing, indexing, permissions, and recorded savings without
# printing MCP environment values (which may contain the Home Assistant access token).
# printing MCP environment values or authentication material.
# shellcheck shell=bash
set +e
set -o pipefail
export NO_COLOR=1
export PATH="/lsiopy/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
RUNTIME_HOME="$(getent passwd abc | cut -d: -f6)"
if [ -z "$RUNTIME_HOME" ]; then
RUNTIME_HOME="/data/data"
fi
section() {
printf '\n=== %s ===\n' "$1"
}
section "Installed binaries"
for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck yamllint hadolint actionlint; do
for tool in claude claude-desktop headroom rtk tokensave codex git gh rg jq shellcheck yamllint hadolint actionlint; do
resolved="$(command -v "$tool" 2> /dev/null || true)"
if [ -n "$resolved" ]; then
printf '%-16s %s\n' "$tool" "$resolved"
@@ -22,13 +27,14 @@ for tool in claude claude-desktop headroom rtk tokensave git gh rg jq shellcheck
done
section "Configured switches"
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_caveman enable_tools_health_report; do
for option in permission_mode install_headroom headroom_wrap_claude_code expose_headroom_dashboard install_rtk install_tokensave install_codex_cli codex_sandbox_mode install_caveman enable_tools_health_report; do
printf '%-30s %s\n' "$option" "$(bashio::config "$option")"
done
section "Runtime identity"
printf '%-30s %s\n' "configured PUID:PGID" "$(bashio::config 'PUID'):$(bashio::config 'PGID')"
printf '%-30s %s\n' "effective abc UID:GID" "$(id -u abc):$(id -g abc)"
printf '%-30s %s\n' "abc runtime home" "$RUNTIME_HOME"
printf '%-30s %s\n' "current process UID:GID" "$(id -u):$(id -g)"
if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
if [ "$(id -u abc)" -eq 0 ]; then
@@ -39,11 +45,12 @@ if [ "$(bashio::config 'permission_mode')" = "bypass" ]; then
fi
section "Claude Code permission state"
python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
import json
import os
from pathlib import Path
path = Path.home() / ".claude/settings.json"
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
try:
data = json.loads(path.read_text())
except FileNotFoundError:
@@ -59,13 +66,15 @@ else:
PY
section "MCP registrations (environment values redacted)"
python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
import json
import os
from pathlib import Path
home = Path(os.environ["RUNTIME_HOME"])
paths = [
Path.home() / ".claude.json",
Path.home() / ".config/Claude/claude_desktop_config.json",
home / ".claude.json",
home / ".config/Claude/claude_desktop_config.json",
]
for path in paths:
print(path)
@@ -95,11 +104,12 @@ for path in paths:
PY
section "Claude Code hooks"
python3 - <<'PY'
RUNTIME_HOME="$RUNTIME_HOME" python3 - <<'PY'
import json
import os
from pathlib import Path
path = Path.home() / ".claude/settings.json"
path = Path(os.environ["RUNTIME_HOME"]) / ".claude/settings.json"
try:
data = json.loads(path.read_text())
except FileNotFoundError:
@@ -148,18 +158,17 @@ section "TokenSave"
if bashio::config.true 'install_tokensave'; then
tokensave doctor --agent claude || true
tokensave gain --all --range 30d || true
# Capture before looping — see the matching comment in 82-claude_tools.sh: feeding the
# loop straight from `< <(bashio::config ...)` yields an empty list under errexit.
# Capture before looping — see the matching comment in 82-claude_tools.sh.
TOKENSAVE_PROJECT_PATHS="$(bashio::config 'tokensave_project_paths')"
while IFS= read -r configured_path || [ -n "$configured_path" ]; do
if [ -z "$configured_path" ] || [ "$configured_path" = "null" ]; then
continue
fi
repo_root="$(s6-setuidgid abc env HOME="$HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
repo_root="$(s6-setuidgid abc env HOME="$RUNTIME_HOME" git -c safe.directory='*' -C "$configured_path" rev-parse --show-toplevel 2> /dev/null || true)"
if [ -z "$repo_root" ]; then
echo "${configured_path}: not a Git repository"
elif [ -f "$repo_root/.tokensave/tokensave.db" ]; then
s6-setuidgid abc env HOME="$HOME" tokensave status "$repo_root" --short || true
s6-setuidgid abc env HOME="$RUNTIME_HOME" tokensave status "$repo_root" --short || true
else
echo "${repo_root}: NOT INITIALIZED"
fi
@@ -168,6 +177,45 @@ else
echo "disabled"
fi
section "Codex"
if bashio::config.true 'install_codex_cli'; then
codex_bin="/data/codex/bin/codex"
if [ -x "$codex_bin" ]; then
printf '%-30s %s\n' "installed" "$("$codex_bin" --version 2> /dev/null || echo 'FAILED TO RUN')"
printf '%-30s %s\n' "installed version stamp" "$(cat /data/codex/bin/.version 2> /dev/null || echo 'MISSING')"
printf '%-30s %s\n' "release policy" "latest stable, SHA-256 verified"
printf '%-30s %s\n' "authentication policy" "ChatGPT subscription only"
# Never forward raw `login status` output: non-ChatGPT modes can include masked secret
# fragments. Only print explicitly allow-listed states.
codex_status="$(
s6-setuidgid abc env -u OPENAI_API_KEY \
HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" \
"$codex_bin" login status 2>&1
)"
codex_status_rc=$?
case "$codex_status" in
*"Logged in using ChatGPT"*)
echo "Logged in using ChatGPT"
;;
*"Not logged in"*)
echo "Not logged in; run 'codex-login' to activate a ChatGPT subscription"
;;
*)
if [ "$codex_status_rc" -eq 0 ]; then
echo "Authenticated with a non-ChatGPT method; run 'codex-login' to enforce subscription authentication"
else
echo "Unable to determine Codex login status safely; run 'codex-login'"
fi
;;
esac
else
echo "enabled but ${codex_bin} is MISSING (download failed or add-on not yet restarted)"
fi
else
echo "disabled"
fi
section "Claude routing"
printf 'PATH claude: %s\n' "$(command -v claude 2> /dev/null || true)"
printf 'real claude: %s\n' "$([ -x /usr/bin/claude ] && echo /usr/bin/claude || echo MISSING)"

View 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 "$@"

View File

@@ -0,0 +1,3 @@
## 2.43.0 (2026-07-27)
- Initial release: Portainer Business Edition add-on, based on the Portainer (CE) add-on (#873)
- Ships the `portainer/portainer-ee` binary and web assets; enter a free Business Edition license key in the web UI to unlock BE features (changelog: https://github.com/portainer/portainer/releases)

37
portainer_be/DOCS.md Normal file
View File

@@ -0,0 +1,37 @@
Portainer can be used to execute custom commands in a docker container. It is a lightweight management UI which allows you to easily manage a Docker host(s) or Docker swarm clusters.
This is the **Business Edition** variant, shipping the `portainer/portainer-ee` build. Business Edition is free for up to 3 nodes with a license key obtained by registering at <https://www.portainer.io/take-3>. Enter that key in the web UI on first launch; without one it runs a time-limited trial. All add-on options and behaviour are otherwise identical to the Community Edition add-on.
# Quick start
- Add my repository using this link
[![Add repository on my Home Assistant][repository-badge]][repository-url]
- Install the Portainer Business Edition addon from my repo
- On first launch, open the web UI and enter your Business Edition license key
- In the configuration panel of the addon, you can change the password
- In the main page of the addon, disable "Protection mode", then start the addon
- Login (default name is `admin`, default password is `homeassistant`)
- Click on `Primary` in the environment (at the center of the page)
- Click on `Containers` in the left menu bar
- Increase the number of items per page to see all your addons
- Click on the symbol `>_` next to the name of your selected addon to open the console page
- Either change the username, or more usually just click connect
- Type your commands, you have full access to the terminal of this specific container (this does not affect other parts of your HA system)
# Impact on your system
- There is no impact of installing, or running portainer
- Installing manually a custom container will modify your HA status to an unsupported/unhealthy state. You will be blocked from upgrading Home Assistant and upgrading any Add-ons you may have. Stopping this custom container will reset the normal status
# Tips and tricks
## Reset database
Just change the password in your addon options and the database will be reset
## Timeout of 60s
The addon includes a very long timeout. However, if you use another layer of proxy such as the addon nginx proxy manager, it will default to a timeout of 60s. You'll have to adapt the proxy layer to increase timeout. More details here : https://github.com/portainer/portainer/issues/2953#issuecomment-1235795256
## Further reference
- Here is a full guide on using it : https://codeopolis.com/posts/beginners-guide-to-portainer/
- Old page on the HA community forum about portainer : https://community.home-assistant.io/t/home-assistant-community-add-on-portainer
[repository-badge]: https://img.shields.io/badge/Add%20repository%20to%20my-Home%20Assistant-41BDF5?logo=home-assistant&style=for-the-badge
[repository-url]: https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons

156
portainer_be/Dockerfile Normal file
View File

@@ -0,0 +1,156 @@
#============================#
# ALEXBELGIUM'S DOCKERFILE #
#============================#
# _.------.
# _.-` ('>.-`"""-.
# '.--'` _'` _ .--.)
# -' '-.-';` `
# ' - _.' ``'--.
# '---` .-'""`
# /`
#=== Home Assistant Addon ===#
#################
# 1 Build Image #
#################
ARG BUILD_FROM=ghcr.io/hassio-addons/base/amd64:11.0.0
# Portainer Business Edition (Enterprise) upstream version.
# Business Edition has no public release tarball like CE does; its binary and
# web assets ship only inside the official portainer/portainer-ee image, so we
# pull them from there. The image is multi-arch (amd64/arm64), so buildx picks
# the variant matching the target architecture automatically.
ARG BUILD_UPSTREAM="2.43.0"
FROM portainer/portainer-ee:${BUILD_UPSTREAM} AS portainer_be
ARG BUILD_FROM
FROM ${BUILD_FROM}
##################
# 2 Modify Image #
##################
# Set S6 wait time
ENV S6_CMD_WAIT_FOR_SERVICES=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_SERVICES_GRACETIME=0
# Set shell
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Setup base
ARG BUILD_UPSTREAM
# Install the Portainer BE binary and its web assets, mirroring the CE add-on
# layout under /opt/portainer (the binary resolves ./public relative to itself,
# so keeping them side by side needs no --assets flag at runtime).
COPY --from=portainer_be /portainer /opt/portainer/portainer
COPY --from=portainer_be /public /opt/portainer/public
##################
# 3 Install apps #
##################
# Add rootfs
COPY rootfs/ /
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
# Uses /bin for compatibility purposes
# hadolint ignore=DL4005
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
# Modules
ARG MODULES="00-banner.sh 01-custom_script.sh"
# Automatic modules download
COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES="nginx"
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
################
# 4 Entrypoint #
################
# Add entrypoint
ENV S6_STAGE2_HOOK=/ha_entrypoint.sh
COPY ha_entrypoint.sh /ha_entrypoint.sh
RUN chmod 777 /ha_entrypoint.sh
# Install bashio
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
RUN chmod 777 /etc/services.d/*/*
#
#WORKDIR /
#ENTRYPOINT [ "/usr/bin/env" ]
#CMD [ "/ha_entrypoint.sh" ]
#SHELL ["/bin/bash", "-o", "pipefail", "-c"]
############
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
ENV BUILD_VERSION="${BUILD_VERSION}"
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
maintainer="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Add-ons" \
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.url="https://github.com/alexbelgium" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/main/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
#################
# 6 Healthcheck #
#################
# Avoid spamming logs
# hadolint ignore=SC2016
RUN \
# Handle Apache configuration
if [ -d /etc/apache2/sites-available ]; then \
for file in /etc/apache2/sites-*/*.conf; do \
sed -i '/<VirtualHost/a \ \n # Match requests with the custom User-Agent "HealthCheck" \n SetEnvIf User-Agent "HealthCheck" dontlog \n # Exclude matching requests from access logs \n CustomLog ${APACHE_LOG_DIR}/access.log combined env=!dontlog' "$file"; \
done; \
fi && \
\
# Handle Nginx configuration
if [ -f /etc/nginx/nginx.conf ]; then \
awk '/http \{/{print; print "map $http_user_agent $dontlog {\n default 0;\n \"~*HealthCheck\" 1;\n}\naccess_log /var/log/nginx/access.log combined if=$dontlog;"; next}1' /etc/nginx/nginx.conf > /etc/nginx/nginx.conf.new && \
mv /etc/nginx/nginx.conf.new /etc/nginx/nginx.conf; \
fi
ENV HEALTH_PORT="9000" \
HEALTH_URL="/api/system/status"
HEALTHCHECK \
--interval=5s \
--retries=5 \
--start-period=30s \
--timeout=25s \
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1

110
portainer_be/README.md Normal file
View File

@@ -0,0 +1,110 @@
# Home assistant add-on: Portainer Business Edition
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
If this add-on saves you time or makes your setup easier, I would be very grateful for your support!
[![Buy me a coffee][donation-badge]](https://www.buymeacoffee.com/alexbelgium)
[![Donate via PayPal][paypal-badge]](https://www.paypal.com/donate/?hosted_button_id=DZFULJZTP3UQA)
## Addon informations
![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fportainer_be%2Fconfig.yaml)
![Ingress](https://img.shields.io/badge/dynamic/yaml?label=Ingress&query=%24.ingress&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fportainer_be%2Fconfig.yaml)
![Arch](https://img.shields.io/badge/dynamic/yaml?color=success&label=Arch&query=%24.arch&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fportainer_be%2Fconfig.yaml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9c6cf10bdbba45ecb202d7f579b5be0e)](https://www.codacy.com/gh/alexbelgium/hassio-addons/dashboard?utm_source=github.com&utm_medium=referral&utm_content=alexbelgium/hassio-addons&utm_campaign=Badge_Grade)
[![GitHub Super-Linter](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/weekly-supelinter.yaml?label=Lint%20code%20base)](https://github.com/alexbelgium/hassio-addons/actions/workflows/weekly-supelinter.yaml)
[![Builder](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/onpush_builder.yaml?label=Builder)](https://github.com/alexbelgium/hassio-addons/actions/workflows/onpush_builder.yaml)
[donation-badge]: https://img.shields.io/badge/Buy%20me%20a%20coffee-%23d32f2f?logo=buy-me-a-coffee&style=flat&logoColor=white
[paypal-badge]: https://img.shields.io/badge/Donate%20via%20PayPal-0070BA?logo=paypal&style=flat&logoColor=white
This is the **Business Edition** variant of the [Portainer add-on](https://github.com/alexbelgium/hassio-addons/tree/master/portainer). It ships the `portainer/portainer-ee` build instead of the Community Edition. Business Edition is free for up to 3 nodes with a license key you obtain by registering at <https://www.portainer.io/take-3>; enter that key in the web UI on first launch. Without a key it runs in a time-limited trial.
Forked from : https://github.com/hassio-addons/addon-portainer
Implemented changes : Business Edition image ; update to latest versions ; ingress ; ssl ; password setting through addon option ; allow manual override
_Thanks to everyone having starred my repo! To star it click on the image below, then it will be on top right. Thanks!_
[![Stargazers repo roster for @alexbelgium/hassio-addons](https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.github/stars2.svg)](https://github.com/alexbelgium/hassio-addons/stargazers)
![downloads evolution](https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/portainer_be/stats.png)
## About
---
Portainer is an open-source lightweight management UI which allows you to
easily manage your a Docker host(s) or Docker swarm clusters.
It has never been so easy to manage Docker. Portainer provides a detailed
overview of Docker and allows you to manage containers, images, networks and
volumes.
## RESTORE BACKUP
Open the addon options and set the password to "empty". Restart the addon, it will allow to restore portainer from a backup. You need to put your backup in an accessible folder such as /share to have it mounted in the addon
## WARNING
The Portainer add-on is really powerful and gives you virtually access to
your whole system. While this add-on is created and maintained with care and
with security in mind, in the wrong or inexperienced hands,
it could damage your system.
## Installation
---
The installation of this add-on is pretty straightforward and not different in comparison to installing any other add-on.
1. Add my add-ons repository to your home assistant instance (in supervisor addons store at top right, or click button below if you have configured my HA)
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
1. Install this add-on.
1. Click the `Save` button to store your configuration.
1. Set the add-on options to your preferences
1. Start the add-on.
1. Check the logs of the add-on to see if everything went well.
1. Open the webUI and adapt the software options
## Configuration
Webui can be found at <http://homeassistant:port> or in your sidebar using Ingress.
The default username is "admin" and the password is the value you set in the add-on `password` option (default `homeassistant`).
### Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `ssl` | bool | `false` | Enable HTTPS for web interface |
| `certfile` | str | `fullchain.pem` | SSL certificate file (in `/ssl/`) |
| `keyfile` | str | `privkey.pem` | SSL private key file (in `/ssl/`) |
| `password` | str | `homeassistant` | Admin password (min 12 characters, leave blank to restore backup) |
### Example Configuration
```yaml
ssl: true
certfile: "fullchain.pem"
keyfile: "privkey.pem"
password: "your-secure-password-123"
```
### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
## Support
Create an issue on github
## Illustration
---
![illustration](https://github.com/hassio-addons/addon-portainer/raw/main/images/screenshot.png)

62
portainer_be/apparmor.txt Normal file
View File

@@ -0,0 +1,62 @@
#include <tunables/global>
profile portainer_be_addon flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
capability chown,
capability dac_override,
capability fowner,
capability setgid,
capability setuid,
capability sys_chroot,
file,
signal,
mount,
umount,
remount,
network udp,
network tcp,
network dgram,
network stream,
network inet,
network inet6,
network netlink raw,
network unix dgram,
# S6-Overlay
/init ix,
/run/{s6,s6-rc*,service}/** ix,
/package/** ix,
/command/** ix,
/run/{,**} rwk,
/dev/tty rw,
/bin/** ix,
/usr/bin/** ix,
/usr/lib/bashio/** ix,
/etc/s6/** rix,
/run/s6/** rix,
/etc/services.d/** rwix,
/etc/cont-init.d/** rwix,
/etc/cont-finish.d/** rwix,
/init rix,
/var/run/** mrwkl,
/var/run/ mrwkl,
/dev/i2c-1 mrwkl,
/dev/fuse mrwkl,
/dev/sda1 mrwkl,
/dev/sdb1 mrwkl,
/dev/nvme0 mrwkl,
/dev/nvme1 mrwkl,
/dev/mmcblk0p1 mrwkl,
# Data access
/data/** rw,
# suppress ptrace denials when using 'docker ps' or using 'ps' inside a container
ptrace (trace,read) peer=docker-default,
# docker daemon confinement requires explict allow rule for signal
signal (receive) set=(kill,term) peer=/usr/bin/docker,
}

6
portainer_be/build.json Normal file
View File

@@ -0,0 +1,6 @@
{
"build_from": {
"aarch64": "ghcr.io/hassio-addons/base/aarch64:16.0.0",
"amd64": "ghcr.io/hassio-addons/base/amd64:16.0.0"
}
}

45
portainer_be/config.yaml Normal file
View File

@@ -0,0 +1,45 @@
arch:
- aarch64
- amd64
backup_exclude:
- backups
- docker_config/cli-plugins
description: Manage your Docker environment with ease (Business Edition)
docker_api: true
hassio_api: true
image: ghcr.io/alexbelgium/portainer_be-{arch}
ingress: true
ingress_port: 1337
ingress_stream: true
init: false
map:
- addon_config:rw
- share:rw
- ssl
name: Portainer Business Edition
options:
env_vars: []
certfile: fullchain.pem
keyfile: privkey.pem
password: homeassistant
ssl: false
panel_admin: false
panel_icon: mdi:docker
ports:
8000/tcp: null
9099/tcp: 9000
ports_description:
8000/tcp: Edge Agent Api (Enable when managing remote edge agents)
9099/tcp: Web UI port
schema:
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
certfile: str
keyfile: str
password: str?
ssl: bool
slug: portainer_be
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "2.43.0"

BIN
portainer_be/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
portainer_be/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,35 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
#################
# NGINX SETTING #
#################
#declare admin_port
declare portainer_protocol=http
# Generate Ingress configuration
if bashio::config.true 'ssl'; then
bashio::config.require.ssl
portainer_protocol=https
sed -i "s|9000|9443|g" /etc/nginx/includes/upstream.conf
sed -i "s|9000|9443|g" /etc/services.d/nginx/run
sed -i "s|9099 default_server|9099 ssl|g" /etc/nginx/templates/ingress.gtpl
sed -i '8 i ssl_certificate /ssl/{{ .certfile }};' /etc/nginx/templates/ingress.gtpl
sed -i '8 i ssl_certificate_key /ssl/{{ .keyfile }};' /etc/nginx/templates/ingress.gtpl
bashio::log.info "Ssl enabled, please use https for connection"
else
sed -i '/connection_upgrade/a\proxy_set_header Origin "";' /etc/nginx/templates/ingress.gtpl
fi
bashio::var.json \
interface "$(bashio::addon.ip_address)" \
port "^$(bashio::addon.ingress_port)" \
protocol "${portainer_protocol}" \
certfile "$(bashio::config 'certfile')" \
keyfile "$(bashio::config 'keyfile')" \
ssl "^$(bashio::config 'ssl')" \
| tempio \
-template /etc/nginx/templates/ingress.gtpl \
-out /etc/nginx/servers/ingress.conf

View File

@@ -0,0 +1,8 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# Home Assistant Community Add-on: Portainer
# Runs some initializations for Portainer
# ==============================================================================
bashio::require.unprotected

View File

@@ -0,0 +1,96 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -0,0 +1,14 @@
proxy_connect_timeout 30m;
proxy_http_version 1.1;
proxy_ignore_client_abort off;
proxy_read_timeout 30m;
proxy_redirect off;
proxy_send_timeout 30m;
proxy_max_temp_file_size 0;
proxy_set_header Accept-Encoding "";
proxy_set_header Origin $http_origin;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -0,0 +1 @@
resolver 127.0.0.11 ipv6=off;

View File

@@ -0,0 +1,6 @@
root /dev/null;
server_name $hostname;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;

View File

@@ -0,0 +1,9 @@
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

View File

@@ -0,0 +1,3 @@
upstream backend {
server 127.0.0.1:9000;
}

View File

@@ -0,0 +1,56 @@
# Run nginx in foreground.
daemon off;
# This is run inside Docker.
user root;
# Pid storage location.
pid /var/run/nginx.pid;
# Set number of worker processes.
worker_processes 1;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Write error log to Hass.io add-on log.
error_log /proc/1/fd/1 error;
# Load allowed environment vars
env HASSIO_TOKEN;
# Load dynamic modules.
include /etc/nginx/modules/*.conf;
# Max num of simultaneous connections by a worker process.
events {
worker_connections 512;
}
http {
include /etc/nginx/includes/mime.types;
log_format hassio '[$time_local] $status '
'$http_x_forwarded_for($remote_addr) '
'$request ($http_user_agent)';
access_log /proc/1/fd/1 hassio;
client_max_body_size 4G;
default_type application/octet-stream;
gzip on;
keepalive_timeout 65;
sendfile on;
server_tokens off;
tcp_nodelay on;
tcp_nopush on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include /etc/nginx/includes/resolver.conf;
include /etc/nginx/includes/upstream.conf;
include /etc/nginx/servers/*.conf;
}

View File

@@ -0,0 +1 @@
Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley)

View File

@@ -0,0 +1,24 @@
server {
listen {{ .interface }}:{{ .port }} default_server;
listen {{ .interface }}:9099 default_server;
include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
client_max_body_size 0;
gzip off;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
add_header X-Frame-Options "SAMEORIGIN";
add_header Content-Security-Policy "frame-ancestors *";
location / {
proxy_pass {{ .protocol }}://backend/;
resolver 127.0.0.11 valid=180s;
# These headers must be under location section, if they moved into proxy_params.conf, even if this is valid, they won't work
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
}
}

View File

@@ -0,0 +1,8 @@
#!/usr/bin/execlineb -S0
# ==============================================================================
# Take down the S6 supervision tree when Nginx fails
# ==============================================================================
if { s6-test ${1} -ne 0 }
if { s6-test ${1} -ne 256 }
s6-svscanctl -t /var/run/s6/services

View File

@@ -0,0 +1,13 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
bashio::log.info "Waiting for port 9000 to open..."
# Wait for transmission to become available
bashio::net.wait_for 9000 localhost 900
bashio::log.info "Starting NGinx..."
exec nginx

View File

@@ -0,0 +1,9 @@
#!/usr/bin/execlineb -S0
# ==============================================================================
# Home Assistant Community Add-on: Portainer
# Take down the S6 supervision tree when Portainer fails
# ==============================================================================
if { s6-test ${1} -ne 0 }
if { s6-test ${1} -ne 256 }
s6-svscanctl -t /var/run/s6/services

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bashio
# shellcheck shell=bash
set -e
bashio::log.info "Starting Portainer..."
##################
# DEFINE OPTIONS #
##################
declare -a options
options+=(--data /data)
options+=(--bind 0.0.0.0:9000)
#options+=(--templates /opt/portainer/templates.json)
docker_socket="/var/run/docker.sock"
if [[ ! -S "$docker_socket" ]]; then
fallback_socket="/run/docker.sock"
if [[ -S "$fallback_socket" ]]; then
docker_socket="$fallback_socket"
bashio::log.info "Docker socket not found at /var/run/docker.sock, using /run/docker.sock."
else
bashio::log.error "Docker socket not found at /var/run/docker.sock or /run/docker.sock."
exit 1
fi
fi
options+=(--host "unix://${docker_socket}")
##############
# SSL CONFIG #
##############
bashio::config.require.ssl
if bashio::config.true 'ssl'; then
bashio::log.info "SSL enabled. If web UI doesn't work, disable SSL or check your certificate paths."
CERTFILE="$(bashio::config 'certfile')"
KEYFILE="$(bashio::config 'keyfile')"
options+=(--sslcert /ssl/"$CERTFILE")
options+=(--sslkey /ssl/"$KEYFILE")
bashio::log.info "... SSL activated."
fi
################
# SET PASSWORD #
################
# Set up the initial password
PASSWORD_FILE="/data/portainer_password"
HIDDEN_FILE="/data/hidden"
if ! bashio::config.has_value 'password'; then
PASSWORD="empty"
else
PASSWORD="$(bashio::config 'password')"
fi
# Check current password
CURRENTPASSWORD=""
touch "$PASSWORD_FILE"
CURRENTPASSWORD="$(cat "$PASSWORD_FILE")"
# Reset password if not first run
if bashio::fs.file_exists "$HIDDEN_FILE"; then
if [[ "$CURRENTPASSWORD" != "$PASSWORD" ]]; then
BACKUPLOCATION="/share/portainer_$(date +%m-%d-%Y)_$RANDOM.backup"
mv -f /data/portainer.db "$BACKUPLOCATION" || true
rm "$HIDDEN_FILE" || true
bashio::log.warning "... password changed, database reset. Previous version stored in $BACKUPLOCATION"
fi
fi
# Define option
echo -n "$PASSWORD" > "$PASSWORD_FILE"
if [[ "$PASSWORD" = "empty" ]]; then
bashio::log.info "... starting without predefined password."
bashio::log.warning "If this is your first boot, you have a 5 minutes time period to perform the initial set-up."
bashio::log.warning "If you don't do it, you would be faced with a 404 error and will need to restart the add-on to access the set-up page."
else
options+=(--admin-password-file "$PASSWORD_FILE")
bashio::log.info "... password set according to add-on options."
fi
###################
# HIDE CONTAINERS #
###################
# Hide Hassio containers by default, but only enforce on first run
if ! bashio::fs.file_exists "$HIDDEN_FILE"; then
options+=(--hide-label io.hass.type=supervisor)
options+=(--hide-label io.hass.type=homeassistant)
options+=(--hide-label io.hass.type=base)
options+=(--hide-label io.hass.type=core)
# options+=(--hide-label io.hass.type=addon)
options+=(--hide-label io.hass.type=audio)
options+=(--hide-label io.hass.type=cli)
options+=(--hide-label io.hass.type=dns)
options+=(--hide-label io.hass.type=multicast)
options+=(--hide-label io.hass.type=observer)
bashio::log.info "... non-addon containers hidden."
touch "$HIDDEN_FILE"
fi
####################
# LAUNCH PORTAINER #
####################
bashio::log.info "... launching Portainer."
exec /opt/portainer/portainer "${options[@]}"

View File

@@ -0,0 +1,9 @@
{
"github_exclude": "-",
"last_update": "2026-07-27",
"repository": "alexbelgium/hassio-addons",
"slug": "portainer_be",
"source": "dockerhub",
"upstream_repo": "portainer/portainer-ee",
"upstream_version": "2.43.0"
}