agentic-runner 3.5.0__tar.gz → 3.7.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.5.0 → agentic_runner-3.7.0}/PKG-INFO +1 -1
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/activities.py +12 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/device_login_activities.py +30 -12
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/sealed_box.py +15 -4
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/service.py +123 -3
- agentic_runner-3.7.0/src/agentic_runner/usage_windows.py +529 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/acp_runtime.py +15 -1
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/agent_runtime.py +4 -0
- agentic_runner-3.7.0/src/agentic_runner/workers/claude_sign_in.py +534 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/contract_device_login.py +32 -14
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/harness_outcome.py +5 -5
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/.gitignore +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/LICENSE +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/README.md +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/pyproject.toml +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/auth_mode.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/cli.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/credentials.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/heartbeat_link.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/llm_proxy.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/testing/control_plane.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/testing/test_conformance.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/settings.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workers/skills.py +0 -0
- {agentic_runner-3.5.0 → agentic_runner-3.7.0}/src/agentic_runner/workstation.py +0 -0
|
@@ -118,6 +118,7 @@ from agentic_runner.message_store import MessageStore, WorkflowSignaller
|
|
|
118
118
|
from agentic_runner.registration import RunnerRegistrationError
|
|
119
119
|
from agentic_runner.runtime import verifier_command
|
|
120
120
|
from agentic_runner.runtime.verifier_command import CommandValidationError
|
|
121
|
+
from agentic_runner.usage_windows import UsageWindows
|
|
121
122
|
from agentic_runner.workers._runtime_support import (
|
|
122
123
|
RESERVED_DIRECTIVE_ENV,
|
|
123
124
|
bound_text,
|
|
@@ -174,6 +175,7 @@ from agentic_runner_contracts.activity_io import (
|
|
|
174
175
|
GitHubCallError,
|
|
175
176
|
GitHubCallFailure,
|
|
176
177
|
HarnessHold,
|
|
178
|
+
HarnessHoldKind,
|
|
177
179
|
LearningDirectiveInput,
|
|
178
180
|
LearningDirectiveOutput,
|
|
179
181
|
MemberDirectiveInput,
|
|
@@ -1616,10 +1618,13 @@ class RunnerRalphActivities:
|
|
|
1616
1618
|
tool_server_health: ToolServerHealthLog | None = None,
|
|
1617
1619
|
attempt_records: AttemptRecords | None = None,
|
|
1618
1620
|
directives_in_flight: DirectivesInFlight | None = None,
|
|
1621
|
+
usage_windows: UsageWindows | None = None,
|
|
1619
1622
|
) -> None:
|
|
1620
1623
|
self._fastapi_client = fastapi_client
|
|
1621
1624
|
# Read by the harness self-test, which never runs beside a Contract's Directive.
|
|
1622
1625
|
self._directives_in_flight = directives_in_flight or DirectivesInFlight()
|
|
1626
|
+
# Local-agents 10: None off a user-hosted Runner, which reports no usage windows.
|
|
1627
|
+
self._usage_windows = usage_windows
|
|
1623
1628
|
# Wall-clock source for per-Directive runtime, accrued against the Budget's
|
|
1624
1629
|
# wall-clock dimension (ADR-0007). Activities run outside the Temporal sandbox,
|
|
1625
1630
|
# so a real monotonic clock is allowed here (it must never be used in the
|
|
@@ -5273,6 +5278,13 @@ class RunnerRalphActivities:
|
|
|
5273
5278
|
error=result.error,
|
|
5274
5279
|
now=datetime.now(UTC),
|
|
5275
5280
|
)
|
|
5281
|
+
if self._usage_windows is not None and runtime_state.contract_id:
|
|
5282
|
+
self._usage_windows.after_directive(
|
|
5283
|
+
runtime_state.contract_id,
|
|
5284
|
+
runtime_state.cli_kind,
|
|
5285
|
+
result.usage_windows,
|
|
5286
|
+
usage_limit=hold is not None and hold.kind == HarnessHoldKind.USAGE_LIMIT,
|
|
5287
|
+
)
|
|
5276
5288
|
if hold is None:
|
|
5277
5289
|
return None
|
|
5278
5290
|
await self._fastapi_client.append_evidence(
|
|
@@ -13,7 +13,8 @@ from datetime import UTC, datetime
|
|
|
13
13
|
from temporalio import activity
|
|
14
14
|
from temporalio.exceptions import ApplicationError
|
|
15
15
|
|
|
16
|
-
from agentic_runner.auth_mode import SHARED_RUNNER_SIGN_IN_RULE
|
|
16
|
+
from agentic_runner.auth_mode import SHARED_RUNNER_SIGN_IN_RULE, is_shared_runner
|
|
17
|
+
from agentic_runner.workers.claude_sign_in import OAUTH_TOKEN
|
|
17
18
|
from agentic_runner.workers.contract_device_login import ContractDeviceLogin
|
|
18
19
|
from agentic_runner.workers.contract_isolation import ContractIsolation
|
|
19
20
|
from agentic_runner_contracts.activity_io import (
|
|
@@ -36,10 +37,10 @@ class ContractDeviceLoginActivities:
|
|
|
36
37
|
host_party: str | None = None,
|
|
37
38
|
) -> None:
|
|
38
39
|
self._contract_isolation = contract_isolation
|
|
39
|
-
# The party this process was registered as hosted by. A shared Runner
|
|
40
|
-
#
|
|
40
|
+
# The party this process was registered as hosted by. A shared Runner signs in only
|
|
41
|
+
# a Claude Code long-lived token (local-agents 04, amended by 21).
|
|
41
42
|
self._host_party = host_party
|
|
42
|
-
# Injectable for tests; None falls through to the real
|
|
43
|
+
# Injectable for tests; None falls through to the real vendor CLIs.
|
|
43
44
|
self._device_login = device_login or ContractDeviceLogin(contract_isolation)
|
|
44
45
|
|
|
45
46
|
def activity_callables(self) -> list[Callable[..., object]]:
|
|
@@ -52,9 +53,12 @@ class ContractDeviceLoginActivities:
|
|
|
52
53
|
async def sign_in_contract_device_login(
|
|
53
54
|
self, request: ContractDeviceLoginInput
|
|
54
55
|
) -> ContractDeviceLoginResult:
|
|
55
|
-
|
|
56
|
+
long_lived = request.runtime_kind == "claude_code" and (request.method or OAUTH_TOKEN) == (
|
|
57
|
+
OAUTH_TOKEN
|
|
58
|
+
)
|
|
59
|
+
self._refuse_on_shared_runner(allow=long_lived)
|
|
56
60
|
prompt = await self._device_login.sign_in(
|
|
57
|
-
request.contract_id, runtime_kind=request.runtime_kind
|
|
61
|
+
request.contract_id, runtime_kind=request.runtime_kind, method=request.method
|
|
58
62
|
)
|
|
59
63
|
return ContractDeviceLoginResult(
|
|
60
64
|
contract_id=prompt.contract_id,
|
|
@@ -62,12 +66,24 @@ class ContractDeviceLoginActivities:
|
|
|
62
66
|
verification_uri=prompt.verification_uri,
|
|
63
67
|
user_code=prompt.user_code,
|
|
64
68
|
expires_at=prompt.expires_at.isoformat(),
|
|
69
|
+
sign_in_id=prompt.sign_in_id,
|
|
65
70
|
)
|
|
66
71
|
|
|
67
72
|
@activity.defn(name="check_contract_device_login_status")
|
|
68
73
|
async def check_contract_device_login_status(
|
|
69
74
|
self, request: ContractDeviceLoginStatusInput
|
|
70
75
|
) -> ContractDeviceLoginStatusResult:
|
|
76
|
+
if request.runtime_kind == "claude_code":
|
|
77
|
+
self._refuse_on_shared_runner(allow=True)
|
|
78
|
+
status = await self._device_login.claude.status(request.contract_id)
|
|
79
|
+
return ContractDeviceLoginStatusResult(
|
|
80
|
+
contract_id=request.contract_id,
|
|
81
|
+
runtime_kind=request.runtime_kind,
|
|
82
|
+
token_present=status.logged_in,
|
|
83
|
+
delivered_at=None,
|
|
84
|
+
auth_method=status.auth_method,
|
|
85
|
+
subscription_type=status.subscription_type,
|
|
86
|
+
)
|
|
71
87
|
self._refuse_on_shared_runner()
|
|
72
88
|
present = self._device_login.token_present(
|
|
73
89
|
request.contract_id, runtime_kind=request.runtime_kind
|
|
@@ -86,18 +102,20 @@ class ContractDeviceLoginActivities:
|
|
|
86
102
|
delivered_at=delivered_at,
|
|
87
103
|
)
|
|
88
104
|
|
|
89
|
-
def _refuse_on_shared_runner(self) -> None:
|
|
105
|
+
def _refuse_on_shared_runner(self, *, allow: bool = False) -> None:
|
|
90
106
|
"""Non-retryable, and typed by the rule: there is no Work Record to write Evidence
|
|
91
107
|
on here, so the failure the platform's sign-in workflow receives names it.
|
|
92
108
|
|
|
93
|
-
Only a Runner known to be the person's own signs in
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
Only a Runner known to be the person's own signs in, except that a shared Runner
|
|
110
|
+
may make (``allow``) a Claude Code long-lived token (local-agents 21). One whose
|
|
111
|
+
state names no host party (registered before issue 42) is refused either way, as
|
|
112
|
+
`choose_auth_mode` never runs a subscription on it."""
|
|
96
113
|
|
|
97
|
-
if self._host_party != "user":
|
|
114
|
+
if self._host_party != "user" and not (allow and is_shared_runner(self._host_party)):
|
|
98
115
|
raise ApplicationError(
|
|
99
116
|
f"this Runner is hosted by {self._host_party!r}; only a person's own Runner "
|
|
100
|
-
"signs in, a shared Runner runs API keys
|
|
117
|
+
"signs in, a shared Runner runs API keys and Claude Code long-lived tokens "
|
|
118
|
+
"only",
|
|
101
119
|
type=SHARED_RUNNER_SIGN_IN_RULE,
|
|
102
120
|
non_retryable=True,
|
|
103
121
|
)
|
|
@@ -61,6 +61,7 @@ from agentic_runner_contracts.sealed_credential import (
|
|
|
61
61
|
SEAL_DOMAIN,
|
|
62
62
|
OpenedCredential,
|
|
63
63
|
SealedCredential,
|
|
64
|
+
SealedSignInCode,
|
|
64
65
|
delivery_binding,
|
|
65
66
|
key_fingerprint,
|
|
66
67
|
)
|
|
@@ -516,15 +517,25 @@ class SealedCredentialStream:
|
|
|
516
517
|
if not self._awaiting:
|
|
517
518
|
self._keys.retire_previous()
|
|
518
519
|
|
|
520
|
+
def open_sign_in_code(self, sealed: SealedSignInCode) -> str | None:
|
|
521
|
+
"""A relayed sign-in code, or None when it does not open (local-agents 05).
|
|
522
|
+
|
|
523
|
+
Opened with the same Recipient Key as a slot, but never held: the caller types it
|
|
524
|
+
into the waiting CLI and drops it.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
return self._open_with(sealed.recipient_key_id, sealed.binding(), sealed.ciphertext)
|
|
528
|
+
|
|
519
529
|
def _open(self, sealed: SealedCredential) -> str | None:
|
|
530
|
+
return self._open_with(sealed.recipient_key_id, sealed.binding(), sealed.ciphertext)
|
|
531
|
+
|
|
532
|
+
def _open_with(self, recipient_key_id: str, binding: bytes, ciphertext: str) -> str | None:
|
|
520
533
|
for pair in (self._keys.current(), self._keys.previous()):
|
|
521
|
-
if pair is None or pair.key_id !=
|
|
534
|
+
if pair is None or pair.key_id != recipient_key_id:
|
|
522
535
|
continue
|
|
523
536
|
try:
|
|
524
537
|
return open_sealed(
|
|
525
|
-
private_key=pair.private_key,
|
|
526
|
-
binding=sealed.binding(),
|
|
527
|
-
ciphertext=sealed.ciphertext,
|
|
538
|
+
private_key=pair.private_key, binding=binding, ciphertext=ciphertext
|
|
528
539
|
)
|
|
529
540
|
except SealedCredentialError:
|
|
530
541
|
return None
|
|
@@ -98,11 +98,19 @@ from agentic_runner.sealed_box import (
|
|
|
98
98
|
)
|
|
99
99
|
from agentic_runner.tiny_http import HttpRequest, read_request, write_json
|
|
100
100
|
from agentic_runner.triage_activities import RunnerTriageActivities
|
|
101
|
+
from agentic_runner.usage_windows import (
|
|
102
|
+
ClaudeUsageScreenProbe,
|
|
103
|
+
CodexRateLimitsProbe,
|
|
104
|
+
UsageWindowProbe,
|
|
105
|
+
UsageWindows,
|
|
106
|
+
)
|
|
101
107
|
from agentic_runner.user_sources import ProxyTriage, UserSourcePoller
|
|
102
108
|
from agentic_runner.workers.acp_runtime import ACP_BRIDGES, AcpRuntime
|
|
103
109
|
from agentic_runner.workers.agent_runtime import AgentRuntime
|
|
104
110
|
from agentic_runner.workers.claude_runtime import ClaudeRuntime
|
|
111
|
+
from agentic_runner.workers.claude_sign_in import ClaudeSignIns
|
|
105
112
|
from agentic_runner.workers.codex_runtime import CodexRuntime
|
|
113
|
+
from agentic_runner.workers.contract_device_login import ContractDeviceLogin
|
|
106
114
|
from agentic_runner.workers.contract_isolation import UID_MAP_FILENAME, ContractIsolation
|
|
107
115
|
from agentic_runner.workers.fastapi_client import DirectiveTokenSource, RunnerFastApiClient
|
|
108
116
|
from agentic_runner.workers.settings import WorkerSettings, get_worker_settings
|
|
@@ -124,7 +132,11 @@ from agentic_runner_contracts.runner_registration import (
|
|
|
124
132
|
ResourcePressure,
|
|
125
133
|
)
|
|
126
134
|
from agentic_runner_contracts.runtime_context import WorkerRuntimeContextResolver
|
|
127
|
-
from agentic_runner_contracts.sealed_credential import
|
|
135
|
+
from agentic_runner_contracts.sealed_credential import (
|
|
136
|
+
SignInCodeOutcome,
|
|
137
|
+
SignInCodeRelay,
|
|
138
|
+
key_fingerprint,
|
|
139
|
+
)
|
|
128
140
|
|
|
129
141
|
__all__ = [
|
|
130
142
|
"AGENT_TOKEN_ENV",
|
|
@@ -575,8 +587,15 @@ class ControlPlaneStream:
|
|
|
575
587
|
runtimes: Mapping[str, AgentRuntime] = field(default_factory=dict)
|
|
576
588
|
contracts: ContractIsolation | None = None
|
|
577
589
|
self_tests: HarnessSelfTests | None = None
|
|
590
|
+
# Local-agents 10: each subscription's usage windows; None off a user-hosted Runner.
|
|
591
|
+
usage_windows: UsageWindows | None = None
|
|
578
592
|
# Local-agents 04b: which delivered references fill the LLM proxy's slot, and where.
|
|
579
593
|
llm_slots: Mapping[str, LlmProvider] = field(default_factory=lambda: dict(LLM_SLOT_REFERENCES))
|
|
594
|
+
# Local-agents 05: the Claude Code sign-ins a relayed code is typed into.
|
|
595
|
+
sign_ins: ClaudeSignIns | None = None
|
|
596
|
+
# Reported once, on the beat after the relay: a lost beat loses the outcome, and the
|
|
597
|
+
# console's own wait on the sign-in is what tells the person.
|
|
598
|
+
_sign_in_outcomes: list[SignInCodeRelay] = field(default_factory=list)
|
|
580
599
|
_delivered: set[tuple[str, str]] = field(default_factory=set)
|
|
581
600
|
_offered_to_proxy: dict[tuple[str, str], str] = field(default_factory=dict)
|
|
582
601
|
_last_seen: tuple[float, datetime] | None = None
|
|
@@ -642,6 +661,11 @@ class ControlPlaneStream:
|
|
|
642
661
|
if self.self_tests is not None
|
|
643
662
|
else None
|
|
644
663
|
),
|
|
664
|
+
usage_windows=(
|
|
665
|
+
self.usage_windows.windows(contract_id, cli_kind)
|
|
666
|
+
if self.usage_windows is not None and not delivered
|
|
667
|
+
else None
|
|
668
|
+
),
|
|
645
669
|
)
|
|
646
670
|
|
|
647
671
|
async def directive_token(self, directive_id: str) -> str:
|
|
@@ -679,11 +703,14 @@ class ControlPlaneStream:
|
|
|
679
703
|
clock_skew_seconds=self._clock_skew(now),
|
|
680
704
|
source_status=self.sources.statuses() if self.sources is not None else [],
|
|
681
705
|
tool_servers=self.tool_servers.latest() if self.tool_servers is not None else [],
|
|
706
|
+
sign_in_codes=self._sign_in_outcomes,
|
|
682
707
|
)
|
|
708
|
+
self._sign_in_outcomes = []
|
|
683
709
|
ack = await self.client.heartbeat(self.state, envelope)
|
|
684
710
|
if self.sources is not None:
|
|
685
711
|
self.sources.assign(ack.user_sources)
|
|
686
712
|
await self._deliver_transcripts(ack)
|
|
713
|
+
await self._relay_sign_in_codes(ack)
|
|
687
714
|
if self.lifecycle is not None:
|
|
688
715
|
self.lifecycle.acknowledge(lifecycle)
|
|
689
716
|
if self.heartbeat_stamp is not None:
|
|
@@ -730,6 +757,28 @@ class ControlPlaneStream:
|
|
|
730
757
|
"transcript %s could not be delivered: %s", request.request_id, error
|
|
731
758
|
)
|
|
732
759
|
|
|
760
|
+
async def _relay_sign_in_codes(self, ack: HeartbeatAck) -> None:
|
|
761
|
+
"""Type each relayed code into the sign-in it names (local-agents 05).
|
|
762
|
+
|
|
763
|
+
The code is opened, written to that sign-in's PTY and dropped here; nothing logs
|
|
764
|
+
it and only its outcome travels back.
|
|
765
|
+
"""
|
|
766
|
+
|
|
767
|
+
for sealed in ack.sign_in_codes:
|
|
768
|
+
code = self.sealed.open_sign_in_code(sealed)
|
|
769
|
+
if code is None:
|
|
770
|
+
outcome = SignInCodeOutcome.UNOPENABLE
|
|
771
|
+
elif self.sign_ins is None:
|
|
772
|
+
outcome = SignInCodeOutcome.UNKNOWN
|
|
773
|
+
else:
|
|
774
|
+
outcome = await self.sign_ins.relay(
|
|
775
|
+
str(sealed.contract_id), sealed.sign_in_id, code
|
|
776
|
+
)
|
|
777
|
+
del code
|
|
778
|
+
self._sign_in_outcomes.append(
|
|
779
|
+
SignInCodeRelay(sign_in_id=sealed.sign_in_id, outcome=outcome)
|
|
780
|
+
)
|
|
781
|
+
|
|
733
782
|
def _sync_delivered(self) -> None:
|
|
734
783
|
"""The resolver holds exactly what the last ack let this installation open."""
|
|
735
784
|
|
|
@@ -980,6 +1029,9 @@ async def _serve_registered(
|
|
|
980
1029
|
served=runtimes,
|
|
981
1030
|
monotonic=time.monotonic,
|
|
982
1031
|
)
|
|
1032
|
+
usage_windows = build_usage_windows(
|
|
1033
|
+
host_party=state.host_party, runtimes=runtimes, isolation=isolation, in_flight=in_flight
|
|
1034
|
+
)
|
|
983
1035
|
stream = ControlPlaneStream(
|
|
984
1036
|
client=RunnerRegistrationClient(base_url=control_plane, client=http_client),
|
|
985
1037
|
state=state,
|
|
@@ -1007,6 +1059,7 @@ async def _serve_registered(
|
|
|
1007
1059
|
runtimes=runtimes,
|
|
1008
1060
|
contracts=isolation,
|
|
1009
1061
|
self_tests=self_tests,
|
|
1062
|
+
usage_windows=usage_windows,
|
|
1010
1063
|
)
|
|
1011
1064
|
stream.sources = UserSourcePoller(
|
|
1012
1065
|
stream=SignedIntakeStream(stream.client, state),
|
|
@@ -1019,6 +1072,8 @@ async def _serve_registered(
|
|
|
1019
1072
|
)
|
|
1020
1073
|
messages = MessageStore(state_dir / MESSAGES_DIR)
|
|
1021
1074
|
stream.messages = messages
|
|
1075
|
+
sign_ins = ClaudeSignIns(build_contract_isolation(settings, can_separate_uids=can_change_uid))
|
|
1076
|
+
stream.sign_ins = sign_ins
|
|
1022
1077
|
tool_servers = ToolServerHealthLog()
|
|
1023
1078
|
stream.tool_servers = tool_servers
|
|
1024
1079
|
link = HeartbeatLink(stream, ceilings=proxy.ceilings)
|
|
@@ -1065,6 +1120,8 @@ async def _serve_registered(
|
|
|
1065
1120
|
token_source=stream,
|
|
1066
1121
|
messages=messages,
|
|
1067
1122
|
tool_servers=tool_servers,
|
|
1123
|
+
sign_ins=sign_ins,
|
|
1124
|
+
usage_windows=usage_windows,
|
|
1068
1125
|
# The wake signal rides the connection this process polls with: one
|
|
1069
1126
|
# namespace, the Organisation's own (PRD issue 52, map ticket 15 §2).
|
|
1070
1127
|
signaller=TemporalWorkflowSignaller(client),
|
|
@@ -1082,16 +1139,24 @@ async def _serve_registered(
|
|
|
1082
1139
|
self_test = asyncio.create_task(
|
|
1083
1140
|
_self_test_forever(self_tests, isolation, SELF_TEST_SWEEP_SECONDS)
|
|
1084
1141
|
)
|
|
1142
|
+
sweeps = [beat, poll, self_test]
|
|
1143
|
+
if usage_windows is not None:
|
|
1144
|
+
sweeps.append(
|
|
1145
|
+
asyncio.create_task(
|
|
1146
|
+
_usage_windows_forever(usage_windows, isolation, proxy, SELF_TEST_SWEEP_SECONDS)
|
|
1147
|
+
)
|
|
1148
|
+
)
|
|
1085
1149
|
readiness.ready = True
|
|
1086
1150
|
await run_runner_lifecycle_hook(hooks, HookName.RUNNER_STARTUP)
|
|
1087
1151
|
try:
|
|
1088
1152
|
await run_worker_until_terminated(worker, stop=stop)
|
|
1089
1153
|
finally:
|
|
1090
1154
|
readiness.ready = False
|
|
1091
|
-
for task in
|
|
1155
|
+
for task in sweeps:
|
|
1092
1156
|
task.cancel()
|
|
1093
1157
|
with contextlib.suppress(asyncio.CancelledError):
|
|
1094
1158
|
await task
|
|
1159
|
+
await sign_ins.close()
|
|
1095
1160
|
await run_runner_lifecycle_hook(hooks, HookName.RUNNER_SHUTDOWN)
|
|
1096
1161
|
await _report_stop(link, lifecycle)
|
|
1097
1162
|
return 1 if revoked.is_set() else 0
|
|
@@ -1187,6 +1252,56 @@ async def _self_test_forever(
|
|
|
1187
1252
|
await asyncio.sleep(interval)
|
|
1188
1253
|
|
|
1189
1254
|
|
|
1255
|
+
def build_usage_windows(
|
|
1256
|
+
*,
|
|
1257
|
+
host_party: str,
|
|
1258
|
+
runtimes: Mapping[str, AgentRuntime],
|
|
1259
|
+
isolation: ContractIsolation,
|
|
1260
|
+
in_flight: DirectivesInFlight,
|
|
1261
|
+
) -> UsageWindows | None:
|
|
1262
|
+
"""A probe per served harness, or None off a user-hosted Runner (local-agents 10).
|
|
1263
|
+
|
|
1264
|
+
Subscriptions follow the person (local-agents 04): only a user-hosted Runner runs one,
|
|
1265
|
+
so only there is a usage window anyone's to read.
|
|
1266
|
+
"""
|
|
1267
|
+
|
|
1268
|
+
if host_party != "user":
|
|
1269
|
+
return None
|
|
1270
|
+
probes: dict[str, UsageWindowProbe] = {
|
|
1271
|
+
"codex_cli": CodexRateLimitsProbe(),
|
|
1272
|
+
"claude_code": ClaudeUsageScreenProbe(),
|
|
1273
|
+
}
|
|
1274
|
+
return UsageWindows(
|
|
1275
|
+
probes={kind: probe for kind, probe in probes.items() if kind in runtimes},
|
|
1276
|
+
sandbox_for=lambda contract_id, cli_kind: isolation.existing_sandbox(
|
|
1277
|
+
contract_id, runtime_kind=cli_kind
|
|
1278
|
+
),
|
|
1279
|
+
in_flight=in_flight,
|
|
1280
|
+
monotonic=time.monotonic,
|
|
1281
|
+
)
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
async def _usage_windows_forever(
|
|
1285
|
+
usage_windows: UsageWindows, isolation: ContractIsolation, proxy: LlmProxy, interval: float
|
|
1286
|
+
) -> None:
|
|
1287
|
+
"""Each signed-in harness root's usage windows, whenever its probe falls due
|
|
1288
|
+
(local-agents 10). A root with a delivered key runs on the proxy, not a subscription."""
|
|
1289
|
+
|
|
1290
|
+
while True:
|
|
1291
|
+
delivered = {
|
|
1292
|
+
(str(slot.contract_id), slot.runtime_kind)
|
|
1293
|
+
for slot in proxy.slots.statuses()
|
|
1294
|
+
if slot.present
|
|
1295
|
+
}
|
|
1296
|
+
try:
|
|
1297
|
+
await usage_windows.run_due(
|
|
1298
|
+
[root for root in isolation.harness_roots() if root not in delivered]
|
|
1299
|
+
)
|
|
1300
|
+
except Exception as error: # noqa: BLE001 - a lost sweep is retried on the next one
|
|
1301
|
+
_logger.warning("usage window sweep failed: %s", error)
|
|
1302
|
+
await asyncio.sleep(interval)
|
|
1303
|
+
|
|
1304
|
+
|
|
1190
1305
|
def _activities(
|
|
1191
1306
|
fastapi: RunnerFastApiClient,
|
|
1192
1307
|
*,
|
|
@@ -1204,6 +1319,8 @@ def _activities(
|
|
|
1204
1319
|
messages: MessageStore | None = None,
|
|
1205
1320
|
signaller: TemporalWorkflowSignaller | None = None,
|
|
1206
1321
|
tool_servers: ToolServerHealthLog | None = None,
|
|
1322
|
+
sign_ins: ClaudeSignIns | None = None,
|
|
1323
|
+
usage_windows: UsageWindows | None = None,
|
|
1207
1324
|
) -> list[Callable[..., Any]]:
|
|
1208
1325
|
github_client = GitHubAppClient(
|
|
1209
1326
|
app_id=os.getenv("GITHUB_APP_ID"),
|
|
@@ -1251,11 +1368,14 @@ def _activities(
|
|
|
1251
1368
|
tool_server_health=tool_servers,
|
|
1252
1369
|
attempt_records=attempt_records,
|
|
1253
1370
|
directives_in_flight=directives_in_flight,
|
|
1371
|
+
usage_windows=usage_windows,
|
|
1254
1372
|
)
|
|
1255
1373
|
return [
|
|
1256
1374
|
*ralph.activity_callables(),
|
|
1257
1375
|
*ContractDeviceLoginActivities(
|
|
1258
|
-
contract_isolation=isolation,
|
|
1376
|
+
contract_isolation=isolation,
|
|
1377
|
+
device_login=ContractDeviceLogin(isolation, claude=sign_ins),
|
|
1378
|
+
host_party=state.host_party or None,
|
|
1259
1379
|
).activity_callables(),
|
|
1260
1380
|
*RunnerTriageActivities(
|
|
1261
1381
|
proxy=proxy,
|