fix(windows): stop os.fchmod crash, correct Desktop config path, add Windows CI#122
Merged
Conversation
mureo crashed on Windows: os.fchmod is Unix-only (AttributeError) in every credential/config write path. And the configure WebUI used the wrong Claude Desktop config location on Windows. - New mureo/fsutil.py: secure_fchmod / secure_chmod — apply owner-only 0o600 on POSIX (byte-identical to before, no Linux/macOS regression), best-effort never-raising no-op on Windows. NTFS confidentiality relies on the %USERPROFILE% profile ACL (documented best-effort, not a silent regression). - Replace all 6 os.fchmod / os.chmod(...,0o600) sites: auth (_save_meta_token), providers.config_writer, cli.settings_remove, auth_setup, web._helpers, mcp.plugin_audit. Drop now-unused imports (contextlib in plugin_audit, os in auth_setup). - host_paths: add Windows branch — Claude Desktop config is %APPDATA%\Claude\claude_desktop_config.json (~/AppData/Roaming/...). macOS unchanged; Linux keeps the Code-style fallback (Claude Desktop has no Linux build). Verified by simulation (no Windows box): test_fsutil.py removes os.fchmod to emulate Windows; test_web_host_paths.py patches platform.system()='Windows'. Clean-env full suite 3262 passed; ruff/black clean; two code-reviewer passes APPROVE (no CRIT/HIGH/MED). Out of scope (not crashes): mureo install-desktop is still macOS-only by explicit design (needs a Windows launcher); no Windows CI yet (added next).
No Windows dev machines exist; the os.fchmod crash fix and the %APPDATA% Claude Desktop path are otherwise only simulated on Linux. Single Python 3.12, no coverage (ubuntu job owns that) — a compat tripwire that runs the full test suite on real Windows.
The new test-windows job surfaced 37 real-Windows failures. Fixes:
Production (1):
- auth_setup: simple_term_menu imports Unix-only termios and raises
NotImplementedError (NOT ImportError) on Windows, so the existing
plain number-input fallback was unreachable (≈14 tests crashed with
'Windows is currently not supported'). Widen both
'except ImportError' → 'except (ImportError, NotImplementedError)'.
Also degrades gracefully in non-terminal envs (CI/pipe/PyCharm)
instead of crashing.
Test portability (POSIX-only assertions made platform-aware; product
behaviour unchanged, POSIX byte-identical):
- core/skills/test_models: hardcoded Path('/tmp/...') (not absolute on
Windows) → OS-anchored _abs() helper; relative negative test kept.
- test_fsutil / test_auth_setup / test_auth_setup_meta: skipif win32
on the 0o600-mode assertions (NTFS perms documented best-effort;
never-raise still tested on Windows).
- test_desktop_installer: skipif win32 on 3 POSIX .sh-wrapper/exec-bit
tests (install-desktop is macOS-only by design).
- test_web_handlers: spoofed-Host tests accept ConnectionError on
win32 (server closes socket before 403 → WinError 10053); POSIX
still strictly requires HTTP 403.
code-reviewer APPROVE (no CRIT/HIGH). POSIX suites green; tests/ ruff
not CI-gated, mureo/ ruff+black clean.
…h shim
test-windows (2nd run) left 20 failures in two clusters:
- tests/core/skills/test_matcher.py: the shared SkillEntry builder
hardcoded Path('/tmp/skills/{name}/SKILL.md') (not absolute on
Windows). Same OS-anchored fix already approved for test_models.py.
- tests/test_auth_setup.py: 12 tests patch
'simple_term_menu.TerminalMenu'; unittest.mock.patch must import
simple_term_menu to resolve the target, which raises
NotImplementedError on Windows (Unix-only termios) BEFORE the
production widened except runs. Add a win32-only sys.modules stub
(only when the real package can't import) so the patch target
resolves and the existing tests run on Windows as on POSIX. Pure
test-harness shim: real Windows product has no simple_term_menu and
uses the numeric fallback (the per-test patch overrides the stub).
POSIX byte-identical (whole shim under sys.platform=='win32').
code-reviewer APPROVE (no CRIT/HIGH/MED). POSIX suites green;
mureo/ ruff+black clean (no production change this round).
Merged
hyoshi
added a commit
that referenced
this pull request
May 19, 2026
Post-merge main CI (run 26077780980) flaked here: the test sampled stop_event.is_set() immediately after the /api/shutdown response, racing the server thread that sets the event (passed on PR #122 and PR #123 with identical code). stop_event is a threading.Event — wait for it with a bounded 5s timeout instead of sampling once. Same property asserted; still fails (with a clear message) if /api/shutdown never triggers stop. Bundled into the v0.9.3 release branch so the released main is robust against this recurring flake.
hyoshi
added a commit
that referenced
this pull request
May 19, 2026
* chore(release): v0.9.3 — Windows compatibility (#122) * fix(test): deflake test_api_shutdown_route_triggers_stop Post-merge main CI (run 26077780980) flaked here: the test sampled stop_event.is_set() immediately after the /api/shutdown response, racing the server thread that sets the event (passed on PR #122 and PR #123 with identical code). stop_event is a threading.Event — wait for it with a bounded 5s timeout instead of sampling once. Same property asserted; still fails (with a clear message) if /api/shutdown never triggers stop. Bundled into the v0.9.3 release branch so the released main is robust against this recurring flake.
Merged
5 tasks
hyoshi
added a commit
that referenced
this pull request
May 21, 2026
…, and throttle backends (#125) * feat(core): add extension protocols for credentials, state, knowledge, and throttle backends Introduce five new Protocol/dataclass types under mureo.core so call sites can later accept pluggable backends (in-memory for tests, alternate stores such as keychains, SQLite, Redis, or hosted file APIs) without touching the legacy file-based defaults. This commit is purely additive — no existing module imports the new types yet, so behaviour is unchanged for every current caller. Follow-up commits will wire concrete default implementations and refactor the existing helpers in mureo.auth, mureo.context, and mureo.mcp to consume the Protocols. * SecretStore — credential round-trip with idempotent delete * StateStore — STATE.json / STRATEGY.md / action-log persistence * KnowledgeStore — two-tier /learn knowledge with operator + workspace * ThrottleStore — async per-key rate-limit gate matching today's mureo.throttle.Throttler shape * RuntimeContext — frozen aggregate, validates non-empty workspace_id Tests: 27 new structural-contract tests in tests/core/; all 335 tests under tests/core/ pass; no existing files modified. * feat(core): add filesystem-backed default implementations for the four extension protocols Add the in-process default implementations of the Protocols introduced in c9713d2: * FilesystemSecretStore — JSON file under ~/.mureo/credentials.json; atomic write via tempfile + os.replace, secure_fchmod for 0o600 on POSIX, ensure_ascii=False to match the legacy file format * FilesystemStateStore — composes the existing helpers in mureo.context.state and mureo.context.strategy so behaviour matches today's CWD-relative call sites verbatim * FilesystemKnowledgeStore — Markdown files under ~/.claude/skills/_mureo-pro-diagnosis/; seeds the same frontmatter scaffold that skills/learn/SKILL.md uses today (drift caught by a regression test) * ProcessLocalThrottleStore — per-key dict of mureo.throttle.Throttler; register(key, config) pre-installs custom buckets matching the MCP server's _PLUGIN_TOOL_THROTTLERS pattern Still purely additive. No existing module imports the new classes yet; behaviour is unchanged for every current caller. The follow-up commit will add the RuntimeContext default factory and re-export the public surface from mureo.core. Tests: 37 new tests in tests/core/ (35 implementation + scaffold drift guard + register custom config); all 372 tests/core/ tests pass; 124 related existing tests (auth/state/strategy/throttle/mcp_context/ fsutil) still pass. * feat(core): add default_runtime_context factory and export public surface * mureo.core.runtime_context.default_runtime_context() wires the four file-backed defaults from c9713d2/3d57367 into a single RuntimeContext; every parameter is keyword-only and optional, falling back to the legacy file locations callers already see today. * DEFAULT_WORKSPACE_ID = "default" promoted to a module-level constant and exported so consumers compare against a named sentinel rather than an inline literal. * mureo.core.__init__.py grows from empty to a curated public surface: 10 Protocols / defaults / RuntimeContext / factory / sentinel, locked by __all__ and a contract test that pins both the positive surface and the negative (providers/skills sub-packages are NOT re-exported). Lint cleanup bundled in: TYPE_CHECKING-only imports for Path, ThrottleConfig, and the three context.models dataclasses; black formatting for runtime_context.py and knowledge_store.py; HOME-env monkeypatching replaced with direct Path.home patching across three tests so the new Windows CI lane (#122) stays green. Tests: 9 new tests (5 factory + 4 public-API); all 382 tests/core pass; related existing tests (auth/state/strategy/throttle/fsutil/cli_providers = 136) still pass; ruff and black clean on mureo/. * feat(core): add entry-point resolver for pluggable RuntimeContext factories Introduce mureo.core.runtime_context.get_runtime_context() — the integration point alternate backends use to inject a custom RuntimeContext without changing call sites. A third-party distribution registers a zero-arg callable under the "mureo.runtime_context_factory" entry-point group; the resolver picks it up automatically. Resolution rules: * 0 entry points: returns default_runtime_context() (today's behaviour for every existing caller). * 1 entry point: loads and calls it; the returned RuntimeContext is cached for the process lifetime. * >1 entry points: raises RuntimeContextFactoryError. Unlike mureo.core.providers.registry (additive, first-wins+warning), the RuntimeContext is a process singleton — silently picking one would hide a packaging bug. Only successfully-constructed contexts are cached; a broken plugin re-runs ep.load() per call so a fix-in-place is visible without a process restart. Public additions to mureo.core: RUNTIME_CONTEXT_FACTORY_ENTRY_POINT_GROUP, RuntimeContextFactoryError, get_runtime_context, reset_runtime_context. Still purely additive: no consumer calls get_runtime_context() yet. Wiring of mureo.auth / mureo.context / mureo.mcp follows in subsequent commits. Tests: 9 new tests in tests/core/test_runtime_resolver.py covering 0/1/multi entry points, cache hit/reset, type validation, factory exception wrapping, and public-API export. 391 tests/core total pass; ruff and black clean on mureo/. * refactor(auth): route load_*_credentials through the SecretStore Protocol `load_google_ads_credentials` and `load_meta_ads_credentials` now read their credential dict from a `SecretStore` instead of calling the file-direct `load_credentials` helper. Resolution rules: * `path=` given → one-shot `FilesystemSecretStore(path=path)`. Bypasses the process-wide `RuntimeContext` so the long-standing per-test credentials file pattern (`tests/test_auth.py`) keeps working unchanged and stays isolated from any installed alternate backend. * `path=None` → `get_runtime_context().secret_store`. In OSS today that is the file-backed default reading `~/.mureo/credentials.json` — byte-for-byte identical to the prior behaviour for every existing caller. When an alternate backend registers a `mureo.runtime_context_factory` entry point (added in c96e99b), the credential read transparently routes through it. `load_credentials` is intentionally kept unchanged: it is part of the public surface and may have out-of-tree callers; in-repo callers are limited to `tests/test_auth.py` and the two helpers above. Removing it would be a separate deprecation. Behaviour change worth calling out: the prior code distinguished "section missing" (returns `None` → falls to env) from "section present but invalid". The Protocol contract returns `{}` for both, so the helpers now also skip empty dicts (`isinstance(..., dict) and section`) to preserve the env-var fallback path. Verified by the existing env-fallback tests in `tests/test_auth.py`. Tests: 4 new tests in `tests/test_auth_runtime_context.py` covering runtime-context routing, explicit-path bypass, and env-fallback when the resolved store is empty. All 26 existing `tests/test_auth.py` tests pass without modification; `tests/core/` 391 tests pass; ruff and black clean on `mureo/`. Test-isolation note: the new test file patches `_cached_context` directly via `monkeypatch.setattr` and uses an autouse fixture to `reset_runtime_context()` before and after each test, so the resolver cache cannot bleed state into other test files. * refactor(mcp): route mureo_context handlers through StateStore workspace The five `mureo_state_*` / `mureo_strategy_*` MCP handlers now resolve paths against the active `StateStore`'s workspace rather than the raw process CWD. With the default file-backed runtime the two are equal, so behaviour is unchanged for every existing caller. When an alternate backend registers via the `mureo.runtime_context_factory` entry-point group (added in c96e99b), the handlers read and write under that backend's workspace transparently. What changed in `mureo/mcp/_handlers_mureo_context.py`: * `_resolve_path` now takes an optional `store_attr` ("state_path" or "strategy_path"). When the `path` argument is missing or empty, it returns the backend-owned path if exposed, otherwise `workspace / default_name`. When `path` is supplied, it resolves relative to the workspace (not raw CWD) and re-runs the symlink-following boundary check against that workspace. * The error message becomes "Refusing to read/write outside workspace" (was "outside cwd") so the security boundary is correctly named. * Removed the now-unused `_opt` import; added `get_runtime_context` import. Empty-string `path` argument behaviour: the old `_opt`-based code dispatched empty strings to `Path(".")` which silently meant CWD. The new code treats `None` and `""` identically as "no override", which is safer and matches what callers actually want. Test infrastructure: * `tests/test_mcp_tools_mureo_context.py` gains an autouse fixture `_clear_runtime_context_cache` that calls `reset_runtime_context()` before and after every test, so the per-test `monkeypatch.chdir` is observed by `_resolve_path` instead of a stale `FilesystemStateStore` cached by an earlier test. * `test_path_argument_refuses_traversal` is updated to match the new error message and includes a docstring explaining why the workspace equals CWD in the default configuration. * New `test_default_path_follows_runtime_context_workspace` injects a workspace different from CWD via `_cached_context` and asserts the action-log append lands in the injected workspace (and NOT in CWD). Tests: 14 in `tests/test_mcp_tools_mureo_context.py` pass (13 existing + 1 new); `tests/core/` 391 pass; `tests/test_auth.py` + `tests/test_auth_runtime_context.py` 30 pass. ruff and black clean on `mureo/`. * refactor(mcp): route rollback handlers through StateStore workspace `mureo/mcp/_handlers_rollback.py`'s `_resolve_state_file` now resolves the user-supplied `state_file` argument against the active StateStore's workspace rather than the raw process CWD. Mirrors the refactor applied to `_handlers_mureo_context.py` in 4f5fcd7. With the default file-backed runtime the two are equal, so behaviour is unchanged for every existing caller. When an alternate backend registers via the `mureo.runtime_context_factory` entry-point group (added in c96e99b), the rollback surface reads STATE.json under the backend's workspace transparently. Differences from the mureo_context refactor: * Argument name is `state_file` (not `path`) — kept for API stability. * The state-path attribute is hardcoded to `state_path` (not parameterised through `store_attr`) because rollback only reads STATE.json, never STRATEGY.md. * The error message becomes "inside the active workspace" (was "inside the current working directory"). * Backend-owned `state_path` is `.resolve()`d so a relative path from a custom backend cannot trip `execute_rollback`'s CWD-relative interpretation downstream. Test infrastructure: `tests/test_mcp_tools_rollback.py` gains the same autouse `_clear_runtime_context_cache` fixture introduced for mureo_context (`4f5fcd7`); the existing `sandboxed_cwd` fixture is re-documented to note the autouse runs first. `test_path_traversal_refused` asserts the updated error message and gains a docstring explaining why workspace == CWD in the default configuration. Tests: 9 in `tests/test_mcp_tools_rollback.py` pass; 23 in `tests/test_mcp_tools_rollback.py` + `tests/test_mcp_tools_mureo_context.py` combined pass (no cache cross-contamination). 440 tests across `tests/core/`, `tests/test_auth*.py`, and the two refactored handler files pass; ruff and black clean on `mureo/`. * refactor(mcp,cli): route analysis handler and rollback CLI through StateStore workspace Two more consumers of STATE.json path resolution now honour the active StateStore's workspace rather than the raw process CWD. With the default file-backed runtime the two are equal, so behaviour is unchanged for every existing caller. An alternate backend registered via the `mureo.runtime_context_factory` entry-point group (added in c96e99b) redirects both surfaces transparently. mureo/mcp/_handlers_analysis.py * `_resolve_state_file` mirrors the workspace-based pattern applied to the rollback handler in 931d93a, with one important addition: the long-standing symlink refusal is preserved (a symlink anywhere in the path chain inside the workspace is rejected because the analysis surface returns derived metrics that an attacker could influence by swapping the symlink target mid-call). * Error message becomes "inside the active workspace" (was "inside the current working directory"); module docstring updated to match. * Backend-owned `state_path` is `.resolve()`d so a relative path from a custom backend cannot trip downstream callers. mureo/cli/rollback_cmd.py * `--state-file` Typer option default flips from `Path("STATE.json")` (CWD-relative at command-invocation time) to `None`. A new helper `_resolve_default_state_file()` resolves the missing value at command time through `get_runtime_context().state_store`. * `rollback_list` and `rollback_show` accept `Path | None` and resolve the default at the top of the body. Callers that pass `--state-file` explicitly see no behaviour change. Test infrastructure: * `tests/test_mcp_tools_analysis.py` gains the autouse cache-reset fixture and asserts the updated workspace-aware error message. * `tests/test_cli_rollback.py` gains the autouse cache-reset fixture and a new `TestStateFileDefault` class with two cases that cover the workspace-default path: - `test_default_resolves_to_cwd_workspace`: omitted `--state-file` + CWD STATE.json → entry found - `test_default_resolves_via_injected_runtime_context`: workspace ≠ CWD via `_cached_context` injection → CLI reads workspace STATE.json, not CWD STATE.json * Stale module docstrings in handler and test fixed to reference workspace, not CWD. Tests: 464 pass across `tests/test_cli_rollback.py` (14), `tests/test_mcp_tools_analysis.py` (13), `tests/test_mcp_tools_rollback.py` (9), `tests/test_mcp_tools_mureo_context.py` (14), `tests/test_auth*` (30), and `tests/core/` (391). ruff and black clean on `mureo/`. * refactor(mcp): route plugin dispatch throttle through ThrottleStore The plugin-dispatch branch in `handle_call_tool` now acquires its throttle slot via `get_runtime_context().throttle_store` instead of the module-level `_PLUGIN_TOOL_THROTTLERS` / `_PLUGIN_THROTTLER` attributes directly. An alternate ThrottleStore backend registered via `mureo.runtime_context_factory` can intercept every plugin call without each handler having to change. Default file-backed runtime: byte-equivalent semantics. * New helper `_acquire_plugin_throttle(name)` performs a lazy, idempotent seeding step on the resolved `ProcessLocalThrottleStore`: the module-level per-tool `Throttler` instances from `_PLUGIN_TOOL_THROTTLERS` are copied (by reference) into `store.throttlers`, and `_PLUGIN_THROTTLER` is installed under the sentinel `_PLUGIN_DEFAULT_BUCKET`. Known names go through their own bucket via `store.acquire(name)`; unknown names go through the shared default bucket directly. * The legacy module attributes `_PLUGIN_THROTTLER` and `_PLUGIN_TOOL_THROTTLERS` are kept verbatim because (a) plugin semantics derivation at module load still populates them and (b) existing tests monkey-patch them to inject spies; the seeding helper reuses those exact instances so the contract is unchanged. Alternate backends (anything other than `ProcessLocalThrottleStore`): the seeding step is skipped and `store.acquire(name)` is called once. The backend owns the full per-key + unknown-name fallback semantics; this is documented explicitly in the helper docstring so backend authors know to implement their own shared-default bucket if they want one. Tests: * `tests/test_mcp_server_plugin_wiring.py::test_throttle_acquired_before_dispatch` now patches `_PLUGIN_THROTTLER`, clears the seeding cache (`_throttle_store_seeded`), and resets the RuntimeContext resolver so the next dispatch re-seeds with the spy throttler. The contract remains: "the plugin dispatch path must await a throttle slot before calling the provider". * `test_declared_throttle_gets_dedicated_bucket` is unchanged — it asserts the module-level `_PLUGIN_TOOL_THROTTLERS` still contains a dedicated bucket per declared throttle, which is true because that dict is still populated at module load. 12 tests in `tests/test_mcp_server_plugin_wiring.py` pass; 484 tests across `tests/core/`, the four refactored handler test files, `tests/test_auth*`, and `tests/test_throttle.py` pass. ruff and black clean on `mureo/`. * refactor(byod): consult RuntimeContext for non-default workspace dir `byod_data_dir()` gains a middle-priority resolution path: when the resolved `RuntimeContext` exposes a non-default workspace (alternate backend registered via the `mureo.runtime_context_factory` entry-point group), BYOD data is read from `<workspace>/byod/` instead of the legacy `~/.mureo/byod/`. Resolution precedence: 1. `MUREO_BYOD_DIR` env var — unchanged, highest priority (install- desktop wrapper still sets this per-workspace). 2. Non-default `RuntimeContext.state_store.workspace` — NEW. 3. Legacy `~/.mureo/byod/` — preserved for existing CLI / Claude Code single-workspace users. The middle path triggers only when `ctx.workspace_id != "default"` so existing OSS users see no behaviour change. `workspace_id` is the canonical sentinel pinned by `tests/core/test_runtime_context.py`. The helper `_runtime_context_workspace()` is added next to `byod_data_dir()` with a lazy import of `mureo.core.runtime_context` so the module keeps its "no external dependencies" promise (the module docstring already states this so the MCP server's startup path stays light). An ImportError is treated as "no runtime layer" defensively, although the lazy import is expected to succeed in practice. Tests: 489 pass across `tests/test_byod*.py` (49), `tests/core/` (391), and `tests/test_auth*.py` (49). ruff and black clean on `mureo/`. * feat(cli,skills): route /learn through the KnowledgeStore Protocol Introduce `mureo learn add <text> [--scope {operator,workspace}]`, a new CLI command that persists diagnostic insights via `get_runtime_context().knowledge_store` instead of writing files directly. The `/learn` skill is updated to shell out to this command so the file-system layout (and the scaffold seeding) becomes an implementation detail of `FilesystemKnowledgeStore` rather than a duplicated piece of skill markdown. New CLI: `mureo/cli/learn_cmd.py` * `learn add <text>` appends to the operator (cross-workspace) tier by default — the existing pro-diagnosis location for the default file-backed runtime. * `--scope workspace` appends to the workspace tier; when the resolved KnowledgeStore has no workspace tier (the OSS default), the command exits non-zero with a helpful hint pointing at `--scope operator`. This is the integration point alternate backends use to split per-workspace insights from operator-wide ones. * Wired into the top-level `mureo` app via `mureo/cli/main.py`. Skill update: `skills/learn/SKILL.md` * Version bumped 0.7.1 → 0.8.0. * Step 2 (manual file-existence-check + scaffold seeding) is gone — the CLI's KnowledgeStore handles both transparently. * Step 4 now instructs Claude to invoke `mureo learn add` instead of using Write/Edit on the file directly. * IMPORTANT note rewritten: "Always save through `mureo learn add`, never by writing the file path manually." Test changes: * New `tests/test_cli_learn.py` (5 tests): default-scope writes to operator tier, explicit operator scope, workspace scope when the backend supports it, workspace scope without backend support exits with `--scope operator` hint, and the subcommand is registered under the top-level CLI. * `tests/core/test_filesystem_knowledge_store.py::test_scaffold_matches_skills_learn_template` is replaced by `test_scaffold_has_expected_frontmatter_and_section`. The previous drift-vs-skill regression test no longer applies because the skill markdown no longer carries its own copy of the scaffold; the new test instead pins the shape of `_OPERATOR_SCAFFOLD` directly (frontmatter starts, expected name, ends with the Learned Insights heading + newline). 407 tests pass across `tests/core/`, `tests/test_cli_learn.py`, and `tests/test_cli_rollback.py`. ruff and black clean on `mureo/`. * fix(skills): sync packaged copy of learn skill with canonical source The canonical skills/learn/SKILL.md was updated in c51b84f to route through 'mureo learn add' (KnowledgeStore Protocol), but the packaged mureo/_data/skills/learn/SKILL.md was not synced. The test_packaged_skills_match_canonical_byte_for_byte invariant requires both to be byte-identical so PyPI users see the same docs as the GitHub canonical source. Tests: tests/test_plugin_manifests.py + tests/test_setup_cmd.py + tests/test_cli_learn.py + tests/core/ — 414 pass, ruff and black clean on mureo/.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
mureo crashed on Windows:
os.fchmodis Unix-only (AttributeError) in every credential/config write path. The configure WebUI also used the wrong Claude Desktop config location on Windows. No Windows CI existed, so this was invisible.Fix
mureo/fsutil.py—secure_fchmod/secure_chmod: owner-only0o600on POSIX (byte-identical to before — no Linux/macOS regression), best-effort never-raising no-op on Windows. NTFS confidentiality relies on the%USERPROFILE%profile ACL (documented best-effort).os.fchmod/os.chmod(...,0o600)sites:auth._save_meta_token,providers.config_writer,cli.settings_remove,auth_setup,web._helpers,mcp.plugin_audit. Dropped now-unused imports.host_paths: added Windows branch — Claude Desktop config is%APPDATA%\Claude\claude_desktop_config.json. macOS unchanged; Linux keeps the Code-style fallback (Claude Desktop has no Linux build).test-windows(windows-latest, Py 3.12) job — the real-Windows verification, since the fixes were otherwise only simulated on Linux.Verification
test_fsutil.pyremovesos.fchmod(emulates Windows);test_web_host_paths.pypatchesplatform.system()='Windows'.test-windowsjob is the first real-Windows run.Out of scope (not crashes)
mureo install-desktopCLI is still macOS-only by explicit design (needs a Windows launcher — separate feature; it errors gracefully today).0o600is best-effort (documented).