mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 04:36:24 +00:00
Fix untrusted-workspace wedge: re-seed claude trust at every launch
Phase 4 (1517e4d) deleted the tmux launch path and with it the only
caller of claude_config.ensure_onboarded, so agent working dirs -
every fresh worktree is a brand-new path - were never pre-trusted in
~/.claude.json. Headless 'claude -p' runs then wedge or refuse on the
workspace-trust dialog with nobody at a TTY to accept it.
Spawn and resume now mark onboarding complete and trust the working
dir right before launch, next to the settings/claude_gen
materialization. Resume matters independently: a cross-worker resume
can land in a container whose ~/.claude.json has never seen the dir.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01731mKtVzsfeT4Vi3TvkR48
This commit is contained in:
@@ -6,6 +6,16 @@ the image workflows publish (plus `latest` from every push to `main`).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Untrusted-workspace wedge on headless runs.** Phase 4's tmux-path deletion also
|
||||||
|
removed the only call to `claude_config.ensure_onboarded`, so agent working dirs —
|
||||||
|
every fresh worktree — were never pre-trusted in `~/.claude.json` and headless
|
||||||
|
`claude -p` runs wedged or refused on the trust dialog with nobody at a TTY. Spawn
|
||||||
|
and resume now re-seed onboarding + per-directory trust before every launch (resume
|
||||||
|
included, so a cross-worker resume landing in a container that has never seen the
|
||||||
|
working dir is covered). 2 regression tests.
|
||||||
|
|
||||||
### Added — mobile app feature parity
|
### Added — mobile app feature parity
|
||||||
|
|
||||||
The iOS app (`app/`) catches up with everything the backend and web dashboard gained
|
The iOS app (`app/`) catches up with everything the backend and web dashboard gained
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from ..config import get_settings
|
|||||||
from ..db import repository as repo
|
from ..db import repository as repo
|
||||||
from ..db.engine import connection
|
from ..db.engine import connection
|
||||||
from . import (
|
from . import (
|
||||||
|
claude_config,
|
||||||
claude_gen,
|
claude_gen,
|
||||||
credentials,
|
credentials,
|
||||||
forge,
|
forge,
|
||||||
@@ -160,6 +161,11 @@ def spawn(
|
|||||||
# half also feeds pi-harness agents (their settings.json points at the same dir).
|
# half also feeds pi-harness agents (their settings.json points at the same dir).
|
||||||
# Scoped to the project's owner: shared rows plus theirs, nobody else's.
|
# Scoped to the project's owner: shared rows plus theirs, nobody else's.
|
||||||
claude_gen.apply(working_dir, visible_to=project.get("owner_user_id"))
|
claude_gen.apply(working_dir, visible_to=project.get("owner_user_id"))
|
||||||
|
# Mark onboarding complete and trust this working dir in ~/.claude.json before
|
||||||
|
# claude boots: a fresh worktree is a brand-new path, and an untrusted workspace
|
||||||
|
# wedges/refuses a headless run with nobody at a TTY to accept the dialog. (The
|
||||||
|
# old tmux launch path did this; it was lost when phase 4 deleted that path.)
|
||||||
|
claude_config.ensure_onboarded(working_dir)
|
||||||
env, harness = _agent_env(project, agent, token, role=role, mise_init=mise_init)
|
env, harness = _agent_env(project, agent, token, role=role, mise_init=mise_init)
|
||||||
|
|
||||||
# Verify the pinned forge version, if one is configured. Non-fatal: a version drift
|
# Verify the pinned forge version, if one is configured. Non-fatal: a version drift
|
||||||
@@ -286,6 +292,9 @@ def resume(agent: dict, answer: str, worker_id: str | None = None) -> tuple[bool
|
|||||||
working_dir = agent["working_dir"]
|
working_dir = agent["working_dir"]
|
||||||
settings_path = settings_gen.write_settings(working_dir)
|
settings_path = settings_gen.write_settings(working_dir)
|
||||||
claude_gen.apply(working_dir, visible_to=project.get("owner_user_id"))
|
claude_gen.apply(working_dir, visible_to=project.get("owner_user_id"))
|
||||||
|
# Cross-worker resume may land in a container whose ~/.claude.json has never seen
|
||||||
|
# this working dir — re-seed trust exactly as spawn does.
|
||||||
|
claude_config.ensure_onboarded(working_dir)
|
||||||
try:
|
try:
|
||||||
token = None
|
token = None
|
||||||
with connection() as conn:
|
with connection() as conn:
|
||||||
|
|||||||
@@ -181,3 +181,38 @@ def test_resume_refused_while_run_live(env, fake_launch):
|
|||||||
ok, detail = spawn.resume(agent, "answer")
|
ok, detail = spawn.resume(agent, "answer")
|
||||||
assert ok is False
|
assert ok is False
|
||||||
assert "live run" in detail
|
assert "live run" in detail
|
||||||
|
|
||||||
|
|
||||||
|
def test_spawn_trusts_working_dir_in_claude_json(env, fake_launch):
|
||||||
|
"""The launch must pre-trust the agent's working dir in ~/.claude.json — an
|
||||||
|
untrusted workspace wedges a headless run on the trust dialog with nobody at a
|
||||||
|
TTY (regression: the call was lost when phase 4 deleted the tmux launch path)."""
|
||||||
|
root = env["tmp"] / "proj"
|
||||||
|
_write_mise(root, with_test=True)
|
||||||
|
_register_project(root)
|
||||||
|
|
||||||
|
spawn.spawn("proj", "api", task="build the thing")
|
||||||
|
|
||||||
|
cfg = json.loads((env["tmp"] / ".claude.json").read_text())
|
||||||
|
assert cfg["hasCompletedOnboarding"] is True
|
||||||
|
entry = cfg["projects"][str(root)]
|
||||||
|
assert entry["hasTrustDialogAccepted"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_resume_trusts_working_dir_in_claude_json(env, fake_launch):
|
||||||
|
"""Cross-worker resume may run in a container that has never seen this working
|
||||||
|
dir; resume must re-seed trust exactly as spawn does."""
|
||||||
|
root = env["tmp"] / "proj"
|
||||||
|
_write_mise(root, with_test=True)
|
||||||
|
_register_project(root)
|
||||||
|
spawn.spawn("proj", "api", task="do it")
|
||||||
|
with get_engine().begin() as conn:
|
||||||
|
agent = repo.get_agent_by_name(conn, "proj", "api")
|
||||||
|
repo.finish_run(conn, repo.get_latest_run(conn, agent["id"])["id"], "completed")
|
||||||
|
(env["tmp"] / ".claude.json").unlink() # a "fresh container": no config at all
|
||||||
|
|
||||||
|
ok, _ = spawn.resume(agent, "use Postgres")
|
||||||
|
|
||||||
|
assert ok is True
|
||||||
|
cfg = json.loads((env["tmp"] / ".claude.json").read_text())
|
||||||
|
assert cfg["projects"][str(root)]["hasTrustDialogAccepted"] is True
|
||||||
|
|||||||
Reference in New Issue
Block a user