Recognize win32 as a Windows platform alias and wrap missing platform lock modules in SwarmLockUnavailableError instead of leaking raw ImportError.
Based-on: #34
Prefer OPENHARNESS_<PROVIDER>_API_KEY variables over provider-native globals while resolving API-key auth. Keep provider-native variables as fallback and avoid applying unrelated native keys across active profiles.
Forward the OpenHarness-scoped auth/provider environment variables to spawned teammates.
Based-on: #93
Forward OpenHarness config/data env vars to spawned teammates and apply CLI permission_mode overrides to Settings.permission.mode so full_auto survives sub-agent startup paths.
Fixes#274
Teammate spawn (`agent` tool, `task_create`) failed on Windows with exit
127 and "command not found" against a backslash-mangled Python path,
even though the same `bash -lc "..."` command worked interactively.
Root cause: `subprocess_backend.spawn` built a single shell command
string with a `KEY='val' python ...` env-prefix, then routed through
`bash -lc` via `asyncio.create_subprocess_exec`. Two failures stacked:
1. The env-prefix string used Python `repr()` for values, and Git
Bash's `-lc` argument parser consumed backslashes inside the
quoted segments as escape sequences (`\U` -> `U`).
2. Even with proper `shlex.quote` of every command part — keeping
backslashes intact — Git Bash launched via
`asyncio.create_subprocess_exec` could not exec a Windows-pathed
Python interpreter, returning `command not found` for the same
argv that interactive bash exec'd fine.
Fix: don't use a shell at all for teammate spawn.
* Add `argv: list[str] | None` to `TaskRecord` plus matching kwargs
on `BackgroundTaskManager.create_shell_task` /
`create_agent_task`. Either `command` (shell) or `argv` (direct
exec) must be supplied; both is rejected.
* `_start_process` runs the argv path via
`asyncio.create_subprocess_exec(*argv, env=...)` directly, with no
bash wrapper.
* `subprocess_backend.spawn` builds an argv list (via
`get_teammate_command()` plus inherited CLI flags) and hands it to
`create_agent_task(argv=..., env=...)`. Inherited env vars now
flow through `env=` rather than being embedded in the command
string.
* The legacy shell-evaluated `command=` path is preserved verbatim
for callers (e.g. `BashTool`) that legitimately want shell
semantics — only teammate spawn migrates to direct exec.
Tests: 5 new unit tests in `tests/test_swarm/test_subprocess_backend.py`
and 3 in `tests/test_tasks/test_manager.py` lock in the contract — env
plumbed via `env=` kwarg, argv list preserves Windows backslashed
paths, command/argv mutual exclusion, direct-exec round-trip.
Validated end-to-end on Windows 11 against a real CEO/Lead workflow:
CEO successfully calls `agent` tool, Lead spawns at depth=1 and
issues 29 tool calls. Same path was producing exit-127 task logs
before this change.
Both built-in agents previously set model="haiku", which caused
subprocess spawning to fail for users running any non-Anthropic provider
(OpenAI, Bedrock, custom base URLs, etc.) because the literal string
"haiku" is not a valid model on those APIs.
Changes:
- Explore and claude-code-guide now use model="inherit", consistent
with the existing Plan and verification built-in agents.
- build_inherited_cli_flags now skips the --model flag when the value
is "inherit", so the subprocess inherits the parent model via the
OPENHARNESS_MODEL env var that build_inherited_env_vars already
forwards. Previously "inherit" was passed verbatim as a model name,
which would have also broken Plan and verification agents.
Tests added:
- build_inherited_cli_flags: model="inherit" and model=None produce no
--model flag; a real model name is included as expected.
- Builtin agent definitions: Explore and claude-code-guide must not
reference Anthropic-only model aliases; Plan, verification, Explore,
and claude-code-guide must all use None or "inherit".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Files under ~/.openharness/ — credentials, settings, session snapshots,
cron registry, memory index — were written with `Path.write_text()` in
truncating mode. A crash, SIGKILL, power loss, or out-of-disk error
during the write leaves a truncated file on disk; concurrent writers
clobber each other's updates; and the credentials file spent a brief
window at the default umask mode (commonly 0o644) before chmod-to-0600
ran.
Introduce `openharness.utils.fs.atomic_write_text` / `atomic_write_bytes`
which write to a same-directory temp file, fsync, apply the target mode
while the file is still private, and `os.replace` into place. Thread
them through all persistence writers. For read-modify-write on shared
files (credentials, settings, cron, memory index), pair atomic writes
with the existing `exclusive_file_lock` primitive so two `oh` processes
no longer race.
The generic lock helper moves from `openharness.swarm.lockfile` to
`openharness.utils.file_lock`. `swarm.lockfile` is retained as a thin
re-export so existing callers keep working.
Co-authored-by: José Maia <glitch-ux@users.noreply.github.com>
The _READ_ONLY_TOOLS frozenset used PascalCase names (e.g. "Read",
"Glob", "WebFetch") carried over from the TypeScript original, but the
Python tool registry registers tools with snake_case names (e.g.
"read_file", "glob", "web_fetch"). This meant _is_read_only() never
matched any tool, so handle_permission_request() never auto-approved
read-only operations — every tool call required explicit permission
evaluation from the leader, including safe read-only tools.
Update _READ_ONLY_TOOLS to use the actual registered tool names and
align the test parametrizations accordingly.