agentic-runner 3.24.0__tar.gz → 3.25.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.24.0 → agentic_runner-3.25.1}/PKG-INFO +2 -2
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/pyproject.toml +1 -1
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/activities.py +94 -15
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/auth_mode.py +98 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/cli.py +92 -18
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/credentials.py +27 -36
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/heartbeat_link.py +0 -1
- agentic_runner-3.25.1/src/agentic_runner/llm_contracts.py +41 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/llm_proxy.py +168 -81
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/service.py +282 -139
- agentic_runner-3.25.1/src/agentic_runner/sign_in.py +156 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +23 -26
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/testing/control_plane.py +23 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/testing/test_conformance.py +8 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/triage_activities.py +19 -2
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/claude_sign_in.py +12 -6
- agentic_runner-3.25.1/src/agentic_runner/workers/harness_logins.py +172 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workstation.py +5 -1
- agentic_runner-3.24.0/src/agentic_runner/device_login_activities.py +0 -119
- agentic_runner-3.24.0/src/agentic_runner/workers/contract_device_login.py +0 -241
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/.gitignore +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/LICENSE +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/README.md +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/launch.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/oauth_connectors.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/runner_limit.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/sealed_box.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/skill.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/usage_windows.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/_runtime_support.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/acp_runtime.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/agent_runtime.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/harness_outcome.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.1}/src/agentic_runner/workers/settings.py +0 -0
- {agentic_runner-3.24.0 → agentic_runner-3.25.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.25.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.25.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.25.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.
|
|
@@ -49,6 +49,7 @@ from agentic_runner.auth_mode import (
|
|
|
49
49
|
AuthModeRefusedError,
|
|
50
50
|
choose_auth_mode,
|
|
51
51
|
is_shared_runner,
|
|
52
|
+
require_named_access,
|
|
52
53
|
)
|
|
53
54
|
from agentic_runner.callback import (
|
|
54
55
|
CALLBACK_TOKEN_ENV,
|
|
@@ -149,7 +150,6 @@ from agentic_runner.workers.agent_runtime import (
|
|
|
149
150
|
PermissionFallback,
|
|
150
151
|
ResumableAgentRuntime,
|
|
151
152
|
)
|
|
152
|
-
from agentic_runner.workers.contract_device_login import holds_login, login_file_counts
|
|
153
153
|
from agentic_runner.workers.contract_isolation import (
|
|
154
154
|
NO_CONTRACT,
|
|
155
155
|
ContractIsolation,
|
|
@@ -161,6 +161,11 @@ from agentic_runner.workers.fastapi_client import (
|
|
|
161
161
|
WorkerFastApiClientError,
|
|
162
162
|
directive_token,
|
|
163
163
|
)
|
|
164
|
+
from agentic_runner.workers.harness_logins import (
|
|
165
|
+
holds_login,
|
|
166
|
+
login_file_counts,
|
|
167
|
+
subscription_report,
|
|
168
|
+
)
|
|
164
169
|
from agentic_runner.workers.harness_outcome import classify_harness_outcome
|
|
165
170
|
from agentic_runner.workers.harness_usage import extract_claude_result, extract_codex_turn_usage
|
|
166
171
|
from agentic_runner.workers.mcp_config import McpServerEntry
|
|
@@ -277,6 +282,7 @@ from agentic_runner_contracts.grants import (
|
|
|
277
282
|
GrantSnapshot,
|
|
278
283
|
)
|
|
279
284
|
from agentic_runner_contracts.grants.seam import VerbDecision, decide_verb
|
|
285
|
+
from agentic_runner_contracts.llm_access import LlmAccess, LlmAccessKind, provider_for
|
|
280
286
|
from agentic_runner_contracts.questions import WORK_ASK_VERB
|
|
281
287
|
from agentic_runner_contracts.redaction import redact_secret_like_text
|
|
282
288
|
from agentic_runner_contracts.routing import (
|
|
@@ -286,6 +292,11 @@ from agentic_runner_contracts.routing import (
|
|
|
286
292
|
assert_routed,
|
|
287
293
|
selector_satisfied,
|
|
288
294
|
)
|
|
295
|
+
from agentic_runner_contracts.runner_registration import (
|
|
296
|
+
ContractLlmTerms,
|
|
297
|
+
SubscriptionRight,
|
|
298
|
+
SubscriptionState,
|
|
299
|
+
)
|
|
289
300
|
from agentic_runner_contracts.runtime_context import (
|
|
290
301
|
EGRESS_ALLOW_LIST_KEY,
|
|
291
302
|
McpServerSpec,
|
|
@@ -1713,6 +1724,7 @@ class RunnerRalphActivities:
|
|
|
1713
1724
|
directives_in_flight: DirectivesInFlight | None = None,
|
|
1714
1725
|
usage_windows: UsageWindows | None = None,
|
|
1715
1726
|
runner_limit: AppliedRunnerLimit | None = None,
|
|
1727
|
+
llm_contracts: Callable[[], Sequence[ContractLlmTerms] | None] | None = None,
|
|
1716
1728
|
) -> None:
|
|
1717
1729
|
self._fastapi_client = fastapi_client
|
|
1718
1730
|
# What this Runner's host denies every Agent on it, checked at start (ADR-0022
|
|
@@ -1787,6 +1799,9 @@ class RunnerRalphActivities:
|
|
|
1787
1799
|
# The retry fence (runner-repo 05). None fences nothing: a process that never
|
|
1788
1800
|
# registered has no state directory to keep the records in.
|
|
1789
1801
|
self._attempt_records = attempt_records
|
|
1802
|
+
# The Contracts the last heartbeat ack named, read at each Directive (QTS-1751).
|
|
1803
|
+
# None, or a callable answering None, before any ack from a 3.25 control plane.
|
|
1804
|
+
self._llm_contracts = llm_contracts
|
|
1790
1805
|
|
|
1791
1806
|
def activity_callables(self) -> list[Callable[..., Any]]:
|
|
1792
1807
|
"""Return decorated activity callables for Temporal worker registration."""
|
|
@@ -2650,7 +2665,8 @@ class RunnerRalphActivities:
|
|
|
2650
2665
|
the bearer defensible under ADR-0011 §9.
|
|
2651
2666
|
"""
|
|
2652
2667
|
|
|
2653
|
-
|
|
2668
|
+
access = routing.llm_access if routing is not None else None
|
|
2669
|
+
auth_mode = await self._directive_auth_mode(work_record_id, state, access)
|
|
2654
2670
|
await self._report_login_residue(work_record_id, state)
|
|
2655
2671
|
facts = AttemptFacts(
|
|
2656
2672
|
work_record_id=work_record_id,
|
|
@@ -2669,9 +2685,7 @@ class RunnerRalphActivities:
|
|
|
2669
2685
|
)
|
|
2670
2686
|
|
|
2671
2687
|
await self._refuse_skill_digest_mismatch(work_record_id, state)
|
|
2672
|
-
credentials = await self._resolve_credentials(
|
|
2673
|
-
work_record_id, state, funding_party=routing.funding_party if routing else None
|
|
2674
|
-
)
|
|
2688
|
+
credentials = await self._resolve_credentials(work_record_id, state)
|
|
2675
2689
|
if credentials is not None:
|
|
2676
2690
|
# Names only, at the moment they resolved (22 A1's Evidence): a support read
|
|
2677
2691
|
# of "which credentials did this Directive have" must not be answerable by
|
|
@@ -2716,7 +2730,8 @@ class RunnerRalphActivities:
|
|
|
2716
2730
|
if self._llm_proxy is not None and auth_mode == AuthMode.API_KEY:
|
|
2717
2731
|
# API-key mode only (issue 43): a subscription Directive never has a slot
|
|
2718
2732
|
# to resolve, and issue 31 meters it from the harness's own output. One
|
|
2719
|
-
# bearer serves the callback socket and the proxy alike.
|
|
2733
|
+
# bearer serves the callback socket and the proxy alike. The attempt spends
|
|
2734
|
+
# its own provider's slot, and a `host_key` Directive the store's alone.
|
|
2720
2735
|
proxy_attempt = await stack.enter_async_context(
|
|
2721
2736
|
self._llm_proxy.attempt(
|
|
2722
2737
|
directive_id=facts.directive_id,
|
|
@@ -2725,6 +2740,11 @@ class RunnerRalphActivities:
|
|
|
2725
2740
|
work_record_id=_optional_uuid(work_record_id),
|
|
2726
2741
|
token=callback_env.get(CALLBACK_TOKEN_ENV),
|
|
2727
2742
|
reserve_max_tokens=reserve_max_tokens,
|
|
2743
|
+
provider=provider_for(state.cli_kind),
|
|
2744
|
+
host_key_only=(
|
|
2745
|
+
access is not None and access.kind == LlmAccessKind.HOST_KEY
|
|
2746
|
+
),
|
|
2747
|
+
delivered_only=access is None and not self._personal_runner(),
|
|
2728
2748
|
)
|
|
2729
2749
|
)
|
|
2730
2750
|
callback_env.update(proxy_attempt.env(state.cli_kind))
|
|
@@ -3081,8 +3101,6 @@ class RunnerRalphActivities:
|
|
|
3081
3101
|
self,
|
|
3082
3102
|
work_record_id: str,
|
|
3083
3103
|
state: _RuntimeContextState,
|
|
3084
|
-
*,
|
|
3085
|
-
funding_party: str | None = None,
|
|
3086
3104
|
) -> DirectiveCredentials | None:
|
|
3087
3105
|
"""Resolve this Contract's Credential References, or fail the Directive closed.
|
|
3088
3106
|
|
|
@@ -3100,7 +3118,6 @@ class RunnerRalphActivities:
|
|
|
3100
3118
|
return resolver.resolve(
|
|
3101
3119
|
contract_id=state.contract_id,
|
|
3102
3120
|
manifest=state.credential_references,
|
|
3103
|
-
funding_party=funding_party,
|
|
3104
3121
|
)
|
|
3105
3122
|
except UnresolvableCredentialReferenceError as error:
|
|
3106
3123
|
await self._fastapi_client.append_evidence(
|
|
@@ -3111,11 +3128,15 @@ class RunnerRalphActivities:
|
|
|
3111
3128
|
raise
|
|
3112
3129
|
|
|
3113
3130
|
async def _directive_auth_mode(
|
|
3114
|
-
self, work_record_id: str, state: _RuntimeContextState
|
|
3131
|
+
self, work_record_id: str, state: _RuntimeContextState, access: LlmAccess | None = None
|
|
3115
3132
|
) -> AuthMode:
|
|
3116
3133
|
"""This Directive's auth mode, or a non-retryable refusal with Evidence naming the
|
|
3117
3134
|
rule (local-agents 04).
|
|
3118
3135
|
|
|
3136
|
+
``access`` is the LLM access the control plane routed the Directive with (rev 3):
|
|
3137
|
+
the Runner runs on exactly that, or refuses and never falls back to other access.
|
|
3138
|
+
None, from a control plane below contracts 3.25, keeps the Runner's own choice.
|
|
3139
|
+
|
|
3119
3140
|
Decided from what this process can check -- the host party it was registered with,
|
|
3120
3141
|
whether a key is present for the Contract, whether the Contract runs under its own
|
|
3121
3142
|
uid here and whether its harness root holds a login (QTS-1450) -- never from the
|
|
@@ -3129,6 +3150,15 @@ class RunnerRalphActivities:
|
|
|
3129
3150
|
host_party = self._routing_identity.host_party if self._routing_identity else None
|
|
3130
3151
|
contract_uid = self._runs_under_contract_uid(state)
|
|
3131
3152
|
try:
|
|
3153
|
+
if access is not None:
|
|
3154
|
+
return require_named_access(
|
|
3155
|
+
access,
|
|
3156
|
+
host_party=host_party,
|
|
3157
|
+
host_key_held=self._host_key_held(state),
|
|
3158
|
+
subscription=self._subscription_state(state),
|
|
3159
|
+
subscription_right=self._subscription_right(state),
|
|
3160
|
+
runtime_modes=getattr(runtime, "auth_modes", frozenset({AuthMode.API_KEY})),
|
|
3161
|
+
)
|
|
3132
3162
|
return choose_auth_mode(
|
|
3133
3163
|
host_party=host_party,
|
|
3134
3164
|
key_present=self._api_key_present(state, runtime),
|
|
@@ -3148,11 +3178,16 @@ class RunnerRalphActivities:
|
|
|
3148
3178
|
source=AGENT_RUNTIME_EVIDENCE_SOURCE,
|
|
3149
3179
|
actor=self._actor(),
|
|
3150
3180
|
payload={
|
|
3151
|
-
"event":
|
|
3181
|
+
"event": (
|
|
3182
|
+
"directive.auth_mode_refused"
|
|
3183
|
+
if access is None
|
|
3184
|
+
else "directive.llm_access_refused"
|
|
3185
|
+
),
|
|
3152
3186
|
"rule": refusal.rule,
|
|
3153
3187
|
"cli_kind": state.cli_kind,
|
|
3154
3188
|
"host_party": host_party,
|
|
3155
3189
|
"contract_id": state.contract_id,
|
|
3190
|
+
**({} if access is None else {"llm_access": access.kind}),
|
|
3156
3191
|
},
|
|
3157
3192
|
)
|
|
3158
3193
|
|
|
@@ -3167,16 +3202,60 @@ class RunnerRalphActivities:
|
|
|
3167
3202
|
)
|
|
3168
3203
|
|
|
3169
3204
|
def _api_key_present(self, state: _RuntimeContextState, runtime: AgentRuntime | None) -> bool:
|
|
3170
|
-
"""A
|
|
3205
|
+
"""A slot the attempt's provider would resolve: delivered for the Contract, or the
|
|
3206
|
+
host store's key (rev 3); or a key the host configured on the runtime itself."""
|
|
3171
3207
|
|
|
3172
3208
|
if bool(getattr(runtime, "host_api_key", False)):
|
|
3173
3209
|
return True
|
|
3174
|
-
|
|
3210
|
+
return self._llm_proxy is not None and self._llm_proxy.slots.holds(
|
|
3211
|
+
_optional_uuid(state.contract_id),
|
|
3212
|
+
provider_for(state.cli_kind),
|
|
3213
|
+
delivered_only=not self._personal_runner(),
|
|
3214
|
+
)
|
|
3215
|
+
|
|
3216
|
+
def _personal_runner(self) -> bool:
|
|
3217
|
+
"""Hosted by its person. Only such a Runner spends its store's key on a Directive
|
|
3218
|
+
that names no access (a control plane below 3.25): a pool Runner's key never
|
|
3219
|
+
reached the proxy before, and that control plane caps none of its spend."""
|
|
3220
|
+
|
|
3221
|
+
return self._routing_identity is not None and self._routing_identity.host_party == "user"
|
|
3222
|
+
|
|
3223
|
+
def _host_key_held(self, state: _RuntimeContextState) -> bool:
|
|
3224
|
+
provider = provider_for(state.cli_kind)
|
|
3175
3225
|
return (
|
|
3176
3226
|
self._llm_proxy is not None
|
|
3177
|
-
and
|
|
3178
|
-
and self._llm_proxy.slots.
|
|
3227
|
+
and provider is not None
|
|
3228
|
+
and self._llm_proxy.slots.holds_host(provider.value)
|
|
3229
|
+
)
|
|
3230
|
+
|
|
3231
|
+
def _subscription_right(self, state: _RuntimeContextState) -> SubscriptionRight | None:
|
|
3232
|
+
"""What the last ack said of this Contract's own subscription; ``no_right`` for a
|
|
3233
|
+
Contract it did not name, since nothing then allowed one here."""
|
|
3234
|
+
|
|
3235
|
+
terms = self._llm_contracts() if self._llm_contracts is not None else None
|
|
3236
|
+
if terms is None:
|
|
3237
|
+
return None
|
|
3238
|
+
contract_id = _optional_uuid(state.contract_id)
|
|
3239
|
+
return next(
|
|
3240
|
+
(term.subscription for term in terms if term.contract_id == contract_id),
|
|
3241
|
+
SubscriptionRight.NO_RIGHT,
|
|
3242
|
+
)
|
|
3243
|
+
|
|
3244
|
+
def _subscription_state(self, state: _RuntimeContextState) -> SubscriptionState:
|
|
3245
|
+
if self._contract_isolation is None or not state.contract_id:
|
|
3246
|
+
return SubscriptionState.NONE
|
|
3247
|
+
windows = (
|
|
3248
|
+
self._usage_windows.windows(state.contract_id, state.cli_kind)
|
|
3249
|
+
if self._usage_windows is not None
|
|
3250
|
+
else None
|
|
3179
3251
|
)
|
|
3252
|
+
return subscription_report(
|
|
3253
|
+
self._contract_isolation,
|
|
3254
|
+
state.contract_id,
|
|
3255
|
+
state.cli_kind,
|
|
3256
|
+
windows=windows,
|
|
3257
|
+
now=datetime.now(UTC),
|
|
3258
|
+
).state
|
|
3180
3259
|
|
|
3181
3260
|
async def _report_login_residue(self, work_record_id: str, state: _RuntimeContextState) -> None:
|
|
3182
3261
|
"""On a shared Runner that cannot run a subscription, say once that a Contract's
|
|
@@ -15,6 +15,12 @@ login is the Contract's alone:
|
|
|
15
15
|
The platform decides who may use a subscription at the door (the Subscription Principal
|
|
16
16
|
and the Organisation's opt-in, QTS-1350); this module is the backstop, so it decides from
|
|
17
17
|
what the Runner itself can check and never from the payload.
|
|
18
|
+
|
|
19
|
+
From contracts 3.25 the platform names the access in the Directive's routing
|
|
20
|
+
(``DirectiveRouting.llm_access``, Spec: Runners rev 3). :func:`require_named_access` then
|
|
21
|
+
replaces the choice: the Runner runs on exactly the named access or refuses, and never
|
|
22
|
+
falls back to another. A Directive without it, from a control plane on an older pin, keeps
|
|
23
|
+
:func:`choose_auth_mode`.
|
|
18
24
|
"""
|
|
19
25
|
|
|
20
26
|
from __future__ import annotations
|
|
@@ -23,16 +29,26 @@ from collections.abc import Collection
|
|
|
23
29
|
from typing import Final
|
|
24
30
|
|
|
25
31
|
from agentic_runner.workers.agent_runtime import AuthMode
|
|
32
|
+
from agentic_runner_contracts.llm_access import LlmAccess, LlmAccessKind
|
|
33
|
+
from agentic_runner_contracts.runner_registration import SubscriptionRight, SubscriptionState
|
|
26
34
|
|
|
27
35
|
__all__ = [
|
|
28
36
|
"NO_RUNTIME_SUBSCRIPTION_RULE",
|
|
37
|
+
"HOST_KEY_NOT_HELD_RULE",
|
|
38
|
+
"LLM_ACCESS_UNKNOWN_RULE",
|
|
39
|
+
"LLM_KEY_UNSUPPORTED_RULE",
|
|
29
40
|
"SETUP_TOKEN_REFUSED_RULE",
|
|
41
|
+
"SUBSCRIPTION_NOT_ACKNOWLEDGED_RULE",
|
|
42
|
+
"SUBSCRIPTION_NOT_SIGNED_IN_RULE",
|
|
43
|
+
"SUBSCRIPTION_NO_RIGHT_RULE",
|
|
44
|
+
"SUBSCRIPTION_OFF_PERSONAL_RUNNER_RULE",
|
|
30
45
|
"SHARED_HOST_PARTIES",
|
|
31
46
|
"SIGN_IN_NEEDS_CONTRACT_UID_RULE",
|
|
32
47
|
"SUBSCRIPTION_NEEDS_CONTRACT_UID_RULE",
|
|
33
48
|
"SUBSCRIPTION_NEEDS_HARNESS_LOGIN_RULE",
|
|
34
49
|
"AuthModeRefusedError",
|
|
35
50
|
"choose_auth_mode",
|
|
51
|
+
"require_named_access",
|
|
36
52
|
"is_setup_token",
|
|
37
53
|
"is_shared_runner",
|
|
38
54
|
"runs_subscriptions",
|
|
@@ -46,6 +62,14 @@ SUBSCRIPTION_NEEDS_HARNESS_LOGIN_RULE: Final[str] = "subscription_needs_harness_
|
|
|
46
62
|
NO_RUNTIME_SUBSCRIPTION_RULE: Final[str] = "runtime_has_no_subscription_mode"
|
|
47
63
|
SIGN_IN_NEEDS_CONTRACT_UID_RULE: Final[str] = "sign_in_needs_contract_uid"
|
|
48
64
|
SETUP_TOKEN_REFUSED_RULE: Final[str] = "setup_token_refused"
|
|
65
|
+
# A Directive whose named `llm_access` this Runner no longer holds (rev 3).
|
|
66
|
+
HOST_KEY_NOT_HELD_RULE: Final[str] = "host_key_not_held"
|
|
67
|
+
SUBSCRIPTION_NOT_SIGNED_IN_RULE: Final[str] = "subscription_not_signed_in"
|
|
68
|
+
SUBSCRIPTION_OFF_PERSONAL_RUNNER_RULE: Final[str] = "subscription_off_personal_runner"
|
|
69
|
+
SUBSCRIPTION_NO_RIGHT_RULE: Final[str] = "subscription_no_right"
|
|
70
|
+
SUBSCRIPTION_NOT_ACKNOWLEDGED_RULE: Final[str] = "subscription_not_acknowledged"
|
|
71
|
+
LLM_KEY_UNSUPPORTED_RULE: Final[str] = "llm_key_unsupported"
|
|
72
|
+
LLM_ACCESS_UNKNOWN_RULE: Final[str] = "llm_access_unknown"
|
|
49
73
|
# `claude setup-token` mints an OAuth bearer under this prefix; an Anthropic API key is
|
|
50
74
|
# `sk-ant-api…`. The sealed wire carries no mode, so the value's own shape is what the
|
|
51
75
|
# Runner can refuse on.
|
|
@@ -122,5 +146,79 @@ def choose_auth_mode(
|
|
|
122
146
|
return AuthMode.SUBSCRIPTION
|
|
123
147
|
|
|
124
148
|
|
|
149
|
+
def require_named_access(
|
|
150
|
+
access: LlmAccess,
|
|
151
|
+
*,
|
|
152
|
+
host_party: str | None,
|
|
153
|
+
host_key_held: bool,
|
|
154
|
+
subscription: SubscriptionState,
|
|
155
|
+
subscription_right: SubscriptionRight | None,
|
|
156
|
+
runtime_modes: Collection[AuthMode],
|
|
157
|
+
) -> AuthMode:
|
|
158
|
+
"""The mode the named access runs in, or a refusal naming what is missing.
|
|
159
|
+
|
|
160
|
+
``host_key_held`` is the provider's key in this Runner's own store, serving in the
|
|
161
|
+
proxy; ``subscription`` the Contract's harness root for the Directive's kind. A
|
|
162
|
+
subscription runs only on its person's own Runner (rev 3: never on a pool Runner),
|
|
163
|
+
and only while it is signed in, unexpired and not at its usage limit.
|
|
164
|
+
|
|
165
|
+
``subscription_right`` is what the last heartbeat ack said of the Contract's own
|
|
166
|
+
subscription (``llm_contracts``), so a misrouted Directive is refused here as well as
|
|
167
|
+
at the platform's routing (QTS-1349 controls 4 and 7, QTS-1751). ``None`` while no ack
|
|
168
|
+
has named the Contracts -- a control plane below 3.25 -- which leaves routing the only
|
|
169
|
+
check, as before.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
if access.kind == LlmAccessKind.HOST_KEY:
|
|
173
|
+
if not host_key_held:
|
|
174
|
+
raise AuthModeRefusedError(
|
|
175
|
+
HOST_KEY_NOT_HELD_RULE,
|
|
176
|
+
"this Directive was routed to the provider's key in this Runner's own store, "
|
|
177
|
+
"and the store no longer holds a key the provider accepts",
|
|
178
|
+
)
|
|
179
|
+
return AuthMode.API_KEY
|
|
180
|
+
if access.kind == LlmAccessKind.SUBSCRIPTION:
|
|
181
|
+
if host_party != "user":
|
|
182
|
+
raise AuthModeRefusedError(
|
|
183
|
+
SUBSCRIPTION_OFF_PERSONAL_RUNNER_RULE,
|
|
184
|
+
f"this Runner is hosted by {host_party!r}; a subscription runs only on its "
|
|
185
|
+
"person's own Runner",
|
|
186
|
+
)
|
|
187
|
+
if subscription_right == SubscriptionRight.NO_RIGHT:
|
|
188
|
+
raise AuthModeRefusedError(
|
|
189
|
+
SUBSCRIPTION_NO_RIGHT_RULE,
|
|
190
|
+
"this Directive was routed to the person's subscription, and the control plane "
|
|
191
|
+
"last told this Runner that the Contract does not allow one",
|
|
192
|
+
)
|
|
193
|
+
if subscription_right == SubscriptionRight.NOT_ACKNOWLEDGED:
|
|
194
|
+
raise AuthModeRefusedError(
|
|
195
|
+
SUBSCRIPTION_NOT_ACKNOWLEDGED_RULE,
|
|
196
|
+
"this Directive was routed to the person's subscription, and the control plane "
|
|
197
|
+
"last told this Runner that the person has not acknowledged the subscription "
|
|
198
|
+
"terms",
|
|
199
|
+
)
|
|
200
|
+
if AuthMode.SUBSCRIPTION not in runtime_modes:
|
|
201
|
+
raise AuthModeRefusedError(
|
|
202
|
+
NO_RUNTIME_SUBSCRIPTION_RULE,
|
|
203
|
+
"this Runner's runtime for the Directive's CLI kind runs on API keys only",
|
|
204
|
+
)
|
|
205
|
+
if subscription != SubscriptionState.SIGNED_IN:
|
|
206
|
+
raise AuthModeRefusedError(
|
|
207
|
+
SUBSCRIPTION_NOT_SIGNED_IN_RULE,
|
|
208
|
+
f"this Directive was routed to the person's subscription, and its sign-in on "
|
|
209
|
+
f"this Runner is {subscription.value}; run `agentic-runner sign-in` here",
|
|
210
|
+
)
|
|
211
|
+
return AuthMode.SUBSCRIPTION
|
|
212
|
+
if access.kind == LlmAccessKind.LLM_KEY:
|
|
213
|
+
raise AuthModeRefusedError(
|
|
214
|
+
LLM_KEY_UNSUPPORTED_RULE,
|
|
215
|
+
"this Directive was routed to one of the pool's LLM Keys, which this Runner "
|
|
216
|
+
"release cannot open; update the Runner",
|
|
217
|
+
)
|
|
218
|
+
raise AuthModeRefusedError(
|
|
219
|
+
LLM_ACCESS_UNKNOWN_RULE, f"this Runner does not know the LLM access {access.kind!r}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
125
223
|
def is_setup_token(value: str) -> bool:
|
|
126
224
|
return value.strip().startswith(_SETUP_TOKEN_PREFIX)
|
|
@@ -15,9 +15,10 @@ the Organisation's cap.
|
|
|
15
15
|
Runner (PRD issue 47, ``workstation.py``): one login agent per Organisation, managed by
|
|
16
16
|
the user who hosts it, with no ``sudo`` anywhere.
|
|
17
17
|
|
|
18
|
-
``sign-in
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
``sign-in [claude|codex]`` gives a person's own Runner their LLM access: their subscription,
|
|
19
|
+
through the vendor's own sign-in in this terminal, or an API key stored in the Runner's
|
|
20
|
+
own store (``sign_in.py``, Spec: Runners rev 3). No ids, and no code or token passes
|
|
21
|
+
through the control plane.
|
|
21
22
|
|
|
22
23
|
``skill install | print`` hands the setup skill to the person's own Claude Code or Codex
|
|
23
24
|
(``skill.py``, console-v2 issue 21).
|
|
@@ -37,10 +38,11 @@ import asyncio
|
|
|
37
38
|
import getpass
|
|
38
39
|
import json
|
|
39
40
|
import os
|
|
41
|
+
import re
|
|
42
|
+
import socket
|
|
40
43
|
import sys
|
|
41
44
|
from collections.abc import Sequence
|
|
42
45
|
from pathlib import Path
|
|
43
|
-
from uuid import UUID
|
|
44
46
|
|
|
45
47
|
from agentic_runner import __version__ as runner_version
|
|
46
48
|
from agentic_runner import service, skill, workstation
|
|
@@ -66,11 +68,14 @@ from agentic_runner.service import (
|
|
|
66
68
|
parse_tags,
|
|
67
69
|
registration_token_from_env,
|
|
68
70
|
)
|
|
71
|
+
from agentic_runner.sign_in import AGENT_RUNTIMES, KeyStore
|
|
69
72
|
from agentic_runner.workers.claude_sign_in import OAUTH_TOKEN, SIGN_IN_METHODS
|
|
70
73
|
from agentic_runner_contracts import __version__ as contracts_version
|
|
71
74
|
from agentic_runner_contracts.runner_registration import (
|
|
72
75
|
HostAttestation,
|
|
73
76
|
InstallChannel,
|
|
77
|
+
InstallTarget,
|
|
78
|
+
RunnerInstall,
|
|
74
79
|
SessionKind,
|
|
75
80
|
StoreKind,
|
|
76
81
|
)
|
|
@@ -212,17 +217,26 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
212
217
|
|
|
213
218
|
sign_in = subcommands.add_parser(
|
|
214
219
|
"sign-in",
|
|
215
|
-
help="
|
|
220
|
+
help="give your own Runner your subscription or an API key, for Claude Code or Codex",
|
|
221
|
+
)
|
|
222
|
+
sign_in.add_argument(
|
|
223
|
+
"runtime",
|
|
224
|
+
nargs="?",
|
|
225
|
+
choices=tuple(AGENT_RUNTIMES),
|
|
226
|
+
help="one Agent Runtime; omitted asks about each one this Runner serves",
|
|
216
227
|
)
|
|
217
|
-
sign_in.add_argument("--contract", required=True, help="the Contract's id")
|
|
218
228
|
sign_in.add_argument(
|
|
219
229
|
"--method",
|
|
220
230
|
default=OAUTH_TOKEN,
|
|
221
231
|
choices=SIGN_IN_METHODS,
|
|
222
|
-
help="oauth_token: a long-lived token (claude setup-token);
|
|
223
|
-
"login (claude auth login --claudeai)",
|
|
232
|
+
help="Claude subscriptions only. oauth_token: a long-lived token (claude setup-token); "
|
|
233
|
+
"claude_ai: a Claude.ai login (claude auth login --claudeai)",
|
|
234
|
+
)
|
|
235
|
+
sign_in.add_argument(
|
|
236
|
+
"--org",
|
|
237
|
+
default=None,
|
|
238
|
+
help="the workstation Organisation, when this workstation runs more than one",
|
|
224
239
|
)
|
|
225
|
-
sign_in.add_argument("--org", default=None, help="a workstation Organisation (issue 47)")
|
|
226
240
|
sign_in.add_argument("--root", default=None, type=Path, help=argparse.SUPPRESS)
|
|
227
241
|
|
|
228
242
|
annotate = subcommands.add_parser("annotate", help="attach an annotation to this Work Record")
|
|
@@ -313,7 +327,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
313
327
|
if arguments.command == "run":
|
|
314
328
|
if arguments.org:
|
|
315
329
|
return workstation.run_process(_org_paths(arguments), login_agent=arguments.login_agent)
|
|
316
|
-
return asyncio.run(service.run(attestation=_image_attestation()))
|
|
330
|
+
return asyncio.run(service.run(attestation=_image_attestation(), install=_run_install()))
|
|
317
331
|
if arguments.command in {"install", "start", "stop", "credential"}:
|
|
318
332
|
return _workstation(arguments)
|
|
319
333
|
if arguments.command == "recipient-key":
|
|
@@ -390,29 +404,89 @@ def _image_attestation() -> HostAttestation:
|
|
|
390
404
|
else InstallChannel.MANUAL
|
|
391
405
|
),
|
|
392
406
|
session=SessionKind.CONTAINER,
|
|
393
|
-
store
|
|
407
|
+
# Always a store: the state directory's own, where `sign-in` keeps a key, read
|
|
408
|
+
# before any mounted Secret (`service.build_credential_resolver`).
|
|
409
|
+
store=StoreKind.FILE,
|
|
394
410
|
path=os.environ.get("PATH", ""),
|
|
395
411
|
)
|
|
396
412
|
|
|
397
413
|
|
|
398
|
-
def
|
|
414
|
+
def _run_install() -> RunnerInstall:
|
|
415
|
+
"""Where a command reaches this ``run``: Helm with the pod name, Docker with the
|
|
416
|
+
container id, and anything else as a workstation, whose hostname can name its person
|
|
417
|
+
and is never reported (rev 3).
|
|
418
|
+
|
|
419
|
+
Docker's hostname is sent only in the container-id shape Docker gives it: with
|
|
420
|
+
``--network host`` it is the machine's own name, and with ``--hostname`` whatever the
|
|
421
|
+
operator chose. A name the schema refuses is sent as none rather than stop the start.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
if os.environ.get("KUBERNETES_SERVICE_HOST"):
|
|
425
|
+
target, hostname = InstallTarget.HELM, socket.gethostname()
|
|
426
|
+
elif _in_docker():
|
|
427
|
+
target, hostname = InstallTarget.DOCKER, socket.gethostname()
|
|
428
|
+
if not _CONTAINER_ID.fullmatch(hostname):
|
|
429
|
+
hostname = ""
|
|
430
|
+
else:
|
|
431
|
+
return RunnerInstall(target=InstallTarget.WORKSTATION)
|
|
399
432
|
try:
|
|
400
|
-
|
|
433
|
+
return RunnerInstall(target=target, hostname=hostname or None)
|
|
401
434
|
except ValueError:
|
|
402
|
-
|
|
403
|
-
|
|
435
|
+
return RunnerInstall(target=target)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
_CONTAINER_ID = re.compile(r"[0-9a-f]{12}")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _in_docker() -> bool:
|
|
442
|
+
return Path("/.dockerenv").exists()
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _in_container() -> bool:
|
|
446
|
+
return bool(os.environ.get("KUBERNETES_SERVICE_HOST")) or _in_docker()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _sign_in(arguments: argparse.Namespace) -> int:
|
|
404
450
|
if not (os.isatty(0) and os.isatty(1)):
|
|
405
451
|
print(
|
|
406
452
|
"sign-in is interactive: run it in a terminal (`kubectl exec -it`, `docker exec -it`)",
|
|
407
453
|
file=sys.stderr,
|
|
408
454
|
)
|
|
409
455
|
return 2
|
|
410
|
-
|
|
411
|
-
|
|
456
|
+
store: KeyStore
|
|
457
|
+
if arguments.org or not _in_container():
|
|
458
|
+
try:
|
|
459
|
+
paths = _sign_in_org(arguments)
|
|
460
|
+
except ValueError as error:
|
|
461
|
+
print(error, file=sys.stderr)
|
|
462
|
+
return 2
|
|
412
463
|
os.environ.update(
|
|
413
464
|
workstation.run_environment(paths, workstation.WorkstationSettings.load(paths))
|
|
414
465
|
)
|
|
415
|
-
|
|
466
|
+
store = open_workstation_store(paths.org, fallback=paths.credentials)
|
|
467
|
+
else:
|
|
468
|
+
store = service.own_store(load_config())
|
|
469
|
+
return asyncio.run(service.sign_in(arguments.runtime, store=store, method=arguments.method))
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _sign_in_org(arguments: argparse.Namespace) -> workstation.OrgPaths:
|
|
473
|
+
"""``--org``, or the one Organisation this workstation runs -- no id to type."""
|
|
474
|
+
|
|
475
|
+
if arguments.org:
|
|
476
|
+
return _org_paths(arguments)
|
|
477
|
+
root = arguments.root or workstation.default_root()
|
|
478
|
+
installed = (
|
|
479
|
+
sorted(path.name for path in root.iterdir() if (path / "workstation.json").is_file())
|
|
480
|
+
if root.is_dir()
|
|
481
|
+
else []
|
|
482
|
+
)
|
|
483
|
+
if len(installed) != 1:
|
|
484
|
+
raise ValueError(
|
|
485
|
+
"no workstation Runner is installed here"
|
|
486
|
+
if not installed
|
|
487
|
+
else f"this workstation runs {', '.join(installed)}: name one with --org"
|
|
488
|
+
)
|
|
489
|
+
return workstation.OrgPaths(root=root, org=installed[0])
|
|
416
490
|
|
|
417
491
|
|
|
418
492
|
def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
|