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.
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/PKG-INFO +6 -5
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/README.md +4 -3
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/pyproject.toml +1 -1
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/cli.py +91 -50
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/service.py +18 -2
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +10 -7
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workstation.py +160 -59
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/.gitignore +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/LICENSE +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/activities.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/auth_mode.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/credentials.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/heartbeat_link.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/launch.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/llm_contracts.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/llm_proxy.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/oauth_connectors.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/pool_key.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/runner_limit.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/sealed_box.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/sign_in.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/skill.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/control_plane.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/test_conformance.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/usage_windows.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/acp_runtime.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_logins.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/settings.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
50
|
-
`agentic-runner install | start | stop | status <
|
|
51
|
-
logon task; never `sudo`), with Credential References in
|
|
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
|
|
31
|
-
`agentic-runner install | start | stop | status <
|
|
32
|
-
logon task; never `sudo`), with Credential References in
|
|
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.
|
|
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.
|
|
@@ -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 <
|
|
15
|
-
Runner (PRD issue 47, ``workstation.py``): one login agent per
|
|
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("--
|
|
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
|
|
181
|
+
"install", help="register a workstation Runner and install its login agent (issue 47)"
|
|
167
182
|
)
|
|
168
|
-
install.add_argument("
|
|
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
|
|
213
|
+
service_verb, help=f"{service_verb} one workstation Runner's login agent"
|
|
199
214
|
)
|
|
200
|
-
driven.add_argument("
|
|
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("
|
|
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("
|
|
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
|
-
"--
|
|
263
|
+
"--name",
|
|
244
264
|
default=None,
|
|
245
|
-
help="the workstation
|
|
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.
|
|
336
|
-
return workstation.run_process(
|
|
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
|
-
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
526
|
+
if arguments.name or not _in_container():
|
|
465
527
|
try:
|
|
466
|
-
paths =
|
|
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.
|
|
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
|
|
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.
|
|
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 <
|
|
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.
|
|
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
|
-
- **
|
|
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
|
|
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 <
|
|
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
|
-
`--
|
|
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 <
|
|
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
|
|
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
|
|
1
|
+
"""The workstation Runner: one login agent per Runner (PRD issue 47, map ticket 23).
|
|
2
2
|
|
|
3
|
-
``agentic-runner install | start | stop | status <
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
22
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
|
119
|
+
if not re.fullmatch(RUNNER_NAME_PATTERN, name):
|
|
110
120
|
raise ValueError(
|
|
111
|
-
f"
|
|
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
|
|
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
|
|
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
|
|
132
|
-
"""``<root>/<
|
|
172
|
+
class RunnerPaths:
|
|
173
|
+
"""``<root>/<name>/`` and everything the process keeps under it."""
|
|
133
174
|
|
|
134
175
|
root: Path
|
|
135
|
-
|
|
176
|
+
name: str
|
|
136
177
|
|
|
137
178
|
@property
|
|
138
179
|
def state_dir(self) -> Path:
|
|
139
|
-
return self.root / self.
|
|
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>/<
|
|
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.
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
233
|
-
f"run `agentic-runner install {paths.
|
|
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:
|
|
266
|
-
"""The environment ``agentic-runner 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
|
|
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(
|
|
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", "--
|
|
451
|
+
return [*prefix, "run", "--name", name, "--root", str(root), "--login-agent"]
|
|
405
452
|
|
|
406
453
|
|
|
407
454
|
def service_definition(
|
|
408
|
-
paths:
|
|
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.
|
|
419
|
-
name = f"{_LABEL_PREFIX}.{paths.
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
592
|
-
"""``status <
|
|
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"
|
|
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:
|
|
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
|
|
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:
|
|
798
|
+
def start(paths: RunnerPaths, **driver: Any) -> None:
|
|
700
799
|
_drive_service(paths, "start", **driver)
|
|
701
800
|
|
|
702
801
|
|
|
703
|
-
def stop(paths:
|
|
704
|
-
"""Stop this
|
|
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:
|
|
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:
|
|
723
|
-
"""``agentic-runner run --
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/contracts.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/evidence.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/git/workspace.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/github/gh_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/integrations/oauth/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/runtime/verifier_command.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/testing/test_conformance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/_runtime_support.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/claude_runtime.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/claude_sign_in.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/command_policy.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/contract_isolation.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/fastapi_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_logins.py
RENAMED
|
File without changes
|
{agentic_runner-3.26.1 → agentic_runner-3.28.0}/src/agentic_runner/workers/harness_outcome.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|