agentic-runner 3.17.0__tar.gz → 3.18.1__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.17.0 → agentic_runner-3.18.1}/PKG-INFO +2 -2
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/pyproject.toml +2 -2
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/cli.py +45 -10
- agentic_runner-3.18.1/src/agentic_runner/launch.py +38 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/service.py +109 -20
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workstation.py +48 -1
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/.gitignore +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/LICENSE +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/README.md +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/activities.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/auth_mode.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/credentials.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/device_login_activities.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/heartbeat_link.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/llm_proxy.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/oauth_connectors.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/sealed_box.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/skill.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/testing/control_plane.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/testing/test_conformance.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/usage_windows.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/_runtime_support.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/acp_runtime.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/agent_runtime.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/claude_sign_in.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/contract_device_login.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/harness_outcome.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/settings.py +0 -0
- {agentic_runner-3.17.0 → agentic_runner-3.18.1}/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.18.1
|
|
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.18.1
|
|
9
9
|
Requires-Dist: cryptography<51,>=50
|
|
10
10
|
Requires-Dist: httpx<1.0,>=0.27
|
|
11
11
|
Requires-Dist: pydantic-settings<3.0,>=2.0
|
|
@@ -10,7 +10,7 @@ dependencies = [
|
|
|
10
10
|
# Exactly this release's contracts: the two ship as one version, and an unpinned
|
|
11
11
|
# requirement let `uv tool upgrade` keep an older contracts wheel the Runner cannot
|
|
12
12
|
# import from (QTS-1473). `scripts/set-version.py` moves it with the version.
|
|
13
|
-
"agentic-runner-contracts==3.
|
|
13
|
+
"agentic-runner-contracts==3.18.1",
|
|
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.
|
|
@@ -31,7 +31,7 @@ testing = [
|
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.scripts]
|
|
34
|
-
agentic-runner = "agentic_runner.
|
|
34
|
+
agentic-runner = "agentic_runner.launch:main"
|
|
35
35
|
|
|
36
36
|
[project.entry-points.pytest11]
|
|
37
37
|
agentic_runner = "agentic_runner.testing.plugin"
|
|
@@ -13,7 +13,8 @@ the Organisation's cap.
|
|
|
13
13
|
|
|
14
14
|
``install | start | stop | status <org>`` and ``credential set`` are the workstation
|
|
15
15
|
Runner (PRD issue 47, ``workstation.py``): one login agent per Organisation, managed by
|
|
16
|
-
the user who hosts it, with no ``sudo`` anywhere.
|
|
16
|
+
the user who hosts it, with no ``sudo`` anywhere. ``add <name>`` registers another Runner
|
|
17
|
+
beside them, reusing their control plane and ``PATH`` (ADR-0022 §21).
|
|
17
18
|
|
|
18
19
|
``sign-in --contract <id>`` signs one Contract's Claude Code in with Anthropic's own flow,
|
|
19
20
|
in this terminal, as that Contract's uid in its harness root (QTS-1448). No code or token
|
|
@@ -179,6 +180,21 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
179
180
|
)
|
|
180
181
|
install.add_argument("--no-start", action="store_true", help="write the agent, do not start")
|
|
181
182
|
install.add_argument("--root", default=None, type=Path, help="state root (default per OS)")
|
|
183
|
+
|
|
184
|
+
add = subcommands.add_parser(
|
|
185
|
+
"add",
|
|
186
|
+
help="register another Runner under this install, with its own process, identity "
|
|
187
|
+
"and Recipient Key (one per Contract)",
|
|
188
|
+
)
|
|
189
|
+
add.add_argument("org", metavar="name", help="the new Runner's name on this machine")
|
|
190
|
+
add.add_argument(
|
|
191
|
+
"--beside",
|
|
192
|
+
default=None,
|
|
193
|
+
help="the installed Runner whose control plane and PATH to reuse; needed only when "
|
|
194
|
+
"the installed Runners talk to different control planes",
|
|
195
|
+
)
|
|
196
|
+
add.add_argument("--no-start", action="store_true", help="write the agent, do not start")
|
|
197
|
+
add.add_argument("--root", default=None, type=Path, help="state root (default per OS)")
|
|
182
198
|
for service_verb in ("start", "stop"):
|
|
183
199
|
driven = subcommands.add_parser(
|
|
184
200
|
service_verb, help=f"{service_verb} one Organisation's login agent"
|
|
@@ -312,7 +328,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
312
328
|
if arguments.org:
|
|
313
329
|
return workstation.run_process(_org_paths(arguments), login_agent=arguments.login_agent)
|
|
314
330
|
return asyncio.run(service.run(attestation=_image_attestation()))
|
|
315
|
-
if arguments.command in {"install", "start", "stop", "credential"}:
|
|
331
|
+
if arguments.command in {"install", "add", "start", "stop", "credential"}:
|
|
316
332
|
return _workstation(arguments)
|
|
317
333
|
if arguments.command == "recipient-key":
|
|
318
334
|
api, kube_ns = in_cluster()
|
|
@@ -359,6 +375,8 @@ def _workstation(arguments: argparse.Namespace) -> int:
|
|
|
359
375
|
try:
|
|
360
376
|
if arguments.command == "install":
|
|
361
377
|
return _install(arguments, paths)
|
|
378
|
+
if arguments.command == "add":
|
|
379
|
+
return _add(arguments, paths)
|
|
362
380
|
if arguments.command == "start":
|
|
363
381
|
workstation.start(paths)
|
|
364
382
|
print(f"started {paths.org}")
|
|
@@ -422,11 +440,7 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
|
|
|
422
440
|
except ValueError as error:
|
|
423
441
|
print(str(error), file=sys.stderr)
|
|
424
442
|
return 2
|
|
425
|
-
|
|
426
|
-
token = registration_token_from_env() or getpass.getpass(
|
|
427
|
-
"Runner Registration Token (yours from /me/runners, or the Organisation Admin's for "
|
|
428
|
-
"an Organisation Runner): "
|
|
429
|
-
)
|
|
443
|
+
token = _registration_token()
|
|
430
444
|
settings = workstation.WorkstationSettings(
|
|
431
445
|
org=paths.org,
|
|
432
446
|
control_plane_url=arguments.control_plane,
|
|
@@ -436,11 +450,19 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
|
|
|
436
450
|
install_channel=workstation.install_channel(),
|
|
437
451
|
acp_cli_kinds=acp_cli_kinds,
|
|
438
452
|
)
|
|
453
|
+
return _register(paths, settings, token, start=not arguments.no_start)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _register(
|
|
457
|
+
paths: workstation.OrgPaths,
|
|
458
|
+
settings: workstation.WorkstationSettings,
|
|
459
|
+
token: str,
|
|
460
|
+
*,
|
|
461
|
+
start: bool,
|
|
462
|
+
) -> int:
|
|
439
463
|
try:
|
|
440
464
|
lines = asyncio.run(
|
|
441
|
-
workstation.install(
|
|
442
|
-
paths, settings, registration_token=token, start_service=not arguments.no_start
|
|
443
|
-
)
|
|
465
|
+
workstation.install(paths, settings, registration_token=token, start_service=start)
|
|
444
466
|
)
|
|
445
467
|
except (workstation.CliMissingError, RunnerRegistrationError, RuntimeError) as error:
|
|
446
468
|
print(f"install refused: {error}", file=sys.stderr)
|
|
@@ -450,6 +472,19 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
|
|
|
450
472
|
return 0
|
|
451
473
|
|
|
452
474
|
|
|
475
|
+
def _add(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
|
|
476
|
+
settings = workstation.settings_to_add(paths, beside=arguments.beside)
|
|
477
|
+
return _register(paths, settings, _registration_token(), start=not arguments.no_start)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _registration_token() -> str:
|
|
481
|
+
# Pasted, never a flag: a flag is in the process table for every uid on the box.
|
|
482
|
+
return registration_token_from_env() or getpass.getpass(
|
|
483
|
+
"Runner Registration Token (yours from /me/runners, or the Organisation Admin's for "
|
|
484
|
+
"an Organisation Runner): "
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
453
488
|
def _acp_cli_kinds(flag: str | None, paths: workstation.OrgPaths) -> str:
|
|
454
489
|
# A reinstall to pick up a new PATH or retry a token must not turn ACP off (QTS-1355).
|
|
455
490
|
if flag is not None:
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""The ``agentic-runner`` console script: a SIGTERM disposition before the CLI imports.
|
|
2
|
+
|
|
3
|
+
Importing the CLI takes seconds (Temporal, httpx, pydantic). In a container the Runner is
|
|
4
|
+
PID 1, and the kernel drops a signal PID 1 has no handler for, so a SIGTERM in that window
|
|
5
|
+
was lost and ``docker stop`` waited out its grace period to SIGKILL (QTS-1536). Nothing
|
|
6
|
+
has run yet, so it exits 0 -- what launchd's ``KeepAlive: SuccessfulExit: false`` reads
|
|
7
|
+
as a clean stop. ``agentic-runner run`` replaces this with its own handler before its
|
|
8
|
+
first await; every other subcommand gets the default disposition back, so a killed
|
|
9
|
+
callback such as ``ask`` never reads as one that succeeded.
|
|
10
|
+
|
|
11
|
+
Imports nothing but the standard library: anything heavier reopens the window.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import signal
|
|
18
|
+
import sys
|
|
19
|
+
from types import FrameType
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> int:
|
|
23
|
+
signal.signal(signal.SIGTERM, _exit_before_start)
|
|
24
|
+
from agentic_runner.cli import main as cli_main
|
|
25
|
+
|
|
26
|
+
if sys.argv[1:2] != ["run"]:
|
|
27
|
+
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
|
28
|
+
return cli_main()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _exit_before_start(signum: int, frame: FrameType | None) -> None:
|
|
32
|
+
# Not SystemExit: raised mid-import, it tears down half-initialised modules and the
|
|
33
|
+
# interpreter prints their errors. Nothing has written state yet to flush or close.
|
|
34
|
+
os._exit(0)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__": # pragma: no cover - `runner_command`'s fallback without the script
|
|
38
|
+
raise SystemExit(main())
|
|
@@ -27,7 +27,7 @@ import signal
|
|
|
27
27
|
import sys
|
|
28
28
|
import tempfile
|
|
29
29
|
import time
|
|
30
|
-
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
30
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence
|
|
31
31
|
from dataclasses import dataclass, field
|
|
32
32
|
from datetime import UTC, datetime, timedelta
|
|
33
33
|
from pathlib import Path
|
|
@@ -558,12 +558,14 @@ async def run_worker_until_terminated(
|
|
|
558
558
|
Directive is mid-run. The chart's ``preStop`` hook (issue 02) signals termination as
|
|
559
559
|
early as possible; this handler is what turns that into ``Worker.shutdown()`` -- stop
|
|
560
560
|
polling for new work, wait for the in-flight activity, then let ``run()`` return.
|
|
561
|
+
|
|
562
|
+
A caller that passes ``stop`` owns SIGTERM already (``run`` installs it before it
|
|
563
|
+
starts up, QTS-1536); one set before the Worker polls never starts it.
|
|
561
564
|
"""
|
|
562
565
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
try:
|
|
566
|
+
if stop is not None and stop.is_set():
|
|
567
|
+
return
|
|
568
|
+
async with _sigterm_sets(stop) as stop_event:
|
|
567
569
|
run_task = asyncio.ensure_future(worker.run())
|
|
568
570
|
stop_task = asyncio.ensure_future(stop_event.wait())
|
|
569
571
|
await asyncio.wait({run_task, stop_task}, return_when=asyncio.FIRST_COMPLETED)
|
|
@@ -573,10 +575,51 @@ async def run_worker_until_terminated(
|
|
|
573
575
|
_logger.info("Worker received SIGTERM: draining in-flight activity before exit")
|
|
574
576
|
await worker.shutdown()
|
|
575
577
|
await run_task
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@contextlib.asynccontextmanager
|
|
581
|
+
async def _sigterm_sets(stop: asyncio.Event | None) -> AsyncIterator[asyncio.Event]:
|
|
582
|
+
"""``stop`` as given, or a new event SIGTERM sets for the length of the block."""
|
|
583
|
+
|
|
584
|
+
if stop is not None:
|
|
585
|
+
yield stop
|
|
586
|
+
return
|
|
587
|
+
loop = asyncio.get_running_loop()
|
|
588
|
+
stop_event = asyncio.Event()
|
|
589
|
+
loop.add_signal_handler(signal.SIGTERM, stop_event.set)
|
|
590
|
+
try:
|
|
591
|
+
yield stop_event
|
|
576
592
|
finally:
|
|
577
593
|
loop.remove_signal_handler(signal.SIGTERM)
|
|
578
594
|
|
|
579
595
|
|
|
596
|
+
class _StoppedBeforeStartError(Exception):
|
|
597
|
+
"""SIGTERM (or ``stop``) arrived before the Worker polled: no Directive is in flight."""
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
async def _before_stop[T](awaitable: Awaitable[T], stop: asyncio.Event) -> T:
|
|
601
|
+
"""``awaitable``'s result, unless ``stop`` is set first; then it is cancelled.
|
|
602
|
+
|
|
603
|
+
Start-up waits on the network -- registration, the first heartbeat, the Temporal
|
|
604
|
+
connection -- for as long as the far end takes to answer. A stop during that wait
|
|
605
|
+
must not wait with it.
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
work = asyncio.ensure_future(awaitable)
|
|
609
|
+
stopped = asyncio.ensure_future(stop.wait())
|
|
610
|
+
try:
|
|
611
|
+
await asyncio.wait({work, stopped}, return_when=asyncio.FIRST_COMPLETED)
|
|
612
|
+
finally:
|
|
613
|
+
stopped.cancel()
|
|
614
|
+
if not work.done():
|
|
615
|
+
work.cancel()
|
|
616
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
617
|
+
await work
|
|
618
|
+
if work.cancelled():
|
|
619
|
+
raise _StoppedBeforeStartError
|
|
620
|
+
return work.result()
|
|
621
|
+
|
|
622
|
+
|
|
580
623
|
# ------------------------------------------------------------------ the stream
|
|
581
624
|
|
|
582
625
|
|
|
@@ -971,6 +1014,40 @@ async def run(
|
|
|
971
1014
|
it says about itself. ``heartbeat_interval`` defaults to the cadence bootstrap gave.
|
|
972
1015
|
"""
|
|
973
1016
|
|
|
1017
|
+
loop = asyncio.get_running_loop()
|
|
1018
|
+
# Before the first await: in a container this process is PID 1, and the kernel drops
|
|
1019
|
+
# a signal PID 1 has no handler for, so a SIGTERM during start-up was lost and
|
|
1020
|
+
# `docker stop` waited out its grace period to SIGKILL (QTS-1536).
|
|
1021
|
+
stop = stop or asyncio.Event()
|
|
1022
|
+
loop.add_signal_handler(signal.SIGTERM, stop.set)
|
|
1023
|
+
try:
|
|
1024
|
+
return await _run(
|
|
1025
|
+
stop=stop,
|
|
1026
|
+
http_client=http_client,
|
|
1027
|
+
connect=connect,
|
|
1028
|
+
worker_factory=worker_factory,
|
|
1029
|
+
can_change_uid=can_change_uid,
|
|
1030
|
+
heartbeat_interval=heartbeat_interval,
|
|
1031
|
+
readiness=readiness,
|
|
1032
|
+
host_store=host_store,
|
|
1033
|
+
attestation=attestation,
|
|
1034
|
+
)
|
|
1035
|
+
finally:
|
|
1036
|
+
loop.remove_signal_handler(signal.SIGTERM)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
async def _run(
|
|
1040
|
+
*,
|
|
1041
|
+
stop: asyncio.Event,
|
|
1042
|
+
http_client: httpx.AsyncClient | None,
|
|
1043
|
+
connect: Connector,
|
|
1044
|
+
worker_factory: WorkerFactory,
|
|
1045
|
+
can_change_uid: bool | None,
|
|
1046
|
+
heartbeat_interval: float | None,
|
|
1047
|
+
readiness: Readiness | None,
|
|
1048
|
+
host_store: HostCredentialStore | None,
|
|
1049
|
+
attestation: HostAttestation | None,
|
|
1050
|
+
) -> int:
|
|
974
1051
|
install_stop_tolerant_child_watcher(asyncio.get_running_loop())
|
|
975
1052
|
config = load_config()
|
|
976
1053
|
mode = isolation_mode()
|
|
@@ -1094,15 +1171,19 @@ async def _serve(
|
|
|
1094
1171
|
http_client: httpx.AsyncClient | None,
|
|
1095
1172
|
connect: Connector,
|
|
1096
1173
|
worker_factory: WorkerFactory,
|
|
1097
|
-
stop: asyncio.Event
|
|
1174
|
+
stop: asyncio.Event,
|
|
1098
1175
|
heartbeat_interval: float | None,
|
|
1099
1176
|
host_store: HostCredentialStore | None,
|
|
1100
1177
|
attestation: HostAttestation | None,
|
|
1101
1178
|
) -> int:
|
|
1102
1179
|
try:
|
|
1103
|
-
state, outcome = await
|
|
1104
|
-
state_dir=state_dir, can_separate_uids=can_change_uid, client=http_client
|
|
1180
|
+
state, outcome = await _before_stop(
|
|
1181
|
+
register(state_dir=state_dir, can_separate_uids=can_change_uid, client=http_client),
|
|
1182
|
+
stop,
|
|
1105
1183
|
)
|
|
1184
|
+
except _StoppedBeforeStartError:
|
|
1185
|
+
print("stopped before registration completed", flush=True)
|
|
1186
|
+
return 0
|
|
1106
1187
|
except RunnerRegistrationError as error:
|
|
1107
1188
|
print(f"registration refused ({error.reason}): {error}", flush=True)
|
|
1108
1189
|
if error.refuses_registration_token:
|
|
@@ -1148,6 +1229,12 @@ async def _serve(
|
|
|
1148
1229
|
attestation=attestation,
|
|
1149
1230
|
lifecycle=lifecycle,
|
|
1150
1231
|
)
|
|
1232
|
+
except _StoppedBeforeStartError:
|
|
1233
|
+
# No beat with a 10 s timeout on the way out (`_report_stop`): the control plane
|
|
1234
|
+
# may be what start-up was waiting for. The next start carries the stop.
|
|
1235
|
+
lifecycle.add(LifecycleKind.STOP, datetime.now(UTC))
|
|
1236
|
+
print("stopped before the Worker started", flush=True)
|
|
1237
|
+
return 0
|
|
1151
1238
|
finally:
|
|
1152
1239
|
pidfile.unlink(missing_ok=True)
|
|
1153
1240
|
|
|
@@ -1166,7 +1253,7 @@ async def _serve_registered(
|
|
|
1166
1253
|
http_client: httpx.AsyncClient | None,
|
|
1167
1254
|
connect: Connector,
|
|
1168
1255
|
worker_factory: WorkerFactory,
|
|
1169
|
-
stop: asyncio.Event
|
|
1256
|
+
stop: asyncio.Event,
|
|
1170
1257
|
interval: float,
|
|
1171
1258
|
host_store: HostCredentialStore | None,
|
|
1172
1259
|
attestation: HostAttestation | None,
|
|
@@ -1240,21 +1327,24 @@ async def _serve_registered(
|
|
|
1240
1327
|
# none, and the Temporal connection below cannot be opened without it.
|
|
1241
1328
|
while True:
|
|
1242
1329
|
try:
|
|
1243
|
-
await link.exchange()
|
|
1330
|
+
await _before_stop(link.exchange(), stop)
|
|
1244
1331
|
break
|
|
1332
|
+
except _StoppedBeforeStartError:
|
|
1333
|
+
raise
|
|
1245
1334
|
except Exception as error: # noqa: BLE001 - keep trying until the control plane answers
|
|
1246
1335
|
if _revoked(error):
|
|
1247
1336
|
return 1
|
|
1248
1337
|
_logger.warning("heartbeat failed before start: %s", error)
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1338
|
+
await _before_stop(asyncio.sleep(interval), stop)
|
|
1339
|
+
|
|
1340
|
+
client = await _before_stop(
|
|
1341
|
+
connect(
|
|
1342
|
+
temporal_address,
|
|
1343
|
+
namespace=state.temporal_namespace,
|
|
1344
|
+
api_key=stream.runner_token,
|
|
1345
|
+
tls=os.environ.get(TEMPORAL_TLS_ENV, "").strip().lower() in {"1", "true", "yes"},
|
|
1346
|
+
),
|
|
1347
|
+
stop,
|
|
1258
1348
|
)
|
|
1259
1349
|
hooks = build_hook_runner(config)
|
|
1260
1350
|
async with (
|
|
@@ -1289,7 +1379,6 @@ async def _serve_registered(
|
|
|
1289
1379
|
graceful_shutdown_timeout=graceful_shutdown_timeout(settings),
|
|
1290
1380
|
max_concurrent_activities=state.max_concurrent_directives,
|
|
1291
1381
|
)
|
|
1292
|
-
stop = stop or asyncio.Event()
|
|
1293
1382
|
revoked = asyncio.Event()
|
|
1294
1383
|
beat = asyncio.create_task(
|
|
1295
1384
|
_heartbeat_forever(link, stream, client, interval, stop=stop, revoked=revoked)
|
|
@@ -7,6 +7,8 @@ precedent does on a developer machine and nothing a daemon would need:
|
|
|
7
7
|
identity, namespace, Runner Token, hooks, credential store and Workspaces under
|
|
8
8
|
``<root>/<org>/``. A contractor with two clients runs two. There is no supervisor over
|
|
9
9
|
N Organisations -- that is the "one Runner straddling Organisations" 12 A6 rejected.
|
|
10
|
+
``add <name>`` puts another such process beside them, a personal Runner per Contract
|
|
11
|
+
(ADR-0022 §21), named freely rather than after its Organisation.
|
|
10
12
|
* **A login agent, never a system daemon** (23 item 1): a LaunchAgent in
|
|
11
13
|
``~/Library/LaunchAgents`` on macOS, a systemd *user* unit (with linger) on Linux, a
|
|
12
14
|
per-user logon task on Windows. No ``sudo`` anywhere: the login keychain is unreachable
|
|
@@ -81,6 +83,7 @@ __all__ = [
|
|
|
81
83
|
"run_environment",
|
|
82
84
|
"run_process",
|
|
83
85
|
"service_definition",
|
|
86
|
+
"settings_to_add",
|
|
84
87
|
"start",
|
|
85
88
|
"status_lines",
|
|
86
89
|
"stop",
|
|
@@ -387,7 +390,7 @@ def runner_command(org: str, root: Path) -> list[str]:
|
|
|
387
390
|
# The console script as found, not resolved: Homebrew's `bin/agentic-runner` is a
|
|
388
391
|
# symlink into a versioned Cellar path that the next `brew upgrade` deletes.
|
|
389
392
|
script = shutil.which("agentic-runner")
|
|
390
|
-
prefix = [script] if script else [sys.executable, "-m", "agentic_runner.
|
|
393
|
+
prefix = [script] if script else [sys.executable, "-m", "agentic_runner.launch"]
|
|
391
394
|
return [*prefix, "run", "--org", org, "--root", str(root), "--login-agent"]
|
|
392
395
|
|
|
393
396
|
|
|
@@ -673,6 +676,50 @@ async def install(
|
|
|
673
676
|
return lines
|
|
674
677
|
|
|
675
678
|
|
|
679
|
+
def settings_to_add(paths: OrgPaths, *, beside: str | None) -> WorkstationSettings:
|
|
680
|
+
"""``add <name>``: what ``install`` registers another Runner with (ADR-0022 §21).
|
|
681
|
+
|
|
682
|
+
A personal Runner serves one Contract, so a person with three Contracts runs three. Each
|
|
683
|
+
is ``install``'s own process with its own state directory -- identity, Runner Token and
|
|
684
|
+
Recipient Key (§37) -- and only the settings an earlier ``install`` was told are copied,
|
|
685
|
+
so the command the console shows carries no Temporal address.
|
|
686
|
+
"""
|
|
687
|
+
|
|
688
|
+
# Registered state, not workstation.json: ``install`` saves the settings before it
|
|
689
|
+
# registers, so a refused token leaves them behind and the retry must go through.
|
|
690
|
+
if load_state(paths.state_dir) is not None:
|
|
691
|
+
raise ValueError(
|
|
692
|
+
f"{paths.org!r} is already installed under {paths.root}; pick a new name for "
|
|
693
|
+
"the Runner you are adding"
|
|
694
|
+
)
|
|
695
|
+
return _settings_beside(paths.root, beside).model_copy(update={"org": paths.org})
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _settings_beside(root: Path, beside: str | None) -> WorkstationSettings:
|
|
699
|
+
if beside is not None:
|
|
700
|
+
return WorkstationSettings.load(OrgPaths(root=root, org=validate_org(beside)))
|
|
701
|
+
installed = [
|
|
702
|
+
WorkstationSettings.load(OrgPaths(root=root, org=file.parent.name))
|
|
703
|
+
for file in sorted(root.glob("*/workstation.json"), key=lambda file: file.stat().st_mtime)
|
|
704
|
+
]
|
|
705
|
+
if not installed:
|
|
706
|
+
raise FileNotFoundError(
|
|
707
|
+
f"no Runner is installed under {root}; run `agentic-runner install` first"
|
|
708
|
+
)
|
|
709
|
+
endpoints = {
|
|
710
|
+
(settings.control_plane_url, settings.temporal_address, settings.temporal_tls)
|
|
711
|
+
for settings in installed
|
|
712
|
+
}
|
|
713
|
+
if len(endpoints) > 1:
|
|
714
|
+
names = ", ".join(sorted(settings.org for settings in installed))
|
|
715
|
+
raise ValueError(
|
|
716
|
+
f"the Runners under {root} ({names}) talk to different control planes; "
|
|
717
|
+
"name the one to add beside with --beside"
|
|
718
|
+
)
|
|
719
|
+
# The newest install's PATH and ACP kinds: the latest the person told this machine.
|
|
720
|
+
return installed[-1]
|
|
721
|
+
|
|
722
|
+
|
|
676
723
|
def start(paths: OrgPaths, **driver: Any) -> None:
|
|
677
724
|
_drive_service(paths, "start", **driver)
|
|
678
725
|
|
|
File without changes
|
|
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.17.0 → agentic_runner-3.18.1}/src/agentic_runner/device_login_activities.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.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/contracts.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/evidence.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/git/workspace.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/github/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/github/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/integrations/github/gh_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/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
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/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.17.0 → agentic_runner-3.18.1}/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.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/_runtime_support.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/claude_runtime.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/claude_sign_in.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/command_policy.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/contract_device_login.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/contract_isolation.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/fastapi_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.17.0 → agentic_runner-3.18.1}/src/agentic_runner/workers/harness_outcome.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|