agentic-runner 3.27.0__tar.gz → 3.28.0__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 (80) hide show
  1. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/PKG-INFO +5 -4
  2. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/README.md +3 -2
  3. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/pyproject.toml +1 -1
  4. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/__init__.py +1 -1
  5. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/cli.py +61 -38
  6. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workstation.py +96 -1
  7. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/.gitignore +0 -0
  8. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/LICENSE +0 -0
  9. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/activities.py +0 -0
  10. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/attempts.py +0 -0
  11. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/auth_mode.py +0 -0
  12. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/build.py +0 -0
  13. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/callback.py +0 -0
  14. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/child_watcher.py +0 -0
  15. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/cli_floor.py +0 -0
  16. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/config.py +0 -0
  17. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/credentials.py +0 -0
  18. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/egress.py +0 -0
  19. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/harness_self_test.py +0 -0
  20. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/heartbeat_link.py +0 -0
  21. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/hooks.py +0 -0
  22. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/host_store.py +0 -0
  23. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/__init__.py +0 -0
  24. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  25. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  26. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  27. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  28. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  29. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  30. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  31. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  32. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  33. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
  34. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
  35. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/launch.py +0 -0
  36. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/lifecycle.py +0 -0
  37. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/llm_contracts.py +0 -0
  38. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/llm_proxy.py +0 -0
  39. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/mcp.py +0 -0
  40. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/message_store.py +0 -0
  41. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/oauth_connectors.py +0 -0
  42. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/pool_key.py +0 -0
  43. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/private_state.py +0 -0
  44. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/py.typed +0 -0
  45. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  46. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/registration.py +0 -0
  47. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/runner_limit.py +0 -0
  48. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/runtime/__init__.py +0 -0
  49. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  50. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/sealed_box.py +0 -0
  51. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/service.py +0 -0
  52. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/sign_in.py +0 -0
  53. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/skill.py +0 -0
  54. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +0 -0
  55. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/testing/__init__.py +0 -0
  56. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/testing/__main__.py +0 -0
  57. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/testing/control_plane.py +0 -0
  58. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/testing/plugin.py +0 -0
  59. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/testing/test_conformance.py +0 -0
  60. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/testing/workflow.py +0 -0
  61. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/tiny_http.py +0 -0
  62. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/triage_activities.py +0 -0
  63. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/usage_windows.py +0 -0
  64. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/user_sources.py +0 -0
  65. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/__init__.py +0 -0
  66. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
  67. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/acp_runtime.py +0 -0
  68. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
  69. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
  70. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
  71. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
  72. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/command_policy.py +0 -0
  73. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
  74. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  75. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_logins.py +0 -0
  76. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
  77. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  78. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  79. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/src/agentic_runner/workers/settings.py +0 -0
  80. {agentic_runner-3.27.0 → agentic_runner-3.28.0}/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.27.0
3
+ Version: 3.28.0
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.27.0
8
+ Requires-Dist: agentic-runner-contracts==3.28.0
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
@@ -47,7 +47,8 @@ root: Python, git, `codex`, `claude`, nothing else) and installs with
47
47
  `charts/agentic-runner`.
48
48
 
49
49
  On a workstation it is a per-user login agent, one per Runner:
50
- `agentic-runner install | start | stop | status <name>` (LaunchAgent, systemd user unit or
51
- logon task; never `sudo`), with Credential References in the OS credential store.
50
+ `agentic-runner install | start | stop | status <name>` and `agentic-runner list`
51
+ (LaunchAgent, systemd user unit or logon task; never `sudo`), with Credential References in
52
+ the OS credential store.
52
53
 
53
54
  Licensed under AGPL-3.0-only.
@@ -28,7 +28,8 @@ root: Python, git, `codex`, `claude`, nothing else) and installs with
28
28
  `charts/agentic-runner`.
29
29
 
30
30
  On a workstation it is a per-user login agent, one per Runner:
31
- `agentic-runner install | start | stop | status <name>` (LaunchAgent, systemd user unit or
32
- logon task; never `sudo`), with Credential References in the OS credential store.
31
+ `agentic-runner install | start | stop | status <name>` and `agentic-runner list`
32
+ (LaunchAgent, systemd user unit or logon task; never `sudo`), with Credential References in
33
+ the OS credential store.
33
34
 
34
35
  Licensed under AGPL-3.0-only.
@@ -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.27.0",
13
+ "agentic-runner-contracts==3.28.0",
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.27.0"
10
+ __version__ = "3.28.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -11,10 +11,10 @@ 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>`` and ``credential set`` are the workstation
15
- Runner (PRD issue 47, ``workstation.py``): one login agent per Runner, managed by the user
16
- who hosts it, with no ``sudo`` anywhere. ``<name>`` is the Runner's, the one the console
17
- lists it under (QTS-1773).
14
+ ``install | start | stop | status <name>``, ``credential set`` and ``list`` are the
15
+ workstation Runner (PRD issue 47, ``workstation.py``): one login agent per Runner, managed
16
+ by the user who hosts it, with no ``sudo`` anywhere. ``<name>`` is the Runner's, the one the
17
+ console lists it under (QTS-1773); a machine with one Runner needs none (QTS-1774).
18
18
 
19
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
@@ -107,6 +107,7 @@ _NAME_HELP = (
107
107
  "this Runner's name: the console lists it under this name, and every command on this "
108
108
  "machine takes it"
109
109
  )
110
+ _INSTALLED_NAME_HELP = "the Runner's name; omitted when this machine runs one Runner"
110
111
 
111
112
 
112
113
  def _old_org_flag(parser: argparse.ArgumentParser) -> None:
@@ -211,14 +212,19 @@ def main(argv: Sequence[str] | None = None) -> int:
211
212
  driven = subcommands.add_parser(
212
213
  service_verb, help=f"{service_verb} one workstation Runner's login agent"
213
214
  )
214
- driven.add_argument("name", help=_NAME_HELP)
215
+ driven.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
215
216
  driven.add_argument("--root", default=None, type=Path)
216
217
 
218
+ listed = subcommands.add_parser(
219
+ "list", help="the workstation Runners installed on this machine, and which are running"
220
+ )
221
+ listed.add_argument("--root", default=None, type=Path)
222
+
217
223
  credential = subcommands.add_parser(
218
224
  "credential", help="install a Credential Reference value into this host's store"
219
225
  )
220
226
  credential.add_argument("action", choices=("set",))
221
- credential.add_argument("name", help=_NAME_HELP)
227
+ credential.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
222
228
  credential.add_argument("reference")
223
229
  credential.add_argument("--root", default=None, type=Path)
224
230
 
@@ -233,7 +239,7 @@ def main(argv: Sequence[str] | None = None) -> int:
233
239
  status = subcommands.add_parser(
234
240
  "status", help="print this installation's versions and Recipient Key fingerprint"
235
241
  )
236
- status.add_argument("name", nargs="?", help="a workstation Runner's name (issue 47)")
242
+ status.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
237
243
  status.add_argument("--root", default=None, type=Path)
238
244
 
239
245
  sign_in = subcommands.add_parser(
@@ -256,7 +262,7 @@ def main(argv: Sequence[str] | None = None) -> int:
256
262
  sign_in.add_argument(
257
263
  "--name",
258
264
  default=None,
259
- help="the workstation Runner's name, when this workstation runs more than one",
265
+ help="the workstation Runner's name, when this machine runs more than one",
260
266
  )
261
267
  _old_org_flag(sign_in)
262
268
  sign_in.add_argument("--root", default=None, type=Path, help=argparse.SUPPRESS)
@@ -351,9 +357,21 @@ def main(argv: Sequence[str] | None = None) -> int:
351
357
  return workstation.run_process(
352
358
  _runner_paths(arguments), login_agent=arguments.login_agent
353
359
  )
360
+ if installed := _installed_instead_of_run():
361
+ print(
362
+ f"this machine runs {', '.join(installed)} as login agents: start one with "
363
+ "`agentic-runner start <name>`. `run` serves a Runner its environment sets "
364
+ "up, as in a container",
365
+ file=sys.stderr,
366
+ )
367
+ return 2
354
368
  return asyncio.run(service.run(attestation=_image_attestation(), install=_run_install()))
355
369
  if arguments.command in {"install", "start", "stop", "credential"}:
356
370
  return _workstation(arguments)
371
+ if arguments.command == "list":
372
+ for line in workstation.list_lines(arguments.root or workstation.default_root()):
373
+ print(line)
374
+ return 0
357
375
  if arguments.command == "recipient-key":
358
376
  api, kube_ns = in_cluster()
359
377
  pair = ensure_recipient_key(
@@ -364,9 +382,17 @@ def main(argv: Sequence[str] | None = None) -> int:
364
382
  )
365
383
  return 0
366
384
  if arguments.command == "status":
367
- lines = (
368
- workstation.status_lines(_runner_paths(arguments)) if arguments.name else status_lines()
369
- )
385
+ if arguments.name or (
386
+ not _in_container()
387
+ and workstation.installed_names(arguments.root or workstation.default_root())
388
+ ):
389
+ try:
390
+ lines = workstation.status_lines(_runner_paths(arguments))
391
+ except ValueError as error:
392
+ print(error, file=sys.stderr)
393
+ return 2
394
+ else:
395
+ lines = status_lines()
370
396
  for line in lines:
371
397
  print(line)
372
398
  return 0
@@ -389,16 +415,33 @@ def main(argv: Sequence[str] | None = None) -> int:
389
415
  return _callback(arguments)
390
416
 
391
417
 
392
- def _runner_paths(arguments: argparse.Namespace) -> workstation.RunnerPaths:
393
- return workstation.RunnerPaths(
394
- root=arguments.root or workstation.default_root(),
395
- name=workstation.validate_name(arguments.name),
396
- )
418
+ def _runner_paths(
419
+ arguments: argparse.Namespace, *, how: str = "name one"
420
+ ) -> workstation.RunnerPaths:
421
+ """``<name>``, or the one Runner this machine runs -- nothing to remember (QTS-1774)."""
422
+
423
+ root = arguments.root or workstation.default_root()
424
+ if not arguments.name:
425
+ return workstation.only_runner(root, how=how)
426
+ return workstation.RunnerPaths(root=root, name=workstation.validate_name(arguments.name))
427
+
428
+
429
+ def _installed_instead_of_run() -> list[str]:
430
+ """The Runners installed here, when a bare ``run`` would start none of them.
431
+
432
+ An installed Runner is its login agent's process; ``run`` without ``--name`` registers
433
+ from the environment, and on a workstation that holds neither a token nor an identity
434
+ it could only fail without saying what to type instead (QTS-1774).
435
+ """
436
+
437
+ if _in_container() or registration_token_from_env() or load_state(_state_dir()):
438
+ return []
439
+ return workstation.installed_names(workstation.default_root())
397
440
 
398
441
 
399
442
  def _workstation(arguments: argparse.Namespace) -> int:
400
- paths = _runner_paths(arguments)
401
443
  try:
444
+ paths = _runner_paths(arguments)
402
445
  if arguments.command == "install":
403
446
  return _install(arguments, paths)
404
447
  if arguments.command == "start":
@@ -482,7 +525,7 @@ def _sign_in(arguments: argparse.Namespace) -> int:
482
525
  store: KeyStore
483
526
  if arguments.name or not _in_container():
484
527
  try:
485
- paths = _sign_in_paths(arguments)
528
+ paths = _runner_paths(arguments, how="name one with --name")
486
529
  except ValueError as error:
487
530
  print(error, file=sys.stderr)
488
531
  return 2
@@ -495,26 +538,6 @@ def _sign_in(arguments: argparse.Namespace) -> int:
495
538
  return asyncio.run(service.sign_in(arguments.runtime, store=store, method=arguments.method))
496
539
 
497
540
 
498
- def _sign_in_paths(arguments: argparse.Namespace) -> workstation.RunnerPaths:
499
- """``--name``, or the one Runner this workstation runs -- no id to type."""
500
-
501
- if arguments.name:
502
- return _runner_paths(arguments)
503
- root = arguments.root or workstation.default_root()
504
- installed = (
505
- sorted(path.name for path in root.iterdir() if (path / "workstation.json").is_file())
506
- if root.is_dir()
507
- else []
508
- )
509
- if len(installed) != 1:
510
- raise ValueError(
511
- "no workstation Runner is installed here"
512
- if not installed
513
- else f"this workstation runs {', '.join(installed)}: name one with --name"
514
- )
515
- return workstation.RunnerPaths(root=root, name=installed[0])
516
-
517
-
518
541
  def _install(arguments: argparse.Namespace, paths: workstation.RunnerPaths) -> int:
519
542
  if not arguments.control_plane:
520
543
  print(f"--control-plane (or {CONTROL_PLANE_ENV}) is required", file=sys.stderr)
@@ -1,6 +1,6 @@
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>``. Everything a CI-runner
3
+ ``agentic-runner install | start | stop | status <name>`` and ``list``. Everything a CI-runner
4
4
  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,
@@ -47,16 +47,19 @@ from xml.sax.saxutils import escape
47
47
  import httpx
48
48
  from pydantic import BaseModel, ConfigDict
49
49
 
50
+ from agentic_runner import __version__ as runner_version
50
51
  from agentic_runner import service
51
52
  from agentic_runner.build import build_id
52
53
  from agentic_runner.cli_floor import CLI_FLOORS, CLI_NAMES, meets_floor, parse_cli_version
53
54
  from agentic_runner.config import CONFIG_FILE_ENV
54
55
  from agentic_runner.host_store import open_workstation_store
55
56
  from agentic_runner.lifecycle import LifecycleOutbox
57
+ from agentic_runner.llm_contracts import load_llm_contracts
56
58
  from agentic_runner.pool_key import parse_pool_key, save_pool_key
57
59
  from agentic_runner.registration import load_state
58
60
  from agentic_runner.service import HEARTBEAT_STAMP_NAME, PIDFILE_NAME
59
61
  from agentic_runner.workers.acp_runtime import ACP_BRIDGES
62
+ from agentic_runner_contracts.public_metadata import NAMESPACE_PREFIX
60
63
  from agentic_runner_contracts.runner_registration import (
61
64
  RUNNER_NAME_PATTERN,
62
65
  CliVersion,
@@ -84,7 +87,10 @@ __all__ = [
84
87
  "default_root",
85
88
  "install",
86
89
  "install_channel",
90
+ "installed_names",
91
+ "list_lines",
87
92
  "locate_clis",
93
+ "only_runner",
88
94
  "parse_acp_cli_kinds",
89
95
  "require_any_cli",
90
96
  "run_environment",
@@ -132,6 +138,36 @@ def default_root(*, platform: str = sys.platform, environ: Mapping[str, str] = o
132
138
  return (Path(state_home) if state_home else home / ".local" / "state") / "agentic-runner"
133
139
 
134
140
 
141
+ def installed_names(root: Path) -> list[str]:
142
+ """Every Runner installed under ``root``, by name.
143
+
144
+ A directory whose name is not a Runner's is skipped: the guide moves an old install
145
+ aside as ``<name>.organisation`` before a new token, and that one is not a Runner here.
146
+ """
147
+
148
+ if not root.is_dir():
149
+ return []
150
+ return sorted(
151
+ path.name
152
+ for path in root.iterdir()
153
+ if re.fullmatch(RUNNER_NAME_PATTERN, path.name)
154
+ and RunnerPaths(root=root, name=path.name).settings_file.is_file()
155
+ )
156
+
157
+
158
+ def only_runner(root: Path, *, how: str = "name one") -> RunnerPaths:
159
+ """The one Runner installed here, so a command needs no name (QTS-1774)."""
160
+
161
+ names = installed_names(root)
162
+ if len(names) == 1:
163
+ return RunnerPaths(root=root, name=names[0])
164
+ if not names:
165
+ raise ValueError(f"no workstation Runner is installed under {root}")
166
+ raise ValueError(
167
+ f"this machine runs {', '.join(names)}: {how} (`agentic-runner list` shows each)"
168
+ )
169
+
170
+
135
171
  @dataclass(frozen=True)
136
172
  class RunnerPaths:
137
173
  """``<root>/<name>/`` and everything the process keeps under it."""
@@ -184,6 +220,12 @@ class RunnerPaths:
184
220
  def log(self) -> Path:
185
221
  return self.state_dir / "runner.log"
186
222
 
223
+ @property
224
+ def started_version(self) -> Path:
225
+ # Written by the process, not `install`: every Runner here runs one console script,
226
+ # and after `uv tool upgrade` a Runner not restarted still runs the old version.
227
+ return self.state_dir / "runner-version"
228
+
187
229
  @property
188
230
  def socket_dir(self) -> Path:
189
231
  # `sun_path` is ~104 bytes and "~/Library/Application Support/..." is most of
@@ -614,6 +656,58 @@ def status_lines(paths: RunnerPaths, *, now: datetime | None = None) -> list[str
614
656
  return lines
615
657
 
616
658
 
659
+ _LIST_HEADER: Final = (
660
+ "NAME",
661
+ "ORGANISATION",
662
+ "CONTRACT",
663
+ "CONTROL PLANE",
664
+ "VERSION",
665
+ "LOGIN AGENT",
666
+ )
667
+
668
+
669
+ def list_lines(root: Path) -> list[str]:
670
+ """``list``: one row per Runner installed here (QTS-1774).
671
+
672
+ Ids, not names: the Runner is told neither the Organisation's nor the Contract's name.
673
+ Eight characters, as the console shows a Runner's id beside its name (Spec: Runners
674
+ rev 7).
675
+ """
676
+
677
+ names = installed_names(root)
678
+ if not names:
679
+ return [f"no workstation Runner is installed under {root}"]
680
+ rows = [_LIST_HEADER, *(_list_row(RunnerPaths(root=root, name=name)) for name in names)]
681
+ widths = [max(len(row[column]) for row in rows) for column in range(len(_LIST_HEADER) - 1)]
682
+ return [
683
+ " ".join([*(cell.ljust(width) for cell, width in zip(row, widths, strict=False)), row[-1]])
684
+ for row in rows
685
+ ]
686
+
687
+
688
+ def _list_row(paths: RunnerPaths) -> tuple[str, str, str, str, str, str]:
689
+ state = load_state(paths.state_dir)
690
+ contracts = load_llm_contracts(paths.state_dir)
691
+ try:
692
+ version = paths.started_version.read_text(encoding="utf-8").strip() or "-"
693
+ except OSError:
694
+ version = "-"
695
+ return (
696
+ paths.name,
697
+ state.temporal_namespace.removeprefix(NAMESPACE_PREFIX)[:8] if state else "-",
698
+ (
699
+ "-"
700
+ if not contracts
701
+ else str(contracts[0].contract_id)[:8]
702
+ if len(contracts) == 1
703
+ else f"{len(contracts)} Contracts"
704
+ ),
705
+ WorkstationSettings.load(paths).control_plane_url,
706
+ version,
707
+ "running" if process_id(paths) else "stopped",
708
+ )
709
+
710
+
617
711
  def next_step_lines(host_party: str) -> list[str]:
618
712
  """Where to go once the Runner is live (QTS-1314): the dogfood install ended at
619
713
  ``started`` and left the person with no idea what the console wanted next.
@@ -735,6 +829,7 @@ def run_process(paths: RunnerPaths, *, login_agent: bool) -> int:
735
829
  return 1
736
830
  for name, value in run_environment(paths, settings).items():
737
831
  os.environ[name] = value
832
+ paths.started_version.write_text(runner_version, encoding="utf-8")
738
833
  store = open_workstation_store(paths.name, fallback=paths.credentials)
739
834
  attestation = collect_attestation(
740
835
  channel=settings.install_channel,
File without changes