Skip to content

feat: add local project and kanban usage surfaces#2

Merged
the24thLetter merged 725 commits into
mainfrom
openclaw/local-main
May 28, 2026
Merged

feat: add local project and kanban usage surfaces#2
the24thLetter merged 725 commits into
mainfrom
openclaw/local-main

Conversation

@the24thLetter

@the24thLetter the24thLetter commented May 20, 2026

Copy link
Copy Markdown
Owner

Purpose

This PR is for local/fork review only. It is not intended for NousResearch/hermes-agent upstream merge.

Summary

  • Add shared project usage ledger helpers.
  • Add Project Usage dashboard plugin API/assets.
  • Surface Kanban token/tool/turn usage in CLI, native tool responses, dashboard API, and card badges.
  • Add scripts/update_local_fork.sh so local customizations can be rebased on upstream origin/main after Hermes updates.

Codex/Cursor sweep summary

Current accepted head: 8f95720bbfb85175187f83ebba28f8f739949c30

Bugs found and fixed

  • Fixed Cursor Bugbot 19cfaecf-6ed7-42d6-bda0-7fbc02f7a571: release manifests always include the release tag and only release events can move :latest; main pushes publish :main only.
  • Fixed Cursor Bugbot b9ce1492-dc9d-4442-b1f3-5586567bf412: :latest updates are guarded by the current registry org.opencontainers.image.revision plus an origin/main ancestry fallback, so backport releases cannot move :latest backwards.
  • Fixed Cursor Bugbot 85d87d13-2d27-415b-a339-8197f901ef5c: Docker label extraction reads both Image/Labels and image/labels JSON casing from docker buildx imagetools inspect.

Implemented changes in the PR

  • Added executable scripts/update_local_fork_and_restart.sh as the operator continuity wrapper around the narrow scripts/update_local_fork.sh Git helper.
  • The wrapper enforces canonical HERMES_HOME by default, clean checkout/branch switching, pre-rebase Hermes backup, editable reinstall via uv pip install --python ./venv/bin/python -e '.[all]', config migrate/check, scalar continuity settings only, YAML verification for review-agent skills, system LaunchDaemon-first gateway restart, GUI LaunchAgent-unloaded verification, no standalone Kanban daemon, dashboard restart, and post-upgrade health checks.
  • Fixed reviewer-identified wrapper blockers: removed hermes status --all, added a non-secret Telegram connection assertion, and verifies post-restart Kanban dispatcher activity from captured log offsets for both LaunchDaemon and manual-fallback paths.
  • Kanban board-scoped config now propagates through implementation fresh-base worktrees, worker environment, Review gate routing, Review merge-captain assignment, review-agent skills, and worker-context instructions.
  • Added regression tests for board-scoped Kanban overrides and Docker publish tag semantics, plus missing contributor author mappings so attribution checks pass.

Validation

  • bash -n scripts/update_local_fork_and_restart.sh — passed.
  • scripts/update_local_fork_and_restart.sh --help — passed.
  • scripts/update_local_fork_and_restart.sh --hermes-home /Users/openclaw/ai-os-migration/configs/hermes --dry-run — passed without mutating state.
  • ruff check hermes_cli/kanban_db.py hermes_cli/kanban.py tools/kanban_tools.py tests/hermes_cli/test_kanban_db.py tests/hermes_cli/test_kanban_core_functionality.py tests/test_docker_publish_workflow.py — passed.
  • env -u HERMES_KANBAN_BOARD -u HERMES_KANBAN_DB -u HERMES_KANBAN_TASK -u HERMES_KANBAN_RUN_ID -u HERMES_KANBAN_CLAIM_LOCK -u HERMES_KANBAN_WORKSPACE -u HERMES_KANBAN_WORKSPACES_ROOT -u HERMES_KANBAN_REQUIRE_REVIEW_BEFORE_DONE -u HERMES_KANBAN_MERGE_CAPTAIN_PROFILE ./venv/bin/python -m pytest tests/hermes_cli/test_kanban_db.py tests/hermes_cli/test_kanban_core_functionality.py tests/test_docker_publish_workflow.py -q — 406 passed, 1 skipped.
  • python3 /Users/openclaw/.codex/skills/cursor-bugbot-sweep/scripts/poll_cursor_review.py --repo the24thLetter/hermes-agent --pr 2 --head-sha current --once — current-head clean for 8f95720bbfb85175187f83ebba28f8f739949c30; check-run metadata still lists the three fixed prior Docker threads above as unresolved in GitHub.
  • gh pr checks 2 --repo the24thLetter/hermes-agent --watch --interval 10 — all required checks passing; Docker build/merge/save-duration jobs are skipped for this fork PR.

Local fork update/restart wrapper

  • Added executable scripts/update_local_fork_and_restart.sh as the operator continuity wrapper around the narrow scripts/update_local_fork.sh Git helper.
  • Encodes safeguards for canonical HERMES_HOME, clean checkout/branch switching, Hermes quick backup, editable reinstall via uv pip install --python ./venv/bin/python -e '.[all]', config migrate/check, scalar continuity settings, YAML-only review skill verification, system LaunchDaemon-first gateway restart, GUI LaunchAgent-unloaded verification, no standalone Kanban daemon, dashboard restart, and post-upgrade health checks.
  • Documents and reports the manual gateway fallback as live but unsupervised: no launchd KeepAlive, no boot/login restart, and no automatic crash recovery.

Validation:

  • bash -n scripts/update_local_fork_and_restart.sh
  • scripts/update_local_fork_and_restart.sh --help
  • scripts/update_local_fork_and_restart.sh --dry-run
  • git diff --cached --check
  • shellcheck not installed locally; bash -n used as shellcheck-equivalent validation.

Note

High Risk
Changes PID 1, image entrypoint, Docker Hub tag semantics, and credential/API client behavior—any regression affects every container user and long-running sessions.

Overview
This changeset is dominated by container runtime and CI delivery, not the Kanban/local-fork items named in the PR title (those do not appear in the provided diff).

Docker image moves from tini + entrypoint.sh to s6-overlay (/init, stage2-hook, main-wrapper, supervised services), ships Node 22 from an upstream stage, bakes HERMES_GIT_SHA for supportability, adds a docker exec privilege-drop shim, extra Python provider extras, and tighter build-context ignores. Smoke tests now hit the real image ENTRYPOINT (fixing a no-op after s6 migration).

Docker publish simplifies tagging: :main on main pushes only; releases tag the version and move :latest only when the release commit is not behind current :latest/origin/main (removes separate move-main / move-sha jobs). Docker integration tests run in the amd64 publish job against the already-loaded :test image. New hadolint/shellcheck workflow lints the Dockerfile and docker/ scripts.

CI tests switch from single-job pytest -n auto to a 6-shard matrix driven by scripts/run_tests_parallel.py with merged duration caching. Docs deploy always rebuilds the skills index; scheduled skills-index runs trigger deploy-site instead of duplicating deploy; a 4-hour freshness watchdog opens GitHub issues on stale live index.

Agent/runtime fixes include credential-pool provider mismatch guards and faster rotation on exhausted keys, custom-provider extra_body merge, platform enabled_toolsets gating for memory/context tools, MiniMax OAuth per-request tokens, atomic rollback on in-place model switch, safer TCP shutdown without cross-thread close (NousResearch#29507), and ACP delivery when transform_llm_output changes the final text after streaming. Anthropic adapter is refactored with safer OAuth credential writes and MCP name double-prefix avoidance.

Supply-chain audit uses three-dot diffs and narrows install-hook file detection to repo-root paths to cut false positives.

Reviewed by Cursor Bugbot for commit 8f95720. Bugbot is set up for automated code reviews on this repo. Configure here.

@the24thLetter

Copy link
Copy Markdown
Owner Author

bugbot run

Comment thread plugins/kanban/dashboard/plugin_api.py Outdated
Comment thread hermes_cli/kanban.py Outdated
@the24thLetter

Copy link
Copy Markdown
Owner Author

bugbot run

Comment thread hermes_cli/project_usage_ledger.py Outdated
@the24thLetter

Copy link
Copy Markdown
Owner Author

bugbot run

Comment thread hermes_cli/kanban.py
@the24thLetter

Copy link
Copy Markdown
Owner Author

bugbot run

Comment thread plugins/kanban/dashboard/plugin_api.py Outdated
@the24thLetter

Copy link
Copy Markdown
Owner Author

bugbot run

Comment thread hermes_cli/project_usage_ledger.py Outdated
@the24thLetter

Copy link
Copy Markdown
Owner Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit aaddb2f. Configure here.

Comment thread hermes_cli/kanban.py
@the24thLetter

Copy link
Copy Markdown
Owner Author

Merge-captain review: changes requested

I found a blocking Review-gate state-machine regression on the current head aa8871d80ad1650324dd96582cd6bd3d5d32dd4d.

Blocking finding:

  • hermes_cli/kanban_db.py: review-column spawn failures still restore the card to ready instead of review. Repro: create a task in review, run dispatch_once(..., spawn_fn=lambda ...: raise RuntimeError(...), failure_limit=3), and the task ends as status=ready, assignee=mergecaptain, with the run closed as spawn_failed. That means the next dispatcher tick treats the mergecaptain as an implementation worker, bypassing the Review-column protocol and potentially causing the exact review crash/respawn confusion this PR is meant to eliminate.

Required fix:

  • Teach the spawn-failure path to use _status_after_failed_run(...) / equivalent for review-origin runs, not unconditional ready.
  • Cover both below-threshold and breaker-tripped paths for review-origin spawn failures. Below threshold should restore review; if the breaker trips, make the terminal state intentional and documented (likely blocked with review metadata), not accidentally ready.
  • Add a regression test mirroring test_detect_crashed_review_worker_returns_to_review but with a review dispatch_once spawn function that raises before _set_worker_pid.

Validation I ran:

  • ./venv/bin/python -m py_compile hermes_cli/kanban_db.py tools/kanban_tools.py
  • ./venv/bin/python -m ruff check hermes_cli/kanban_db.py tools/kanban_tools.py tests/hermes_cli/test_kanban_db.py tests/tools/test_kanban_tools.py
  • ./venv/bin/python -m pytest -q tests/hermes_cli/test_kanban_db.py tests/tools/test_kanban_tools.py => 245 passed
  • Manual repro above => failed with status ready assignee mergecaptain run_status spawn_failed run_outcome spawn_failed

I stopped the local Bugbot polling after finding this blocker; Cursor Bugbot was still in progress at that point.

Comment thread hermes_cli/kanban_db.py
@the24thLetter

Copy link
Copy Markdown
Owner Author

Fixed the mergecaptain blocker on current head 4dc5853179f635c74cddd85d9d2c1773e9912a06: Review-origin dispatch spawn failures now restore to Review below the breaker threshold, and breaker-tripped failures intentionally block with restore_status=review recorded in run metadata/events. Validation: git diff --check, py_compile, ruff, pytest tests/hermes_cli/test_kanban_db.py tests/tools/test_kanban_tools.py => 247 passed.

Comment thread hermes_cli/project_usage_ledger.py Outdated
@the24thLetter

Copy link
Copy Markdown
Owner Author

Fixed Cursor Bugbot issue 1a1032a0 on current head b8ed1a9f2bf1f0f2f5a2d63652e1a023b868d080: unroutable Review rejections now record explicit review_rejection, missing_implementation_assignee, and explanatory metadata/event payloads instead of silently falling through to a generic block. Validation: git diff --check; py_compile; ruff; focused pytest => 248 passed; synthetic repro confirms the explicit metadata/event fields. Current-head Cursor Bugbot is still pending.

@the24thLetter

Copy link
Copy Markdown
Owner Author

Merge-captain blocker on current head b8ed1a9f2bf1f0f2f5a2d63652e1a023b868d080:

The Review gate still does not work end-to-end for real mergecaptain workers because _default_spawn switches the child to the profile-scoped HERMES_HOME, but does not propagate the root Review-gate settings. In this checkout, root config has kanban.require_review_before_done: true and merge_captain_profile: mergecaptain, while /profiles/mergecaptain/config.yaml does not. A dispatched mergecaptain worker calling kanban_block therefore evaluates review_gate_enabled() as false and leaves the task blocked/mergecaptain with a generic blocked event instead of returning it to the original implementer.

Repro from /Users/openclaw/ai-os-migration/configs/hermes/hermes-agent:

TMP=$(mktemp -d)
DB="$TMP/kanban.db"
HERMES_HOME=/Users/openclaw/ai-os-migration/configs/hermes \
HERMES_KANBAN_DB="$DB" \
./venv/bin/python - <<'PY' > "$TMP/task.txt"
from hermes_cli import kanban_db as kb
kb.init_db()
with kb.connect() as conn:
    t=kb.create_task(conn,title='profile review config gap',assignee='default')
    impl=kb.claim_task(conn,t)
    assert kb.complete_task(conn,t,summary='ready',expected_run_id=impl.current_run_id)
    task=kb.get_task(conn,t)
    print(t, task.status, task.assignee)
PY
TID=$(awk '{print $1}' "$TMP/task.txt")
HERMES_HOME=/Users/openclaw/ai-os-migration/configs/hermes/profiles/mergecaptain \
HERMES_PROFILE=mergecaptain \
HERMES_KANBAN_DB="$DB" \
./venv/bin/python - <<PY
from hermes_cli import kanban_db as kb
with kb.connect() as conn:
    review=kb.claim_review_task(conn,'$TID')
    assert kb.block_task(conn,'$TID',reason='review rejected',expected_run_id=review.current_run_id)
    task=kb.get_task(conn,'$TID')
    run=kb.latest_run(conn,'$TID')
    print('after_block', task.status, task.assignee, run.status, run.metadata)
    print('last_event', kb.list_events(conn,'$TID')[-1].kind, kb.list_events(conn,'$TID')[-1].payload)
PY

Observed:

t_3374280f review mergecaptain
claimed running mergecaptain
after_block blocked mergecaptain blocked None
last_event blocked {'reason': 'review rejected'}

Required fix:

  • Ensure review workers spawned by _default_spawn carry the Review-gate control-plane settings they need after profile activation. Either propagate HERMES_KANBAN_REQUIRE_REVIEW_BEFORE_DONE / HERMES_KANBAN_MERGE_CAPTAIN_PROFILE into the child env from the dispatcher/root config, or make the Review-gate decision derive from durable board/run metadata instead of the worker profile config.
  • Add a regression test that simulates a root dispatcher config with Review enabled plus a mergecaptain profile without that config, then verifies a Review-origin block_task still returns to the original implementation assignee and emits review_rejected metadata/event.

Validation run on current head before blocking:

  • ./venv/bin/python -m py_compile hermes_cli/kanban_db.py tools/kanban_tools.py
  • ./venv/bin/python -m ruff check hermes_cli/kanban_db.py tests/hermes_cli/test_kanban_db.py tools/kanban_tools.py tests/tools/test_kanban_tools.py
  • ./venv/bin/python -m pytest -q tests/hermes_cli/test_kanban_db.py tests/tools/test_kanban_tools.py => 248 passed
  • Broader focused suite: ./venv/bin/python -m pytest -q tests/hermes_cli/test_kanban_db.py tests/tools/test_kanban_tools.py tests/hermes_cli/test_kanban_cli.py tests/hermes_cli/test_project_usage_ledger.py tests/plugins/test_kanban_dashboard_plugin.py tests/plugins/test_project_usage_dashboard_plugin.py => 409 passed

Comment thread hermes_cli/kanban_db.py Outdated
Comment thread plugins/kanban/dashboard/plugin_api.py Outdated
Comment thread hermes_cli/kanban_db.py
Comment thread hermes_cli/kanban_db.py
Comment thread hermes_cli/kanban_db.py Outdated
Comment thread hermes_cli/kanban_db.py
Comment thread hermes_cli/kanban_db.py
Comment thread hermes_cli/kanban_db.py
Comment thread tools/kanban_tools.py
Comment thread plugins/kanban/dashboard/plugin_api.py Outdated
Comment thread plugins/kanban/dashboard/plugin_api.py Outdated
Comment thread hermes_cli/kanban_db.py
@the24thLetter

Copy link
Copy Markdown
Owner Author

Cursor/Bugbot sweep final status for 0a9ba08c67cc1eaf4d3fcfa62e3c5ef1c38ce50b:

  • Cursor Bugbot completed successfully on the exact current head (check_run 77237494677): no new issues found.
  • GitHub reviewThreads enumeration confirms 0 unresolved non-outdated Cursor Bugbot threads, including previous-review threads.
  • Focused validation passed: py_compile, ruff, git diff --check, and 376 focused pytest tests.
  • PR intentionally left open / not merged per fork-local no-merge instruction on Kanban card t_b9ad5294.

…, ClawHub, browse.sh, OpenAI, …) (NousResearch#32336)

The Skills Hub page was stuck on a stale Feb 25 snapshot, showing only Built-in
+ Optional + Anthropic + LobeHub. The unified index already has 2078 skills
from skills.sh / ClawHub / LobeHub / GitHub taps / Claude Marketplace, and
BrowseShSource adds another ~330 — none of it was reaching the page.

Changes:

- website/scripts/extract-skills.py: read website/static/api/skills-index.json
  (the unified multi-source catalog, rebuilt twice daily) as the canonical
  external source. Keep the legacy skills/index-cache/ fallback for offline
  builds. Add friendly per-source labels (skills.sh, ClawHub, browse.sh,
  OpenAI, HuggingFace, Anthropic, LobeHub, etc.) and per-entry installCmd.
- website/src/pages/skills/index.tsx: add source pills + ordering for the 11
  new sources; render installCmd from the index entry.
- website/scripts/prebuild.mjs: when no local skills-index.json exists, fetch
  the live one from hermes-agent.nousresearch.com so local 'npm run build'
  matches production without burning GitHub API quota.
- scripts/build_skills_index.py: crawl BrowseShSource so browse.sh entries
  land in the unified index. Adjust source_order.
- tools/skills_hub.py: GitHubSource.DEFAULT_TAPS — openai/skills moved its
  skills into skills/.curated/ and skills/.system/, so add both as explicit
  taps (the listing code skips dotted dirs by design). Drop
  VoltAgent/awesome-agent-skills (README-only, no SKILL.md files) and
  MiniMax-AI/cli (singular skill, not a tap directory). Net effect: github
  source jumps from 83 → 143 skills, with OpenAI properly included.
- .github/workflows/deploy-site.yml: build the unified index BEFORE running
  extract-skills.py — previous order meant extract-skills always fell back
  to the legacy cache. Drop the 'skip if file exists' guard; the file is
  gitignored and must be rebuilt every deploy.
- .github/workflows/skills-index.yml: drop the broken 'deploy-with-index'
  job (it cp'd 'landingpage/\*' which no longer exists, failing every cron
  run since the landingpage move). Replace it with a workflow_dispatch
  trigger of deploy-site.yml so the index refresh still reaches production
  on schedule.
- website/docs/user-guide/features/skills.md: drop VoltAgent from the
  default-taps doc list to match the code.

Before: 695 skills (Built-in 90, Optional 84, Anthropic 16, LobeHub 505).
After:  2168 skills across 9 source pills, including the 1212 skills.sh
        entries the user expected to see.
@the24thLetter
the24thLetter force-pushed the openclaw/local-main branch from bd876a8 to 7976c00 Compare May 28, 2026 14:51
-t "${IMAGE_NAME}:main" \
-t "${IMAGE_NAME}:latest" \
"${args[@]}"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

:latest tag races and regresses on backport releases

Medium Severity

Both main pushes and releases unconditionally tag :latest, removing the ancestor check that previously prevented backport releases from dragging :latest backwards. A patch release on an older branch (e.g., v1.1.6 after v1.2.3) will overwrite :latest with older code. Additionally, every commit to main now overwrites :latest, making it track unstable dev code rather than the latest stable release—conflating the purpose of :main and :latest.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7c0bd37. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7ed5aa8. Configure here.

Comment thread .github/workflows/docker-publish.yml Outdated
tag_commit="$(git -C "$GITHUB_WORKSPACE" rev-parse "${TAG}^{commit}")"
latest_revision=""
if latest_metadata="$(docker buildx imagetools inspect "${IMAGE_NAME}:latest" --format '{{json .}}' 2>/dev/null)"; then
latest_revision="$(python3 -c 'import json,sys; data=json.load(sys.stdin); print((data.get("image") or {}).get("labels", {}).get("org.opencontainers.image.revision", ""))' <<<"$latest_metadata")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Label extraction uses wrong JSON key casing

Medium Severity

The Python one-liner extracting the OCI revision label from docker buildx imagetools inspect --format '{{json .}}' uses data.get("image"), but the Go template field is .Image (uppercase). Go's default JSON serialization preserves the uppercase field name, so data.get("image") always returns None, making latest_revision always empty. This causes the :latest backport guard to silently fall through to the weaker origin/main ancestry check instead of comparing against the actual current :latest commit.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7ed5aa8. Configure here.

@the24thLetter
the24thLetter merged commit f35e01f into main May 28, 2026
26 checks passed
@the24thLetter
the24thLetter deleted the openclaw/local-main branch May 28, 2026 18:41
the24thLetter pushed a commit that referenced this pull request Jun 18, 2026
… OAuth gates

Two parallel public-path allowlists drifted: _PUBLIC_API_PATHS in
hermes_cli/web_server.py (legacy _SESSION_TOKEN middleware) and
_GATE_PUBLIC_PREFIXES in hermes_cli/dashboard_auth/middleware.py
(OAuth gate). The legacy list included /api/status (documented as a
non-sensitive read-only liveness target); the OAuth gate's list did not.

Effect: every wildcard-subdomain agent surfaced as STARTING/down to the
portal even though the dashboard was serving correctly. Nous account
service (src/server/agents/fly-provider.ts
getInstanceRuntimeStatus) fetches ``/api/status`` without a cookie
as its sole liveness probe; the OAuth gate's 401 looked identical to
'agent dead' on the portal side.

Fix: lift the allowlist into hermes_cli/dashboard_auth/public_paths.py
and have both middlewares import it. _path_is_public now consults
the shared frozenset first, then falls back to the gate's
auth-bootstrap/static prefix list. Future additions to the public list
hit both gates automatically.

Endpoint inventory (verified safe to remain public):

* /api/status            — version, gateway state, active session count,
                           auth-gate shape. Portal liveness probe target.
* /api/config/defaults   — config-defaults feed for the SPA's Config page
* /api/config/schema     — config schema for the SPA's Config page
* /api/model/info        — model catalogue metadata (context windows)
* /api/dashboard/themes  — theme manifests for the skin engine
* /api/dashboard/plugins — plugin manifests for the dashboard

No user data, no session content, no secrets. Same shape an external
monitoring agent would hit on /healthz.

Tests:

* New: test_gated_status_is_public (regression guard with the NAS
  fly-provider.ts liveness-probe rationale spelled out in the docstring)
* New: test_other_public_api_paths_are_public_under_gate (parametrised
  over the rest of PUBLIC_API_PATHS — proves 401 / 302-to-login is
  never the response)
* New: docker integration check #3 in
  test_dashboard_oauth_gate_engaged_by_default — /api/status
  remains 200 under the gate AND reports auth_required=True so the
  portal can distinguish modes
* Updated: test_full_login_round_trip_unlocks_gated_api now probes
  /api/sessions instead of /api/status (status is public, so it
  can no longer distinguish 'logged in' from 'gate accidentally
  disabled')
* Updated: TestApi401Envelope (the no-cookie / invalid-cookie /
  dead-cookie tests) probes /api/sessions for the same reason
* Updated: docker integration check #2 in
  test_dashboard_oauth_gate_engaged_by_default probes
  /api/sessions to prove the gate is intercepting
* Removed: dead _login() helper in
  test_dashboard_auth_status_endpoint.py (no longer needed since
  /api/status is reachable cold)

Companion to docs/handover/hermes-agent-dashboard-s6-insecure-fix.md
(the --insecure flag fix that shipped earlier).
the24thLetter pushed a commit that referenced this pull request Jun 18, 2026
…NousResearch#34192) (NousResearch#34382)

NousResearch#34192 reports Hostinger's 'Hermes WebUI' catalog crashes on startup
with:

  /usr/bin/tini: No such file or directory

The image moved from tini to s6-overlay as PID 1 (/init) earlier in
2026. Orchestration templates that still pin /usr/bin/tini as the
entrypoint \u2014 like the Hostinger Hermes WebUI catalog \u2014 have no
binary to exec and the container crashes immediately.

Hermes has no control over the Hostinger catalog template, but we can
make the image backward-compatible by symlinking /usr/bin/tini -> /init
during the s6-overlay install step. External wrappers that exec
/usr/bin/tini will land on the same s6-overlay reaper they would have
landed on if they'd used the canonical /init entrypoint.

The image's own ENTRYPOINT continues to be /init verbatim \u2014 the shim
is purely for legacy external wrappers, not for the image's own
runtime path. Once affected catalogs are updated, the symlink can be
removed.

Other issues NousResearch#34192 raises that are NOT addressed by this PR:

  * Problem #2 (UID 1024 vs 10000 mismatch): already fixed by NousResearch#33148
    (S6_KEEP_ENV=1) and NousResearch#32412 (with-contenv shebangs). The Hostinger
    template likely needs to update its env-var propagation.

  * Problem #3 (incompatible session formats): RFC for pluggable
    SessionDB is tracked in NousResearch#23717.

  * Problem #4 (Telegram polling conflict): an operations problem on
    Hostinger's side, not in this codebase.

This PR is scoped to the one issue that can be fixed inside
Dockerfile: the missing /usr/bin/tini binary.

Tests (3 in test_dockerfile_tini_compat_shim.py):

  - test_tini_compat_symlink_present
    Guard: the symlink line must exist in Dockerfile.
  - test_tini_compat_comment_explains_why
    The NousResearch#34192 anchor comment must be present so future readers know
    why the shim is there (avoid accidental removal).
  - test_entrypoint_still_init_not_tini
    Sanity check: ENTRYPOINT remains /init (s6-overlay). The shim is
    only for external wrappers.

Refs: NousResearch#34192
Partial fix: addresses the immediate tini-binary crash. Catalog-side
fixes still needed by Hostinger for the UID and session-format
problems documented in the issue.

Co-authored-by: Cursor <[email protected]>
the24thLetter pushed a commit that referenced this pull request Jun 18, 2026
…bes + test-leak fix (NousResearch#40909)

* fix(gateway,windows): reliability — supervisor task, JOB breakaway, status --deep

Three coordinated fixes for the Windows gateway reliability story:

1. CREATE_BREAKAWAY_FROM_JOB on every detached spawn

   The 'hermes update' triggered from the Electron Desktop GUI ran inside
   Electron's job object. Without breakaway, the post-update gateway
   watcher spawned by update — already DETACHED_PROCESS — was still
   reaped when Electron's job tore down, so the gateway never came back
   after a GUI-initiated update. Adds CREATE_BREAKAWAY_FROM_JOB (0x01000000)
   to:
     - hermes_cli/_subprocess_compat.py::windows_detach_flags() — used by
       every helper that calls windows_detach_popen_kwargs(), including
       launch_detached_profile_gateway_restart()
     - The watcher subprocess's own respawn snippet in
       hermes_cli/gateway.py (inlined flags so the watcher's child
       respawn also breaks away)

   _spawn_detached() in gateway_windows.py already had the flag; this
   change brings the rest of the codebase to parity.

2. Per-minute supervisor Scheduled Task — Windows equivalent of
   systemd Restart=always

   Introduces hermes_cli/gateway_supervisor.py and registers it as a
   second Scheduled Task ('Hermes_Gateway_Supervisor', SC MINUTE /MO 1,
   LIMITED rights) alongside the existing ONLOGON task. Every minute,
   the supervisor uses the same gateway.status.get_running_pid() probe
   as 'hermes gateway status' and, if no gateway is alive, calls
   gateway_windows._spawn_detached() (which now includes BREAKAWAY) to
   bring one back.

   Covers every crash mode, not just 'machine rebooted': taskkill,
   OOM, GUI update SIGTERM, parent job teardown. Cheap — one pythonw
   startup per minute when down, one PID-existence check per minute
   when up.

   Wired into both the schtasks-success and Startup-folder-fallback
   install paths via _install_supervisor_best_effort(), and removed in
   uninstall(). Best-effort: a failing supervisor install logs a
   warning but doesn't roll back the primary install.

3. 'hermes gateway status --deep' shows per-probe PASS/FAIL

   Replaces the existing terse '--deep' output (which only printed
   paths) with an actual diagnostic table:
     [1] PID file present
     [2] Lock file held by a live process
     [3] get_running_pid() result
     [4] _pid_exists(pid) — OS-level liveness
     [5] gateway_state.json (state + age)
     [6] Last lifecycle event from gateway-exit-diag.log

   When the high-level summary disagrees with reality, the user can
   see exactly which signal is lying.

Test-leak fix
-------------

tests/hermes_cli/test_gateway_wsl.py::TestGatewayCommandWSLMessages
monkey-patched is_linux/is_wsl/supports_systemd_services to simulate
WSL but did NOT stub is_windows(). On a Windows host, the dispatcher
in _gateway_command_inner takes the is_windows() branch BEFORE the
WSL guidance branch, so the test invoked gateway_windows.install()
for real. install() writes to %APPDATA%\...\Startup\Hermes_Gateway.cmd
— the REAL user Startup folder, never sandboxed by tmp_path — pointing
at the test's pytest-of-<user>/pytest-<N>/.../gateway-service/ wrapper.
When pytest tore down the tmp_path, every subsequent Windows login
flashed a cmd.exe window that failed to find the missing target.

Stubs is_windows=False on all four affected tests:
  test_install_wsl_no_systemd
  test_start_wsl_no_systemd
  test_status_wsl_running_manual
  test_status_wsl_not_running

Defense-in-depth: _build_startup_launcher() now prefixes the launcher
with 'if not exist <target> exit /b 0', so any future stale Startup
entry silently no-ops instead of flashing a console window.

Status enhancements
-------------------

- status() now reports supervisor task presence alongside the existing
  schtasks/Startup info, and nudges the user to reinstall if the
  supervisor isn't registered.
- Deep mode dumps both the supervisor task name + script path.

* fix(gateway,windows): drop the per-minute supervisor task — keep breakaway + deep probes

Earlier in this branch we added a per-minute schtasks-based supervisor to
respawn the gateway after crashes / GUI-update SIGTERMs. The implementation
flashed a brief console window on every firing, which stole window focus.
We tried several variants:

  - cmd.exe wrapper invoking pythonw  -> flashes (cmd.exe is console-subsystem)
  - schtasks /TR pointing at pythonw  -> flashes (uv venv launcher pythonw is
    actually subsystem=Console, not GUI; it respawns the real pythonw)
  - schtasks /TR pointing at base uv  -> still flashes (Task Scheduler-side
    conhost preallocation; documented Windows quirk)
  - XML registration with <Hidden>true>  -> still flashes (<Hidden> only hides
    the task in the Task Scheduler UI, not the spawned window)

Researched what leading projects do:

  - Ollama: GUI-subsystem tray exe + Startup-folder shortcut. No supervisor.
  - Tailscale: real Windows Service via SCM. Session 0, no console possible.
  - Syncthing: --no-console flag inside the binary + Startup folder.
  - openclaw: VBS Run(..., 0, False) wrapper. Suppresses the *window* but
    Super User Q971162 confirms focus-steal still occurs in some cases.

None of these use a per-minute polling scheduled task. The 'auto-restart on
crash' responsibility belongs INSIDE the daemon (Tailscale's in-process
recovery / Ollama's monitor+worker pair) OR is delegated to the Windows
Service Control Manager — not Task Scheduler.

So this commit drops the supervisor entirely. The CREATE_BREAKAWAY_FROM_JOB
fix in _subprocess_compat.py (from commit c1e5fa4) survives — that is the
*real* fix for problem #2 (GUI-update kills gateway): the post-update
watcher in launch_detached_profile_gateway_restart() now breaks out of
Electron's job object, so the gateway respawn watcher survives the GUI
quit and successfully respawns the gateway.

Surviving from c1e5fa4:
  * CREATE_BREAKAWAY_FROM_JOB in hermes_cli/_subprocess_compat.py (fixes #2)
  * Inlined breakaway flag in the watcher respawn snippet in gateway.py
  * hermes gateway status --deep PASS/FAIL probes (fixes #1 — visibility)
  * 'if not exist <target> exit /b 0' guard in _build_startup_launcher
    (fixes #3 — silent no-op for stale Startup entries)
  * tests/hermes_cli/test_gateway_wsl.py is_windows=False stubs (root cause
    of #3 — pytest WSL tests no longer leak Startup entries on Win hosts)

Removed in this commit:
  * hermes_cli/gateway_supervisor.py (entire file)
  * Supervisor section in hermes_cli/gateway_windows.py (~180 lines):
      get_supervisor_task_name, get_supervisor_script_path,
      _build_supervisor_cmd_script, _write_supervisor_script,
      _install_supervisor_task, is_supervisor_task_registered,
      _install_supervisor_best_effort
  * _install_supervisor_best_effort() calls in install() (3 spots)
  * supervisor cleanup block in uninstall()
  * supervisor display lines in status() / status(deep=True)

Future direction (out of scope for this PR): the right place for Windows
'Restart=always' semantics is a real Windows Service installed via
pywin32's win32serviceutil.ServiceFramework — session-0 isolation, SCM
auto-restart, no console window possible. That's a meaningful next-PR
project, not a band-aid.

Tests: 51 pass / 2 pre-existing failures in
tests/hermes_cli/test_gateway_{windows,wsl}.py (the 2 failures are
TestSupportsSystemdServicesWSL cases that fail on origin/main too —
unrelated to this PR).
Sign up for free to 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.