fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL - #446

Merged
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port
May 8, 2026
Merged

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL#446
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port

Conversation

@nickh-obol

@nickh-obolnickh-obol commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Linux-specific bugs in fresh obol stack up flows that don't surface on macOS.

1. Hermes pod CrashLoopBackOff on fresh PVC (internal/hermes/hermes.go)

The Stack's local-path-provisioner setup script chowns each new PV to 1000:1000, and KubeletInUserNamespace=true (set in internal/embed/k3d-config.yaml) silently skips the fsGroup recursive-chown that would otherwise correct it. Hermes pods run as UID 10000 and crashloop trying to mkdir /data/.hermes/home with Permission denied.

macOS Docker Desktop hides this — its bind-mount filesystem driver fakes file ownership to whoever's asking, so the mismatch never materializes. OpenClaw doesn't hit it either: it pins fsGroup: 1000, accidentally matching the host UID.

Fix: prepend an init-hermes-perms container that runs as root and chowns /data to 10000:10000. Idempotent — also self-heals existing broken PVCs on upgrade.

2. Bootstrap probe + browser URL hardcoded to :8080 (cmd/obol/bootstrap.go)

When ports 80/8080 are in use at start time, k3d remaps the loadbalancer to a random high port and the rest of obol stack up correctly surfaces it via stack.LocalIngressURL(cfg). Three other places in cmd/obol/bootstrap.go were hardcoded to :8080: the readiness probe (hung until timeout), the browser-open URL, and the "view stack interface at" hint.

Fix: reuse stack.LocalIngressURL(cfg) in all three places.

Test plan

  • go test ./internal/hermes/... ./cmd/obol/... passes
  • go build ./... clean
  • Verified end-to-end on Linux: fresh obol agent init lands the agent in Running without manual chown
  • Verified end-to-end on Linux: obol bootstrap with port 8080 occupied completes without hang; all four URL surfaces (warning / visit / browser-open / next-steps) agree on the alternate port
  • Smoke-test on macOS to confirm no regression (existing fsGroup path still works; new init container is a no-op on already-correct ownership)

@bussyjdbussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated against latest origin/main in an isolated worktree.

Checks run locally:

  • git merge --no-commit --no-ff refs/remotes/pr/446/head onto origin/main
  • git diff --check --cached
  • go test ./internal/hermes ./internal/stack ./cmd/obol
  • go build ./cmd/obol
  • go test ./...

All passed. The Hermes PVC ownership fix and bootstrap LocalIngressURL wiring look legitimate.

@bussyjd
bussyjd merged commit c066baa into mainMay 8, 2026
6 checks passed
@bussyjd
bussyjd deleted the fix/hermes-pvc-perms-and-ingress-port branch May 8, 2026 03:03
bussyjd added a commit that referenced this pull request May 15, 2026
The in-pod `init-hermes-perms` init container from #446 (c066baa) is
neutered on Linux k3d because the embedded k3d config sets
`KubeletInUserNamespace=true` (internal/embed/k3d-config.yaml). With
user-namespacing, the pod's "root" maps to a host subuid that lacks
chown authority over the host bind-mount path, so the in-pod
`chown -R 10000:10000 /data` silently no-ops. The next init container
(`init-hermes-data`) then fails with `mkdir /data/.hermes/home:
Permission denied` and the pod CrashLoopBackOffs.
local-path-provisioner's helper-pod sets the volume to 1000:1000
(internal/embed/infrastructure/base/templates/local-path.yaml), which
happens to suit OpenClaw but not Hermes (containerUID = 10000).
Fix: after `helmfile sync`, host-side chown the PVC backing dirs to
containerUID:containerGID by exec-ing into the k3d server container
via `docker exec`. That runs at the Docker daemon's real root and is
not subject to the user-namespacing that silently breaks the in-pod
attempt. The existing `fixRuntimeVolumeOwnership` helper already does
exactly this for wallet keystore paths; we wire it into the agent
deploy path via a new `ensureHermesPVCOwnership` that:
1. Waits up to 60s for each PVC (`hermes-data`, `remote-signer-
keystores`) to be Bound — local-path is WaitForFirstConsumer so
the host dir doesn't exist until the pod is scheduled.
2. Chowns each backing dir.
3. If a Hermes pod is currently stuck in Init:CrashLoopBackOff,
deletes it so kubelet recreates immediately rather than after
exponential backoff (~5 min worst case). Skips the delete when
no pod is stuck so routine syncs (e.g. `obol model sync` after
`obol model prefer`) do not gratuitously restart a healthy
agent.
Called from `hermes.Sync` after `helmfile sync` succeeds, so every
Onboard / Setup / Sync call exercises it.
Validated on spark2 (Linux ARM64, Ubuntu 24.04, NVIDIA GB10) by
reverting the PVC dirs to 1000:1000, deleting the Hermes pod, and
running `obol model sync`. Before: pod stuck in Init:CrashLoopBackOff
with "Permission denied" in init-hermes-data logs. After: PVC dirs
flip to 10000:10000 within the sync, pod reaches `Running 2/2` in
~30s with zero CrashLoopBackOff cycles.
Test:
- `TestHermesPVCPaths` pins the two host paths the helper chowns, so
renaming `hermes-data`/`remote-signer-keystores` or relocating the
namespace prefix can't silently regress the fix.
- Full chown side-effect needs a live k3d cluster and is exercised
by the spark2 validation above plus all existing integration
flows; no unit-mocked k3d test added.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickh-obol@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL - #446

Merged
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port
May 8, 2026
Merged

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL#446
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port

Conversation

@nickh-obol

@nickh-obolnickh-obol commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Linux-specific bugs in fresh obol stack up flows that don't surface on macOS.

1. Hermes pod CrashLoopBackOff on fresh PVC (internal/hermes/hermes.go)

The Stack's local-path-provisioner setup script chowns each new PV to 1000:1000, and KubeletInUserNamespace=true (set in internal/embed/k3d-config.yaml) silently skips the fsGroup recursive-chown that would otherwise correct it. Hermes pods run as UID 10000 and crashloop trying to mkdir /data/.hermes/home with Permission denied.

macOS Docker Desktop hides this — its bind-mount filesystem driver fakes file ownership to whoever's asking, so the mismatch never materializes. OpenClaw doesn't hit it either: it pins fsGroup: 1000, accidentally matching the host UID.

Fix: prepend an init-hermes-perms container that runs as root and chowns /data to 10000:10000. Idempotent — also self-heals existing broken PVCs on upgrade.

2. Bootstrap probe + browser URL hardcoded to :8080 (cmd/obol/bootstrap.go)

When ports 80/8080 are in use at start time, k3d remaps the loadbalancer to a random high port and the rest of obol stack up correctly surfaces it via stack.LocalIngressURL(cfg). Three other places in cmd/obol/bootstrap.go were hardcoded to :8080: the readiness probe (hung until timeout), the browser-open URL, and the "view stack interface at" hint.

Fix: reuse stack.LocalIngressURL(cfg) in all three places.

Test plan

  • go test ./internal/hermes/... ./cmd/obol/... passes
  • go build ./... clean
  • Verified end-to-end on Linux: fresh obol agent init lands the agent in Running without manual chown
  • Verified end-to-end on Linux: obol bootstrap with port 8080 occupied completes without hang; all four URL surfaces (warning / visit / browser-open / next-steps) agree on the alternate port
  • Smoke-test on macOS to confirm no regression (existing fsGroup path still works; new init container is a no-op on already-correct ownership)

@bussyjdbussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated against latest origin/main in an isolated worktree.

Checks run locally:

  • git merge --no-commit --no-ff refs/remotes/pr/446/head onto origin/main
  • git diff --check --cached
  • go test ./internal/hermes ./internal/stack ./cmd/obol
  • go build ./cmd/obol
  • go test ./...

All passed. The Hermes PVC ownership fix and bootstrap LocalIngressURL wiring look legitimate.

@bussyjd
bussyjd merged commit c066baa into mainMay 8, 2026
6 checks passed
@bussyjd
bussyjd deleted the fix/hermes-pvc-perms-and-ingress-port branch May 8, 2026 03:03
bussyjd added a commit that referenced this pull request May 15, 2026
The in-pod `init-hermes-perms` init container from #446 (c066baa) is
neutered on Linux k3d because the embedded k3d config sets
`KubeletInUserNamespace=true` (internal/embed/k3d-config.yaml). With
user-namespacing, the pod's "root" maps to a host subuid that lacks
chown authority over the host bind-mount path, so the in-pod
`chown -R 10000:10000 /data` silently no-ops. The next init container
(`init-hermes-data`) then fails with `mkdir /data/.hermes/home:
Permission denied` and the pod CrashLoopBackOffs.
local-path-provisioner's helper-pod sets the volume to 1000:1000
(internal/embed/infrastructure/base/templates/local-path.yaml), which
happens to suit OpenClaw but not Hermes (containerUID = 10000).
Fix: after `helmfile sync`, host-side chown the PVC backing dirs to
containerUID:containerGID by exec-ing into the k3d server container
via `docker exec`. That runs at the Docker daemon's real root and is
not subject to the user-namespacing that silently breaks the in-pod
attempt. The existing `fixRuntimeVolumeOwnership` helper already does
exactly this for wallet keystore paths; we wire it into the agent
deploy path via a new `ensureHermesPVCOwnership` that:
1. Waits up to 60s for each PVC (`hermes-data`, `remote-signer-
keystores`) to be Bound — local-path is WaitForFirstConsumer so
the host dir doesn't exist until the pod is scheduled.
2. Chowns each backing dir.
3. If a Hermes pod is currently stuck in Init:CrashLoopBackOff,
deletes it so kubelet recreates immediately rather than after
exponential backoff (~5 min worst case). Skips the delete when
no pod is stuck so routine syncs (e.g. `obol model sync` after
`obol model prefer`) do not gratuitously restart a healthy
agent.
Called from `hermes.Sync` after `helmfile sync` succeeds, so every
Onboard / Setup / Sync call exercises it.
Validated on spark2 (Linux ARM64, Ubuntu 24.04, NVIDIA GB10) by
reverting the PVC dirs to 1000:1000, deleting the Hermes pod, and
running `obol model sync`. Before: pod stuck in Init:CrashLoopBackOff
with "Permission denied" in init-hermes-data logs. After: PVC dirs
flip to 10000:10000 within the sync, pod reaches `Running 2/2` in
~30s with zero CrashLoopBackOff cycles.
Test:
- `TestHermesPVCPaths` pins the two host paths the helper chowns, so
renaming `hermes-data`/`remote-signer-keystores` or relocating the
namespace prefix can't silently regress the fix.
- Full chown side-effect needs a live k3d cluster and is exercised
by the spark2 validation above plus all existing integration
flows; no unit-mocked k3d test added.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickh-obol@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL - #446

Merged
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port
May 8, 2026
Merged

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL#446
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port

Conversation

@nickh-obol

@nickh-obolnickh-obol commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Linux-specific bugs in fresh obol stack up flows that don't surface on macOS.

1. Hermes pod CrashLoopBackOff on fresh PVC (internal/hermes/hermes.go)

The Stack's local-path-provisioner setup script chowns each new PV to 1000:1000, and KubeletInUserNamespace=true (set in internal/embed/k3d-config.yaml) silently skips the fsGroup recursive-chown that would otherwise correct it. Hermes pods run as UID 10000 and crashloop trying to mkdir /data/.hermes/home with Permission denied.

macOS Docker Desktop hides this — its bind-mount filesystem driver fakes file ownership to whoever's asking, so the mismatch never materializes. OpenClaw doesn't hit it either: it pins fsGroup: 1000, accidentally matching the host UID.

Fix: prepend an init-hermes-perms container that runs as root and chowns /data to 10000:10000. Idempotent — also self-heals existing broken PVCs on upgrade.

2. Bootstrap probe + browser URL hardcoded to :8080 (cmd/obol/bootstrap.go)

When ports 80/8080 are in use at start time, k3d remaps the loadbalancer to a random high port and the rest of obol stack up correctly surfaces it via stack.LocalIngressURL(cfg). Three other places in cmd/obol/bootstrap.go were hardcoded to :8080: the readiness probe (hung until timeout), the browser-open URL, and the "view stack interface at" hint.

Fix: reuse stack.LocalIngressURL(cfg) in all three places.

Test plan

  • go test ./internal/hermes/... ./cmd/obol/... passes
  • go build ./... clean
  • Verified end-to-end on Linux: fresh obol agent init lands the agent in Running without manual chown
  • Verified end-to-end on Linux: obol bootstrap with port 8080 occupied completes without hang; all four URL surfaces (warning / visit / browser-open / next-steps) agree on the alternate port
  • Smoke-test on macOS to confirm no regression (existing fsGroup path still works; new init container is a no-op on already-correct ownership)

@bussyjdbussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated against latest origin/main in an isolated worktree.

Checks run locally:

  • git merge --no-commit --no-ff refs/remotes/pr/446/head onto origin/main
  • git diff --check --cached
  • go test ./internal/hermes ./internal/stack ./cmd/obol
  • go build ./cmd/obol
  • go test ./...

All passed. The Hermes PVC ownership fix and bootstrap LocalIngressURL wiring look legitimate.

@bussyjd
bussyjd merged commit c066baa into mainMay 8, 2026
6 checks passed
@bussyjd
bussyjd deleted the fix/hermes-pvc-perms-and-ingress-port branch May 8, 2026 03:03
bussyjd added a commit that referenced this pull request May 15, 2026
The in-pod `init-hermes-perms` init container from #446 (c066baa) is
neutered on Linux k3d because the embedded k3d config sets
`KubeletInUserNamespace=true` (internal/embed/k3d-config.yaml). With
user-namespacing, the pod's "root" maps to a host subuid that lacks
chown authority over the host bind-mount path, so the in-pod
`chown -R 10000:10000 /data` silently no-ops. The next init container
(`init-hermes-data`) then fails with `mkdir /data/.hermes/home:
Permission denied` and the pod CrashLoopBackOffs.
local-path-provisioner's helper-pod sets the volume to 1000:1000
(internal/embed/infrastructure/base/templates/local-path.yaml), which
happens to suit OpenClaw but not Hermes (containerUID = 10000).
Fix: after `helmfile sync`, host-side chown the PVC backing dirs to
containerUID:containerGID by exec-ing into the k3d server container
via `docker exec`. That runs at the Docker daemon's real root and is
not subject to the user-namespacing that silently breaks the in-pod
attempt. The existing `fixRuntimeVolumeOwnership` helper already does
exactly this for wallet keystore paths; we wire it into the agent
deploy path via a new `ensureHermesPVCOwnership` that:
1. Waits up to 60s for each PVC (`hermes-data`, `remote-signer-
keystores`) to be Bound — local-path is WaitForFirstConsumer so
the host dir doesn't exist until the pod is scheduled.
2. Chowns each backing dir.
3. If a Hermes pod is currently stuck in Init:CrashLoopBackOff,
deletes it so kubelet recreates immediately rather than after
exponential backoff (~5 min worst case). Skips the delete when
no pod is stuck so routine syncs (e.g. `obol model sync` after
`obol model prefer`) do not gratuitously restart a healthy
agent.
Called from `hermes.Sync` after `helmfile sync` succeeds, so every
Onboard / Setup / Sync call exercises it.
Validated on spark2 (Linux ARM64, Ubuntu 24.04, NVIDIA GB10) by
reverting the PVC dirs to 1000:1000, deleting the Hermes pod, and
running `obol model sync`. Before: pod stuck in Init:CrashLoopBackOff
with "Permission denied" in init-hermes-data logs. After: PVC dirs
flip to 10000:10000 within the sync, pod reaches `Running 2/2` in
~30s with zero CrashLoopBackOff cycles.
Test:
- `TestHermesPVCPaths` pins the two host paths the helper chowns, so
renaming `hermes-data`/`remote-signer-keystores` or relocating the
namespace prefix can't silently regress the fix.
- Full chown side-effect needs a live k3d cluster and is exercised
by the spark2 validation above plus all existing integration
flows; no unit-mocked k3d test added.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickh-obol@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL - #446

Merged
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port
May 8, 2026
Merged

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL#446
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port

Conversation

@nickh-obol

@nickh-obolnickh-obol commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Linux-specific bugs in fresh obol stack up flows that don't surface on macOS.

1. Hermes pod CrashLoopBackOff on fresh PVC (internal/hermes/hermes.go)

The Stack's local-path-provisioner setup script chowns each new PV to 1000:1000, and KubeletInUserNamespace=true (set in internal/embed/k3d-config.yaml) silently skips the fsGroup recursive-chown that would otherwise correct it. Hermes pods run as UID 10000 and crashloop trying to mkdir /data/.hermes/home with Permission denied.

macOS Docker Desktop hides this — its bind-mount filesystem driver fakes file ownership to whoever's asking, so the mismatch never materializes. OpenClaw doesn't hit it either: it pins fsGroup: 1000, accidentally matching the host UID.

Fix: prepend an init-hermes-perms container that runs as root and chowns /data to 10000:10000. Idempotent — also self-heals existing broken PVCs on upgrade.

2. Bootstrap probe + browser URL hardcoded to :8080 (cmd/obol/bootstrap.go)

When ports 80/8080 are in use at start time, k3d remaps the loadbalancer to a random high port and the rest of obol stack up correctly surfaces it via stack.LocalIngressURL(cfg). Three other places in cmd/obol/bootstrap.go were hardcoded to :8080: the readiness probe (hung until timeout), the browser-open URL, and the "view stack interface at" hint.

Fix: reuse stack.LocalIngressURL(cfg) in all three places.

Test plan

  • go test ./internal/hermes/... ./cmd/obol/... passes
  • go build ./... clean
  • Verified end-to-end on Linux: fresh obol agent init lands the agent in Running without manual chown
  • Verified end-to-end on Linux: obol bootstrap with port 8080 occupied completes without hang; all four URL surfaces (warning / visit / browser-open / next-steps) agree on the alternate port
  • Smoke-test on macOS to confirm no regression (existing fsGroup path still works; new init container is a no-op on already-correct ownership)

@bussyjdbussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated against latest origin/main in an isolated worktree.

Checks run locally:

  • git merge --no-commit --no-ff refs/remotes/pr/446/head onto origin/main
  • git diff --check --cached
  • go test ./internal/hermes ./internal/stack ./cmd/obol
  • go build ./cmd/obol
  • go test ./...

All passed. The Hermes PVC ownership fix and bootstrap LocalIngressURL wiring look legitimate.

@bussyjd
bussyjd merged commit c066baa into mainMay 8, 2026
6 checks passed
@bussyjd
bussyjd deleted the fix/hermes-pvc-perms-and-ingress-port branch May 8, 2026 03:03
bussyjd added a commit that referenced this pull request May 15, 2026
The in-pod `init-hermes-perms` init container from #446 (c066baa) is
neutered on Linux k3d because the embedded k3d config sets
`KubeletInUserNamespace=true` (internal/embed/k3d-config.yaml). With
user-namespacing, the pod's "root" maps to a host subuid that lacks
chown authority over the host bind-mount path, so the in-pod
`chown -R 10000:10000 /data` silently no-ops. The next init container
(`init-hermes-data`) then fails with `mkdir /data/.hermes/home:
Permission denied` and the pod CrashLoopBackOffs.
local-path-provisioner's helper-pod sets the volume to 1000:1000
(internal/embed/infrastructure/base/templates/local-path.yaml), which
happens to suit OpenClaw but not Hermes (containerUID = 10000).
Fix: after `helmfile sync`, host-side chown the PVC backing dirs to
containerUID:containerGID by exec-ing into the k3d server container
via `docker exec`. That runs at the Docker daemon's real root and is
not subject to the user-namespacing that silently breaks the in-pod
attempt. The existing `fixRuntimeVolumeOwnership` helper already does
exactly this for wallet keystore paths; we wire it into the agent
deploy path via a new `ensureHermesPVCOwnership` that:
1. Waits up to 60s for each PVC (`hermes-data`, `remote-signer-
keystores`) to be Bound — local-path is WaitForFirstConsumer so
the host dir doesn't exist until the pod is scheduled.
2. Chowns each backing dir.
3. If a Hermes pod is currently stuck in Init:CrashLoopBackOff,
deletes it so kubelet recreates immediately rather than after
exponential backoff (~5 min worst case). Skips the delete when
no pod is stuck so routine syncs (e.g. `obol model sync` after
`obol model prefer`) do not gratuitously restart a healthy
agent.
Called from `hermes.Sync` after `helmfile sync` succeeds, so every
Onboard / Setup / Sync call exercises it.
Validated on spark2 (Linux ARM64, Ubuntu 24.04, NVIDIA GB10) by
reverting the PVC dirs to 1000:1000, deleting the Hermes pod, and
running `obol model sync`. Before: pod stuck in Init:CrashLoopBackOff
with "Permission denied" in init-hermes-data logs. After: PVC dirs
flip to 10000:10000 within the sync, pod reaches `Running 2/2` in
~30s with zero CrashLoopBackOff cycles.
Test:
- `TestHermesPVCPaths` pins the two host paths the helper chowns, so
renaming `hermes-data`/`remote-signer-keystores` or relocating the
namespace prefix can't silently regress the fix.
- Full chown side-effect needs a live k3d cluster and is exercised
by the spark2 validation above plus all existing integration
flows; no unit-mocked k3d test added.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickh-obol@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL - #446

Merged
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port
May 8, 2026
Merged

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL#446
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port

Conversation

@nickh-obol

@nickh-obolnickh-obol commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Linux-specific bugs in fresh obol stack up flows that don't surface on macOS.

1. Hermes pod CrashLoopBackOff on fresh PVC (internal/hermes/hermes.go)

The Stack's local-path-provisioner setup script chowns each new PV to 1000:1000, and KubeletInUserNamespace=true (set in internal/embed/k3d-config.yaml) silently skips the fsGroup recursive-chown that would otherwise correct it. Hermes pods run as UID 10000 and crashloop trying to mkdir /data/.hermes/home with Permission denied.

macOS Docker Desktop hides this — its bind-mount filesystem driver fakes file ownership to whoever's asking, so the mismatch never materializes. OpenClaw doesn't hit it either: it pins fsGroup: 1000, accidentally matching the host UID.

Fix: prepend an init-hermes-perms container that runs as root and chowns /data to 10000:10000. Idempotent — also self-heals existing broken PVCs on upgrade.

2. Bootstrap probe + browser URL hardcoded to :8080 (cmd/obol/bootstrap.go)

When ports 80/8080 are in use at start time, k3d remaps the loadbalancer to a random high port and the rest of obol stack up correctly surfaces it via stack.LocalIngressURL(cfg). Three other places in cmd/obol/bootstrap.go were hardcoded to :8080: the readiness probe (hung until timeout), the browser-open URL, and the "view stack interface at" hint.

Fix: reuse stack.LocalIngressURL(cfg) in all three places.

Test plan

  • go test ./internal/hermes/... ./cmd/obol/... passes
  • go build ./... clean
  • Verified end-to-end on Linux: fresh obol agent init lands the agent in Running without manual chown
  • Verified end-to-end on Linux: obol bootstrap with port 8080 occupied completes without hang; all four URL surfaces (warning / visit / browser-open / next-steps) agree on the alternate port
  • Smoke-test on macOS to confirm no regression (existing fsGroup path still works; new init container is a no-op on already-correct ownership)

@bussyjdbussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated against latest origin/main in an isolated worktree.

Checks run locally:

  • git merge --no-commit --no-ff refs/remotes/pr/446/head onto origin/main
  • git diff --check --cached
  • go test ./internal/hermes ./internal/stack ./cmd/obol
  • go build ./cmd/obol
  • go test ./...

All passed. The Hermes PVC ownership fix and bootstrap LocalIngressURL wiring look legitimate.

@bussyjd
bussyjd merged commit c066baa into mainMay 8, 2026
6 checks passed
@bussyjd
bussyjd deleted the fix/hermes-pvc-perms-and-ingress-port branch May 8, 2026 03:03
bussyjd added a commit that referenced this pull request May 15, 2026
The in-pod `init-hermes-perms` init container from #446 (c066baa) is
neutered on Linux k3d because the embedded k3d config sets
`KubeletInUserNamespace=true` (internal/embed/k3d-config.yaml). With
user-namespacing, the pod's "root" maps to a host subuid that lacks
chown authority over the host bind-mount path, so the in-pod
`chown -R 10000:10000 /data` silently no-ops. The next init container
(`init-hermes-data`) then fails with `mkdir /data/.hermes/home:
Permission denied` and the pod CrashLoopBackOffs.
local-path-provisioner's helper-pod sets the volume to 1000:1000
(internal/embed/infrastructure/base/templates/local-path.yaml), which
happens to suit OpenClaw but not Hermes (containerUID = 10000).
Fix: after `helmfile sync`, host-side chown the PVC backing dirs to
containerUID:containerGID by exec-ing into the k3d server container
via `docker exec`. That runs at the Docker daemon's real root and is
not subject to the user-namespacing that silently breaks the in-pod
attempt. The existing `fixRuntimeVolumeOwnership` helper already does
exactly this for wallet keystore paths; we wire it into the agent
deploy path via a new `ensureHermesPVCOwnership` that:
1. Waits up to 60s for each PVC (`hermes-data`, `remote-signer-
keystores`) to be Bound — local-path is WaitForFirstConsumer so
the host dir doesn't exist until the pod is scheduled.
2. Chowns each backing dir.
3. If a Hermes pod is currently stuck in Init:CrashLoopBackOff,
deletes it so kubelet recreates immediately rather than after
exponential backoff (~5 min worst case). Skips the delete when
no pod is stuck so routine syncs (e.g. `obol model sync` after
`obol model prefer`) do not gratuitously restart a healthy
agent.
Called from `hermes.Sync` after `helmfile sync` succeeds, so every
Onboard / Setup / Sync call exercises it.
Validated on spark2 (Linux ARM64, Ubuntu 24.04, NVIDIA GB10) by
reverting the PVC dirs to 1000:1000, deleting the Hermes pod, and
running `obol model sync`. Before: pod stuck in Init:CrashLoopBackOff
with "Permission denied" in init-hermes-data logs. After: PVC dirs
flip to 10000:10000 within the sync, pod reaches `Running 2/2` in
~30s with zero CrashLoopBackOff cycles.
Test:
- `TestHermesPVCPaths` pins the two host paths the helper chowns, so
renaming `hermes-data`/`remote-signer-keystores` or relocating the
namespace prefix can't silently regress the fix.
- Full chown side-effect needs a live k3d cluster and is exercised
by the spark2 validation above plus all existing integration
flows; no unit-mocked k3d test added.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickh-obol@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL - #446

Merged
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port
May 8, 2026
Merged

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL#446
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port

Conversation

@nickh-obol

@nickh-obolnickh-obol commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Linux-specific bugs in fresh obol stack up flows that don't surface on macOS.

1. Hermes pod CrashLoopBackOff on fresh PVC (internal/hermes/hermes.go)

The Stack's local-path-provisioner setup script chowns each new PV to 1000:1000, and KubeletInUserNamespace=true (set in internal/embed/k3d-config.yaml) silently skips the fsGroup recursive-chown that would otherwise correct it. Hermes pods run as UID 10000 and crashloop trying to mkdir /data/.hermes/home with Permission denied.

macOS Docker Desktop hides this — its bind-mount filesystem driver fakes file ownership to whoever's asking, so the mismatch never materializes. OpenClaw doesn't hit it either: it pins fsGroup: 1000, accidentally matching the host UID.

Fix: prepend an init-hermes-perms container that runs as root and chowns /data to 10000:10000. Idempotent — also self-heals existing broken PVCs on upgrade.

2. Bootstrap probe + browser URL hardcoded to :8080 (cmd/obol/bootstrap.go)

When ports 80/8080 are in use at start time, k3d remaps the loadbalancer to a random high port and the rest of obol stack up correctly surfaces it via stack.LocalIngressURL(cfg). Three other places in cmd/obol/bootstrap.go were hardcoded to :8080: the readiness probe (hung until timeout), the browser-open URL, and the "view stack interface at" hint.

Fix: reuse stack.LocalIngressURL(cfg) in all three places.

Test plan

  • go test ./internal/hermes/... ./cmd/obol/... passes
  • go build ./... clean
  • Verified end-to-end on Linux: fresh obol agent init lands the agent in Running without manual chown
  • Verified end-to-end on Linux: obol bootstrap with port 8080 occupied completes without hang; all four URL surfaces (warning / visit / browser-open / next-steps) agree on the alternate port
  • Smoke-test on macOS to confirm no regression (existing fsGroup path still works; new init container is a no-op on already-correct ownership)

@bussyjdbussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated against latest origin/main in an isolated worktree.

Checks run locally:

  • git merge --no-commit --no-ff refs/remotes/pr/446/head onto origin/main
  • git diff --check --cached
  • go test ./internal/hermes ./internal/stack ./cmd/obol
  • go build ./cmd/obol
  • go test ./...

All passed. The Hermes PVC ownership fix and bootstrap LocalIngressURL wiring look legitimate.

@bussyjd
bussyjd merged commit c066baa into mainMay 8, 2026
6 checks passed
@bussyjd
bussyjd deleted the fix/hermes-pvc-perms-and-ingress-port branch May 8, 2026 03:03
bussyjd added a commit that referenced this pull request May 15, 2026
The in-pod `init-hermes-perms` init container from #446 (c066baa) is
neutered on Linux k3d because the embedded k3d config sets
`KubeletInUserNamespace=true` (internal/embed/k3d-config.yaml). With
user-namespacing, the pod's "root" maps to a host subuid that lacks
chown authority over the host bind-mount path, so the in-pod
`chown -R 10000:10000 /data` silently no-ops. The next init container
(`init-hermes-data`) then fails with `mkdir /data/.hermes/home:
Permission denied` and the pod CrashLoopBackOffs.
local-path-provisioner's helper-pod sets the volume to 1000:1000
(internal/embed/infrastructure/base/templates/local-path.yaml), which
happens to suit OpenClaw but not Hermes (containerUID = 10000).
Fix: after `helmfile sync`, host-side chown the PVC backing dirs to
containerUID:containerGID by exec-ing into the k3d server container
via `docker exec`. That runs at the Docker daemon's real root and is
not subject to the user-namespacing that silently breaks the in-pod
attempt. The existing `fixRuntimeVolumeOwnership` helper already does
exactly this for wallet keystore paths; we wire it into the agent
deploy path via a new `ensureHermesPVCOwnership` that:
1. Waits up to 60s for each PVC (`hermes-data`, `remote-signer-
keystores`) to be Bound — local-path is WaitForFirstConsumer so
the host dir doesn't exist until the pod is scheduled.
2. Chowns each backing dir.
3. If a Hermes pod is currently stuck in Init:CrashLoopBackOff,
deletes it so kubelet recreates immediately rather than after
exponential backoff (~5 min worst case). Skips the delete when
no pod is stuck so routine syncs (e.g. `obol model sync` after
`obol model prefer`) do not gratuitously restart a healthy
agent.
Called from `hermes.Sync` after `helmfile sync` succeeds, so every
Onboard / Setup / Sync call exercises it.
Validated on spark2 (Linux ARM64, Ubuntu 24.04, NVIDIA GB10) by
reverting the PVC dirs to 1000:1000, deleting the Hermes pod, and
running `obol model sync`. Before: pod stuck in Init:CrashLoopBackOff
with "Permission denied" in init-hermes-data logs. After: PVC dirs
flip to 10000:10000 within the sync, pod reaches `Running 2/2` in
~30s with zero CrashLoopBackOff cycles.
Test:
- `TestHermesPVCPaths` pins the two host paths the helper chowns, so
renaming `hermes-data`/`remote-signer-keystores` or relocating the
namespace prefix can't silently regress the fix.
- Full chown side-effect needs a live k3d cluster and is exercised
by the spark2 validation above plus all existing integration
flows; no unit-mocked k3d test added.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickh-obol@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL - #446

Merged
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port
May 8, 2026
Merged

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL#446
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port

Conversation

@nickh-obol

@nickh-obolnickh-obol commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Linux-specific bugs in fresh obol stack up flows that don't surface on macOS.

1. Hermes pod CrashLoopBackOff on fresh PVC (internal/hermes/hermes.go)

The Stack's local-path-provisioner setup script chowns each new PV to 1000:1000, and KubeletInUserNamespace=true (set in internal/embed/k3d-config.yaml) silently skips the fsGroup recursive-chown that would otherwise correct it. Hermes pods run as UID 10000 and crashloop trying to mkdir /data/.hermes/home with Permission denied.

macOS Docker Desktop hides this — its bind-mount filesystem driver fakes file ownership to whoever's asking, so the mismatch never materializes. OpenClaw doesn't hit it either: it pins fsGroup: 1000, accidentally matching the host UID.

Fix: prepend an init-hermes-perms container that runs as root and chowns /data to 10000:10000. Idempotent — also self-heals existing broken PVCs on upgrade.

2. Bootstrap probe + browser URL hardcoded to :8080 (cmd/obol/bootstrap.go)

When ports 80/8080 are in use at start time, k3d remaps the loadbalancer to a random high port and the rest of obol stack up correctly surfaces it via stack.LocalIngressURL(cfg). Three other places in cmd/obol/bootstrap.go were hardcoded to :8080: the readiness probe (hung until timeout), the browser-open URL, and the "view stack interface at" hint.

Fix: reuse stack.LocalIngressURL(cfg) in all three places.

Test plan

  • go test ./internal/hermes/... ./cmd/obol/... passes
  • go build ./... clean
  • Verified end-to-end on Linux: fresh obol agent init lands the agent in Running without manual chown
  • Verified end-to-end on Linux: obol bootstrap with port 8080 occupied completes without hang; all four URL surfaces (warning / visit / browser-open / next-steps) agree on the alternate port
  • Smoke-test on macOS to confirm no regression (existing fsGroup path still works; new init container is a no-op on already-correct ownership)

@bussyjdbussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated against latest origin/main in an isolated worktree.

Checks run locally:

  • git merge --no-commit --no-ff refs/remotes/pr/446/head onto origin/main
  • git diff --check --cached
  • go test ./internal/hermes ./internal/stack ./cmd/obol
  • go build ./cmd/obol
  • go test ./...

All passed. The Hermes PVC ownership fix and bootstrap LocalIngressURL wiring look legitimate.

@bussyjd
bussyjd merged commit c066baa into mainMay 8, 2026
6 checks passed
@bussyjd
bussyjd deleted the fix/hermes-pvc-perms-and-ingress-port branch May 8, 2026 03:03
bussyjd added a commit that referenced this pull request May 15, 2026
The in-pod `init-hermes-perms` init container from #446 (c066baa) is
neutered on Linux k3d because the embedded k3d config sets
`KubeletInUserNamespace=true` (internal/embed/k3d-config.yaml). With
user-namespacing, the pod's "root" maps to a host subuid that lacks
chown authority over the host bind-mount path, so the in-pod
`chown -R 10000:10000 /data` silently no-ops. The next init container
(`init-hermes-data`) then fails with `mkdir /data/.hermes/home:
Permission denied` and the pod CrashLoopBackOffs.
local-path-provisioner's helper-pod sets the volume to 1000:1000
(internal/embed/infrastructure/base/templates/local-path.yaml), which
happens to suit OpenClaw but not Hermes (containerUID = 10000).
Fix: after `helmfile sync`, host-side chown the PVC backing dirs to
containerUID:containerGID by exec-ing into the k3d server container
via `docker exec`. That runs at the Docker daemon's real root and is
not subject to the user-namespacing that silently breaks the in-pod
attempt. The existing `fixRuntimeVolumeOwnership` helper already does
exactly this for wallet keystore paths; we wire it into the agent
deploy path via a new `ensureHermesPVCOwnership` that:
1. Waits up to 60s for each PVC (`hermes-data`, `remote-signer-
keystores`) to be Bound — local-path is WaitForFirstConsumer so
the host dir doesn't exist until the pod is scheduled.
2. Chowns each backing dir.
3. If a Hermes pod is currently stuck in Init:CrashLoopBackOff,
deletes it so kubelet recreates immediately rather than after
exponential backoff (~5 min worst case). Skips the delete when
no pod is stuck so routine syncs (e.g. `obol model sync` after
`obol model prefer`) do not gratuitously restart a healthy
agent.
Called from `hermes.Sync` after `helmfile sync` succeeds, so every
Onboard / Setup / Sync call exercises it.
Validated on spark2 (Linux ARM64, Ubuntu 24.04, NVIDIA GB10) by
reverting the PVC dirs to 1000:1000, deleting the Hermes pod, and
running `obol model sync`. Before: pod stuck in Init:CrashLoopBackOff
with "Permission denied" in init-hermes-data logs. After: PVC dirs
flip to 10000:10000 within the sync, pod reaches `Running 2/2` in
~30s with zero CrashLoopBackOff cycles.
Test:
- `TestHermesPVCPaths` pins the two host paths the helper chowns, so
renaming `hermes-data`/`remote-signer-keystores` or relocating the
namespace prefix can't silently regress the fix.
- Full chown side-effect needs a live k3d cluster and is exercised
by the spark2 validation above plus all existing integration
flows; no unit-mocked k3d test added.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickh-obol@bussyjd
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL - #446

Merged
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port
May 8, 2026
Merged

fix: hermes PVC ownership on Linux k3d + bootstrap ingress URL#446
bussyjd merged 4 commits into
mainfrom
fix/hermes-pvc-perms-and-ingress-port

Conversation

@nickh-obol

@nickh-obolnickh-obol commented May 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Two Linux-specific bugs in fresh obol stack up flows that don't surface on macOS.

1. Hermes pod CrashLoopBackOff on fresh PVC (internal/hermes/hermes.go)

The Stack's local-path-provisioner setup script chowns each new PV to 1000:1000, and KubeletInUserNamespace=true (set in internal/embed/k3d-config.yaml) silently skips the fsGroup recursive-chown that would otherwise correct it. Hermes pods run as UID 10000 and crashloop trying to mkdir /data/.hermes/home with Permission denied.

macOS Docker Desktop hides this — its bind-mount filesystem driver fakes file ownership to whoever's asking, so the mismatch never materializes. OpenClaw doesn't hit it either: it pins fsGroup: 1000, accidentally matching the host UID.

Fix: prepend an init-hermes-perms container that runs as root and chowns /data to 10000:10000. Idempotent — also self-heals existing broken PVCs on upgrade.

2. Bootstrap probe + browser URL hardcoded to :8080 (cmd/obol/bootstrap.go)

When ports 80/8080 are in use at start time, k3d remaps the loadbalancer to a random high port and the rest of obol stack up correctly surfaces it via stack.LocalIngressURL(cfg). Three other places in cmd/obol/bootstrap.go were hardcoded to :8080: the readiness probe (hung until timeout), the browser-open URL, and the "view stack interface at" hint.

Fix: reuse stack.LocalIngressURL(cfg) in all three places.

Test plan

  • go test ./internal/hermes/... ./cmd/obol/... passes
  • go build ./... clean
  • Verified end-to-end on Linux: fresh obol agent init lands the agent in Running without manual chown
  • Verified end-to-end on Linux: obol bootstrap with port 8080 occupied completes without hang; all four URL surfaces (warning / visit / browser-open / next-steps) agree on the alternate port
  • Smoke-test on macOS to confirm no regression (existing fsGroup path still works; new init container is a no-op on already-correct ownership)

@bussyjdbussyjd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated against latest origin/main in an isolated worktree.

Checks run locally:

  • git merge --no-commit --no-ff refs/remotes/pr/446/head onto origin/main
  • git diff --check --cached
  • go test ./internal/hermes ./internal/stack ./cmd/obol
  • go build ./cmd/obol
  • go test ./...

All passed. The Hermes PVC ownership fix and bootstrap LocalIngressURL wiring look legitimate.

@bussyjd
bussyjd merged commit c066baa into mainMay 8, 2026
6 checks passed
@bussyjd
bussyjd deleted the fix/hermes-pvc-perms-and-ingress-port branch May 8, 2026 03:03
bussyjd added a commit that referenced this pull request May 15, 2026
The in-pod `init-hermes-perms` init container from #446 (c066baa) is
neutered on Linux k3d because the embedded k3d config sets
`KubeletInUserNamespace=true` (internal/embed/k3d-config.yaml). With
user-namespacing, the pod's "root" maps to a host subuid that lacks
chown authority over the host bind-mount path, so the in-pod
`chown -R 10000:10000 /data` silently no-ops. The next init container
(`init-hermes-data`) then fails with `mkdir /data/.hermes/home:
Permission denied` and the pod CrashLoopBackOffs.
local-path-provisioner's helper-pod sets the volume to 1000:1000
(internal/embed/infrastructure/base/templates/local-path.yaml), which
happens to suit OpenClaw but not Hermes (containerUID = 10000).
Fix: after `helmfile sync`, host-side chown the PVC backing dirs to
containerUID:containerGID by exec-ing into the k3d server container
via `docker exec`. That runs at the Docker daemon's real root and is
not subject to the user-namespacing that silently breaks the in-pod
attempt. The existing `fixRuntimeVolumeOwnership` helper already does
exactly this for wallet keystore paths; we wire it into the agent
deploy path via a new `ensureHermesPVCOwnership` that:
1. Waits up to 60s for each PVC (`hermes-data`, `remote-signer-
keystores`) to be Bound — local-path is WaitForFirstConsumer so
the host dir doesn't exist until the pod is scheduled.
2. Chowns each backing dir.
3. If a Hermes pod is currently stuck in Init:CrashLoopBackOff,
deletes it so kubelet recreates immediately rather than after
exponential backoff (~5 min worst case). Skips the delete when
no pod is stuck so routine syncs (e.g. `obol model sync` after
`obol model prefer`) do not gratuitously restart a healthy
agent.
Called from `hermes.Sync` after `helmfile sync` succeeds, so every
Onboard / Setup / Sync call exercises it.
Validated on spark2 (Linux ARM64, Ubuntu 24.04, NVIDIA GB10) by
reverting the PVC dirs to 1000:1000, deleting the Hermes pod, and
running `obol model sync`. Before: pod stuck in Init:CrashLoopBackOff
with "Permission denied" in init-hermes-data logs. After: PVC dirs
flip to 10000:10000 within the sync, pod reaches `Running 2/2` in
~30s with zero CrashLoopBackOff cycles.
Test:
- `TestHermesPVCPaths` pins the two host paths the helper chowns, so
renaming `hermes-data`/`remote-signer-keystores` or relocating the
namespace prefix can't silently regress the fix.
- Full chown side-effect needs a live k3d cluster and is exercised
by the spark2 validation above plus all existing integration
flows; no unit-mocked k3d test added.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@nickh-obol@bussyjd