Skip to content

feat(workspace): add integrated Git controls - #2578

Closed
stocky789 wants to merge 1 commit into
nesquena:masterfrom
stocky789:feat/workspace-git-controls
Closed

stocky789 wants to merge 1 commit into
nesquena:masterfrom
stocky789:feat/workspace-git-controls

Conversation

@stocky789

@stocky789 stocky789 commented May 19, 2026 •

Copy link
Copy Markdown
Contributor

Thinking Path

  • Hermes WebUI already lets users browse and edit workspace files from the browser.
  • Once files can be edited in the workspace window, users need a safe way to inspect Git state, review diffs, select files, and commit changes without leaving the WebUI.
  • This PR adds that workflow directly to the workspace panel while preserving the existing Python + vanilla JavaScript architecture.
  • The implementation is scoped to workspace Git/source-control operations: status, diffs, staging, selected-file commits, branch switching, remote sync actions, and the preview/back chrome needed to review Git diffs inside the workspace panel.

What Changed

  • Added api/workspace_git.py for workspace-scoped Git operations:
    • status
    • diffs
    • staging/unstaging
    • discard/delete
    • selected-file commits through a temporary index
    • branch listing and checkout
    • fetch/pull/push
    • structured, sanitized Git error codes
  • Added API routes for workspace Git operations and routed legacy /api/git-info through the same status implementation.
  • Added a workspace Changes tab with changed-file groups, per-file selection, selected counts, Git badges, and branch/remote controls.
  • Added muted .gitignore visibility in the Files tab so ignored files/folders are labelled Ignored without being offered as committable Changes entries.
  • Added split/unified Git diff previews inside the existing workspace preview surface, including a back target to return to the Changes list.
  • Added selected-file commit message generation with explicit provider/privacy copy.
  • Added workspace freshness polling and bounded background fetch-only remote refresh so Git indicators and visible file-tree state stay current while the panel is open.
  • Added regression coverage for workspace Git behavior, ignored file visibility, selected-file commits, branch/remote actions, and auto-refresh behavior.

Why It Matters

This makes the workspace editor more complete: users can edit files, inspect Git state, review diffs, select exactly which files to commit, and run common remote actions from Hermes WebUI without switching to a terminal.

The selected-file commit path avoids accidentally committing unrelated staged files by committing through a temporary index, then resetting only the selected paths in the real index after success.

Scope

This PR is a larger cohesive feature PR because the workspace Git flow spans backend Git helpers, route wiring, frontend state, and diff preview UI.

Although the diff touches backend routes, frontend workspace UI, tests, and PR media, those changes support the same workspace source-control workflow. There are no dependency changes, framework changes, build-step changes, product-doc edits, changelog edits, or broad refactors.

Suggested Review Order

  1. api/workspace_git.py — Git operation helpers, workspace/path scoping, temporary-index selected commits, branch/remote operations, and error classification.
  2. api/routes.py — HTTP route wiring and request/response handling for Git operations.
  3. static/workspace.js — workspace Git state, Changes tab, diff rendering, branch controls, selected-file commit flow, and refresh guards.
  4. static/ui.js, static/boot.js, static/index.html, static/style.css, static/i18n.js — panel shell, preview/back affordances, labels, and styling.
  5. tests/test_workspace_git.py and tests/test_workspace_auto_refresh.py — regression coverage for the new behavior.
  6. docs/pr-media/workspace-git/ — screenshot evidence for the workflow states.

UI Media

Workflow overview with staged, unstaged, deleted, added, and untracked files:

  1. Files tab with Git badges for pending workspace changes:

Files tab with Git badges

  1. Changes tab showing mixed file states, staged entries with Unstage controls, and unstaged/untracked entries:

Changes tab with mixed states

  1. Selected-file workflow with Stage selected / Discard selected counts and selected commit count:

Selected files workflow

  1. Git diff preview with Changes back target, file path, Stage/Discard/Open controls, and split diff view:

Git diff preview

  1. Commit-ready state with selected files, commit message, provider note, Generate message, and Commit button:

Commit-ready selected files

  1. Gitignored files/folders muted in the Files tab and labelled Ignored, while the Changes tab still counts only committable changes:

Ignored files muted in file tree

Verification

Automated:

  • git diff --check refs/remotes/upstream/master...HEAD
  • .venv/bin/python -m py_compile api/routes.py api/workspace.py api/workspace_git.py
  • node --check static/workspace.js && node --check static/ui.js && node --check static/boot.js
  • .venv/bin/python -m pytest tests/test_workspace_panel_session_list.py::TestWorkspacePanelCollapsePriority::test_container_query_hides_git_badge_first -q --timeout=60 — 1 passed
  • .venv/bin/python -m pytest tests/test_workspace_git.py tests/test_workspace_auto_refresh.py tests/test_issue2554_workspace_tree_file_indent.py -q --timeout=60 — 40 passed
  • .venv/bin/python -m pytest tests/ -q --timeout=60 — 5933 passed, 47 skipped, 3 xpassed, 8 subtests passed

Risks / Follow-ups

  • This is a larger PR because the workspace Git workflow is integrated across backend, frontend, tests, and PR media.
  • Git operations are intentionally scoped to the active workspace/repository and return structured error codes for common failure modes.
  • Background remote refresh is fetch-only; it does not pull, merge, rebase, stage, commit, or mutate the working tree.
  • Ignored files are surfaced only as muted Files-tab entries; they are excluded from Changes-tab commit/stage/discard groups and changed-file totals.
  • Follow-up improvements could refine visual polish or add more browser-level regression coverage after maintainer feedback.

Model Used

AI-assisted.

  • Provider: OpenAI
  • Model: GPT-5.5

@stocky789
stocky789 force-pushed the feat/workspace-git-controls branch 3 times, most recently from 2c1bfbf to f0dd5d4 Compare May 19, 2026 08:15
@stocky789
stocky789 force-pushed the feat/workspace-git-controls branch from b0e1b2c to 4d5c75c Compare May 19, 2026 08:48
@stocky789
stocky789 marked this pull request as ready for review May 19, 2026 09:04
@stocky789
stocky789 marked this pull request as draft May 19, 2026 09:04
@stocky789
stocky789 marked this pull request as ready for review May 19, 2026 09:20
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Summary

Reading the diff end-to-end: api/workspace_git.py (new, 1134 LOC), the api/routes.py GET/POST plumbing for 12 new /api/git/* endpoints, the static/workspace.js Changes tab (now ~2074 LOC total), and the 980-line tests/test_workspace_git.py suite. The implementation quality is good — porcelain v2 parsing, shell-free subprocess, structured error codes, path traversal validation via safe_resolve_ws, per-repo mutation lock, selected-files commits via temporary GIT_INDEX_FILE, and CSRF coverage through the existing _check_csrf middleware. But this PR is going to be very hard to land in its current shape, and I want to flag concerns before more time is invested.

Code reference

The shell-free Git invocation is correctly scoped (api/workspace_git.py:103-111):

result = subprocess.run(
    ["git", *args],
    cwd=str(cwd),
    shell=False,
    capture_output=True,
    text=True,
    timeout=timeout,
    env=env,
)

Path traversal guard inside the temp-index commit flow (:141-156):

def _repo_rel(ctx: GitContext, workspace_rel: str) -> str:
    try:
        target = safe_resolve_ws(ctx.workspace, workspace_rel or ".")
    except ValueError as exc:
        raise GitWorkspaceError(str(exc), "path_outside_workspace") from exc
    ...

The selected-files commit uses a private index (:895-908):

fd, index_path = tempfile.mkstemp(prefix="hermes-webui-git-index-")
os.close(fd)
Path(index_path).unlink(missing_ok=True)
env = os.environ.copy()
env["GIT_INDEX_FILE"] = index_path
...
_run_git(ctx, ["add", "-A", "--", *specs], check=True, env=env)

Diagnosis

1. Scope (PR hygiene)

AGENTS.md is explicit: "Keep one logical change per PR; split unrelated refactors or cleanup." 5390 additions across 19 files is a real review-friction problem. The change spans a new backend module, 12 new routes, ~1700 LOC of frontend, a 980-line test file, CSS, i18n (638 LOC of locale additions across 11 locales), and route reorganization. Two reviewable slices would be much easier to land:

  1. Slice A (backend) — api/workspace_git.py + routes + tests, behind a feature flag or with no frontend exposure. Reviewable in isolation, lands the safe subprocess surface.
  2. Slice B (UI) — static/workspace.js, index.html, CSS, i18n. Wires into the already-landed API.

That also lets the maintainer reason about the backend surface separately from the UI choices.

2. CI not run

statusCheckRollup is empty on this PR — no test (3.11/3.12/3.13) jobs have started. For a change of this size that's worrying; please push a small no-op commit or rebase to retrigger and confirm the 980-line test_workspace_git.py suite + the existing tree all pass.

3. Git hooks execute under the WebUI process

git commit (and pull) at api/workspace_git.py:1064, 1083, 1112 will execute any user-installed pre-commit, commit-msg, post-commit, pre-push hooks in the user's repo. That means a browser button now triggers arbitrary scripts from .git/hooks/ or core.hooksPath under the WebUI process's privileges and env. This is correct Git behavior, but for WebUI it's a notable trust boundary expansion: before this PR, the worst a malicious workspace could do via the WebUI was render its files. After this PR, the workspace can run code on commit/pull via hooks.

Suggest at minimum: document this in the workspace Git docs, and consider an env scrub before invoking remote/commit ops (env.pop("GIT_DIR", None); env.pop("GIT_WORK_TREE", None); env.pop("GIT_CONFIG_GLOBAL", None)) so a malicious shell env in the parent process can't redirect Git operations.

4. No coordination with active agent runs

The workspace mutation lock at :51-60 is per-repo, but the agent run for the same session also writes to the workspace via write_file / patch tools. A user pressing Commit while the agent is mid-tool-call could race the agent's writes; a user pressing Pull mid-run could fast-forward over uncommitted agent edits and produce a non-deterministic result.

The agent contract is owned by ~/.hermes/hermes-agent/agent/conversation_loop.py — checked: it doesn't take a workspace-level lock. So this is fundamentally a WebUI-side issue. Worth at least gating destructive ops (commit, pull, checkout) on s.active_stream_id is None, or putting up a warning in the UI when there is a stream active.

5. Smaller observations

  • _classify_git_error at :81 uses string matching on lowercase output for "non-fast-forward" or "rejected" and "push" in joined — operator precedence puts that as ("non-fast-forward" in text) or ("rejected" in text and "push" in joined), which is what's intended, but the test coverage around non_fast_forward classification isn't obvious. Worth adding a regression.
  • The synthetic untracked diff at :720 builds the unified diff in Python via difflib.unified_diff. For untracked binary files you correctly bail before this; for very large untracked text files you cap at DIFF_SIZE_LIMIT=512KB after materializing the entire file via read_bytes(). Consider streaming or a pre-check on st_size before the read.
  • _handle_git_status and friends in api/routes.py:8479-8540 do a lazy from api.workspace_git import ... inside each handler. Fine for cold-start, but the imports could be hoisted to module level.

Test plan

I did not run the test suite — per cron policy I never execute PR code. But the 30 test cases in tests/test_workspace_git.py look like good coverage: porcelain v2 edge cases (CRLF noise, filemode noise, nested workspace scoping, traversal rejection, conflict rejection, initial commit, rename semantics, dirty checkout) plus 3 route-integration cases that exercise the full HTTP path. Recommended additional cases before merge:

  • test_pull_during_active_session_stream_is_gated — pin the "no commits/pulls while a stream is running" behavior once that gate exists.
  • test_commit_with_pre_commit_hook_failure_returns_hook_failed_code — currently the classifier maps "hook" in text to hook_failed, but the test suite doesn't exercise a real hook.

Recommendation

If splitting isn't feasible at this stage, I'd at least ask for: (1) CI green, (2) the env scrub on subprocess Git, (3) an explicit gate against concurrent agent streams for destructive ops. The implementation is solid — this is mostly a scope concern. Nice work on the test coverage and the structured error codes.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Phase 0 fit assessment — maintainer-review needed, scope split recommended

Thanks for the substantial workspace Git surface, @stocky789. The implementation quality is genuinely high — porcelain v2 parsing, shell-free subprocess, structured error codes, path-traversal validation via safe_resolve_ws, per-repo mutation lock, selected-files commits via temporary GIT_INDEX_FILE, CSRF coverage through the existing middleware, plus a 980-line test suite. Reading through, the code-level review I'd give is mostly nits.

But at +5390/-80 LOC across 19 files this is going to be very hard to land in its current shape, and I want to flag five concerns before more time gets invested:

1. Scope (Phase 0 Q5 + AGENTS.md)

AGENTS.md explicitly says: "Keep one logical change per PR; split unrelated refactors or cleanup." This PR spans:

  • A new backend module (api/workspace_git.py, 1134 LOC)
  • 12 new /api/git/* routes in api/routes.py
  • ~1700 LOC of frontend changes in static/workspace.js, index.html, CSS
  • 638 LOC of locale additions across 11 locales in static/i18n.js
  • A 980-line test file (tests/test_workspace_git.py)

Strongly suggest splitting:

  • Slice A — backend + tests: api/workspace_git.py + routes + tests/test_workspace_git.py, with no frontend exposure (or a feature-flag gate). Reviewable in isolation, lands the safe subprocess surface independent of UI choices.
  • Slice B — UI: static/workspace.js, index.html, CSS, i18n, screenshots. Wires into the already-landed API.

That separation makes the backend trust-boundary discussion (below) reviewable independent of the UX choices.

2. CI not run

statusCheckRollup is empty on this PR — the test (3.11/3.12/3.13) jobs haven't started for the latest head. For a change this size that's a real concern. Please push a small no-op or rebase to retrigger, and confirm the 980-line test suite plus the existing tree all pass before further review.

3. Git hooks execute under the WebUI process (trust boundary)

git commit and git pull at api/workspace_git.py:1064, 1083, 1112 will execute any pre-commit, commit-msg, post-commit, pre-push hooks in the user's repo (and any path from core.hooksPath). That means a browser button click now triggers arbitrary scripts from .git/hooks/ under the WebUI process's privileges and env.

This is correct Git behavior, but for WebUI it's a notable trust-boundary expansion: before this PR, the worst a malicious workspace could do via the WebUI was render its files. After this PR, the workspace can run code on commit/pull via hooks.

Suggested minimum mitigations:

  • Document the trust expansion in workspace Git docs
  • Scrub the env before invoking commit/pull: env.pop("GIT_DIR", None); env.pop("GIT_WORK_TREE", None); env.pop("GIT_CONFIG_GLOBAL", None) so a malicious shell env in the parent process can't redirect Git ops
  • Consider a default-off config flag for the destructive operations (HERMES_WEBUI_WORKSPACE_GIT_DESTRUCTIVE=1 to enable commit/pull/push)

4. No coordination with active agent runs

The per-repo mutation lock at :51-60 is good, but the agent run for the same session also writes to the workspace via write_file / patch tools. A user pressing Commit while the agent is mid-tool-call could race the agent's writes; a user pressing Pull mid-run could fast-forward over uncommitted agent edits.

The agent contract in ~/.hermes/hermes-agent/agent/conversation_loop.py doesn't take a workspace-level lock — so this is fundamentally a WebUI-side coordination issue. Worth at least gating destructive ops on s.active_stream_id is None, or surfacing a warning in the UI when a stream is active.

5. Smaller observations

  • _classify_git_error at :81 uses string matching on lowercase output. Operator precedence on "non-fast-forward" or "rejected" and "push" in joined works out to what's intended, but test coverage around non_fast_forward classification isn't obvious — worth a targeted regression.
  • The synthetic untracked diff at :720 materializes the entire file via read_bytes() before checking against DIFF_SIZE_LIMIT=512KB. For very large untracked text files this can waste memory; consider a st_size pre-check.
  • _handle_git_status and friends do lazy from api.workspace_git import … inside each handler. Fine for cold-start cost but the imports could be hoisted to module level.

Recommendation

Apply maintainer-review for the scope and trust-boundary decisions before further code review. If splitting isn't feasible at this stage, I'd at least ask for: (1) CI green, (2) env scrub on subprocess Git, (3) explicit gate against concurrent agent streams for destructive ops, (4) document the hook execution surface.

The implementation work is solid — this is mostly a project-fit and trust-surface question. Happy to coordinate on the slice plan if you'd like to split.

@nesquena-hermes nesquena-hermes added the maintainer-review Maintainer fit-assessment needed — may not merge even with fixes label May 19, 2026
@stocky789

Copy link
Copy Markdown
Contributor Author

Phase 0 fit assessment — maintainer-review needed, scope split recommended

Thanks for the substantial workspace Git surface, @stocky789. The implementation quality is genuinely high — porcelain v2 parsing, shell-free subprocess, structured error codes, path-traversal validation via safe_resolve_ws, per-repo mutation lock, selected-files commits via temporary GIT_INDEX_FILE, CSRF coverage through the existing middleware, plus a 980-line test suite. Reading through, the code-level review I'd give is mostly nits.

But at +5390/-80 LOC across 19 files this is going to be very hard to land in its current shape, and I want to flag five concerns before more time gets invested:

1. Scope (Phase 0 Q5 + AGENTS.md)

AGENTS.md explicitly says: "Keep one logical change per PR; split unrelated refactors or cleanup." This PR spans:

  • A new backend module (api/workspace_git.py, 1134 LOC)
  • 12 new /api/git/* routes in api/routes.py
  • ~1700 LOC of frontend changes in static/workspace.js, index.html, CSS
  • 638 LOC of locale additions across 11 locales in static/i18n.js
  • A 980-line test file (tests/test_workspace_git.py)

Strongly suggest splitting:

  • Slice A — backend + tests: api/workspace_git.py + routes + tests/test_workspace_git.py, with no frontend exposure (or a feature-flag gate). Reviewable in isolation, lands the safe subprocess surface independent of UI choices.
  • Slice B — UI: static/workspace.js, index.html, CSS, i18n, screenshots. Wires into the already-landed API.

That separation makes the backend trust-boundary discussion (below) reviewable independent of the UX choices.

2. CI not run

statusCheckRollup is empty on this PR — the test (3.11/3.12/3.13) jobs haven't started for the latest head. For a change this size that's a real concern. Please push a small no-op or rebase to retrigger, and confirm the 980-line test suite plus the existing tree all pass before further review.

3. Git hooks execute under the WebUI process (trust boundary)

git commit and git pull at api/workspace_git.py:1064, 1083, 1112 will execute any pre-commit, commit-msg, post-commit, pre-push hooks in the user's repo (and any path from core.hooksPath). That means a browser button click now triggers arbitrary scripts from .git/hooks/ under the WebUI process's privileges and env.

This is correct Git behavior, but for WebUI it's a notable trust-boundary expansion: before this PR, the worst a malicious workspace could do via the WebUI was render its files. After this PR, the workspace can run code on commit/pull via hooks.

Suggested minimum mitigations:

  • Document the trust expansion in workspace Git docs
  • Scrub the env before invoking commit/pull: env.pop("GIT_DIR", None); env.pop("GIT_WORK_TREE", None); env.pop("GIT_CONFIG_GLOBAL", None) so a malicious shell env in the parent process can't redirect Git ops
  • Consider a default-off config flag for the destructive operations (HERMES_WEBUI_WORKSPACE_GIT_DESTRUCTIVE=1 to enable commit/pull/push)

4. No coordination with active agent runs

The per-repo mutation lock at :51-60 is good, but the agent run for the same session also writes to the workspace via write_file / patch tools. A user pressing Commit while the agent is mid-tool-call could race the agent's writes; a user pressing Pull mid-run could fast-forward over uncommitted agent edits.

The agent contract in ~/.hermes/hermes-agent/agent/conversation_loop.py doesn't take a workspace-level lock — so this is fundamentally a WebUI-side coordination issue. Worth at least gating destructive ops on s.active_stream_id is None, or surfacing a warning in the UI when a stream is active.

5. Smaller observations

  • _classify_git_error at :81 uses string matching on lowercase output. Operator precedence on "non-fast-forward" or "rejected" and "push" in joined works out to what's intended, but test coverage around non_fast_forward classification isn't obvious — worth a targeted regression.
  • The synthetic untracked diff at :720 materializes the entire file via read_bytes() before checking against DIFF_SIZE_LIMIT=512KB. For very large untracked text files this can waste memory; consider a st_size pre-check.
  • _handle_git_status and friends do lazy from api.workspace_git import … inside each handler. Fine for cold-start cost but the imports could be hoisted to module level.

Recommendation

Apply maintainer-review for the scope and trust-boundary decisions before further code review. If splitting isn't feasible at this stage, I'd at least ask for: (1) CI green, (2) env scrub on subprocess Git, (3) explicit gate against concurrent agent streams for destructive ops, (4) document the hook execution surface.

The implementation work is solid — this is mostly a project-fit and trust-surface question. Happy to coordinate on the slice plan if you'd like to split.

Appreciate the feedback
I will go through what you have given me here and get back to you. Cheers

@stocky789

Copy link
Copy Markdown
Contributor Author

Thanks again for the review.

I split the original PR into the backend only and made a few tweaks as I did spot a couple bugs.

Backend draft PR:
#2625

That PR contains the workspace Git backend/API work, backend tests, and docs/workspace-git.md.

It also includes the safety changes from your feedback:

  • scrub GIT_DIR, GIT_WORK_TREE, and GIT_CONFIG_GLOBAL before Git subprocess calls
  • preserve GIT_INDEX_FILE for temporary-index selected-file commits
  • require HERMES_WEBUI_WORKSPACE_GIT_DESTRUCTIVE=1 for mutating Git operations
  • reject mutating Git operations while the session has an active stream
  • keep the per-repository mutation lock
  • document hook execution from .git/hooks / core.hooksPath under the WebUI process user
  • add regressions for hook failures, non-fast-forward errors, and large untracked file diffs

The UI slice is separate on stocky789:feat/workspace-git-ui. I’ll leave that out of review until the backend PR is done if you would prefer otherwise I can submit the PR for it as well.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Closing — superseded by your split #2625 (backend) + the frontend draft

Thanks for splitting the original 5390-LOC workspace Git PR into focused slices @stocky789. Per your follow-up comment, the backend-only PR #2625 ships:

  • Workspace Git backend + api/workspace_git.py
  • Backend tests
  • docs/workspace-git.md
  • The safety hardening from the earlier review (scrub GIT_DIR/GIT_WORK_TREE/GIT_CONFIG_GLOBAL, preserve GIT_INDEX_FILE for selected-file commits, require HERMES_WEBUI_WORKSPACE_GIT_DESTRUCTIVE=1 for mutating ops)

That's a much better merge target than the combined 20-file PR — backend can land first under feature-flag-off-by-default, frontend comes through the second PR once we've validated the backend in isolation.

Closing this combined PR in favor of #2625 (backend) and the upcoming frontend draft. Branch will be deleted via the merge of #2625, no action needed on your end here.

@stocky789
stocky789 deleted the feat/workspace-git-controls branch May 26, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hold maintainer-review Maintainer fit-assessment needed — may not merge even with fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants