agentic-runner 3.26.1__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.26.1 → agentic_runner-3.28.0}/PKG-INFO +6 -5
  2. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/README.md +4 -3
  3. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/pyproject.toml +1 -1
  4. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/__init__.py +1 -1
  5. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/cli.py +91 -50
  6. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/service.py +18 -2
  7. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +10 -7
  8. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workstation.py +160 -59
  9. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/.gitignore +0 -0
  10. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/LICENSE +0 -0
  11. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/activities.py +0 -0
  12. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/attempts.py +0 -0
  13. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/auth_mode.py +0 -0
  14. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/build.py +0 -0
  15. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/callback.py +0 -0
  16. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/child_watcher.py +0 -0
  17. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/cli_floor.py +0 -0
  18. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/config.py +0 -0
  19. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/credentials.py +0 -0
  20. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/egress.py +0 -0
  21. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/harness_self_test.py +0 -0
  22. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/heartbeat_link.py +0 -0
  23. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/hooks.py +0 -0
  24. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/host_store.py +0 -0
  25. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/__init__.py +0 -0
  26. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  27. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  28. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  29. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  30. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  31. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  32. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  33. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  34. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  35. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
  36. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
  37. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/launch.py +0 -0
  38. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/lifecycle.py +0 -0
  39. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/llm_contracts.py +0 -0
  40. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/llm_proxy.py +0 -0
  41. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/mcp.py +0 -0
  42. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/message_store.py +0 -0
  43. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/oauth_connectors.py +0 -0
  44. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/pool_key.py +0 -0
  45. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/private_state.py +0 -0
  46. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/py.typed +0 -0
  47. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  48. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/registration.py +0 -0
  49. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/runner_limit.py +0 -0
  50. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/runtime/__init__.py +0 -0
  51. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  52. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/sealed_box.py +0 -0
  53. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/sign_in.py +0 -0
  54. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/skill.py +0 -0
  55. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/__init__.py +0 -0
  56. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/__main__.py +0 -0
  57. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/control_plane.py +0 -0
  58. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/plugin.py +0 -0
  59. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/test_conformance.py +0 -0
  60. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/workflow.py +0 -0
  61. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/tiny_http.py +0 -0
  62. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/triage_activities.py +0 -0
  63. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/usage_windows.py +0 -0
  64. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/user_sources.py +0 -0
  65. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/__init__.py +0 -0
  66. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
  67. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/acp_runtime.py +0 -0
  68. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
  69. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
  70. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
  71. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
  72. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/command_policy.py +0 -0
  73. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
  74. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  75. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_logins.py +0 -0
  76. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
  77. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  78. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  79. {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/settings.py +0 -0
  80. {agentic_runner-3.26.1 → 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.26.1
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.26.1
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
@@ -46,8 +46,9 @@ It ships as `ghcr.io/qtsone/agentic-runner` (`Dockerfile.runner` at the reposito
46
46
  root: Python, git, `codex`, `claude`, nothing else) and installs with
47
47
  `charts/agentic-runner`.
48
48
 
49
- On a workstation it is a per-user login agent, one per Organisation:
50
- `agentic-runner install | start | stop | status <org>` (LaunchAgent, systemd user unit or
51
- logon task; never `sudo`), with Credential References in the OS credential store.
49
+ On a workstation it is a per-user login agent, one per Runner:
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.
@@ -27,8 +27,9 @@ It ships as `ghcr.io/qtsone/agentic-runner` (`Dockerfile.runner` at the reposito
27
27
  root: Python, git, `codex`, `claude`, nothing else) and installs with
28
28
  `charts/agentic-runner`.
29
29
 
30
- On a workstation it is a per-user login agent, one per Organisation:
31
- `agentic-runner install | start | stop | status <org>` (LaunchAgent, systemd user unit or
32
- logon task; never `sudo`), with Credential References in the OS credential store.
30
+ On a workstation it is a per-user login agent, one per Runner:
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.26.1",
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.26.1"
10
+ __version__ = "3.28.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -11,9 +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 <org>`` and ``credential set`` are the workstation
15
- Runner (PRD issue 47, ``workstation.py``): one login agent per Organisation, managed by
16
- the user who hosts it, with no ``sudo`` anywhere.
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).
17
18
 
18
19
  ``sign-in [claude|codex]`` gives a person's own Runner their LLM access: their subscription,
19
20
  through the vendor's own sign-in in this terminal, or an API key stored in the Runner's
@@ -102,6 +103,19 @@ __all__ = [
102
103
  STATE_DIR_ENV = "AGENTIC_RUNNER_STATE_DIR"
103
104
 
104
105
 
106
+ _NAME_HELP = (
107
+ "this Runner's name: the console lists it under this name, and every command on this "
108
+ "machine takes it"
109
+ )
110
+ _INSTALLED_NAME_HELP = "the Runner's name; omitted when this machine runs one Runner"
111
+
112
+
113
+ def _old_org_flag(parser: argparse.ArgumentParser) -> None:
114
+ # A login agent written before QTS-1773 runs `run --org <name>` and is not rewritten
115
+ # until its next `install`, so the old spelling keeps parsing, unlisted.
116
+ parser.add_argument("--org", dest="name", help=argparse.SUPPRESS)
117
+
118
+
105
119
  def _state_dir() -> Path:
106
120
  return load_config().state_dir
107
121
 
@@ -158,14 +172,15 @@ def main(argv: Sequence[str] | None = None) -> int:
158
172
  )
159
173
 
160
174
  run = subcommands.add_parser("run", help="register if needed, then heartbeat and poll for work")
161
- run.add_argument("--org", default=None, help="run an installed workstation Organisation")
175
+ run.add_argument("--name", default=None, help="run an installed workstation Runner")
176
+ _old_org_flag(run)
162
177
  run.add_argument("--root", default=None, type=Path, help=argparse.SUPPRESS)
163
178
  run.add_argument("--login-agent", action="store_true", help=argparse.SUPPRESS)
164
179
 
165
180
  install = subcommands.add_parser(
166
- "install", help="register a workstation Runner for one Organisation (issue 47)"
181
+ "install", help="register a workstation Runner and install its login agent (issue 47)"
167
182
  )
168
- install.add_argument("org")
183
+ install.add_argument("name", help=_NAME_HELP)
169
184
  install.add_argument(
170
185
  "--control-plane", default=os.environ.get(CONTROL_PLANE_ENV), help="control plane URL"
171
186
  )
@@ -195,16 +210,21 @@ def main(argv: Sequence[str] | None = None) -> int:
195
210
  install.add_argument("--root", default=None, type=Path, help="state root (default per OS)")
196
211
  for service_verb in ("start", "stop"):
197
212
  driven = subcommands.add_parser(
198
- service_verb, help=f"{service_verb} one Organisation's login agent"
213
+ service_verb, help=f"{service_verb} one workstation Runner's login agent"
199
214
  )
200
- driven.add_argument("org")
215
+ driven.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
201
216
  driven.add_argument("--root", default=None, type=Path)
202
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
+
203
223
  credential = subcommands.add_parser(
204
224
  "credential", help="install a Credential Reference value into this host's store"
205
225
  )
206
226
  credential.add_argument("action", choices=("set",))
207
- credential.add_argument("org")
227
+ credential.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
208
228
  credential.add_argument("reference")
209
229
  credential.add_argument("--root", default=None, type=Path)
210
230
 
@@ -219,7 +239,7 @@ def main(argv: Sequence[str] | None = None) -> int:
219
239
  status = subcommands.add_parser(
220
240
  "status", help="print this installation's versions and Recipient Key fingerprint"
221
241
  )
222
- status.add_argument("org", nargs="?", help="a workstation Organisation (issue 47)")
242
+ status.add_argument("name", nargs="?", help=_INSTALLED_NAME_HELP)
223
243
  status.add_argument("--root", default=None, type=Path)
224
244
 
225
245
  sign_in = subcommands.add_parser(
@@ -240,10 +260,11 @@ def main(argv: Sequence[str] | None = None) -> int:
240
260
  "claude_ai: a Claude.ai login (claude auth login --claudeai)",
241
261
  )
242
262
  sign_in.add_argument(
243
- "--org",
263
+ "--name",
244
264
  default=None,
245
- help="the workstation Organisation, when this workstation runs more than one",
265
+ help="the workstation Runner's name, when this machine runs more than one",
246
266
  )
267
+ _old_org_flag(sign_in)
247
268
  sign_in.add_argument("--root", default=None, type=Path, help=argparse.SUPPRESS)
248
269
 
249
270
  annotate = subcommands.add_parser("annotate", help="attach an annotation to this Work Record")
@@ -332,11 +353,25 @@ def main(argv: Sequence[str] | None = None) -> int:
332
353
  return 1
333
354
  return 0
334
355
  if arguments.command == "run":
335
- if arguments.org:
336
- return workstation.run_process(_org_paths(arguments), login_agent=arguments.login_agent)
356
+ if arguments.name:
357
+ return workstation.run_process(
358
+ _runner_paths(arguments), login_agent=arguments.login_agent
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
337
368
  return asyncio.run(service.run(attestation=_image_attestation(), install=_run_install()))
338
369
  if arguments.command in {"install", "start", "stop", "credential"}:
339
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
340
375
  if arguments.command == "recipient-key":
341
376
  api, kube_ns = in_cluster()
342
377
  pair = ensure_recipient_key(
@@ -347,7 +382,17 @@ def main(argv: Sequence[str] | None = None) -> int:
347
382
  )
348
383
  return 0
349
384
  if arguments.command == "status":
350
- lines = workstation.status_lines(_org_paths(arguments)) if arguments.org else status_lines()
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()
351
396
  for line in lines:
352
397
  print(line)
353
398
  return 0
@@ -370,28 +415,45 @@ def main(argv: Sequence[str] | None = None) -> int:
370
415
  return _callback(arguments)
371
416
 
372
417
 
373
- def _org_paths(arguments: argparse.Namespace) -> workstation.OrgPaths:
374
- return workstation.OrgPaths(
375
- root=arguments.root or workstation.default_root(),
376
- org=workstation.validate_org(arguments.org),
377
- )
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())
378
440
 
379
441
 
380
442
  def _workstation(arguments: argparse.Namespace) -> int:
381
- paths = _org_paths(arguments)
382
443
  try:
444
+ paths = _runner_paths(arguments)
383
445
  if arguments.command == "install":
384
446
  return _install(arguments, paths)
385
447
  if arguments.command == "start":
386
448
  workstation.start(paths)
387
- print(f"started {paths.org}")
449
+ print(f"started {paths.name}")
388
450
  return 0
389
451
  if arguments.command == "stop":
390
452
  workstation.stop(paths)
391
- print(f"stopped {paths.org}; every other Organisation's Runner keeps polling")
453
+ print(f"stopped {paths.name}; every other Runner on this machine keeps polling")
392
454
  return 0
393
455
  # The value arrives on stdin so it is never in argv or shell history.
394
- store = open_workstation_store(paths.org, fallback=paths.credentials)
456
+ store = open_workstation_store(paths.name, fallback=paths.credentials)
395
457
  store.put(arguments.reference, sys.stdin.read().strip())
396
458
  print(f"{arguments.reference} stored in the {store.kind.value} store")
397
459
  return 0
@@ -461,42 +523,22 @@ def _sign_in(arguments: argparse.Namespace) -> int:
461
523
  )
462
524
  return 2
463
525
  store: KeyStore
464
- if arguments.org or not _in_container():
526
+ if arguments.name or not _in_container():
465
527
  try:
466
- paths = _sign_in_org(arguments)
528
+ paths = _runner_paths(arguments, how="name one with --name")
467
529
  except ValueError as error:
468
530
  print(error, file=sys.stderr)
469
531
  return 2
470
532
  os.environ.update(
471
533
  workstation.run_environment(paths, workstation.WorkstationSettings.load(paths))
472
534
  )
473
- store = open_workstation_store(paths.org, fallback=paths.credentials)
535
+ store = open_workstation_store(paths.name, fallback=paths.credentials)
474
536
  else:
475
537
  store = service.own_store(load_config())
476
538
  return asyncio.run(service.sign_in(arguments.runtime, store=store, method=arguments.method))
477
539
 
478
540
 
479
- def _sign_in_org(arguments: argparse.Namespace) -> workstation.OrgPaths:
480
- """``--org``, or the one Organisation this workstation runs -- no id to type."""
481
-
482
- if arguments.org:
483
- return _org_paths(arguments)
484
- root = arguments.root or workstation.default_root()
485
- installed = (
486
- sorted(path.name for path in root.iterdir() if (path / "workstation.json").is_file())
487
- if root.is_dir()
488
- else []
489
- )
490
- if len(installed) != 1:
491
- raise ValueError(
492
- "no workstation Runner is installed here"
493
- if not installed
494
- else f"this workstation runs {', '.join(installed)}: name one with --org"
495
- )
496
- return workstation.OrgPaths(root=root, org=installed[0])
497
-
498
-
499
- def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
541
+ def _install(arguments: argparse.Namespace, paths: workstation.RunnerPaths) -> int:
500
542
  if not arguments.control_plane:
501
543
  print(f"--control-plane (or {CONTROL_PLANE_ENV}) is required", file=sys.stderr)
502
544
  return 2
@@ -518,7 +560,6 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
518
560
  else None
519
561
  )
520
562
  settings = workstation.WorkstationSettings(
521
- org=paths.org,
522
563
  control_plane_url=arguments.control_plane,
523
564
  temporal_address=arguments.temporal_address,
524
565
  temporal_tls=not arguments.temporal_plaintext,
@@ -549,7 +590,7 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
549
590
  return 0
550
591
 
551
592
 
552
- def _acp_cli_kinds(flag: str | None, paths: workstation.OrgPaths) -> str:
593
+ def _acp_cli_kinds(flag: str | None, paths: workstation.RunnerPaths) -> str:
553
594
  # A reinstall to pick up a new PATH or retry a token must not turn ACP off (QTS-1355).
554
595
  if flag is not None:
555
596
  return workstation.parse_acp_cli_kinds(flag)
@@ -250,7 +250,7 @@ _VERIFIER_ACTIVITY_BUDGET_SECONDS = 20 * 60
250
250
  # chart's value and this must move together).
251
251
  _SHUTDOWN_HEADROOM_SECONDS = 60
252
252
 
253
- # What `agentic-runner status <org>` reads (PRD issue 47): the live pid, and a file the
253
+ # What `agentic-runner status <name>` reads (PRD issue 47): the live pid, and a file the
254
254
  # process touches on every acknowledged heartbeat, whose mtime is the heartbeat age.
255
255
  PIDFILE_NAME = "runner.pid"
256
256
  HEARTBEAT_STAMP_NAME = "last-heartbeat"
@@ -849,6 +849,9 @@ class ControlPlaneStream:
849
849
  # control plane on 3.25 named them, and the LLM access report is sent only after.
850
850
  _llm_contracts: list[ContractLlmTerms] | None = None
851
851
  _pool: PoolTerms | None = None
852
+ # QTS-1773: the control plane's contracts version, from its ack; None until an ack from
853
+ # one on 3.27 or later, the first to send it and to parse `RunnerInstall.name`.
854
+ _control_plane_contracts: str | None = None
852
855
  _last_seen: tuple[float, datetime] | None = None
853
856
 
854
857
  def _observe_sleep(self, now: datetime) -> None:
@@ -976,6 +979,17 @@ class ControlPlaneStream:
976
979
  for terms in self._llm_contracts
977
980
  ]
978
981
 
982
+ def _install_report(self) -> RunnerInstall | None:
983
+ """Where a command reaches this Runner, sent once an ack named the Contracts -- a
984
+ control plane below 3.25 refuses the field -- and with its name only once an ack
985
+ came from a control plane on 3.27, since one below refuses that key (QTS-1773)."""
986
+
987
+ if self.install is None or self._llm_contracts is None:
988
+ return None
989
+ if self._control_plane_contracts is None:
990
+ return self.install.model_copy(update={"name": None})
991
+ return self.install
992
+
979
993
  def _credential_report(self) -> list[CredentialValidity]:
980
994
  """Per Contract the last ack named, whether each reference would resolve (§7).
981
995
 
@@ -1037,7 +1051,7 @@ class ControlPlaneStream:
1037
1051
  oauth_outcomes=self.oauth.take_outcomes() if self.oauth else [],
1038
1052
  runner_limit=self.runner_limit,
1039
1053
  llm_access=self._llm_access_report(),
1040
- install=self.install if self._llm_contracts is not None else None,
1054
+ install=self._install_report(),
1041
1055
  pool_key_fingerprint=self.pool_key_fingerprint if self._pool is not None else None,
1042
1056
  )
1043
1057
  ack = await self.client.heartbeat(self.state, envelope)
@@ -1065,6 +1079,8 @@ class ControlPlaneStream:
1065
1079
  self._manifests = list(ack.credential_manifests)
1066
1080
  self._take_llm_contracts(ack.llm_contracts)
1067
1081
  self._take_pool(ack.pool)
1082
+ if ack.contracts_version is not None:
1083
+ self._control_plane_contracts = ack.contracts_version
1068
1084
  await self._sync_llm_slots()
1069
1085
  if ack.runner_token is not None:
1070
1086
  self.runner_token = ack.runner_token
@@ -8,7 +8,8 @@ license: AGPL-3.0-only
8
8
 
9
9
  You help a person who has never run a Runner. A **Runner** is the program that does their
10
10
  Agents' work: it runs Codex or Claude Code on a machine they choose and connects out to the
11
- Agentic OS control plane. It belongs to one **Organisation** (the company the work is for).
11
+ Agentic OS control plane. It belongs to one **Organisation** (the company the work is for),
12
+ and has a **name** the person chose for it in the console.
12
13
 
13
14
  The install documents in the `qtsone/agentic-runner` repository are the source for every
14
15
  command. Do not reconstruct a command from memory: open the section named in each step and
@@ -18,7 +19,9 @@ follow it.
18
19
 
19
20
  The prompt they pasted from the console holds these. Ask for any that is missing.
20
21
 
21
- - **Organisation**: a short local name for it, lowercase letters, digits and `-` (`acme`).
22
+ - **Runner name**: the name the person gave the Runner in the console, lowercase letters,
23
+ digits and `-` (`alice-laptop`). A prompt from an older console calls it **Organisation**;
24
+ it is the same value.
22
25
  - **Control plane URL**: the Runner's `controlPlane.url`, copied from the console's
23
26
  Runners page. It is the only control plane address you use, and only as a value you
24
27
  pass to the Runner. Never call it or any other control plane address yourself.
@@ -75,13 +78,13 @@ missing.
75
78
 
76
79
  **This machine.** Follow
77
80
  https://github.com/qtsone/agentic-runner/blob/main/docs/workstation.md#install. Install the
78
- Runner, then show the person the command that registers it for the Organisation and ask
81
+ Runner, then show the person the command that registers it under its name and ask
79
82
  them to run it in their own terminal. The Runner asks them for the Runner Registration Token there,
80
83
  without showing it:
81
84
 
82
85
  ```sh
83
86
  uv tool install agentic-runner
84
- agentic-runner install <org> --control-plane <control-plane-url>
87
+ agentic-runner install <name> --control-plane <control-plane-url>
85
88
  ```
86
89
 
87
90
  You run the first line. Do not run the second yourself: wait until the person says it
@@ -113,7 +116,7 @@ agentic-runner sign-in claude
113
116
  ```
114
117
 
115
118
  `codex` in place of `claude` for Codex; with no argument it asks about each. Add
116
- `--org <org>` only if this machine runs Runners for several Organisations. On Docker it is
119
+ `--name <name>` only if this machine runs more than one Runner. On Docker it is
117
120
  `docker exec -it <container> agentic-runner sign-in claude`, on Kubernetes
118
121
  `kubectl exec -it <pod> -c runner -- agentic-runner sign-in claude`.
119
122
 
@@ -130,7 +133,7 @@ it: it uses the Organisation's keys, which its admin puts in the Runner's own st
130
133
  On this machine, run every 10 seconds, for at most 3 minutes:
131
134
 
132
135
  ```sh
133
- agentic-runner status <org>
136
+ agentic-runner status <name>
134
137
  ```
135
138
 
136
139
  The Runner is online when `process` says `running` and `heartbeat` shows a number of
@@ -141,7 +144,7 @@ it" section. If 3 minutes pass, stop and show the last output and the "Check it"
141
144
 
142
145
  One screen, nothing more:
143
146
 
144
- - the way, the Organisation and the Runner's identity line from `status`;
147
+ - the way, the Runner's name and its identity line from `status`;
145
148
  - whether the LLM credential is in place, or the one thing the person still does in the
146
149
  console;
147
150
  - the `next` line `status` prints: where in the console to see the Runner and give it work.
@@ -1,12 +1,16 @@
1
- """The workstation Runner: one login agent per Organisation (PRD issue 47, map ticket 23).
1
+ """The workstation Runner: one login agent per Runner (PRD issue 47, map ticket 23).
2
2
 
3
- ``agentic-runner install | start | stop | status <org>``. 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
- * **One process per Organisation** (23 item 3), each with its own state directory,
7
- identity, namespace, Runner Token, hooks, credential store and Workspaces under
8
- ``<root>/<org>/``. A contractor with two clients runs two. There is no supervisor over
9
- N Organisations -- that is the "one Runner straddling Organisations" 12 A6 rejected.
6
+ * **One process per Runner** (23 item 3), each with its own state directory, identity,
7
+ namespace, Runner Token, hooks, credential store and Workspaces under ``<root>/<name>/``.
8
+ A contractor with two clients runs two. There is no supervisor over N Runners -- that
9
+ is the "one Runner straddling Organisations" 12 A6 rejected.
10
+ * **The name is the Runner's** (QTS-1773): the console lists the Runner under the name it
11
+ was installed as, and every local command takes it. It was an Organisation label before,
12
+ so the name a person chose on /me/runners read as an Organisation; an install made then
13
+ keeps its directory, login agent and keychain entries, all keyed by the same string.
10
14
  * **A login agent, never a system daemon** (23 item 1): a LaunchAgent in
11
15
  ``~/Library/LaunchAgents`` on macOS, a systemd *user* unit (with linger) on Linux, a
12
16
  per-user logon task on Windows. No ``sudo`` anywhere: the login keychain is unreachable
@@ -18,8 +22,8 @@ precedent does on a developer machine and nothing a daemon would need:
18
22
  under a harness root -- the per-Contract ``CODEX_HOME`` / ``CLAUDE_CONFIG_DIR`` belongs
19
23
  to the login the funder created there (issues 30, 31).
20
24
  * **``isolation: none``** (17 A2): no root and no uid switch, so the control plane routes
21
- at most one Contract at a time here (issue 42), which one Organisation per process and
22
- one active Contract per (Organisation, User Profile) pair already implied.
25
+ at most one Contract at a time here (issue 42), which one Contract per personal Runner
26
+ already implied.
23
27
  """
24
28
 
25
29
  from __future__ import annotations
@@ -43,17 +47,21 @@ from xml.sax.saxutils import escape
43
47
  import httpx
44
48
  from pydantic import BaseModel, ConfigDict
45
49
 
50
+ from agentic_runner import __version__ as runner_version
46
51
  from agentic_runner import service
47
52
  from agentic_runner.build import build_id
48
53
  from agentic_runner.cli_floor import CLI_FLOORS, CLI_NAMES, meets_floor, parse_cli_version
49
54
  from agentic_runner.config import CONFIG_FILE_ENV
50
55
  from agentic_runner.host_store import open_workstation_store
51
56
  from agentic_runner.lifecycle import LifecycleOutbox
57
+ from agentic_runner.llm_contracts import load_llm_contracts
52
58
  from agentic_runner.pool_key import parse_pool_key, save_pool_key
53
59
  from agentic_runner.registration import load_state
54
60
  from agentic_runner.service import HEARTBEAT_STAMP_NAME, PIDFILE_NAME
55
61
  from agentic_runner.workers.acp_runtime import ACP_BRIDGES
62
+ from agentic_runner_contracts.public_metadata import NAMESPACE_PREFIX
56
63
  from agentic_runner_contracts.runner_registration import (
64
+ RUNNER_NAME_PATTERN,
57
65
  CliVersion,
58
66
  EgressPosture,
59
67
  HostAttestation,
@@ -71,7 +79,7 @@ __all__ = [
71
79
  "CLI_NAMES",
72
80
  "ROOT_ENV",
73
81
  "CliMissingError",
74
- "OrgPaths",
82
+ "RunnerPaths",
75
83
  "ServiceDefinition",
76
84
  "WorkstationSettings",
77
85
  "cli_versions",
@@ -79,7 +87,10 @@ __all__ = [
79
87
  "default_root",
80
88
  "install",
81
89
  "install_channel",
90
+ "installed_names",
91
+ "list_lines",
82
92
  "locate_clis",
93
+ "only_runner",
83
94
  "parse_acp_cli_kinds",
84
95
  "require_any_cli",
85
96
  "run_environment",
@@ -88,12 +99,11 @@ __all__ = [
88
99
  "start",
89
100
  "status_lines",
90
101
  "stop",
91
- "validate_org",
102
+ "validate_name",
92
103
  ]
93
104
 
94
105
  ROOT_ENV: Final = "AGENTIC_RUNNER_WORKSTATION_ROOT"
95
106
 
96
- _ORG = re.compile(r"^[a-z0-9][a-z0-9-]{0,31}$")
97
107
  _LABEL_PREFIX: Final = "agentic-runner"
98
108
 
99
109
  Run = Callable[..., subprocess.CompletedProcess[str]]
@@ -103,18 +113,19 @@ class CliMissingError(RuntimeError):
103
113
  pass
104
114
 
105
115
 
106
- def validate_org(org: str) -> str:
116
+ def validate_name(name: str) -> str:
107
117
  """One path segment, one launchd label, one systemd instance name -- all at once."""
108
118
 
109
- if not _ORG.fullmatch(org):
119
+ if not re.fullmatch(RUNNER_NAME_PATTERN, name):
110
120
  raise ValueError(
111
- f"Organisation name {org!r} must be 1-32 of lowercase letters, digits and '-'"
121
+ f"Runner name {name!r} must be 1-32 of lowercase letters, digits and '-', "
122
+ "starting with a letter or digit"
112
123
  )
113
- return org
124
+ return name
114
125
 
115
126
 
116
127
  def default_root(*, platform: str = sys.platform, environ: Mapping[str, str] = os.environ) -> Path:
117
- """Where every Organisation's directory lives, per the OS's per-user convention."""
128
+ """Where every Runner's directory lives, per the OS's per-user convention."""
118
129
 
119
130
  if environ.get(ROOT_ENV, "").strip():
120
131
  return Path(environ[ROOT_ENV])
@@ -127,20 +138,50 @@ def default_root(*, platform: str = sys.platform, environ: Mapping[str, str] = o
127
138
  return (Path(state_home) if state_home else home / ".local" / "state") / "agentic-runner"
128
139
 
129
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
+
130
171
  @dataclass(frozen=True)
131
- class OrgPaths:
132
- """``<root>/<org>/`` and everything the process keeps under it."""
172
+ class RunnerPaths:
173
+ """``<root>/<name>/`` and everything the process keeps under it."""
133
174
 
134
175
  root: Path
135
- org: str
176
+ name: str
136
177
 
137
178
  @property
138
179
  def state_dir(self) -> Path:
139
- return self.root / self.org
180
+ return self.root / self.name
140
181
 
141
182
  @property
142
183
  def hooks(self) -> Path:
143
- # Issue 45's path, per map ticket 26: `<state_dir>/<org>/hooks/`.
184
+ # Issue 45's path, per map ticket 26: `<state_dir>/<name>/hooks/`.
144
185
  return self.state_dir / "hooks"
145
186
 
146
187
  @property
@@ -179,6 +220,12 @@ class OrgPaths:
179
220
  def log(self) -> Path:
180
221
  return self.state_dir / "runner.log"
181
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
+
182
229
  @property
183
230
  def socket_dir(self) -> Path:
184
231
  # `sun_path` is ~104 bytes and "~/Library/Application Support/..." is most of
@@ -189,7 +236,7 @@ class OrgPaths:
189
236
  tempfile.gettempdir() if sys.platform == "darwin" else ""
190
237
  )
191
238
  if runtime:
192
- return Path(runtime) / f"agentic-runner-{self.org}"
239
+ return Path(runtime) / f"agentic-runner-{self.name}"
193
240
  return self.state_dir / "sockets"
194
241
 
195
242
  def create(self) -> None:
@@ -203,10 +250,10 @@ class WorkstationSettings(BaseModel):
203
250
  """What ``install`` was told, so ``run`` and ``status`` need no flags."""
204
251
 
205
252
  # `ignore`, not `forbid`: only `save` writes this file, and an install made before
206
- # local-agents 01 retired the per-process `cli_kind` still has to `run` and `status`.
253
+ # local-agents 01 retired the per-process `cli_kind`, or before QTS-1773 dropped the
254
+ # `org` its directory already names, still has to `run` and `status`.
207
255
  model_config = ConfigDict(extra="ignore", frozen=True)
208
256
 
209
- org: str
210
257
  control_plane_url: str
211
258
  # A fallback only: `run` uses the address the registration response carries (ADR-0022
212
259
  # §4). An install made before 3.17 saved one and keeps it.
@@ -222,15 +269,15 @@ class WorkstationSettings(BaseModel):
222
269
  # which every `install` rewrites (QTS-1355).
223
270
  acp_cli_kinds: str = ""
224
271
 
225
- def save(self, paths: OrgPaths) -> None:
272
+ def save(self, paths: RunnerPaths) -> None:
226
273
  paths.settings_file.write_text(self.model_dump_json(indent=2), encoding="utf-8")
227
274
 
228
275
  @classmethod
229
- def load(cls, paths: OrgPaths) -> WorkstationSettings:
276
+ def load(cls, paths: RunnerPaths) -> WorkstationSettings:
230
277
  if not paths.settings_file.is_file():
231
278
  raise FileNotFoundError(
232
- f"no Runner is installed for {paths.org!r} under {paths.root}; "
233
- f"run `agentic-runner install {paths.org}` first"
279
+ f"no Runner is installed for {paths.name!r} under {paths.root}; "
280
+ f"run `agentic-runner install {paths.name}` first"
234
281
  )
235
282
  return cls.model_validate_json(paths.settings_file.read_text(encoding="utf-8"))
236
283
 
@@ -262,8 +309,8 @@ def install_channel(
262
309
  return InstallChannel.SYSTEMD_USER
263
310
 
264
311
 
265
- def run_environment(paths: OrgPaths, settings: WorkstationSettings) -> dict[str, str]:
266
- """The environment ``agentic-runner run --org`` gives ``service.run``.
312
+ def run_environment(paths: RunnerPaths, settings: WorkstationSettings) -> dict[str, str]:
313
+ """The environment ``agentic-runner run --name`` gives ``service.run``.
267
314
 
268
315
  Set here rather than in the service definition so one definition format -- a plist,
269
316
  a unit, a task -- stays a one-liner, and the three OSes cannot drift apart.
@@ -285,7 +332,7 @@ def run_environment(paths: OrgPaths, settings: WorkstationSettings) -> dict[str,
285
332
  "TEMPORAL_ADDRESS": settings.temporal_address,
286
333
  "WORKSPACE_ROOT": str(paths.workspaces),
287
334
  "WORKER_STATE_DIR": str(paths.contract_state),
288
- # Ephemeral: one process per Organisation shares the host, so a fixed readiness
335
+ # Ephemeral: one process per Runner shares the host, so a fixed readiness
289
336
  # port inherited from a pod-style environment would collide on the second one.
290
337
  service.READINESS_PORT_ENV: "0",
291
338
  }
@@ -396,16 +443,16 @@ class ServiceDefinition:
396
443
  stop: list[list[str]]
397
444
 
398
445
 
399
- def runner_command(org: str, root: Path) -> list[str]:
446
+ def runner_command(name: str, root: Path) -> list[str]:
400
447
  # The console script as found, not resolved: Homebrew's `bin/agentic-runner` is a
401
448
  # symlink into a versioned Cellar path that the next `brew upgrade` deletes.
402
449
  script = shutil.which("agentic-runner")
403
450
  prefix = [script] if script else [sys.executable, "-m", "agentic_runner.launch"]
404
- return [*prefix, "run", "--org", org, "--root", str(root), "--login-agent"]
451
+ return [*prefix, "run", "--name", name, "--root", str(root), "--login-agent"]
405
452
 
406
453
 
407
454
  def service_definition(
408
- paths: OrgPaths,
455
+ paths: RunnerPaths,
409
456
  settings: WorkstationSettings,
410
457
  *,
411
458
  platform: str = sys.platform,
@@ -415,8 +462,8 @@ def service_definition(
415
462
  command: Sequence[str] | None = None,
416
463
  ) -> ServiceDefinition:
417
464
  home = home or Path.home()
418
- argv = list(command or runner_command(paths.org, paths.root))
419
- name = f"{_LABEL_PREFIX}.{paths.org}"
465
+ argv = list(command or runner_command(paths.name, paths.root))
466
+ name = f"{_LABEL_PREFIX}.{paths.name}"
420
467
  if platform == "darwin":
421
468
  return _launch_agent(paths, settings, argv, name=name, home=home, uid=uid)
422
469
  if platform == "win32":
@@ -425,7 +472,7 @@ def service_definition(
425
472
 
426
473
 
427
474
  def _launch_agent(
428
- paths: OrgPaths,
475
+ paths: RunnerPaths,
429
476
  settings: WorkstationSettings,
430
477
  argv: list[str],
431
478
  *,
@@ -458,19 +505,19 @@ def _launch_agent(
458
505
 
459
506
 
460
507
  def _systemd_user_unit(
461
- paths: OrgPaths,
508
+ paths: RunnerPaths,
462
509
  settings: WorkstationSettings,
463
510
  argv: list[str],
464
511
  *,
465
512
  home: Path,
466
513
  user: str | None,
467
514
  ) -> ServiceDefinition:
468
- unit_name = f"{_LABEL_PREFIX}-{paths.org}.service"
515
+ unit_name = f"{_LABEL_PREFIX}-{paths.name}.service"
469
516
  unit = home / ".config" / "systemd" / "user" / unit_name
470
517
  exec_start = " ".join(_systemd_quote(arg) for arg in argv)
471
518
  content = (
472
519
  "[Unit]\n"
473
- f"Description=Agentic OS Runner for {paths.org}\n"
520
+ f"Description=Agentic OS Runner for {paths.name}\n"
474
521
  "After=network-online.target\n\n"
475
522
  "[Service]\n"
476
523
  f"ExecStart={exec_start}\n"
@@ -504,16 +551,16 @@ def _systemd_quote(value: str) -> str:
504
551
 
505
552
 
506
553
  def _logon_task(
507
- paths: OrgPaths, argv: list[str], *, name: str, user: str | None
554
+ paths: RunnerPaths, argv: list[str], *, name: str, user: str | None
508
555
  ) -> ServiceDefinition:
509
556
  task_xml = paths.state_dir / f"{name}.task.xml"
510
- task_name = f"\\agentic-runner\\{paths.org}"
557
+ task_name = f"\\agentic-runner\\{paths.name}"
511
558
  who = user or os.environ.get("USERNAME", "")
512
559
  arguments = subprocess.list2cmdline(argv[1:])
513
560
  content = f"""<?xml version="1.0" encoding="UTF-16"?>
514
561
  <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
515
562
  <RegistrationInfo>
516
- <Description>Agentic OS Runner for {escape(paths.org)}</Description>
563
+ <Description>Agentic OS Runner for {escape(paths.name)}</Description>
517
564
  </RegistrationInfo>
518
565
  <Triggers>
519
566
  <LogonTrigger><Enabled>true</Enabled><UserId>{escape(who)}</UserId></LogonTrigger>
@@ -564,7 +611,7 @@ def drive(commands: list[list[str]], *, run: Run = subprocess.run) -> None:
564
611
  # ------------------------------------------------------------------ status
565
612
 
566
613
 
567
- def process_id(paths: OrgPaths) -> int | None:
614
+ def process_id(paths: RunnerPaths) -> int | None:
568
615
  """The running process's pid, if the pidfile names a live one."""
569
616
 
570
617
  try:
@@ -580,7 +627,7 @@ def process_id(paths: OrgPaths) -> int | None:
580
627
  return pid
581
628
 
582
629
 
583
- def heartbeat_age(paths: OrgPaths, *, now: datetime | None = None) -> float | None:
630
+ def heartbeat_age(paths: RunnerPaths, *, now: datetime | None = None) -> float | None:
584
631
  try:
585
632
  stamped = paths.heartbeat_stamp.stat().st_mtime
586
633
  except OSError:
@@ -588,14 +635,14 @@ def heartbeat_age(paths: OrgPaths, *, now: datetime | None = None) -> float | No
588
635
  return max(0.0, (now or datetime.now(UTC)).timestamp() - stamped)
589
636
 
590
637
 
591
- def status_lines(paths: OrgPaths, *, now: datetime | None = None) -> list[str]:
592
- """``status <org>``: the process, its identity and its heartbeat age."""
638
+ def status_lines(paths: RunnerPaths, *, now: datetime | None = None) -> list[str]:
639
+ """``status <name>``: the process, its identity and its heartbeat age."""
593
640
 
594
641
  pid = process_id(paths)
595
642
  state = load_state(paths.state_dir)
596
643
  age = heartbeat_age(paths, now=now)
597
644
  lines = [
598
- f"organisation {paths.org} ({paths.state_dir})",
645
+ f"runner name {paths.name} ({paths.state_dir})",
599
646
  f"process {'running, pid ' + str(pid) if pid else 'not running'}",
600
647
  (
601
648
  f"identity {state.runner_id} in {state.temporal_namespace} on {state.task_queue}"
@@ -609,6 +656,58 @@ def status_lines(paths: OrgPaths, *, now: datetime | None = None) -> list[str]:
609
656
  return lines
610
657
 
611
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
+
612
711
  def next_step_lines(host_party: str) -> list[str]:
613
712
  """Where to go once the Runner is live (QTS-1314): the dogfood install ended at
614
713
  ``started`` and left the person with no idea what the console wanted next.
@@ -634,7 +733,7 @@ def next_step_lines(host_party: str) -> list[str]:
634
733
 
635
734
 
636
735
  async def install(
637
- paths: OrgPaths,
736
+ paths: RunnerPaths,
638
737
  settings: WorkstationSettings,
639
738
  *,
640
739
  registration_token: str,
@@ -645,7 +744,7 @@ async def install(
645
744
  run: Run = subprocess.run,
646
745
  http_client: httpx.AsyncClient | None = None,
647
746
  ) -> list[str]:
648
- """Register this Organisation's process and install its login agent.
747
+ """Register this Runner and install its login agent.
649
748
 
650
749
  Registration happens here, once, with the Runner Registration Token this user minted
651
750
  on /me/runners or the Organisation's Admin issued them (12 A6, QTS-1304) -- pasted at the
@@ -696,18 +795,18 @@ async def install(
696
795
  return lines
697
796
 
698
797
 
699
- def start(paths: OrgPaths, **driver: Any) -> None:
798
+ def start(paths: RunnerPaths, **driver: Any) -> None:
700
799
  _drive_service(paths, "start", **driver)
701
800
 
702
801
 
703
- def stop(paths: OrgPaths, **driver: Any) -> None:
704
- """Stop this Organisation's process; every other Organisation's keeps polling."""
802
+ def stop(paths: RunnerPaths, **driver: Any) -> None:
803
+ """Stop this Runner; every other Runner on this machine keeps polling."""
705
804
 
706
805
  _drive_service(paths, "stop", **driver)
707
806
 
708
807
 
709
808
  def _drive_service(
710
- paths: OrgPaths,
809
+ paths: RunnerPaths,
711
810
  verb: str,
712
811
  *,
713
812
  platform: str = sys.platform,
@@ -719,8 +818,8 @@ def _drive_service(
719
818
  drive(definition.start if verb == "start" else definition.stop, run=run)
720
819
 
721
820
 
722
- def run_process(paths: OrgPaths, *, login_agent: bool) -> int:
723
- """``agentic-runner run --org``: what the login agent executes."""
821
+ def run_process(paths: RunnerPaths, *, login_agent: bool) -> int:
822
+ """``agentic-runner run --name``: what the login agent executes."""
724
823
 
725
824
  settings = WorkstationSettings.load(paths)
726
825
  try:
@@ -730,13 +829,15 @@ def run_process(paths: OrgPaths, *, login_agent: bool) -> int:
730
829
  return 1
731
830
  for name, value in run_environment(paths, settings).items():
732
831
  os.environ[name] = value
733
- store = open_workstation_store(paths.org, fallback=paths.credentials)
832
+ paths.started_version.write_text(runner_version, encoding="utf-8")
833
+ store = open_workstation_store(paths.name, fallback=paths.credentials)
734
834
  attestation = collect_attestation(
735
835
  channel=settings.install_channel,
736
836
  session=SessionKind.LOGIN_AGENT if login_agent else SessionKind.INTERACTIVE,
737
837
  store=store.kind,
738
838
  path=settings.path,
739
839
  )
740
- # A workstation's hostname can name its person, so it is never reported (rev 3).
741
- install = RunnerInstall(target=InstallTarget.WORKSTATION)
840
+ # A workstation's hostname can name its person, so it is never reported (rev 3); the
841
+ # name it was installed as is the person's choice, and the console lists it (QTS-1773).
842
+ install = RunnerInstall(target=InstallTarget.WORKSTATION, name=paths.name)
742
843
  return asyncio.run(service.run(host_store=store, attestation=attestation, install=install))
File without changes