agentic-runner 3.16.2__tar.gz → 3.18.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 (76) hide show
  1. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/PKG-INFO +2 -2
  2. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/pyproject.toml +1 -1
  3. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/__init__.py +1 -1
  4. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/activities.py +1 -1
  5. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/cli.py +45 -10
  6. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/registration.py +4 -0
  7. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/service.py +11 -1
  8. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/testing/control_plane.py +9 -1
  9. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/settings.py +9 -1
  10. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workstation.py +47 -0
  11. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/.gitignore +0 -0
  12. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/LICENSE +0 -0
  13. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/README.md +0 -0
  14. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/attempts.py +0 -0
  15. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/auth_mode.py +0 -0
  16. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/build.py +0 -0
  17. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/callback.py +0 -0
  18. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/child_watcher.py +0 -0
  19. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/cli_floor.py +0 -0
  20. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/config.py +0 -0
  21. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/credentials.py +0 -0
  22. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/device_login_activities.py +0 -0
  23. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/egress.py +0 -0
  24. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/harness_self_test.py +0 -0
  25. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/heartbeat_link.py +0 -0
  26. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/hooks.py +0 -0
  27. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/host_store.py +0 -0
  28. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/__init__.py +0 -0
  29. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  30. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  31. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  32. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  33. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  34. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  35. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  36. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  37. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  38. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
  39. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
  40. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/lifecycle.py +0 -0
  41. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/llm_proxy.py +0 -0
  42. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/mcp.py +0 -0
  43. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/message_store.py +0 -0
  44. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/oauth_connectors.py +0 -0
  45. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/private_state.py +0 -0
  46. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/py.typed +0 -0
  47. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  48. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/runtime/__init__.py +0 -0
  49. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  50. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/sealed_box.py +0 -0
  51. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/skill.py +0 -0
  52. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +0 -0
  53. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/testing/__init__.py +0 -0
  54. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/testing/__main__.py +0 -0
  55. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/testing/plugin.py +0 -0
  56. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/testing/test_conformance.py +0 -0
  57. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/testing/workflow.py +0 -0
  58. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/tiny_http.py +0 -0
  59. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/triage_activities.py +0 -0
  60. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/usage_windows.py +0 -0
  61. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/user_sources.py +0 -0
  62. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/__init__.py +0 -0
  63. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
  64. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/acp_runtime.py +0 -0
  65. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
  66. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
  67. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
  68. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
  69. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/command_policy.py +0 -0
  70. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/contract_device_login.py +0 -0
  71. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
  72. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  73. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
  74. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  75. {agentic_runner-3.16.2 → agentic_runner-3.18.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  76. {agentic_runner-3.16.2 → agentic_runner-3.18.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.16.2
3
+ Version: 3.18.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.16.2
8
+ Requires-Dist: agentic-runner-contracts==3.18.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
@@ -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.16.2",
13
+ "agentic-runner-contracts==3.18.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.16.2"
10
+ __version__ = "3.18.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -3888,7 +3888,7 @@ class RunnerRalphActivities:
3888
3888
  "event": "directive.repository_refused",
3889
3889
  "product_id": product_id,
3890
3890
  "runner_id": str(self._routing_identity.runner_id),
3891
- "selector": dict(selector or {}),
3891
+ "selector": [requirement.as_payload() for requirement in selector or ()],
3892
3892
  },
3893
3893
  )
3894
3894
 
@@ -13,7 +13,8 @@ the Organisation's cap.
13
13
 
14
14
  ``install | start | stop | status <org>`` and ``credential set`` are the workstation
15
15
  Runner (PRD issue 47, ``workstation.py``): one login agent per Organisation, managed by
16
- the user who hosts it, with no ``sudo`` anywhere.
16
+ the user who hosts it, with no ``sudo`` anywhere. ``add <name>`` registers another Runner
17
+ beside them, reusing their control plane and ``PATH`` (ADR-0022 §21).
17
18
 
18
19
  ``sign-in --contract <id>`` signs one Contract's Claude Code in with Anthropic's own flow,
19
20
  in this terminal, as that Contract's uid in its harness root (QTS-1448). No code or token
@@ -179,6 +180,21 @@ def main(argv: Sequence[str] | None = None) -> int:
179
180
  )
180
181
  install.add_argument("--no-start", action="store_true", help="write the agent, do not start")
181
182
  install.add_argument("--root", default=None, type=Path, help="state root (default per OS)")
183
+
184
+ add = subcommands.add_parser(
185
+ "add",
186
+ help="register another Runner under this install, with its own process, identity "
187
+ "and Recipient Key (one per Contract)",
188
+ )
189
+ add.add_argument("org", metavar="name", help="the new Runner's name on this machine")
190
+ add.add_argument(
191
+ "--beside",
192
+ default=None,
193
+ help="the installed Runner whose control plane and PATH to reuse; needed only when "
194
+ "the installed Runners talk to different control planes",
195
+ )
196
+ add.add_argument("--no-start", action="store_true", help="write the agent, do not start")
197
+ add.add_argument("--root", default=None, type=Path, help="state root (default per OS)")
182
198
  for service_verb in ("start", "stop"):
183
199
  driven = subcommands.add_parser(
184
200
  service_verb, help=f"{service_verb} one Organisation's login agent"
@@ -312,7 +328,7 @@ def main(argv: Sequence[str] | None = None) -> int:
312
328
  if arguments.org:
313
329
  return workstation.run_process(_org_paths(arguments), login_agent=arguments.login_agent)
314
330
  return asyncio.run(service.run(attestation=_image_attestation()))
315
- if arguments.command in {"install", "start", "stop", "credential"}:
331
+ if arguments.command in {"install", "add", "start", "stop", "credential"}:
316
332
  return _workstation(arguments)
317
333
  if arguments.command == "recipient-key":
318
334
  api, kube_ns = in_cluster()
@@ -359,6 +375,8 @@ def _workstation(arguments: argparse.Namespace) -> int:
359
375
  try:
360
376
  if arguments.command == "install":
361
377
  return _install(arguments, paths)
378
+ if arguments.command == "add":
379
+ return _add(arguments, paths)
362
380
  if arguments.command == "start":
363
381
  workstation.start(paths)
364
382
  print(f"started {paths.org}")
@@ -422,11 +440,7 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
422
440
  except ValueError as error:
423
441
  print(str(error), file=sys.stderr)
424
442
  return 2
425
- # Pasted, never a flag: a flag is in the process table for every uid on the box.
426
- token = registration_token_from_env() or getpass.getpass(
427
- "Runner Registration Token (yours from /me/runners, or the Organisation Admin's for "
428
- "an Organisation Runner): "
429
- )
443
+ token = _registration_token()
430
444
  settings = workstation.WorkstationSettings(
431
445
  org=paths.org,
432
446
  control_plane_url=arguments.control_plane,
@@ -436,11 +450,19 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
436
450
  install_channel=workstation.install_channel(),
437
451
  acp_cli_kinds=acp_cli_kinds,
438
452
  )
453
+ return _register(paths, settings, token, start=not arguments.no_start)
454
+
455
+
456
+ def _register(
457
+ paths: workstation.OrgPaths,
458
+ settings: workstation.WorkstationSettings,
459
+ token: str,
460
+ *,
461
+ start: bool,
462
+ ) -> int:
439
463
  try:
440
464
  lines = asyncio.run(
441
- workstation.install(
442
- paths, settings, registration_token=token, start_service=not arguments.no_start
443
- )
465
+ workstation.install(paths, settings, registration_token=token, start_service=start)
444
466
  )
445
467
  except (workstation.CliMissingError, RunnerRegistrationError, RuntimeError) as error:
446
468
  print(f"install refused: {error}", file=sys.stderr)
@@ -450,6 +472,19 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
450
472
  return 0
451
473
 
452
474
 
475
+ def _add(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
476
+ settings = workstation.settings_to_add(paths, beside=arguments.beside)
477
+ return _register(paths, settings, _registration_token(), start=not arguments.no_start)
478
+
479
+
480
+ def _registration_token() -> str:
481
+ # Pasted, never a flag: a flag is in the process table for every uid on the box.
482
+ return registration_token_from_env() or getpass.getpass(
483
+ "Runner Registration Token (yours from /me/runners, or the Organisation Admin's for "
484
+ "an Organisation Runner): "
485
+ )
486
+
487
+
453
488
  def _acp_cli_kinds(flag: str | None, paths: workstation.OrgPaths) -> str:
454
489
  # A reinstall to pick up a new PATH or retry a token must not turn ACP off (QTS-1355).
455
490
  if flag is not None:
@@ -123,6 +123,9 @@ class RunnerState(BaseModel):
123
123
  # The control plane's cadence, as bootstrap told it (06 §4's 30 s by default); a
124
124
  # state file written before issue 47 carries none and keeps the default.
125
125
  heartbeat_interval_seconds: int = 30
126
+ # Where bootstrap said the namespace lives (ADR-0022 §4). Blank in a state file written
127
+ # before it, or from an older control plane: the process then uses `TEMPORAL_ADDRESS`.
128
+ temporal_address: str = ""
126
129
 
127
130
  def signer(self) -> Ed25519PrivateKey:
128
131
  key = serialization.load_pem_private_key(
@@ -279,6 +282,7 @@ class RunnerRegistrationClient:
279
282
  tags=dict(tags),
280
283
  max_concurrent_directives=max_concurrent_directives,
281
284
  heartbeat_interval_seconds=response.heartbeat_interval_seconds,
285
+ temporal_address=response.temporal_address,
282
286
  )
283
287
  return state, response
284
288
 
@@ -1112,6 +1112,14 @@ async def _serve(
1112
1112
  print(f"refusing to start ({error.reason}): {error}", flush=True)
1113
1113
  return 1
1114
1114
  print(outcome, flush=True)
1115
+ temporal_address = state.temporal_address or settings.TEMPORAL_ADDRESS
1116
+ if not temporal_address:
1117
+ print(
1118
+ "refusing to start (configuration_missing): the control plane gave no Temporal "
1119
+ "address and TEMPORAL_ADDRESS is not set",
1120
+ flush=True,
1121
+ )
1122
+ return 1
1115
1123
  interval = (
1116
1124
  heartbeat_interval if heartbeat_interval is not None else state.heartbeat_interval_seconds
1117
1125
  )
@@ -1125,6 +1133,7 @@ async def _serve(
1125
1133
  config=config,
1126
1134
  settings=settings,
1127
1135
  state=state,
1136
+ temporal_address=temporal_address,
1128
1137
  mode=mode,
1129
1138
  can_change_uid=can_change_uid,
1130
1139
  control_plane=control_plane,
@@ -1148,6 +1157,7 @@ async def _serve_registered(
1148
1157
  config: RunnerConfig,
1149
1158
  settings: WorkerSettings,
1150
1159
  state: RunnerState,
1160
+ temporal_address: str,
1151
1161
  mode: IsolationMode,
1152
1162
  can_change_uid: bool,
1153
1163
  control_plane: str,
@@ -1241,7 +1251,7 @@ async def _serve_registered(
1241
1251
  await asyncio.sleep(interval)
1242
1252
 
1243
1253
  client = await connect(
1244
- settings.TEMPORAL_ADDRESS,
1254
+ temporal_address,
1245
1255
  namespace=state.temporal_namespace,
1246
1256
  api_key=stream.runner_token,
1247
1257
  tls=os.environ.get(TEMPORAL_TLS_ENV, "").strip().lower() in {"1", "true", "yes"},
@@ -167,6 +167,8 @@ class FakeControlPlane:
167
167
  contract_id: UUID = DEFAULT_CONTRACT_ID
168
168
  agent_id: UUID = DEFAULT_AGENT_ID
169
169
  cli_kind: str = "codex_cli"
170
+ # Blank answers as a control plane from before ADR-0022 §4 does: no address at all.
171
+ temporal_address: str = ""
170
172
 
171
173
  bootstraps: list[BootstrapRequest] = field(default_factory=list)
172
174
  runner_ids: list[UUID] = field(default_factory=list)
@@ -396,8 +398,14 @@ class FakeControlPlane:
396
398
  contracts_floor=self.contracts_floor,
397
399
  host_party=self.host_party,
398
400
  heartbeat_interval_seconds=self.heartbeat_interval_seconds,
401
+ temporal_address=self.temporal_address,
402
+ )
403
+ return _Answer(
404
+ 200,
405
+ response.model_dump(
406
+ mode="json", exclude=None if self.temporal_address else {"temporal_address"}
407
+ ),
399
408
  )
400
- return _Answer(200, response.model_dump(mode="json"))
401
409
 
402
410
  def _heartbeat(self, method: str, target: str, headers: dict[str, str], body: bytes) -> _Answer:
403
411
  runner_id, runner = self._signed(method, target, headers, body)
@@ -13,7 +13,13 @@ from agentic_runner_contracts.activity_io import DEFAULT_BUDGET, Budget
13
13
  class WorkerSettings(BaseSettings):
14
14
  """Runtime configuration for worker-only processes."""
15
15
 
16
- TEMPORAL_ADDRESS: str = Field(..., description="Temporal server address in host:port format")
16
+ TEMPORAL_ADDRESS: str = Field(
17
+ default="",
18
+ description=(
19
+ "Temporal server address in host:port format. Only a fallback: the Runner uses "
20
+ "the address its registration response carries (ADR-0022 §4)."
21
+ ),
22
+ )
17
23
  TEMPORAL_NAMESPACE: str = Field(default="default", description="Temporal namespace name")
18
24
  PLATFORM_WORKER_SHARD_INDEX: int = Field(
19
25
  default=0,
@@ -216,6 +222,8 @@ class WorkerSettings(BaseSettings):
216
222
  """Validate host:port shape for Temporal server address."""
217
223
 
218
224
  stripped_value = value.strip()
225
+ if not stripped_value:
226
+ return stripped_value
219
227
  if stripped_value.count(":") < 1:
220
228
  raise ValueError("TEMPORAL_ADDRESS must follow host:port")
221
229
 
@@ -7,6 +7,8 @@ precedent does on a developer machine and nothing a daemon would need:
7
7
  identity, namespace, Runner Token, hooks, credential store and Workspaces under
8
8
  ``<root>/<org>/``. A contractor with two clients runs two. There is no supervisor over
9
9
  N Organisations -- that is the "one Runner straddling Organisations" 12 A6 rejected.
10
+ ``add <name>`` puts another such process beside them, a personal Runner per Contract
11
+ (ADR-0022 §21), named freely rather than after its Organisation.
10
12
  * **A login agent, never a system daemon** (23 item 1): a LaunchAgent in
11
13
  ``~/Library/LaunchAgents`` on macOS, a systemd *user* unit (with linger) on Linux, a
12
14
  per-user logon task on Windows. No ``sudo`` anywhere: the login keychain is unreachable
@@ -81,6 +83,7 @@ __all__ = [
81
83
  "run_environment",
82
84
  "run_process",
83
85
  "service_definition",
86
+ "settings_to_add",
84
87
  "start",
85
88
  "status_lines",
86
89
  "stop",
@@ -673,6 +676,50 @@ async def install(
673
676
  return lines
674
677
 
675
678
 
679
+ def settings_to_add(paths: OrgPaths, *, beside: str | None) -> WorkstationSettings:
680
+ """``add <name>``: what ``install`` registers another Runner with (ADR-0022 §21).
681
+
682
+ A personal Runner serves one Contract, so a person with three Contracts runs three. Each
683
+ is ``install``'s own process with its own state directory -- identity, Runner Token and
684
+ Recipient Key (§37) -- and only the settings an earlier ``install`` was told are copied,
685
+ so the command the console shows carries no Temporal address.
686
+ """
687
+
688
+ # Registered state, not workstation.json: ``install`` saves the settings before it
689
+ # registers, so a refused token leaves them behind and the retry must go through.
690
+ if load_state(paths.state_dir) is not None:
691
+ raise ValueError(
692
+ f"{paths.org!r} is already installed under {paths.root}; pick a new name for "
693
+ "the Runner you are adding"
694
+ )
695
+ return _settings_beside(paths.root, beside).model_copy(update={"org": paths.org})
696
+
697
+
698
+ def _settings_beside(root: Path, beside: str | None) -> WorkstationSettings:
699
+ if beside is not None:
700
+ return WorkstationSettings.load(OrgPaths(root=root, org=validate_org(beside)))
701
+ installed = [
702
+ WorkstationSettings.load(OrgPaths(root=root, org=file.parent.name))
703
+ for file in sorted(root.glob("*/workstation.json"), key=lambda file: file.stat().st_mtime)
704
+ ]
705
+ if not installed:
706
+ raise FileNotFoundError(
707
+ f"no Runner is installed under {root}; run `agentic-runner install` first"
708
+ )
709
+ endpoints = {
710
+ (settings.control_plane_url, settings.temporal_address, settings.temporal_tls)
711
+ for settings in installed
712
+ }
713
+ if len(endpoints) > 1:
714
+ names = ", ".join(sorted(settings.org for settings in installed))
715
+ raise ValueError(
716
+ f"the Runners under {root} ({names}) talk to different control planes; "
717
+ "name the one to add beside with --beside"
718
+ )
719
+ # The newest install's PATH and ACP kinds: the latest the person told this machine.
720
+ return installed[-1]
721
+
722
+
676
723
  def start(paths: OrgPaths, **driver: Any) -> None:
677
724
  _drive_service(paths, "start", **driver)
678
725
 
File without changes