fix(docker): exclude .playwright from rsync staging to avoid error 23

The agent source at /opt/hermes inside the container may contain a
.playwright/ directory with browser dependency files that have restricted
permissions. rsync fails with exit code 23 when attempting to read them,
which kills the container build ("Failed to stage hermes-agent source").

- Add --exclude=.playwright to rsync in docker_init.bash
- Add rm -rf .playwright to cp -a fallback path for symmetry
- Update test_docker_init_excludes_egg_info_during_staging to assert
  both the rsync --exclude and a broad .playwright presence check
- Add a brief note in AGENTS.md Contribution style about mirroring
  directory exclusions in both rsync and cp paths

Fixes #5315
This commit is contained in:
nanw 2026-07-01 13:00:15 +08:00
parent 2a40893730
commit 4b202d1267
4 changed files with 56 additions and 222 deletions

View file

@ -1,5 +0,0 @@
# CodeGraph data files — local to each machine, not for committing.
# Ignore everything in .codegraph/ except this file itself, so transient
# files (the database, daemon.pid, sockets, logs) never show up in git.
*
!.gitignore

261
AGENTS.md
View file

@ -1,118 +1,49 @@
# Agent instructions for Hermes WebUI
This file is the shared entry point for AI assistants working in this repository.
Keep it project-specific and safe to publish. Do not put personal machine setup,
private network details, credentials, tokens, or local-only workflow notes here.
This file is the shared entry point for AI assistants working in this
repository. Keep it project-specific and safe to publish. Do not put personal
machine setup, private network details, credentials, tokens, or local-only
workflow notes here.
## What this repo is
## Read first
Hermes WebUI is a browser frontend for [Hermes Agent](https://hermes-agent.nousresearch.com/).
It is deliberately a **Python + vanilla JavaScript** app with no build step, no
bundler, no frontend framework. The Python server lives in `server.py` + `api/`
and the frontend is plain `<script>` tags in `static/` plus a single `style.css`.
Do not add a build pipeline, framework, or bundler without explicit justification
and a rollback story — this is a hard design constraint, not a preference.
Before making changes, read:
## First two minutes
1. `README.md`
2. `CONTRIBUTING.md`
3. `docs/CONTRACTS.md`
4. `CHANGELOG.md`
1. Read `README.md` (quick start, features, env vars, Docker).
2. Read `CONTRIBUTING.md` (PR shape, what goes in the description, AI disclosure).
3. Read `docs/CONTRACTS.md` (subsystem routing) and follow links into the area
you will touch. Start with `docs/rfcs/README.md` for runtime/streaming work
and `docs/UIUX-GUIDE.md` + `DESIGN.md` for UI work.
4. For install / bootstrap / provider / first-run / Docker / WSL support, read
`docs/onboarding-agent-checklist.md` **before** running commands or reading
logs. It has hard safety rules and the role split between human and assistant.
5. `ARCHITECTURE.md` is the deep reference (modules, endpoints, data flow).
`TESTING.md` is the manual browser test plan + automated test commands.
For architecture, testing, or setup work, also read the matching reference:
## Commands an agent actually runs
- `ARCHITECTURE.md` for design constraints and current module layout
- `TESTING.md` for local verification commands and manual test guidance
- `docs/onboarding.md` for first-run onboarding behavior
- `docs/troubleshooting.md` for diagnostic flows
- `docs/rfcs/README.md` for larger RFCs and state/durability contracts
| Goal | Command |
|---|---|
| Run the full test suite locally | `./scripts/test.sh` |
| Run a focused test set | `./scripts/test.sh tests/test_regressions.py -v` |
| Pin a specific Python for tests | `HERMES_WEBUI_TEST_PYTHON=/path/to/python3.12 ./scripts/test.sh` |
| Launch the server (foreground) | `python3 bootstrap.py` |
| Launch as a daemon (PID at `~/.hermes/webui.pid`) | `./ctl.sh start` / `status` / `logs --lines 100` / `restart` / `stop` |
| Ship-style launcher | `./start.sh` |
| Static-JS runtime guard (`no-const-assign` / `no-import-assign`) | `npm run lint:runtime` (after `npm install --no-save eslint`) |
| Python forward-only lint gate (curated ruff rules on **changed lines only**) | `python3 scripts/ruff_lint.py --diff origin/master` |
| Whole-tree ruff backlog (informational, never blocks) | `python3 scripts/ruff_lint.py --all` |
| Reference undefined-globals gate (#3696 class) | `python3 scripts/scope_undef_gate.py` |
| Headless browser brick-class smoke | `python tests/browser_smoke.py` (needs `playwright install chromium`) |
For UI or UX work, read `docs/UIUX-GUIDE.md` and `DESIGN.md` before
changing layout, interaction flow, themes, chat rendering, or composer chrome.
> **Always use `./scripts/test.sh`, never bare `python3 -m pytest` or `pytest`.**
> The script creates/uses the repo `.venv`, pins to Python 3.11–3.13, and
> installs the dev test deps (`requirements-dev.txt`). A direct pytest on an
> unsupported system interpreter is the #1 way to get a misleading failure
> during validation. If you see "unsupported interpreter", rerun through
> `scripts/test.sh` before debugging product code.
## Onboarding and reinstall support
> The forward-only ruff gate (CI `lint` job + `scripts/ruff_lint.py`) only
> reports findings on **lines the PR adds or modifies** vs `origin/master`.
> The existing tree has a cosmetic F401 backlog tracked in #3273; do not
> reformat it as drive-by cleanup. The cleanest way to silence a real
> false positive is `# noqa: <CODE>` with a one-line reason.
If the task involves install, reinstall, bootstrap, first-run onboarding,
provider setup, local model server setup, Docker onboarding, WSL onboarding, or
support for a failed first run, read `docs/onboarding-agent-checklist.md`
before running commands or inspecting logs.
## Architecture in one screen
Follow that checklist's safety rules:
- **Backend**: `server.py` is a thin routing shell (`BaseHTTPRequestHandler`,
`ThreadingHTTPServer`); all GET/POST handlers live in `api/routes.py` (large,
if/elif dispatch — no decorator framework). Helpers in `api/helpers.py`
(`j`, `bad`, `require`, `safe_resolve`); security headers set in one place.
- **Auth**: optional password + passkeys in `api/auth.py`. Off by default; set
`HERMES_WEBUI_PASSWORD` to enable. Cookie is signed HMAC, HTTP-only, 24h TTL.
- **SSE streaming**: `api/streaming.py` + `api/streaming.py:_run_agent_streaming`.
Watch the `/api/upload` ordering rule in `do_POST` — multipart reads the
request body, so the upload check must come **before** any `read_body()`.
- **Sessions**: `api/models.py` (plain class, JSON-per-file, in-memory LRU with
`_index.json` for `all_sessions()`). `INFLIGHT` and the active-sid guard in
`static/messages.js` prevent session-switch-mid-flight from clobbering state.
- **Frontend modules** in `static/`: `ui.js`, `workspace.js`, `sessions.js`,
`messages.js`, `panels.js`, `commands.js`, `boot.js`, `onboarding.js`,
`i18n.js`, `login.js`, `icons.js`, `sw.js`, `terminal.js`,
`assistant_turn_anchors.js`, `outline.js`, `pwa-startup.js`. Loaded as
classic `<script>` tags sharing one implicit global scope — see
`scripts/scope_undef_gate.py` for the brick class that catches.
- **State directory** (runtime, **outside the repo**): `~/.hermes/webui/`
contains `sessions/`, `workspaces.json`, `last_workspace.txt`, `settings.json`,
`projects.json`. Override with `HERMES_WEBUI_STATE_DIR`.
- **Logs**: `~/.hermes/webui/bootstrap-8787.log` for `start.sh`/`bootstrap.py`,
`~/.hermes/webui.log` for `ctl.sh`. Structured JSON per request.
- use isolated `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` for trials unless the
human explicitly asks to use real state
- do not delete or overwrite a real `~/.hermes` directory without explicit
approval
- do not print API keys, OAuth tokens, cookies, full `.env` files, full
`auth.json` files, or password hashes
- collect non-secret status and log evidence before recommending a fix
## Repo-specific gotchas agents miss
## Contribution style
- **No build step.** Editing `static/*.js` or `static/style.css` takes effect
on the next browser reload. There is no `npm run build`, no `dist/`, no
transpile step.
- **Process-global env vars for the agent run.** `TERMINAL_CWD`,
`HERMES_EXEC_ASK`, `HERMES_SESSION_KEY`, `HERMES_HOME` are set via
`os.environ` in `_run_agent_streaming`. Two concurrent chat requests clobber
each other; this is single-user/single-concurrent-request safe. Tracked as
Architecture Phase B in `ARCHITECTURE.md`. Don't add per-request isolation
in your PR — it's a known architecture migration, not a local fix.
- **State coupling to hermes-agent.** WebUI imports Agent modules directly
(`api/config.py`, `api/providers.py`, `api/streaming.py`) and reads Agent
state layout directly. The release boundary in `README.md` Compatibility
section says: **upgrade WebUI and hermes-agent together**, record both
versions in issue reports. This is not a stable API boundary yet (#1925,
#2491).
- **Tooling is dev-only.** `package.json` is *not* a build manifest; its only
dependency is `eslint`, used solely as the runtime-error guard. The app
itself remains pure Python + vanilla JS.
- **Auto-discovery, not config files, picks the agent.** `bootstrap.py` and
`start.sh` walk `HERMES_WEBUI_AGENT_DIR` env, then `$HERMES_HOME/hermes-agent`,
then a sibling `../hermes-agent`, then auto-install via the official Hermes
installer. Trust auto-discovery; do not hardcode a path in a script.
- **Classic-script shared global scope.** A function declared *inside* another
function is **not** global. Calling it bare from a sibling top-level scope
throws `ReferenceError` at runtime (only). `scripts/scope_undef_gate.py`
exists to catch this class — run it after editing any `static/*.js`.
- **`api/routes.py` is large** (≈770KB, thousands of lines). Prefer the
helper functions in `api/helpers.py` (`j`, `bad`, `require`, `safe_resolve`)
and the existing per-module separation over weaving new logic into the
monolithic dispatch.
- Keep one logical change per PR; split unrelated refactors or cleanup.
- Read `docs/CONTRACTS.md` and the linked contract/RFC for the touched
subsystem before editing.
@ -138,128 +69,24 @@ and a rollback story — this is a hard design constraint, not a preference.
the manual verification performed.
- For runtime, streaming, recovery, replay, compression, or sidebar metadata
changes, name the state layer being mutated and prove the relevant invariant.
- For Docker build changes in `docker_init.bash`, mirror directory exclusions
in both the `rsync` and `cp -a` paths — `/opt/hermes` may contain subdirectories
with restricted permissions (e.g. `.playwright/`).
## Onboarding / install / first-run safety
## Local state and secrets
The repo reads and writes real agent state, sessions, workspaces, credentials,
and cron data. **Treat local validation as potentially destructive** unless
you have confirmed the active state directories.
Hermes WebUI can read and write real agent state, sessions, workspaces,
credentials, and cron data. Treat local validation as potentially destructive
unless you have confirmed the active state directories.
Hard rules (from `docs/onboarding-agent-checklist.md`):
- Use isolated `HERMES_HOME` and `HERMES_WEBUI_STATE_DIR` for trials unless
the human explicitly asks to use real state.
- Never delete or overwrite a real `~/.hermes` directory without explicit
approval.
- Never print API keys, OAuth tokens, cookies, full `.env` files, full
`auth.json` files, or password hashes.
- Collect non-secret status and log evidence before recommending a fix:
`/health`, `/api/onboarding/status`, structured log tail, and `find` of
the state dir.
- Do not modify real cron jobs, real sessions, real profiles, or real memory
files during an onboarding trial.
- Do not expose WebUI on a public interface without password protection
(`HERMES_WEBUI_PASSWORD`) and explicit human approval.
- Do not proxy or tunnel `localhost` / `127.0.0.1` / private LAN / Docker
container loopback paths through external services.
Isolated trial pattern:
Prefer isolated trial state for experiments:
```bash
mkdir -p ~/hermes-onboarding-test
HERMES_HOME=~/hermes-onboarding-test/.hermes \
HERMES_WEBUI_STATE_DIR=~/hermes-onboarding-test/webui \
HERMES_HOME=/tmp/hermes-webui-agent-home \
HERMES_WEBUI_STATE_DIR=/tmp/hermes-webui-agent-state \
HERMES_WEBUI_PORT=8789 \
python3 bootstrap.py
# open http://127.0.0.1:8789
# log at ~/hermes-onboarding-test/webui/bootstrap-8789.log
```
## PR / change shape
- One logical change per PR; split unrelated refactors and cleanup.
- Read `docs/CONTRACTS.md` and the linked contract/RFC for the touched
subsystem before editing. Contract-affecting PRs need a `Contract Routing`
section in the PR body naming the contract family and evidence used.
- For runtime/streaming/recovery/replay/compression/sidebar-metadata changes,
name the state layer being mutated and prove the invariant (regression test
or explicit manual check).
- For UI/UX changes, include before/after evidence and test relevant desktop,
narrow, and mobile states. Mobile breakpoints matter; touch targets ≥44px.
- Update `CHANGELOG.md` for user-visible behavior, setup, workflow, or doc
changes that should be release-note ready. Update the matching
`README.md` / `ARCHITECTURE.md` / `TESTING.md` / `docs/CONTRACTS.md` in
the same PR.
- PR body must contain: Thinking Path, What Changed, Why It Matters,
Verification, Risks / Follow-ups, Model Used. If AI helped, disclose
provider + exact model + notable tool use. If no AI: `None -- human-authored`.
- Do not silently redefine product behavior by changing tests alone — update
the corresponding docs in the same PR.
## Files agents should not reach for first
- `mcp_server.py` — the MCP integration is a separate surface; check whether
the task is about WebUI proper or the MCP server before reading.
- `docs/why-hermes.md` — competitor comparison; only relevant for product
positioning / messaging edits, never for code work.
- `CONTRIBUTORS.md` — auto-generated credit roll; do not hand-edit.
- `CHANGELOG.md` (full file) — append-only; consult the latest entries for
release-note style but do not rewrite history.
## What this file is not
It is not a re-statement of `README.md` / `CONTRIBUTING.md` / `ARCHITECTURE.md`
/ `TESTING.md` / `docs/CONTRACTS.md`. Read those for the full story. This file
captures the agent-specific shorthand — commands, gotchas, and safety rules —
that an agent would otherwise miss or guess wrong.
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **hermes-webui** (26315 symbols, 52973 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).
## Always Do
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user.
- **MUST run `detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. For regression review, compare against the default branch: `detect_changes({scope: "compare", base_ref: "master"})`.
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
- When exploring unfamiliar code, use `query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `context({name: "symbolName"})`.
## Never Do
- NEVER edit a function, class, or method without first running `impact` on it.
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
- NEVER rename symbols with find-and-replace — use `rename` which understands the call graph.
- NEVER commit changes without running `detect_changes()` to check affected scope.
## Resources
| Resource | Use for |
|----------|---------|
| `gitnexus://repo/hermes-webui/context` | Codebase overview, check index freshness |
| `gitnexus://repo/hermes-webui/clusters` | All functional areas |
| `gitnexus://repo/hermes-webui/processes` | All execution flows |
| `gitnexus://repo/hermes-webui/process/{name}` | Step-by-step execution trace |
## CLI
| Task | Read this skill file |
|------|---------------------|
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` |
| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` |
| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` |
| Work in the Tests area (3774 symbols) | `.claude/skills/generated/tests/SKILL.md` |
| Work in the Api area (1214 symbols) | `.claude/skills/generated/api/SKILL.md` |
| Work in the Static area (934 symbols) | `.claude/skills/generated/static/SKILL.md` |
| Work in the Scripts area (21 symbols) | `.claude/skills/generated/scripts/SKILL.md` |
| Work in the Manual area (11 symbols) | `.claude/skills/generated/manual/SKILL.md` |
| Work in the Cluster_360 area (9 symbols) | `.claude/skills/generated/cluster-360/SKILL.md` |
| Work in the Cluster_369 area (4 symbols) | `.claude/skills/generated/cluster-369/SKILL.md` |
<!-- gitnexus:end -->
Do not include private machine instructions in this tracked file. Use a
git-ignored local note for personal workflow details.

View file

@ -452,6 +452,7 @@ else
rsync -a \
--exclude='*.egg-info' --exclude='build' --exclude='dist' \
--exclude='__pycache__' --exclude='.git' \
--exclude='.playwright' \
"$_agent_src"/ "$_stage_src"/ \
|| error_exit "Failed to stage hermes-agent source to writable build dir"
else
@ -460,6 +461,7 @@ else
cp -a "$_agent_src"/. "$_stage_src"/ \
|| error_exit "Failed to copy hermes-agent source to writable build dir"
rm -rf "$_stage_src"/*.egg-info "$_stage_src"/build "$_stage_src"/dist 2>/dev/null || true
rm -rf "$_stage_src"/.playwright 2>/dev/null || true
find "$_stage_src" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
fi
chmod -R u+w "$_stage_src" \

View file

@ -253,6 +253,16 @@ def test_docker_init_excludes_egg_info_during_staging():
"rsync staging must --exclude='__pycache__' to keep the copy minimal."
)
assert "--exclude='.playwright'" in stage_block, (
"rsync staging must --exclude='.playwright' so unreadable Playwright "
"browser dependency files (e.g. deb.deps, rpm.deps) don't cause "
"rsync error code 23 and kill container startup."
)
assert ".playwright" in stage_block, (
"Both rsync (--exclude) and cp-fallback (rm -rf) must handle "
".playwright so the build doesn't choke on unreadable browser files."
)
def test_docker_init_makes_staged_dir_writable_after_ro_mount_copy():
"""Regression test for the docker-init "could not create hermes_agent.egg-info: