agentic-runner 3.15.0__tar.gz → 3.16.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.15.0 → agentic_runner-3.16.0}/PKG-INFO +6 -5
  2. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/README.md +4 -3
  3. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/pyproject.toml +4 -1
  4. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/__init__.py +1 -1
  5. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/activities.py +13 -7
  6. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/cli.py +11 -7
  7. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/mcp.py +1 -1
  8. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/registration.py +19 -6
  9. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/service.py +37 -7
  10. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +6 -6
  11. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/testing/plugin.py +2 -2
  12. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/acp_runtime.py +33 -1
  13. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/agent_runtime.py +6 -0
  14. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/claude_runtime.py +5 -3
  15. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/codex_runtime.py +9 -2
  16. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workstation.py +7 -7
  17. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/.gitignore +0 -0
  18. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/LICENSE +0 -0
  19. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/attempts.py +0 -0
  20. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/auth_mode.py +0 -0
  21. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/build.py +0 -0
  22. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/callback.py +0 -0
  23. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/child_watcher.py +0 -0
  24. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/cli_floor.py +0 -0
  25. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/config.py +0 -0
  26. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/credentials.py +0 -0
  27. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/device_login_activities.py +0 -0
  28. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/egress.py +0 -0
  29. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/harness_self_test.py +0 -0
  30. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/heartbeat_link.py +0 -0
  31. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/hooks.py +0 -0
  32. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/host_store.py +0 -0
  33. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/__init__.py +0 -0
  34. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  35. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  36. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  37. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  38. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  39. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  40. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  41. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  42. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  43. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
  44. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
  45. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/lifecycle.py +0 -0
  46. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/llm_proxy.py +0 -0
  47. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/message_store.py +0 -0
  48. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/oauth_connectors.py +0 -0
  49. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/private_state.py +0 -0
  50. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/py.typed +0 -0
  51. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  52. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/runtime/__init__.py +0 -0
  53. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  54. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/sealed_box.py +0 -0
  55. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/skill.py +0 -0
  56. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/testing/__init__.py +0 -0
  57. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/testing/__main__.py +0 -0
  58. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/testing/control_plane.py +0 -0
  59. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/testing/test_conformance.py +0 -0
  60. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/testing/workflow.py +0 -0
  61. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/tiny_http.py +0 -0
  62. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/triage_activities.py +0 -0
  63. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/usage_windows.py +0 -0
  64. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/user_sources.py +0 -0
  65. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/__init__.py +0 -0
  66. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
  67. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
  68. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/command_policy.py +0 -0
  69. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/contract_device_login.py +0 -0
  70. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
  71. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  72. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
  73. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  74. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  75. {agentic_runner-3.15.0 → agentic_runner-3.16.0}/src/agentic_runner/workers/settings.py +0 -0
  76. {agentic_runner-3.15.0 → agentic_runner-3.16.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.15.0
3
+ Version: 3.16.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
8
+ Requires-Dist: agentic-runner-contracts==3.16.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
@@ -33,9 +33,10 @@ no intake. That inertness, not obfuscation, is the boundary (ADR-0013 §5).
33
33
 
34
34
  ## Running it
35
35
 
36
- `agentic-runner run` is the process: it exchanges the Agent Token in
37
- `AGENTIC_AGENT_TOKEN` for a durable identity at `AGENTIC_CONTROL_PLANE_URL` (once,
38
- persisted under `AGENTIC_RUNNER_STATE_DIR`), heartbeats every 30 s, connects to the
36
+ `agentic-runner run` is the process: it exchanges the Runner Registration Token in
37
+ `AGENTIC_RUNNER_REGISTRATION_TOKEN` (or, deprecated, `AGENTIC_AGENT_TOKEN`) for a durable
38
+ identity at `AGENTIC_CONTROL_PLANE_URL` (once, persisted under `AGENTIC_RUNNER_STATE_DIR`),
39
+ heartbeats every 30 s, connects to the
39
40
  namespace it was handed with the Runner Token the heartbeat refreshes, and polls its own
40
41
  `runner.{runner_id}` queue. `AGENTIC_RUNNER_ISOLATION` is `contract_uid` (default: every
41
42
  Contract its own uid, needs `CAP_SETUID` — refused otherwise) or `none`. Readiness is
@@ -14,9 +14,10 @@ no intake. That inertness, not obfuscation, is the boundary (ADR-0013 §5).
14
14
 
15
15
  ## Running it
16
16
 
17
- `agentic-runner run` is the process: it exchanges the Agent Token in
18
- `AGENTIC_AGENT_TOKEN` for a durable identity at `AGENTIC_CONTROL_PLANE_URL` (once,
19
- persisted under `AGENTIC_RUNNER_STATE_DIR`), heartbeats every 30 s, connects to the
17
+ `agentic-runner run` is the process: it exchanges the Runner Registration Token in
18
+ `AGENTIC_RUNNER_REGISTRATION_TOKEN` (or, deprecated, `AGENTIC_AGENT_TOKEN`) for a durable
19
+ identity at `AGENTIC_CONTROL_PLANE_URL` (once, persisted under `AGENTIC_RUNNER_STATE_DIR`),
20
+ heartbeats every 30 s, connects to the
20
21
  namespace it was handed with the Runner Token the heartbeat refreshes, and polls its own
21
22
  `runner.{runner_id}` queue. `AGENTIC_RUNNER_ISOLATION` is `contract_uid` (default: every
22
23
  Contract its own uid, needs `CAP_SETUID` — refused otherwise) or `none`. Readiness is
@@ -7,7 +7,10 @@ license-files = ["LICENSE"]
7
7
  requires-python = ">=3.12"
8
8
  dynamic = ["version"]
9
9
  dependencies = [
10
- "agentic-runner-contracts",
10
+ # Exactly this release's contracts: the two ship as one version, and an unpinned
11
+ # requirement let `uv tool upgrade` keep an older contracts wheel the Runner cannot
12
+ # import from (QTS-1473). `scripts/set-version.py` moves it with the version.
13
+ "agentic-runner-contracts==3.16.0",
11
14
  # Imported directly by `registration.py` (the Ed25519 identity that signs every
12
15
  # heartbeat); already present transitively through pyjwt[crypto], declared because
13
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.15.0"
10
+ __version__ = "3.16.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -604,8 +604,8 @@ class _RuntimeContextState:
604
604
  # Which harness this Work Record runs, and so which per-Contract config root
605
605
  # (`{contract_id}/harness/{cli_kind}`) its Directives read (ADR-0015 §4).
606
606
  cli_kind: str = _DEFAULT_CLI_KIND
607
- # PRD issue 31: what the Agent is configured to run, for pricing a Codex harness
608
- # usage event that carries no model of its own.
607
+ # The Agent's model, which every Directive runs (ADR-0021); None for the Runner's
608
+ # default for `cli_kind`.
609
609
  model: str | None = None
610
610
  # The Agent Runtime Profile's per-process memory ceiling (map ticket 17 A7), carried
611
611
  # on the Profile's `command_policy` blob — the sandbox floor already lives there, and
@@ -1516,6 +1516,8 @@ def _codex_evidence_payload(
1516
1516
  codex_result.error, _RUNTIME_EVIDENCE_STREAM_LIMIT_BYTES, notes, "error"
1517
1517
  ),
1518
1518
  "command_hash": codex_result.command_hash,
1519
+ # What ran, which the Agent's configuration alone does not prove (ADR-0021).
1520
+ "model": codex_result.model or None,
1519
1521
  "workspace": asdict(codex_result.evidence),
1520
1522
  "persona_slug": persona_slug,
1521
1523
  "persona_instructions_hash": _persona_instructions_hash(persona_instructions),
@@ -2121,6 +2123,7 @@ class RunnerRalphActivities:
2121
2123
  auth_mode=attempt.auth_mode,
2122
2124
  permission_fallback=runtime_state.permission_fallback,
2123
2125
  acp_command=runtime_state.acp_command,
2126
+ model=runtime_state.model or "",
2124
2127
  prompt=attempt.skills_preamble
2125
2128
  + _directive_prompt(
2126
2129
  completion_criteria=runtime_state.completion_criteria,
@@ -5067,6 +5070,7 @@ class RunnerRalphActivities:
5067
5070
  auth_mode=attempt.auth_mode,
5068
5071
  permission_fallback=state.permission_fallback,
5069
5072
  acp_command=state.acp_command,
5073
+ model=state.model or "",
5070
5074
  prompt=attempt.skills_preamble
5071
5075
  + _learning_prompt(
5072
5076
  request,
@@ -5333,6 +5337,7 @@ class RunnerRalphActivities:
5333
5337
  auth_mode=attempt.auth_mode,
5334
5338
  permission_fallback=state.permission_fallback,
5335
5339
  acp_command=state.acp_command,
5340
+ model=state.model or "",
5336
5341
  prompt=attempt.skills_preamble + await kind.prompt(state, prompt_notes),
5337
5342
  base_branch=request.base_ref,
5338
5343
  work_branch=state.work_branch,
@@ -5602,16 +5607,17 @@ class RunnerRalphActivities:
5602
5607
  reports = [(usage_event, None)] if usage_event else []
5603
5608
  else:
5604
5609
  usage_event = extract_codex_turn_usage(result.stdout)
5605
- reports = [(usage_event, runtime_state.model or None)] if usage_event else []
5610
+ reports = [(usage_event, result.model or None)] if usage_event else []
5606
5611
  for usage_event, model_name in reports:
5607
5612
  if runtime_state.cli_kind != "claude_code" and model_name is None:
5608
5613
  # The backend 422s a Codex harness report with no model
5609
5614
  # (`report_harness_usage` requires one to price it), and the `suppress`
5610
- # below swallows that -- an Agent with no model configured would otherwise
5611
- # lose this Usage Record with no trace.
5615
+ # below swallows that -- a Directive that ran Codex's own default model,
5616
+ # which its JSON stream never names, would otherwise lose this Usage
5617
+ # Record with no trace.
5612
5618
  activity.logger.warning(
5613
- "harness usage unreported: Agent %s has no model configured for a Codex "
5614
- "harness Directive (work record %s)",
5619
+ "harness usage unreported: Agent %s names no model, so its Codex "
5620
+ "harness Directive ran one the Runner cannot name (work record %s)",
5615
5621
  runtime_state.agent_id,
5616
5622
  work_record_id,
5617
5623
  )
@@ -5,8 +5,8 @@ contracts version is the compatibility floor the control plane checks before it
5
5
  Directive token (ADR-0013 §7). An operator debugging "why did this Runner stop getting
6
6
  Directives" needs both numbers from one command.
7
7
 
8
- ``register`` is first boot (PRD issue 41): it presents the Agent Token from the
9
- environment, persists the durable identity the exchange hands back, and prints the
8
+ ``register`` is first boot (PRD issue 41): it presents the Runner Registration Token from
9
+ the environment, persists the durable identity the exchange hands back, and prints the
10
10
  namespace and queue this process was assigned. It is idempotent by state file -- a
11
11
  restart re-reads what is already there rather than registering a second Runner against
12
12
  the Organisation's cap.
@@ -61,8 +61,10 @@ from agentic_runner.service import (
61
61
  ISOLATION_ENV,
62
62
  RECIPIENT_KEY_ID_ENV,
63
63
  RECIPIENT_PUBLIC_KEY_ENV,
64
+ REGISTRATION_TOKEN_ENV,
64
65
  TAGS_ENV,
65
66
  parse_tags,
67
+ registration_token_from_env,
66
68
  )
67
69
  from agentic_runner.workers.claude_sign_in import OAUTH_TOKEN, SIGN_IN_METHODS
68
70
  from agentic_runner_contracts import __version__ as contracts_version
@@ -80,6 +82,7 @@ __all__ = [
80
82
  "ISOLATION_ENV",
81
83
  "RECIPIENT_KEY_ID_ENV",
82
84
  "RECIPIENT_PUBLIC_KEY_ENV",
85
+ "REGISTRATION_TOKEN_ENV",
83
86
  "STATE_DIR_ENV",
84
87
  "TAGS_ENV",
85
88
  "main",
@@ -144,7 +147,8 @@ def main(argv: Sequence[str] | None = None) -> int:
144
147
  subcommands = parser.add_subparsers(dest="command")
145
148
 
146
149
  subcommands.add_parser(
147
- "register", help="exchange the Agent Token for this process's durable identity"
150
+ "register",
151
+ help="exchange the Runner Registration Token for this process's durable identity",
148
152
  )
149
153
 
150
154
  run = subcommands.add_parser("run", help="register if needed, then heartbeat and poll for work")
@@ -419,9 +423,9 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
419
423
  print(str(error), file=sys.stderr)
420
424
  return 2
421
425
  # Pasted, never a flag: a flag is in the process table for every uid on the box.
422
- token = os.environ.get(AGENT_TOKEN_ENV, "").strip() or getpass.getpass(
423
- "Agent Token (yours from /me/runners, or the Organisation Admin's for an "
424
- "Organisation Runner): "
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): "
425
429
  )
426
430
  settings = workstation.WorkstationSettings(
427
431
  org=paths.org,
@@ -435,7 +439,7 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
435
439
  try:
436
440
  lines = asyncio.run(
437
441
  workstation.install(
438
- paths, settings, agent_token=token, start_service=not arguments.no_start
442
+ paths, settings, registration_token=token, start_service=not arguments.no_start
439
443
  )
440
444
  )
441
445
  except (workstation.CliMissingError, RunnerRegistrationError, RuntimeError) as error:
@@ -56,7 +56,7 @@ MCP_PATH: Final[str] = "/mcp"
56
56
 
57
57
  # What a Runner-hosted server inherits from the Runner besides its credential. A
58
58
  # credential-bearing child must not also inherit the Runner's own environment (its
59
- # Agent Token, a git token): only what a program needs to start.
59
+ # Runner Registration Token, a git token): only what a program needs to start.
60
60
  _HOSTED_ENV_ALLOWLIST: Final[tuple[str, ...]] = ("PATH", "HOME", "LANG", "TZ")
61
61
  _DEFAULT_CALL_TIMEOUT_SECONDS: Final[float] = 300.0
62
62
  # The heartbeat's own bound on `tool_servers`.
@@ -1,8 +1,8 @@
1
1
  """The Runner's half of registration and heartbeat (PRD issue 41, ADR-0013 §8).
2
2
 
3
- Outbound-only: this process dials the control plane, presents its Agent Token once, keeps
4
- what comes back in its own state directory, and then says what it is every 30 s. Nothing
5
- listens on a port here, which is also why revocation is entirely the control plane's --
3
+ Outbound-only: this process dials the control plane, presents its Runner Registration Token
4
+ once, keeps what comes back in its own state directory, and then says what it is every 30 s.
5
+ Nothing listens on a port here, which is also why revocation is entirely the control plane's --
6
6
  the lever it pulls is the refused heartbeat.
7
7
 
8
8
  Two things fail closed on this side, and both fail *before* anything registers:
@@ -70,6 +70,13 @@ INTAKE_IGNORED_PATH = "/api/runner/v1/runners/intake/ignored"
70
70
  # heartbeat). The one reason this process stops on rather than retries.
71
71
  RUNNER_REVOKED_REASON = "runner_revoked"
72
72
  CONTROL_PLANE_OLDER_REASON = "control_plane_older"
73
+ # The bootstrap refusals that mean "this Runner Registration Token will never work": unknown,
74
+ # revoked, expired. A control plane before ADR-0021 names them `agent_token_*`.
75
+ _REGISTRATION_TOKEN_REASON_PREFIXES = ("registration_token_", "agent_token_")
76
+ REGISTRATION_TOKEN_REFUSED_HINT = (
77
+ "the Runner Registration Token is unknown, revoked or expired: mint a new one on the "
78
+ "Runners page and register with that"
79
+ )
73
80
 
74
81
  # What the state directory holds after a successful bootstrap. One file, mode 0600: the
75
82
  # private key is in it, and a Runner that loses it re-bootstraps rather than recovering.
@@ -83,6 +90,12 @@ class RunnerRegistrationError(RuntimeError):
83
90
  super().__init__(message)
84
91
  self.reason = reason
85
92
 
93
+ @property
94
+ def refuses_registration_token(self) -> bool:
95
+ """Whether retrying with the same Runner Registration Token can never succeed."""
96
+
97
+ return self.reason.startswith(_REGISTRATION_TOKEN_REASON_PREFIXES)
98
+
86
99
 
87
100
  class RunnerState(BaseModel):
88
101
  """What this process is, once registered. Persisted; reread on every restart.
@@ -236,18 +249,18 @@ class RunnerRegistrationClient:
236
249
  async def bootstrap(
237
250
  self,
238
251
  *,
239
- agent_token: str,
252
+ registration_token: str,
240
253
  tags: dict[str, str],
241
254
  isolation_mode: IsolationMode,
242
255
  recipient_key: RecipientKey,
243
256
  max_concurrent_directives: int = 1,
244
257
  can_separate_uids: bool,
245
258
  ) -> tuple[RunnerState, BootstrapResponse]:
246
- """Exchange the Agent Token for this process's durable identity."""
259
+ """Exchange the Runner Registration Token for this process's durable identity."""
247
260
 
248
261
  require_isolation_supported(isolation_mode, can_separate_uids=can_separate_uids)
249
262
  request = BootstrapRequest(
250
- agent_token=agent_token,
263
+ registration_token=registration_token,
251
264
  tags=tags,
252
265
  isolation_mode=isolation_mode,
253
266
  contracts_version=contracts_version,
@@ -85,6 +85,7 @@ from agentic_runner.private_state import (
85
85
  require_private_files,
86
86
  )
87
87
  from agentic_runner.registration import (
88
+ REGISTRATION_TOKEN_REFUSED_HINT,
88
89
  RUNNER_REVOKED_REASON,
89
90
  STATE_FILENAME,
90
91
  RunnerRegistrationClient,
@@ -148,6 +149,7 @@ __all__ = [
148
149
  "READINESS_PORT_ENV",
149
150
  "RECIPIENT_KEY_ID_ENV",
150
151
  "RECIPIENT_PUBLIC_KEY_ENV",
152
+ "REGISTRATION_TOKEN_ENV",
151
153
  "TAGS_ENV",
152
154
  "TEMPORAL_TLS_ENV",
153
155
  "ControlPlaneStream",
@@ -172,8 +174,10 @@ __all__ = [
172
174
  _logger = logging.getLogger(__name__)
173
175
 
174
176
  # The Helm values and the workstation installer both land here as environment (ADR-0013
175
- # §8). The Agent Token is read from the environment and never from a flag: a flag would
176
- # put it in the process table for every other uid on the host to read.
177
+ # §8). The Runner Registration Token is read from the environment and never from a flag: a
178
+ # flag would put it in the process table for every other uid on the host to read.
179
+ REGISTRATION_TOKEN_ENV = "AGENTIC_RUNNER_REGISTRATION_TOKEN"
180
+ # Its name before ADR-0021, still read so an installed Runner upgrades without an edit.
177
181
  AGENT_TOKEN_ENV = "AGENTIC_AGENT_TOKEN"
178
182
  CONTROL_PLANE_ENV = "AGENTIC_CONTROL_PLANE_URL"
179
183
  TAGS_ENV = "AGENTIC_RUNNER_TAGS"
@@ -272,6 +276,24 @@ def _require_env(name: str) -> str:
272
276
  return value
273
277
 
274
278
 
279
+ def registration_token_from_env() -> str:
280
+ """``AGENTIC_RUNNER_REGISTRATION_TOKEN``, else the deprecated ``AGENTIC_AGENT_TOKEN``.
281
+
282
+ An empty string when neither is set, so a caller that can ask for it still asks.
283
+ """
284
+
285
+ if token := os.environ.get(REGISTRATION_TOKEN_ENV, "").strip():
286
+ return token
287
+ if token := os.environ.get(AGENT_TOKEN_ENV, "").strip():
288
+ print(
289
+ f"warning: {AGENT_TOKEN_ENV} is deprecated; set {REGISTRATION_TOKEN_ENV} instead",
290
+ file=sys.stderr,
291
+ flush=True,
292
+ )
293
+ return token
294
+ return ""
295
+
296
+
275
297
  def _max_concurrent_directives() -> int:
276
298
  return int(os.environ.get(MAX_CONCURRENT_DIRECTIVES_ENV, "").strip() or 1)
277
299
 
@@ -282,15 +304,16 @@ async def register(
282
304
  can_separate_uids: bool,
283
305
  client: httpx.AsyncClient | None = None,
284
306
  control_plane: str | None = None,
285
- agent_token: str | None = None,
307
+ registration_token: str | None = None,
286
308
  isolation: IsolationMode | None = None,
287
309
  ) -> tuple[RunnerState, str]:
288
- """First boot (PRD issue 41): exchange the Agent Token for this process's identity.
310
+ """First boot (PRD issue 41): exchange the Runner Registration Token for this
311
+ process's identity.
289
312
 
290
313
  Idempotent by state file -- a restart re-reads what is already there rather than
291
314
  registering a second Runner against the Organisation's cap (21 A7). The keyword
292
- overrides are the workstation ``install`` (issue 47), which takes the Agent Token
293
- pasted at a prompt rather than from a Helm-set environment.
315
+ overrides are the workstation ``install`` (issue 47), which takes the Runner
316
+ Registration Token pasted at a prompt rather than from a Helm-set environment.
294
317
  """
295
318
 
296
319
  existing = load_state(state_dir)
@@ -300,8 +323,13 @@ async def register(
300
323
  registration = RunnerRegistrationClient(
301
324
  base_url=control_plane or _require_env(CONTROL_PLANE_ENV), client=client
302
325
  )
326
+ token = registration_token or registration_token_from_env()
327
+ if not token:
328
+ raise RunnerRegistrationError(
329
+ "configuration_missing", f"{REGISTRATION_TOKEN_ENV} is not set"
330
+ )
303
331
  state, response = await registration.bootstrap(
304
- agent_token=agent_token or _require_env(AGENT_TOKEN_ENV),
332
+ registration_token=token,
305
333
  tags=parse_tags(os.environ.get(TAGS_ENV, "")),
306
334
  isolation_mode=isolation or isolation_mode(),
307
335
  recipient_key=recipient,
@@ -1077,6 +1105,8 @@ async def _serve(
1077
1105
  )
1078
1106
  except RunnerRegistrationError as error:
1079
1107
  print(f"registration refused ({error.reason}): {error}", flush=True)
1108
+ if error.refuses_registration_token:
1109
+ print(REGISTRATION_TOKEN_REFUSED_HINT, flush=True)
1080
1110
  return 1
1081
1111
  except UnsafeStateError as error:
1082
1112
  print(f"refusing to start ({error.reason}): {error}", flush=True)
@@ -24,7 +24,7 @@ The prompt they pasted from the console holds these. Ask for any that is missing
24
24
  pass to the Runner. Never call it or any other control plane address yourself.
25
25
  - **Temporal address**: `host:port`, from the same page.
26
26
 
27
- The **Agent Token** is not one of them. It is the reusable credential a Runner registers
27
+ The **Runner Registration Token** is not one of them. It is the reusable credential a Runner registers
28
28
  with, and the person types it only where the Runner reads it, never into this
29
29
  conversation.
30
30
 
@@ -34,10 +34,10 @@ conversation.
34
34
  it does, in one line.
35
35
  - **Stop on the first failure.** Show the exact error, link the doc section below that
36
36
  covers it, and stop. Do not retry with other flags, do not work around it.
37
- - **Never ask for the Agent Token or any credential value in this conversation.** Anything
38
- said here reaches the model vendor and stays in this CLI's history. Every command that
39
- reads a credential is one the person runs in their own terminal, and the person types the
40
- value there. If the person pastes a token or a key here anyway, do not use it: tell them
37
+ - **Never ask for the Runner Registration Token or any credential value in this
38
+ conversation.** Anything said here reaches the model vendor and stays in this CLI's
39
+ history. Every command that reads a credential is one the person runs in their own
40
+ terminal, and the person types the value there. If the person pastes a token or a key here anyway, do not use it: tell them
41
41
  to revoke it in the console and mint or create a new one.
42
42
  - You use only the `agentic-runner` command, the standard tools named in the docs (`uv`,
43
43
  `docker`, `kubectl`, `helm`) and read-only checks such as `command -v`.
@@ -77,7 +77,7 @@ missing.
77
77
  **This machine.** Follow
78
78
  https://github.com/qtsone/agentic-runner/blob/main/docs/workstation.md#install. Install the
79
79
  Runner, then show the person the command that registers it for the Organisation and ask
80
- them to run it in their own terminal. The Runner asks them for the Agent Token there,
80
+ them to run it in their own terminal. The Runner asks them for the Runner Registration Token there,
81
81
  without showing it:
82
82
 
83
83
  ```sh
@@ -33,7 +33,7 @@ from agentic_runner.testing.control_plane import DEFAULT_NAMESPACE, FakeControlP
33
33
 
34
34
  # A conformance Runner never runs a Directive that needs isolation or a credential, so it
35
35
  # runs `none`, unprivileged, with every path under the test's own directory.
36
- AGENT_TOKEN = "conformance-agent-token-0123456789"
36
+ REGISTRATION_TOKEN = "conformance-registration-token-0123"
37
37
  _RUN = "import sys; from agentic_runner.cli import main; sys.exit(main(['run']))"
38
38
 
39
39
 
@@ -90,7 +90,7 @@ class RunnerLauncher:
90
90
  environment = {
91
91
  **os.environ,
92
92
  "AGENTIC_CONTROL_PLANE_URL": control_plane_url,
93
- "AGENTIC_AGENT_TOKEN": AGENT_TOKEN,
93
+ "AGENTIC_RUNNER_REGISTRATION_TOKEN": REGISTRATION_TOKEN,
94
94
  "AGENTIC_RUNNER_ISOLATION": "none",
95
95
  "AGENTIC_RUNNER_STATE_DIR": str(state),
96
96
  "AGENTIC_RUNNER_WORKSPACE_ROOT": str(self.root / "workspaces"),
@@ -65,6 +65,7 @@ from __future__ import annotations
65
65
  import asyncio
66
66
  import contextlib
67
67
  import json
68
+ import logging
68
69
  import os
69
70
  import re
70
71
  import shlex
@@ -114,6 +115,8 @@ from agentic_runner_contracts.runner_registration import UsageWindow
114
115
 
115
116
  __all__ = ["ACP_BRIDGES", "AcpBridge", "AcpRuntime", "profile_acp_executables"]
116
117
 
118
+ _logger = logging.getLogger(__name__)
119
+
117
120
 
118
121
  @dataclass(frozen=True, slots=True)
119
122
  class AcpBridge:
@@ -320,6 +323,7 @@ class AcpRuntime:
320
323
  # Tests drive a fake ACP agent through this; production runs the pinned bridge.
321
324
  self._bridge_argv = list(bridge_argv) if bridge_argv is not None else None
322
325
  self._ask = ask
326
+ self._logged_model_ignored = False
323
327
 
324
328
  @property
325
329
  def _is_codex(self) -> bool:
@@ -329,6 +333,15 @@ class AcpRuntime:
329
333
  def _is_claude(self) -> bool:
330
334
  return self._cli_kind == "claude_code"
331
335
 
336
+ def _model(self, request: DirectiveRequest) -> str:
337
+ """The model this turn runs, blank where the harness chooses its own."""
338
+
339
+ if self._is_claude:
340
+ return request.model or self._settings.CLAUDE_MODEL
341
+ if self._is_codex:
342
+ return request.model
343
+ return ""
344
+
332
345
  @property
333
346
  def _executable(self) -> str:
334
347
  return self._bridge.executable if self._bridge is not None else self._cli_kind
@@ -450,6 +463,17 @@ class AcpRuntime:
450
463
  turn = _Turn()
451
464
  if self._bridge is None:
452
465
  turn.notes.append(f"profile ACP command: {redact_secret_like_text(shlex.join(argv))}")
466
+ if request.model:
467
+ # The Runner knows no way to name a model to a harness it does not know,
468
+ # so the turn runs on that harness's own choice rather than not at all.
469
+ turn.notes.append(f"model ignored: {self._cli_kind} takes no model from the Runner")
470
+ if not self._logged_model_ignored:
471
+ self._logged_model_ignored = True
472
+ _logger.warning(
473
+ "%s takes no model from the Runner; its Directives run the harness's "
474
+ "own model",
475
+ self._cli_kind,
476
+ )
453
477
  timed_out = False
454
478
  async with contextlib.AsyncExitStack() as stack:
455
479
  env = self._env(request, harness_root)
@@ -621,11 +645,18 @@ class AcpRuntime:
621
645
  if self._is_claude:
622
646
  session_params["_meta"] = {
623
647
  "claudeCode": {
624
- "options": {**_CLAUDE_SESSION_OPTIONS, "model": self._settings.CLAUDE_MODEL}
648
+ "options": {**_CLAUDE_SESSION_OPTIONS, "model": self._model(request)}
625
649
  }
626
650
  }
627
651
  session = await connection.request("session/new", session_params)
628
652
  session_id = turn.session_id = str(session.get("sessionId") or "")
653
+ if self._is_codex and request.model:
654
+ # codex-acp starts a session on the harness root's model; this outranks it
655
+ # for the turn, and an unknown model is an ACP error that fails the turn.
656
+ await connection.request(
657
+ "session/set_config_option",
658
+ {"sessionId": session_id, "configId": "model", "value": request.model},
659
+ )
629
660
  if self._is_claude and _offers_mode(session, _CLAUDE_MODE):
630
661
  await connection.request(
631
662
  "session/set_mode", {"sessionId": session_id, "modeId": _CLAUDE_MODE}
@@ -746,6 +777,7 @@ class AcpRuntime:
746
777
  ),
747
778
  model_usage=model_usage,
748
779
  usage_windows=turn.usage_windows,
780
+ model=self._model(request),
749
781
  )
750
782
 
751
783
  def _evidence(
@@ -106,6 +106,9 @@ class DirectiveRequest:
106
106
  # The Profile-named ACP command (local-agents 18). Read only by the ACP runtime, and
107
107
  # only for a `cli_kind` it pins no bridge for.
108
108
  acp_command: tuple[str, ...] = ()
109
+ # The Agent's model, from its runtime context (ADR-0021). Blank runs the Runner's
110
+ # default for this `cli_kind`: `CLAUDE_MODEL` for Claude Code, Codex's own config.
111
+ model: str = ""
109
112
 
110
113
 
111
114
  @dataclass(frozen=True, slots=True)
@@ -138,6 +141,9 @@ class DirectiveResult:
138
141
  # The subscription usage windows the harness's own stream last reported, where it
139
142
  # carries them (claude-agent-acp's rate-limit events, local-agents 10).
140
143
  usage_windows: tuple[UsageWindow, ...] = ()
144
+ # The model the harness was told to run, for Evidence and usage. Blank when the
145
+ # harness chose its own: Codex on its config's default, a Profile-named ACP harness.
146
+ model: str = ""
141
147
 
142
148
 
143
149
  # The permission mode of a runtime whose guard configuration is incomplete: it refuses
@@ -103,7 +103,8 @@ class ClaudeRuntime(ResumableAgentRuntime):
103
103
  evidence_limit_bytes=self._settings.CLAUDE_CLI_OUTPUT_LIMIT_BYTES,
104
104
  )
105
105
 
106
- argv = _claude_argv(self._settings)
106
+ model = request.model or self._settings.CLAUDE_MODEL
107
+ argv = _claude_argv(model)
107
108
  if request.mcp_servers is not None:
108
109
  # Only what the Grant let through: `--strict-mcp-config` ignores every other
109
110
  # MCP source, so nothing in the harness root can add an ungranted server.
@@ -193,6 +194,7 @@ class ClaudeRuntime(ResumableAgentRuntime):
193
194
  error=redacted_error,
194
195
  command_hash=command_hash,
195
196
  evidence=evidence,
197
+ model=model,
196
198
  )
197
199
 
198
200
  def has_session(self, session_id: str, sandbox: DirectiveSandbox | None) -> bool:
@@ -210,7 +212,7 @@ def _guard_mode(settings: WorkerSettings) -> str | None:
210
212
  return None
211
213
 
212
214
 
213
- def _claude_argv(settings: WorkerSettings) -> list[str]:
215
+ def _claude_argv(model: str) -> list[str]:
214
216
  return [
215
217
  "claude",
216
218
  "--print",
@@ -218,7 +220,7 @@ def _claude_argv(settings: WorkerSettings) -> list[str]:
218
220
  "--output-format",
219
221
  "json",
220
222
  "--model",
221
- settings.CLAUDE_MODEL,
223
+ model,
222
224
  "--dangerously-skip-permissions",
223
225
  ]
224
226
 
@@ -142,7 +142,7 @@ class CodexRuntime(ResumableAgentRuntime):
142
142
  evidence_limit_bytes=self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
143
143
  )
144
144
 
145
- argv = _codex_argv(self._settings, allowlisted=allowlisted)
145
+ argv = _codex_argv(self._settings, allowlisted=allowlisted, model=request.model)
146
146
  # Locked with no server bound too: otherwise a `[mcp_servers.*]` table in the
147
147
  # Workspace or harness-root config.toml starts a process before any model turn (LA-19b).
148
148
  argv[-1:-1] = codex_mcp_argv(request.mcp_servers or (), workspace_path)
@@ -267,6 +267,7 @@ class CodexRuntime(ResumableAgentRuntime):
267
267
  error=redacted_error,
268
268
  command_hash=command_hash,
269
269
  evidence=evidence,
270
+ model=request.model,
270
271
  )
271
272
 
272
273
  def has_session(self, session_id: str, sandbox: DirectiveSandbox | None) -> bool:
@@ -320,7 +321,9 @@ def _guard_mode(settings: WorkerSettings, *, allowlisted: bool = False) -> str |
320
321
  return None
321
322
 
322
323
 
323
- def _codex_argv(settings: WorkerSettings, *, allowlisted: bool = False) -> list[str]:
324
+ def _codex_argv(
325
+ settings: WorkerSettings, *, allowlisted: bool = False, model: str = ""
326
+ ) -> list[str]:
324
327
  argv = [
325
328
  "codex",
326
329
  "exec",
@@ -340,6 +343,10 @@ def _codex_argv(settings: WorkerSettings, *, allowlisted: bool = False) -> list[
340
343
  if settings.CODEX_SANDBOX_MODE == "workspace-write":
341
344
  network_access = "true" if allowlisted else "false"
342
345
  argv += ["--config", f"sandbox_workspace_write.network_access={network_access}"]
346
+ if model:
347
+ # Outranks the harness root's `config.toml`; without it Codex runs that file's
348
+ # model, which is the Runner's default for `codex_cli`.
349
+ argv += ["--model", model]
343
350
  argv.append("-")
344
351
  return argv
345
352
 
@@ -624,7 +624,7 @@ async def install(
624
624
  paths: OrgPaths,
625
625
  settings: WorkstationSettings,
626
626
  *,
627
- agent_token: str,
627
+ registration_token: str,
628
628
  platform: str = sys.platform,
629
629
  home: Path | None = None,
630
630
  start_service: bool = True,
@@ -633,11 +633,11 @@ async def install(
633
633
  ) -> list[str]:
634
634
  """Register this Organisation's process and install its login agent.
635
635
 
636
- Registration happens here, once, with the Agent Token this user minted on /me/runners
637
- or the Organisation's Admin issued them (12 A6, QTS-1304) -- pasted at the prompt,
638
- never stored: the identity bootstrap hands back is what every later start uses, and a
639
- lost state directory means a reinstall and a re-delivery of any sealed value (22 A4),
640
- by design.
636
+ Registration happens here, once, with the Runner Registration Token this user minted
637
+ on /me/runners or the Organisation's Admin issued them (12 A6, QTS-1304) -- pasted at the
638
+ prompt, never stored: the identity bootstrap hands back is what every later start uses,
639
+ and a lost state directory means a reinstall and a re-delivery of any sealed value
640
+ (22 A4), by design.
641
641
  """
642
642
 
643
643
  require_any_cli(settings.path)
@@ -649,7 +649,7 @@ async def install(
649
649
  can_separate_uids=False,
650
650
  client=http_client,
651
651
  control_plane=settings.control_plane_url,
652
- agent_token=agent_token,
652
+ registration_token=registration_token,
653
653
  isolation=IsolationMode.NONE,
654
654
  )
655
655
  LifecycleOutbox(paths.state_dir).add(LifecycleKind.INSTALL, datetime.now(UTC))
File without changes