Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 40 additions & 8 deletions modules/agent-box.nix

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 40 additions & 8 deletions modules/src/settings-daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,17 +871,41 @@ def write_sessions(sessions, version=REGISTRY_VERSION):
_session_start_notices = {}


def ensure_claude_profile():
"""Offer the login worker in Add session, preserving existing profiles.

An explicit successful login can recreate a deleted starter profile;
ordinary supervisor seeding still respects its once-per-name stamp.
A name already used for another harness belongs to the user.
"""
if not PROFILE_BIN:
return None, []
index = 1
while True:
name = "claude" if index == 1 else "claude-%d" % index
path = profile_path(name)
with locked(path):
if not os.path.lexists(path):
save(path, [("HARNESS", "claude")], profile_header(name))
data = as_dict(load(path))
if data.get("HARNESS") == "claude":
resolved = profile_launch(name)
if resolved and resolved.get("harness") == "claude":
return name, [str(a) for a in (resolved.get("args") or [])]
return None, []
index += 1


def ensure_harness_session(agent, remote_control):
"""Auto-start one bare session for `agent` the moment its connect card
"""Auto-start one session for `agent` the moment its connect card
signs in (issue #504), so install+login leaves an actual running
session behind rather than just a signed-in CLI nobody has started yet.

Skipped if the user already has ANY session on that harness -- a repeat
sign-in (a token refresh, "Sign in again") must not mint a second one
every time the card cycles through "connected". No profile, no working
directory: this is the bare pseudo-profile shape `shell` already has
(see SHELL_PSEUDO_PROFILE), the one with no model, effort or prompt to
carry.
every time the card cycles through "connected". Claude gets a reusable
starter profile, with its launch arguments resolved just like Add
session. Codex retains its bare interactive session.

`remote_control` is the one thing that differs by harness. claude's rc
is a flag on the ordinary TUI (supervisor.sh appends --remote-control),
Expand All @@ -897,6 +921,14 @@ def ensure_harness_session(agent, remote_control):
"""
if agent not in AGENTS:
return
profile, args = None, []
if agent == "claude":
try:
profile, args = ensure_claude_profile()
except OSError:
# Profile storage failure must not undo a successful login or
# prevent its worker from starting.
pass
try:
with sessions_lock():
sessions, version = load_sessions()
Expand All @@ -911,8 +943,8 @@ def ensure_harness_session(agent, remote_control):
"remoteControl": remote_control,
"remoteControlName": None,
"workingDirectory": None,
"extraArgs": [],
"profile": None,
"extraArgs": args,
"profile": profile,
"initialPrompt": None,
"resumePrompt": None,
"boxSessionId": None,
Expand Down Expand Up @@ -2495,7 +2527,7 @@ def connect_state(flow, keys=None, tmux_state=None):
connect_cancel(flow_id)
running = False
state = "connected"
# One bare session per harness, started the moment sign-in
# One session per harness, started the moment sign-in
# lands (issue #504) -- codex gets an interactive worker
# session (remote_control=False) since pairing for
# phone/desktop already happened in this flow's own pane;
Expand Down
2 changes: 1 addition & 1 deletion tests/connect.nix
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ in
# claude's rc is a flag on the ordinary worker session, so the one
# auto-created session is both usable AND remote-visible.
assert claude_session["remoteControl"] is True, claude_session
assert claude_session["profile"] is None, claude_session
assert claude_session["profile"] == "claude", claude_session

with subtest("the card shows the code the CLI printed, not its prose"):
assert post("/agent/settings/connect/start", "flow=github") == "303"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1282,17 +1282,41 @@ def write_sessions(sessions, version=REGISTRY_VERSION):
_session_start_notices = {}


def ensure_claude_profile():
"""Offer the login worker in Add session, preserving existing profiles.

An explicit successful login can recreate a deleted starter profile;
ordinary supervisor seeding still respects its once-per-name stamp.
A name already used for another harness belongs to the user.
"""
if not PROFILE_BIN:
return None, []
index = 1
while True:
name = "claude" if index == 1 else "claude-%d" % index
path = profile_path(name)
with locked(path):
if not os.path.lexists(path):
save(path, [("HARNESS", "claude")], profile_header(name))
data = as_dict(load(path))
if data.get("HARNESS") == "claude":
resolved = profile_launch(name)
if resolved and resolved.get("harness") == "claude":
return name, [str(a) for a in (resolved.get("args") or [])]
return None, []
index += 1


def ensure_harness_session(agent, remote_control):
"""Auto-start one bare session for `agent` the moment its connect card
"""Auto-start one session for `agent` the moment its connect card
signs in (issue #504), so install+login leaves an actual running
session behind rather than just a signed-in CLI nobody has started yet.

Skipped if the user already has ANY session on that harness -- a repeat
sign-in (a token refresh, "Sign in again") must not mint a second one
every time the card cycles through "connected". No profile, no working
directory: this is the bare pseudo-profile shape `shell` already has
(see SHELL_PSEUDO_PROFILE), the one with no model, effort or prompt to
carry.
every time the card cycles through "connected". Claude gets a reusable
starter profile, with its launch arguments resolved just like Add
session. Codex retains its bare interactive session.

`remote_control` is the one thing that differs by harness. claude's rc
is a flag on the ordinary TUI (supervisor.sh appends --remote-control),
Expand All @@ -1308,6 +1332,14 @@ def ensure_harness_session(agent, remote_control):
"""
if agent not in AGENTS:
return
profile, args = None, []
if agent == "claude":
try:
profile, args = ensure_claude_profile()
except OSError:
# Profile storage failure must not undo a successful login or
# prevent its worker from starting.
pass
try:
with sessions_lock():
sessions, version = load_sessions()
Expand All @@ -1322,8 +1354,8 @@ def ensure_harness_session(agent, remote_control):
"remoteControl": remote_control,
"remoteControlName": None,
"workingDirectory": None,
"extraArgs": [],
"profile": None,
"extraArgs": args,
"profile": profile,
"initialPrompt": None,
"resumePrompt": None,
"boxSessionId": None,
Expand Down Expand Up @@ -2906,7 +2938,7 @@ def connect_state(flow, keys=None, tmux_state=None):
connect_cancel(flow_id)
running = False
state = "connected"
# One bare session per harness, started the moment sign-in
# One session per harness, started the moment sign-in
# lands (issue #504) -- codex gets an interactive worker
# session (remote_control=False) since pairing for
# phone/desktop already happened in this flow's own pane;
Expand Down
74 changes: 74 additions & 0 deletions tests/test-profile-panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,64 @@ def daemon(self, **extra):
return daemon_with(**env)


class LoginProfile(ProfileFixture):
def daemon(self, **extra):
module = super().daemon(**extra)
module.capacity_live = lambda: set()
module.capacity_limit = lambda: 100
return module

def test_login_creates_a_launchable_profile_and_remote_session(self):
module = self.daemon()
module.ensure_harness_session("claude", True)
session = self.read_sessions()["claude"]
self.assertEqual(session["profile"], "claude")
self.assertTrue(session["remoteControl"])
self.assertEqual(session["extraArgs"], [])
self.assertEqual(module.profile_launch("claude")["harness"], "claude")
self.assertIn('value="claude"', module.render_profile_options(module.read_profiles()))

def test_login_preserves_custom_settings_and_resolves_them(self):
self.write_profile("claude", "HARNESS=claude\nMODEL=sonnet\nTOKEN=private\n")
module = self.daemon()
module.ensure_harness_session("claude", True)
self.assertEqual(self.read_sessions()["claude"]["extraArgs"], ["--model", "sonnet"])
self.assertEqual(module.as_dict(module.load(module.profile_path("claude")))["TOKEN"], "private")

def test_existing_session_still_gets_a_profile_without_a_duplicate(self):
self.write_sessions({"existing": {"agent": "claude", "profile": None}})
module = self.daemon()
module.ensure_harness_session("claude", True)
module.ensure_harness_session("claude", True)
self.assertEqual(list(self.read_sessions()), ["existing"])
self.assertEqual(list(module.read_profiles()), ["claude"])

def test_another_harness_profile_is_not_overwritten(self):
self.write_profile("claude", "HARNESS=codex\n")
module = self.daemon()
module.ensure_harness_session("claude", True)
self.assertEqual(self.read_sessions()["claude"]["profile"], "claude-2")
self.assertEqual(module.profile_launch("claude")["harness"], "codex")

def test_profile_storage_failure_still_starts_the_worker(self):
module = self.daemon()
module.PROFILES_DIR = self.env_file
module.ensure_harness_session("claude", True)
self.assertIsNone(self.read_sessions()["claude"]["profile"])

def test_codex_login_keeps_its_existing_behavior(self):
module = self.daemon()
module.ensure_harness_session("codex", False)
self.assertIsNone(self.read_sessions()["codex"]["profile"])
self.assertFalse(self.read_sessions()["codex"]["remoteControl"])
self.assertEqual(module.read_profiles(), {})

def test_no_resolver_retains_the_login_worker(self):
module = self.daemon(AGENT_BOX_PROFILE_BIN="")
module.ensure_harness_session("claude", True)
self.assertIsNone(self.read_sessions()["claude"]["profile"])


class ProfilePanel(ProfileFixture):
"""What the panel shows."""

Expand Down Expand Up @@ -739,6 +797,22 @@ def test_an_unknown_verb_is_a_404_not_a_traceback(self):
status, _ = self.post(base, "/profiles/nope", name="ok")
self.assertEqual(status, 404)

def test_login_profile_can_launch_another_remote_session(self):
module, base = self.serve()
module.ensure_harness_session("claude", True)
status, _ = self.post(base, "/sessions/add", back="settings",
profile="claude", cwd="~", prompt="")
self.assertEqual(status, 303)
sessions = self.read_sessions()
self.assertEqual(len(sessions), 2)
for session in sessions.values():
self.assertEqual(session["profile"], "claude")
self.assertEqual(session["agent"], "claude")
self.assertTrue(session["remoteControl"])
self.assertEqual(session["extraArgs"], [])
module.ensure_harness_session("claude", True)
self.assertEqual(self.read_sessions(), sessions)

def test_adding_a_session_records_the_profile_name_not_just_its_args(self):
"""The load-bearing one. The spawn wrapper re-reads the NAME at
every start to apply the profile's environment (#337), so a
Expand Down