agentic-runner 3.29.0__tar.gz → 3.30.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/PKG-INFO +2 -2
  2. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/pyproject.toml +1 -1
  3. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/__init__.py +1 -1
  4. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/auth_mode.py +3 -2
  5. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/cli.py +63 -4
  6. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/service.py +25 -8
  7. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/sign_in.py +8 -15
  8. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +1 -1
  9. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workstation.py +54 -5
  10. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/.gitignore +0 -0
  11. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/LICENSE +0 -0
  12. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/README.md +0 -0
  13. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/activities.py +0 -0
  14. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/attempts.py +0 -0
  15. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/build.py +0 -0
  16. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/callback.py +0 -0
  17. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/child_watcher.py +0 -0
  18. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/cli_floor.py +0 -0
  19. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/config.py +0 -0
  20. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/credentials.py +0 -0
  21. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/egress.py +0 -0
  22. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/harness_self_test.py +0 -0
  23. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/heartbeat_link.py +0 -0
  24. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/hooks.py +0 -0
  25. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/host_store.py +0 -0
  26. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/__init__.py +0 -0
  27. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/git/__init__.py +0 -0
  28. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/git/contracts.py +0 -0
  29. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/git/evidence.py +0 -0
  30. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  31. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/git/workspace.py +0 -0
  32. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/github/__init__.py +0 -0
  33. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/github/auth.py +0 -0
  34. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  35. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  36. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
  37. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
  38. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/launch.py +0 -0
  39. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/lifecycle.py +0 -0
  40. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/llm_contracts.py +0 -0
  41. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/llm_proxy.py +0 -0
  42. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/mcp.py +0 -0
  43. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/message_store.py +0 -0
  44. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/oauth_connectors.py +0 -0
  45. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/organisation_name.py +0 -0
  46. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/pool_key.py +0 -0
  47. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/private_state.py +0 -0
  48. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/py.typed +0 -0
  49. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/recipient_key_secret.py +0 -0
  50. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/registration.py +0 -0
  51. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/runner_limit.py +0 -0
  52. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/runtime/__init__.py +0 -0
  53. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/runtime/verifier_command.py +0 -0
  54. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/sealed_box.py +0 -0
  55. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/skill.py +0 -0
  56. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/testing/__init__.py +0 -0
  57. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/testing/__main__.py +0 -0
  58. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/testing/control_plane.py +0 -0
  59. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/testing/plugin.py +0 -0
  60. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/testing/test_conformance.py +0 -0
  61. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/testing/workflow.py +0 -0
  62. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/tiny_http.py +0 -0
  63. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/triage_activities.py +0 -0
  64. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/usage_windows.py +0 -0
  65. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/user_sources.py +0 -0
  66. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/__init__.py +0 -0
  67. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/_runtime_support.py +0 -0
  68. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/acp_runtime.py +0 -0
  69. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/agent_runtime.py +0 -0
  70. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/claude_runtime.py +0 -0
  71. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/claude_sign_in.py +0 -0
  72. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/codex_runtime.py +0 -0
  73. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/command_policy.py +0 -0
  74. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/contract_isolation.py +0 -0
  75. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/fastapi_client.py +0 -0
  76. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/harness_logins.py +0 -0
  77. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/harness_outcome.py +0 -0
  78. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/harness_usage.py +0 -0
  79. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/mcp_config.py +0 -0
  80. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/settings.py +0 -0
  81. {agentic_runner-3.29.0 → agentic_runner-3.30.1}/src/agentic_runner/workers/skills.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-runner
3
- Version: 3.29.0
3
+ Version: 3.30.1
4
4
  Summary: The Agentic OS Runner: an activity-only Temporal executor for workspaces, Agent Runtimes and verb seams
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.12
8
- Requires-Dist: agentic-runner-contracts==3.29.0
8
+ Requires-Dist: agentic-runner-contracts==3.30.1
9
9
  Requires-Dist: cryptography<51,>=50
10
10
  Requires-Dist: httpx<1.0,>=0.27
11
11
  Requires-Dist: pydantic-settings<3.0,>=2.0
@@ -10,7 +10,7 @@ dependencies = [
10
10
  # Exactly this release's contracts: the two ship as one version, and an unpinned
11
11
  # requirement let `uv tool upgrade` keep an older contracts wheel the Runner cannot
12
12
  # import from (QTS-1473). `scripts/set-version.py` moves it with the version.
13
- "agentic-runner-contracts==3.29.0",
13
+ "agentic-runner-contracts==3.30.1",
14
14
  # Imported directly by `registration.py` (the Ed25519 identity that signs every
15
15
  # heartbeat); already present transitively through pyjwt[crypto], declared because
16
16
  # we import it.
@@ -7,6 +7,6 @@ stay on the platform; this package imports none of them, and a CI partition test
7
7
  (``tests/test_package_partition.py``) fails the build if that changes.
8
8
  """
9
9
 
10
- __version__ = "3.29.0"
10
+ __version__ = "3.30.1"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -142,7 +142,7 @@ def choose_auth_mode(
142
142
  raise AuthModeRefusedError(
143
143
  SUBSCRIPTION_NEEDS_HARNESS_LOGIN_RULE,
144
144
  "no API key is present for this Contract and its harness root on this Runner holds "
145
- "no login; sign it in on this Runner with `agentic-runner sign-in`",
145
+ "no login; sign it in on this Runner with `agentic-runner sign-in claude` (or `codex`)",
146
146
  )
147
147
  if AuthMode.SUBSCRIPTION not in runtime_modes:
148
148
  raise AuthModeRefusedError(
@@ -216,7 +216,8 @@ def require_named_access(
216
216
  raise AuthModeRefusedError(
217
217
  SUBSCRIPTION_NOT_SIGNED_IN_RULE,
218
218
  f"this Directive was routed to the person's subscription, and its sign-in on "
219
- f"this Runner is {subscription.value}; run `agentic-runner sign-in` here",
219
+ f"this Runner is {subscription.value}; run `agentic-runner sign-in claude` (or "
220
+ "`codex`) here",
220
221
  )
221
222
  return AuthMode.SUBSCRIPTION
222
223
  if access.kind == LlmAccessKind.LLM_KEY:
@@ -11,12 +11,12 @@ namespace and queue this process was assigned. It is idempotent by state file --
11
11
  restart re-reads what is already there rather than registering a second Runner against
12
12
  the Organisation's cap.
13
13
 
14
- ``install | start | stop | status <name>``, ``credential set`` and ``list`` are the
14
+ ``install | start | stop | restart | status <name>``, ``credential set`` and ``list`` are the
15
15
  workstation Runner (PRD issue 47, ``workstation.py``): one login agent per Runner, managed
16
16
  by the user who hosts it, with no ``sudo`` anywhere. ``<name>`` is the Runner's, the one the
17
17
  console lists it under (QTS-1773); a machine with one Runner needs none (QTS-1774).
18
18
 
19
- ``sign-in [claude|codex]`` gives a person's own Runner their LLM access: their subscription,
19
+ ``sign-in claude|codex`` gives a person's own Runner their LLM access: their subscription,
20
20
  through the vendor's own sign-in in this terminal, or an API key stored in the Runner's
21
21
  own store (``sign_in.py``, Spec: Runners rev 3). No ids, and no code or token passes
22
22
  through the control plane.
@@ -215,6 +215,17 @@ def main(argv: Sequence[str] | None = None) -> int:
215
215
  driven.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
216
216
  driven.add_argument("--root", default=None, type=Path)
217
217
 
218
+ restart = subcommands.add_parser(
219
+ "restart",
220
+ help="restart running workstation Runners on the agentic-runner installed now, as "
221
+ "after `uv tool upgrade`",
222
+ )
223
+ restart.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
224
+ which = restart.add_mutually_exclusive_group()
225
+ which.add_argument("--name", dest="name_flag", default=None, help=_INSTALLED_NAME_HELP)
226
+ which.add_argument("--all", action="store_true", help="every Runner running on this machine")
227
+ restart.add_argument("--root", default=None, type=Path)
228
+
218
229
  listed = subcommands.add_parser(
219
230
  "list", help="the workstation Runners installed on this machine, and which are running"
220
231
  )
@@ -245,12 +256,15 @@ def main(argv: Sequence[str] | None = None) -> int:
245
256
  sign_in = subcommands.add_parser(
246
257
  "sign-in",
247
258
  help="give your own Runner your subscription or an API key, for Claude Code or Codex",
259
+ # The runtime is named, never defaulted: a bare `sign-in` read as a Claude sign-in
260
+ # because Claude Code was asked about first (QTS-2159). Usage lists one form each.
261
+ usage="%(prog)s claude [--method {oauth_token,claude_ai}] [--name NAME]\n"
262
+ " %(prog)s codex [--name NAME]",
248
263
  )
249
264
  sign_in.add_argument(
250
265
  "runtime",
251
- nargs="?",
252
266
  choices=tuple(AGENT_RUNTIMES),
253
- help="one Agent Runtime; omitted asks about each one this Runner serves",
267
+ help="the Agent Runtime to sign in: claude (Claude Code) or codex (Codex)",
254
268
  )
255
269
  sign_in.add_argument(
256
270
  "--method",
@@ -368,6 +382,8 @@ def main(argv: Sequence[str] | None = None) -> int:
368
382
  return asyncio.run(service.run(attestation=_image_attestation(), install=_run_install()))
369
383
  if arguments.command in {"install", "start", "stop", "credential"}:
370
384
  return _workstation(arguments)
385
+ if arguments.command == "restart":
386
+ return _restart(arguments)
371
387
  if arguments.command == "list":
372
388
  for line in workstation.list_lines(arguments.root or workstation.default_root()):
373
389
  print(line)
@@ -462,6 +478,49 @@ def _workstation(arguments: argparse.Namespace) -> int:
462
478
  return 1
463
479
 
464
480
 
481
+ def _restart(arguments: argparse.Namespace) -> int:
482
+ """``restart <name> | --name <name> | --all``: exit 2 names the Runners installed here.
483
+
484
+ A stopped Runner is not started: it was stopped on purpose, and ``systemctl restart``
485
+ would start it where ``launchctl kickstart`` refuses to, so the two OSes would differ.
486
+ """
487
+
488
+ root = arguments.root or workstation.default_root()
489
+ name = arguments.name or arguments.name_flag
490
+ if arguments.all and name:
491
+ print("restart takes a name or --all, not both", file=sys.stderr)
492
+ return 2
493
+ try:
494
+ if arguments.all:
495
+ targets = [
496
+ workstation.RunnerPaths(root=root, name=installed)
497
+ for installed in workstation.installed_names(root)
498
+ ]
499
+ if not targets:
500
+ raise ValueError(f"no workstation Runner is installed under {root}")
501
+ elif name:
502
+ targets = [workstation.installed_runner(root, name)]
503
+ else:
504
+ targets = [workstation.only_runner(root, how="name one, or restart --all")]
505
+ except ValueError as error:
506
+ print(error, file=sys.stderr)
507
+ return 2
508
+ failed = False
509
+ for paths in targets:
510
+ if workstation.process_id(paths) is None:
511
+ print(f"{paths.name} is stopped; `agentic-runner start {paths.name}` starts it")
512
+ failed = failed or not arguments.all
513
+ continue
514
+ try:
515
+ pid, version = workstation.restart(paths)
516
+ except (FileNotFoundError, RuntimeError) as error:
517
+ print(f"{paths.name}: restart failed: {error}", file=sys.stderr)
518
+ failed = True
519
+ continue
520
+ print(f"restarted {paths.name}: pid {pid}, agentic-runner {version}")
521
+ return 1 if failed else 0
522
+
523
+
465
524
  def _image_attestation() -> HostAttestation:
466
525
  """What a container Runner says about itself, where issue 47 gave the workstation its
467
526
  own: the CLIs it found are what the registry serves and routing matches on."""
@@ -1284,8 +1284,8 @@ def _temporal_worker(client: Client, **kwargs: Any) -> Worker:
1284
1284
  return Worker(client, **kwargs)
1285
1285
 
1286
1286
 
1287
- async def sign_in(which: str | None, *, store: KeyStore, method: str = OAUTH_TOKEN) -> int:
1288
- """``agentic-runner sign-in [claude|codex]`` (``sign_in.SignIn``), composed here.
1287
+ async def sign_in(which: str, *, store: KeyStore, method: str = OAUTH_TOKEN) -> int:
1288
+ """``agentic-runner sign-in claude|codex`` (``sign_in.SignIn``), composed here.
1289
1289
 
1290
1290
  Runs with the Runner's environment (``kubectl exec``, ``docker exec``, or a
1291
1291
  workstation's own), so a subscription gets the uid and the harness root the Contract's
@@ -1301,14 +1301,17 @@ async def sign_in(which: str | None, *, store: KeyStore, method: str = OAUTH_TOK
1301
1301
  can = can_separate_uids()
1302
1302
  try:
1303
1303
  require_isolation_supported(mode, can_separate_uids=can)
1304
+ # `run` gives the settings this default; a sign-in must build the same ones, or a
1305
+ # workstation, whose environment sets only the control plane, is refused (QTS-2157).
1306
+ _default_callback_url(os.environ.get(CONTROL_PLANE_ENV, "").strip())
1304
1307
  settings = get_worker_settings()
1305
1308
  except RunnerRegistrationError as error:
1306
1309
  print(f"refusing to sign in ({error.reason}): {error}", file=sys.stderr)
1307
1310
  return 1
1308
- except ValidationError:
1311
+ except ValidationError as error:
1309
1312
  print(
1310
- "refusing to sign in: run this with the Runner's environment (`docker exec`, "
1311
- "`kubectl exec`, or on the workstation that runs it)",
1313
+ f"refusing to sign in: {_invalid_settings(error)}; run this with the Runner's "
1314
+ "environment (`docker exec`, `kubectl exec`, or on the workstation that runs it)",
1312
1315
  file=sys.stderr,
1313
1316
  )
1314
1317
  return 2
@@ -1336,6 +1339,22 @@ async def sign_in(which: str | None, *, store: KeyStore, method: str = OAUTH_TOK
1336
1339
  _SIGN_IN_CLIS: Final[Mapping[str, str]] = {"claude_code": "claude", "codex_cli": "codex"}
1337
1340
 
1338
1341
 
1342
+ def _default_callback_url(control_plane: str) -> None:
1343
+ # The control plane the activities call back is the one this process registered
1344
+ # with, unless the operator points them at an internal address explicitly.
1345
+ if control_plane:
1346
+ os.environ.setdefault("INTERNAL_FASTAPI_BASE_URL", control_plane)
1347
+
1348
+
1349
+ def _invalid_settings(error: ValidationError) -> str:
1350
+ """``INTERNAL_FASTAPI_BASE_URL (Field required)``, one per field, never the value."""
1351
+
1352
+ return ", ".join(
1353
+ f"{'.'.join(str(part) for part in detail['loc'])} ({detail['msg']})"
1354
+ for detail in error.errors(include_input=False, include_url=False)
1355
+ )
1356
+
1357
+
1339
1358
  async def run(
1340
1359
  *,
1341
1360
  http_client: httpx.AsyncClient | None = None,
@@ -1416,9 +1435,7 @@ async def _run(
1416
1435
  except (RunnerRegistrationError, UnsafeStateError) as error:
1417
1436
  print(f"refusing to start ({error.reason}): {error}", flush=True)
1418
1437
  return 1
1419
- # The control plane the activities call back is the one this process registered
1420
- # with, unless the operator points them at an internal address explicitly.
1421
- os.environ.setdefault("INTERNAL_FASTAPI_BASE_URL", control_plane)
1438
+ _default_callback_url(control_plane)
1422
1439
  settings = get_worker_settings()
1423
1440
  try:
1424
1441
  attestation = with_profile_acp_cli_kinds(with_acp_bridges(attestation, settings), settings)
@@ -1,9 +1,10 @@
1
- """``agentic-runner sign-in [claude|codex]``: a person gives their own Runner its LLM access.
1
+ """``agentic-runner sign-in claude|codex``: a person gives their own Runner its LLM access.
2
2
 
3
3
  One command for Claude Code and Codex, for a subscription or an API key, on a laptop, in
4
4
  ``docker exec -it`` or in ``kubectl exec -it`` (Spec: Runners rev 3). It takes no ids: a
5
5
  personal Runner serves one Contract, which the last heartbeat ack named
6
- (``llm_contracts``). With no argument it asks about each Agent Runtime the Runner serves.
6
+ (``llm_contracts``). It names the one Agent Runtime it signs in: a bare ``sign-in`` read as
7
+ a Claude Code sign-in, so the console script refuses it (QTS-2159).
7
8
 
8
9
  * **An API key** is read without echo and stored in the Runner's own store under the
9
10
  provider's key name (``ANTHROPIC_API_KEY``, ``OPENAI_API_KEY``). The running Runner reads
@@ -66,7 +67,7 @@ class SignIn:
66
67
  secret: Callable[[str], str]
67
68
  say: Callable[[str], None]
68
69
 
69
- async def run(self, which: str | None) -> int:
70
+ async def run(self, which: str) -> int:
70
71
  if self.host_party != "user":
71
72
  hosted = f"hosted by {self.host_party!r}" if self.host_party else "not registered"
72
73
  self.say(
@@ -74,22 +75,14 @@ class SignIn:
74
75
  "Runner their access; a pool Runner uses only its pool's keys"
75
76
  )
76
77
  return 1
77
- kinds = [AGENT_RUNTIMES[which]] if which is not None else sorted(self.served)
78
- missing = [kind for kind in kinds if kind not in self.served]
79
- if missing:
78
+ kind = AGENT_RUNTIMES[which]
79
+ if kind not in self.served:
80
80
  self.say(
81
- f"refusing to sign in: this Runner does not serve {_LABELS[missing[0]]}; "
81
+ f"refusing to sign in: this Runner does not serve {_LABELS[kind]}; "
82
82
  "install its CLI on this Runner's PATH and restart it"
83
83
  )
84
84
  return 1
85
- if not kinds:
86
- self.say("this Runner serves no Agent Runtime: install `claude` or `codex` first")
87
- return 1
88
- for kind in kinds:
89
- outcome = await self._one(kind)
90
- if outcome != 0:
91
- return outcome
92
- return 0
85
+ return await self._one(kind)
93
86
 
94
87
  async def _one(self, cli_kind: str) -> int:
95
88
  label = _LABELS[cli_kind]
@@ -115,7 +115,7 @@ own terminal, the one command for their way of installing. On this machine:
115
115
  agentic-runner sign-in claude
116
116
  ```
117
117
 
118
- `codex` in place of `claude` for Codex; with no argument it asks about each. Add
118
+ `codex` in place of `claude` for Codex; `sign-in` alone is refused. Add
119
119
  `--name <name>` only if this machine runs more than one Runner. On Docker it is
120
120
  `docker exec -it <container> agentic-runner sign-in claude`, on Kubernetes
121
121
  `kubectl exec -it <pod> -c runner -- agentic-runner sign-in claude`.
@@ -1,7 +1,7 @@
1
1
  """The workstation Runner: one login agent per Runner (PRD issue 47, map ticket 23).
2
2
 
3
- ``agentic-runner install | start | stop | status <name>`` and ``list``. Everything a CI-runner
4
- precedent does on a developer machine and nothing a daemon would need:
3
+ ``agentic-runner install | start | stop | restart | status <name>`` and ``list``. Everything
4
+ a CI-runner precedent does on a developer machine and nothing a daemon would need:
5
5
 
6
6
  * **One process per Runner** (23 item 3), each with its own state directory, identity,
7
7
  namespace, Runner Token, hooks, credential store and Workspaces under ``<root>/<name>/``.
@@ -37,11 +37,12 @@ import shutil
37
37
  import subprocess
38
38
  import sys
39
39
  import tempfile
40
+ import time
40
41
  from collections.abc import Callable, Mapping, Sequence
41
42
  from dataclasses import dataclass
42
43
  from datetime import UTC, datetime
43
44
  from pathlib import Path
44
- from typing import Any, Final
45
+ from typing import Any, Final, Literal
45
46
  from xml.sax.saxutils import escape
46
47
 
47
48
  import httpx
@@ -91,11 +92,13 @@ __all__ = [
91
92
  "install",
92
93
  "install_channel",
93
94
  "installed_names",
95
+ "installed_runner",
94
96
  "list_lines",
95
97
  "locate_clis",
96
98
  "only_runner",
97
99
  "parse_acp_cli_kinds",
98
100
  "require_any_cli",
101
+ "restart",
99
102
  "run_environment",
100
103
  "run_process",
101
104
  "service_definition",
@@ -158,6 +161,16 @@ def installed_names(root: Path) -> list[str]:
158
161
  )
159
162
 
160
163
 
164
+ def installed_runner(root: Path, name: str) -> RunnerPaths:
165
+ """``<name>``, refused with the names installed here when it is not one of them."""
166
+
167
+ names = installed_names(root)
168
+ if name in names:
169
+ return RunnerPaths(root=root, name=name)
170
+ installed = ", ".join(names) if names else "none"
171
+ raise ValueError(f"no Runner named {name!r} is installed under {root}; installed: {installed}")
172
+
173
+
161
174
  def only_runner(root: Path, *, how: str = "name one") -> RunnerPaths:
162
175
  """The one Runner installed here, so a command needs no name (QTS-1774)."""
163
176
 
@@ -444,6 +457,7 @@ class ServiceDefinition:
444
457
  install: list[list[str]]
445
458
  start: list[list[str]]
446
459
  stop: list[list[str]]
460
+ restart: list[list[str]]
447
461
 
448
462
 
449
463
  def runner_command(name: str, root: Path) -> list[str]:
@@ -504,6 +518,9 @@ def _launch_agent(
504
518
  install=[],
505
519
  start=[["launchctl", "bootstrap", domain, str(plist)]],
506
520
  stop=[["launchctl", "bootout", f"{domain}/{name}"]],
521
+ # One call, not bootout then bootstrap: launchd refuses a bootstrap of a label it
522
+ # still holds, and it holds it until the old process has drained.
523
+ restart=[["launchctl", "kickstart", "-k", f"{domain}/{name}"]],
507
524
  )
508
525
 
509
526
 
@@ -546,6 +563,7 @@ def _systemd_user_unit(
546
563
  ],
547
564
  start=[[*systemctl, "enable", "--now", unit_name]],
548
565
  stop=[[*systemctl, "disable", "--now", unit_name]],
566
+ restart=[[*systemctl, "restart", unit_name]],
549
567
  )
550
568
 
551
569
 
@@ -599,6 +617,10 @@ def _logon_task(
599
617
  ["schtasks", "/End", "/TN", task_name],
600
618
  ["schtasks", "/Change", "/TN", task_name, "/DISABLE"],
601
619
  ],
620
+ restart=[
621
+ ["schtasks", "/End", "/TN", task_name],
622
+ ["schtasks", "/Run", "/TN", task_name],
623
+ ],
602
624
  )
603
625
 
604
626
 
@@ -837,9 +859,36 @@ def stop(paths: RunnerPaths, **driver: Any) -> None:
837
859
  _drive_service(paths, "stop", **driver)
838
860
 
839
861
 
862
+ def restart(
863
+ paths: RunnerPaths,
864
+ *,
865
+ timeout: float = 60.0,
866
+ sleep: Callable[[float], None] = time.sleep,
867
+ clock: Callable[[], float] = time.monotonic,
868
+ **driver: Any,
869
+ ) -> tuple[int, str]:
870
+ """Restart a running Runner and wait for its new process: ``(pid, version)``.
871
+
872
+ ``uv tool upgrade`` replaces the console script, not the process, so a login agent keeps
873
+ running the old version until this (QTS-2158). The process writes its version before
874
+ its pidfile, so a new pid means the version beside it is the new process's.
875
+ """
876
+
877
+ previous = process_id(paths)
878
+ _drive_service(paths, "restart", **driver)
879
+ deadline = clock() + timeout
880
+ while (pid := process_id(paths)) is None or pid == previous:
881
+ if clock() >= deadline:
882
+ raise RuntimeError(
883
+ f"no new process wrote {paths.pidfile} within {timeout:.0f}s; see {paths.log}"
884
+ )
885
+ sleep(0.5)
886
+ return pid, paths.started_version.read_text(encoding="utf-8").strip()
887
+
888
+
840
889
  def _drive_service(
841
890
  paths: RunnerPaths,
842
- verb: str,
891
+ verb: Literal["start", "stop", "restart"],
843
892
  *,
844
893
  platform: str = sys.platform,
845
894
  home: Path | None = None,
@@ -847,7 +896,7 @@ def _drive_service(
847
896
  ) -> None:
848
897
  settings = WorkstationSettings.load(paths)
849
898
  definition = service_definition(paths, settings, platform=platform, home=home)
850
- drive(definition.start if verb == "start" else definition.stop, run=run)
899
+ drive(getattr(definition, verb), run=run)
851
900
 
852
901
 
853
902
  def run_process(paths: RunnerPaths, *, login_agent: bool) -> int:
File without changes