agentic-runner 3.25.1__tar.gz → 3.26.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.25.1 → agentic_runner-3.26.1}/PKG-INFO +2 -2
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/pyproject.toml +1 -1
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/activities.py +108 -22
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/auth_mode.py +23 -10
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/cli.py +25 -2
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/llm_proxy.py +7 -1
- agentic_runner-3.26.1/src/agentic_runner/pool_key.py +175 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/service.py +67 -11
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/sign_in.py +42 -35
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/testing/control_plane.py +6 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/fastapi_client.py +14 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workstation.py +11 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/.gitignore +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/LICENSE +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/README.md +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/credentials.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/heartbeat_link.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/launch.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/llm_contracts.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/oauth_connectors.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/runner_limit.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/sealed_box.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/skill.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/testing/test_conformance.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/usage_windows.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/_runtime_support.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/acp_runtime.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/agent_runtime.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/claude_sign_in.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/harness_logins.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/harness_outcome.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/settings.py +0 -0
- {agentic_runner-3.25.1 → agentic_runner-3.26.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.26.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.26.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.26.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.
|
|
@@ -36,6 +36,7 @@ from pathlib import Path
|
|
|
36
36
|
from typing import Any, NoReturn, Protocol
|
|
37
37
|
from uuid import UUID
|
|
38
38
|
|
|
39
|
+
from pydantic import ValidationError
|
|
39
40
|
from temporalio import activity
|
|
40
41
|
from temporalio.exceptions import ApplicationError
|
|
41
42
|
|
|
@@ -46,6 +47,8 @@ from agentic_runner.attempts import (
|
|
|
46
47
|
refuse_if_prior_attempt_alive,
|
|
47
48
|
)
|
|
48
49
|
from agentic_runner.auth_mode import (
|
|
50
|
+
LLM_KEY_RELEASE_REFUSED_RULE,
|
|
51
|
+
LLM_KEY_UNOPENABLE_RULE,
|
|
49
52
|
AuthModeRefusedError,
|
|
50
53
|
choose_auth_mode,
|
|
51
54
|
is_shared_runner,
|
|
@@ -115,7 +118,7 @@ from agentic_runner.integrations.git.workspace import (
|
|
|
115
118
|
contract_workspace_path,
|
|
116
119
|
)
|
|
117
120
|
from agentic_runner.integrations.github.auth import GitHubAppAuthenticationError
|
|
118
|
-
from agentic_runner.llm_proxy import LlmProxy
|
|
121
|
+
from agentic_runner.llm_proxy import CredentialSlot, LlmProxy
|
|
119
122
|
from agentic_runner.mcp import (
|
|
120
123
|
MCP_ASSEMBLY_SOURCE,
|
|
121
124
|
McpPlan,
|
|
@@ -126,6 +129,7 @@ from agentic_runner.mcp import (
|
|
|
126
129
|
plan_mcp,
|
|
127
130
|
)
|
|
128
131
|
from agentic_runner.message_store import MessageStore, WorkflowSignaller
|
|
132
|
+
from agentic_runner.pool_key import LlmKeyOpener, LlmKeyUnopenableError
|
|
129
133
|
from agentic_runner.registration import RunnerRegistrationError
|
|
130
134
|
from agentic_runner.runner_limit import (
|
|
131
135
|
EMPTY_RUNNER_LIMIT,
|
|
@@ -159,6 +163,7 @@ from agentic_runner.workers.contract_isolation import (
|
|
|
159
163
|
from agentic_runner.workers.fastapi_client import (
|
|
160
164
|
DirectiveTokenSource,
|
|
161
165
|
WorkerFastApiClientError,
|
|
166
|
+
WorkerFastApiClientRejectionError,
|
|
162
167
|
directive_token,
|
|
163
168
|
)
|
|
164
169
|
from agentic_runner.workers.harness_logins import (
|
|
@@ -283,6 +288,7 @@ from agentic_runner_contracts.grants import (
|
|
|
283
288
|
)
|
|
284
289
|
from agentic_runner_contracts.grants.seam import VerbDecision, decide_verb
|
|
285
290
|
from agentic_runner_contracts.llm_access import LlmAccess, LlmAccessKind, provider_for
|
|
291
|
+
from agentic_runner_contracts.llm_key import LlmKeyRelease, LlmKeyReleaseRequest
|
|
286
292
|
from agentic_runner_contracts.questions import WORK_ASK_VERB
|
|
287
293
|
from agentic_runner_contracts.redaction import redact_secret_like_text
|
|
288
294
|
from agentic_runner_contracts.routing import (
|
|
@@ -556,6 +562,11 @@ class RunnerRalphFastApiClient(Protocol):
|
|
|
556
562
|
) -> dict[str, Any]:
|
|
557
563
|
"""Read one Directive's metered token aggregate (PRD issue 13)."""
|
|
558
564
|
|
|
565
|
+
async def release_llm_key(
|
|
566
|
+
self, work_record_id: str, llm_key_id: str, payload: Mapping[str, Any]
|
|
567
|
+
) -> dict[str, Any]:
|
|
568
|
+
"""The sealed shares of the pool's LLM Key a Directive was routed to (rev 3)."""
|
|
569
|
+
|
|
559
570
|
async def report_harness_usage(self, payload: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
560
571
|
"""One Usage Record per model off a harness's own JSON (PRD issue 31, 17 A9)."""
|
|
561
572
|
|
|
@@ -1725,8 +1736,12 @@ class RunnerRalphActivities:
|
|
|
1725
1736
|
usage_windows: UsageWindows | None = None,
|
|
1726
1737
|
runner_limit: AppliedRunnerLimit | None = None,
|
|
1727
1738
|
llm_contracts: Callable[[], Sequence[ContractLlmTerms] | None] | None = None,
|
|
1739
|
+
llm_keys: LlmKeyOpener | None = None,
|
|
1728
1740
|
) -> None:
|
|
1729
1741
|
self._fastapi_client = fastapi_client
|
|
1742
|
+
# Opens the pool's LLM Keys with the Pool Key (Spec: Runners rev 3). None, or one
|
|
1743
|
+
# holding no Pool Key, fails every `llm_key` Directive closed.
|
|
1744
|
+
self._llm_keys = llm_keys
|
|
1730
1745
|
# What this Runner's host denies every Agent on it, checked at start (ADR-0022
|
|
1731
1746
|
# §52-§55). Empty by default, which narrows nothing.
|
|
1732
1747
|
self._runner_limit = runner_limit or EMPTY_RUNNER_LIMIT
|
|
@@ -2667,12 +2682,18 @@ class RunnerRalphActivities:
|
|
|
2667
2682
|
|
|
2668
2683
|
access = routing.llm_access if routing is not None else None
|
|
2669
2684
|
auth_mode = await self._directive_auth_mode(work_record_id, state, access)
|
|
2685
|
+
attempt_directive_id = directive_id(
|
|
2686
|
+
work_record_id=work_record_id, directive_number=directive_number
|
|
2687
|
+
)
|
|
2688
|
+
llm_key = (
|
|
2689
|
+
await self._release_llm_key(work_record_id, state, access, attempt_directive_id)
|
|
2690
|
+
if access is not None and access.kind == LlmAccessKind.LLM_KEY
|
|
2691
|
+
else None
|
|
2692
|
+
)
|
|
2670
2693
|
await self._report_login_residue(work_record_id, state)
|
|
2671
2694
|
facts = AttemptFacts(
|
|
2672
2695
|
work_record_id=work_record_id,
|
|
2673
|
-
directive_id=
|
|
2674
|
-
work_record_id=work_record_id, directive_number=directive_number
|
|
2675
|
-
),
|
|
2696
|
+
directive_id=attempt_directive_id,
|
|
2676
2697
|
contract_id=contract_path_segment(state.contract_id),
|
|
2677
2698
|
agent_id=state.agent_id or "",
|
|
2678
2699
|
persona=state.persona_slug or "",
|
|
@@ -2731,7 +2752,8 @@ class RunnerRalphActivities:
|
|
|
2731
2752
|
# API-key mode only (issue 43): a subscription Directive never has a slot
|
|
2732
2753
|
# to resolve, and issue 31 meters it from the harness's own output. One
|
|
2733
2754
|
# bearer serves the callback socket and the proxy alike. The attempt spends
|
|
2734
|
-
# its own provider's slot,
|
|
2755
|
+
# its own provider's slot, a `host_key` Directive the store's alone, and an
|
|
2756
|
+
# `llm_key` Directive the key released for it, which leaves with the attempt.
|
|
2735
2757
|
proxy_attempt = await stack.enter_async_context(
|
|
2736
2758
|
self._llm_proxy.attempt(
|
|
2737
2759
|
directive_id=facts.directive_id,
|
|
@@ -2745,6 +2767,7 @@ class RunnerRalphActivities:
|
|
|
2745
2767
|
access is not None and access.kind == LlmAccessKind.HOST_KEY
|
|
2746
2768
|
),
|
|
2747
2769
|
delivered_only=access is None and not self._personal_runner(),
|
|
2770
|
+
llm_key=llm_key,
|
|
2748
2771
|
)
|
|
2749
2772
|
)
|
|
2750
2773
|
callback_env.update(proxy_attempt.env(state.cli_kind))
|
|
@@ -3158,6 +3181,7 @@ class RunnerRalphActivities:
|
|
|
3158
3181
|
subscription=self._subscription_state(state),
|
|
3159
3182
|
subscription_right=self._subscription_right(state),
|
|
3160
3183
|
runtime_modes=getattr(runtime, "auth_modes", frozenset({AuthMode.API_KEY})),
|
|
3184
|
+
pool_key_held=self._llm_keys is not None and self._llm_keys.holds_pool_key,
|
|
3161
3185
|
)
|
|
3162
3186
|
return choose_auth_mode(
|
|
3163
3187
|
host_party=host_party,
|
|
@@ -3171,25 +3195,87 @@ class RunnerRalphActivities:
|
|
|
3171
3195
|
),
|
|
3172
3196
|
)
|
|
3173
3197
|
except AuthModeRefusedError as refusal:
|
|
3174
|
-
await
|
|
3175
|
-
|
|
3176
|
-
|
|
3198
|
+
await self._refuse_access(work_record_id, state, access, refusal)
|
|
3199
|
+
|
|
3200
|
+
async def _release_llm_key(
|
|
3201
|
+
self,
|
|
3202
|
+
work_record_id: str,
|
|
3203
|
+
state: _RuntimeContextState,
|
|
3204
|
+
access: LlmAccess,
|
|
3205
|
+
attempt_directive_id: str,
|
|
3206
|
+
) -> CredentialSlot:
|
|
3207
|
+
"""The pool's LLM Key this Directive was routed to, opened in memory (rev 3).
|
|
3208
|
+
|
|
3209
|
+
The control plane releases it only to a live Runner of the key's pool for a
|
|
3210
|
+
Directive in flight on it, which the Directive token proves. A refusal or a release
|
|
3211
|
+
that does not open fails the Directive closed, like any access the Runner no longer
|
|
3212
|
+
holds: a retry would be refused the same way.
|
|
3213
|
+
"""
|
|
3214
|
+
|
|
3215
|
+
fingerprint = self._llm_keys.fingerprint if self._llm_keys is not None else None
|
|
3216
|
+
try:
|
|
3217
|
+
# Refused already by `require_named_access`; this narrows the types.
|
|
3218
|
+
if self._llm_keys is None or fingerprint is None:
|
|
3219
|
+
raise LlmKeyUnopenableError("this Runner holds no Pool Key")
|
|
3220
|
+
payload = await self._fastapi_client.release_llm_key(
|
|
3177
3221
|
work_record_id,
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
"directive.auth_mode_refused"
|
|
3183
|
-
if access is None
|
|
3184
|
-
else "directive.llm_access_refused"
|
|
3185
|
-
),
|
|
3186
|
-
"rule": refusal.rule,
|
|
3187
|
-
"cli_kind": state.cli_kind,
|
|
3188
|
-
"host_party": host_party,
|
|
3189
|
-
"contract_id": state.contract_id,
|
|
3190
|
-
**({} if access is None else {"llm_access": access.kind}),
|
|
3191
|
-
},
|
|
3222
|
+
access.llm_key_id,
|
|
3223
|
+
LlmKeyReleaseRequest(
|
|
3224
|
+
directive_id=attempt_directive_id, pool_key_fingerprint=fingerprint
|
|
3225
|
+
).model_dump(mode="json"),
|
|
3192
3226
|
)
|
|
3227
|
+
try:
|
|
3228
|
+
release = LlmKeyRelease.model_validate(payload)
|
|
3229
|
+
except ValidationError as error:
|
|
3230
|
+
unreadable = "the control plane released an LLM Key this Runner cannot read"
|
|
3231
|
+
raise LlmKeyUnopenableError(unreadable) from error
|
|
3232
|
+
if release.llm_key_id != access.llm_key_id or release.provider != provider_for(
|
|
3233
|
+
state.cli_kind
|
|
3234
|
+
):
|
|
3235
|
+
raise LlmKeyUnopenableError(
|
|
3236
|
+
f"the control plane released LLM Key {release.llm_key_id} for "
|
|
3237
|
+
f"{release.provider.value}, "
|
|
3238
|
+
f"not {access.llm_key_id} for the Directive's {state.cli_kind}"
|
|
3239
|
+
)
|
|
3240
|
+
return self._llm_keys.open(release)
|
|
3241
|
+
except WorkerFastApiClientRejectionError as error:
|
|
3242
|
+
refusal = AuthModeRefusedError(
|
|
3243
|
+
LLM_KEY_RELEASE_REFUSED_RULE,
|
|
3244
|
+
f"the control plane refused to release LLM Key {access.llm_key_id} to this Runner "
|
|
3245
|
+
f"({error.status_code})",
|
|
3246
|
+
)
|
|
3247
|
+
except LlmKeyUnopenableError as error:
|
|
3248
|
+
refusal = AuthModeRefusedError(LLM_KEY_UNOPENABLE_RULE, str(error))
|
|
3249
|
+
await self._refuse_access(work_record_id, state, access, refusal)
|
|
3250
|
+
|
|
3251
|
+
async def _refuse_access(
|
|
3252
|
+
self,
|
|
3253
|
+
work_record_id: str,
|
|
3254
|
+
state: _RuntimeContextState,
|
|
3255
|
+
access: LlmAccess | None,
|
|
3256
|
+
refusal: AuthModeRefusedError,
|
|
3257
|
+
) -> NoReturn:
|
|
3258
|
+
await _raise_recorded(
|
|
3259
|
+
ApplicationError(str(refusal), type=refusal.rule, non_retryable=True),
|
|
3260
|
+
self._fastapi_client,
|
|
3261
|
+
work_record_id,
|
|
3262
|
+
source=AGENT_RUNTIME_EVIDENCE_SOURCE,
|
|
3263
|
+
actor=self._actor(),
|
|
3264
|
+
payload={
|
|
3265
|
+
"event": (
|
|
3266
|
+
"directive.auth_mode_refused"
|
|
3267
|
+
if access is None
|
|
3268
|
+
else "directive.llm_access_refused"
|
|
3269
|
+
),
|
|
3270
|
+
"rule": refusal.rule,
|
|
3271
|
+
"cli_kind": state.cli_kind,
|
|
3272
|
+
"host_party": (
|
|
3273
|
+
self._routing_identity.host_party if self._routing_identity else None
|
|
3274
|
+
),
|
|
3275
|
+
"contract_id": state.contract_id,
|
|
3276
|
+
**({} if access is None else {"llm_access": access.kind}),
|
|
3277
|
+
},
|
|
3278
|
+
)
|
|
3193
3279
|
|
|
3194
3280
|
def _runs_under_contract_uid(self, state: _RuntimeContextState) -> bool:
|
|
3195
3281
|
"""Whether this Directive spawns as its own Contract's uid. A Work Record with no
|
|
@@ -36,7 +36,9 @@ __all__ = [
|
|
|
36
36
|
"NO_RUNTIME_SUBSCRIPTION_RULE",
|
|
37
37
|
"HOST_KEY_NOT_HELD_RULE",
|
|
38
38
|
"LLM_ACCESS_UNKNOWN_RULE",
|
|
39
|
-
"
|
|
39
|
+
"LLM_KEY_RELEASE_REFUSED_RULE",
|
|
40
|
+
"LLM_KEY_UNOPENABLE_RULE",
|
|
41
|
+
"POOL_KEY_NOT_HELD_RULE",
|
|
40
42
|
"SETUP_TOKEN_REFUSED_RULE",
|
|
41
43
|
"SUBSCRIPTION_NOT_ACKNOWLEDGED_RULE",
|
|
42
44
|
"SUBSCRIPTION_NOT_SIGNED_IN_RULE",
|
|
@@ -68,7 +70,12 @@ SUBSCRIPTION_NOT_SIGNED_IN_RULE: Final[str] = "subscription_not_signed_in"
|
|
|
68
70
|
SUBSCRIPTION_OFF_PERSONAL_RUNNER_RULE: Final[str] = "subscription_off_personal_runner"
|
|
69
71
|
SUBSCRIPTION_NO_RIGHT_RULE: Final[str] = "subscription_no_right"
|
|
70
72
|
SUBSCRIPTION_NOT_ACKNOWLEDGED_RULE: Final[str] = "subscription_not_acknowledged"
|
|
71
|
-
|
|
73
|
+
# An `llm_key` Directive on a Runner given no Pool Key; one whose release the control plane
|
|
74
|
+
# refused; and
|
|
75
|
+
# one whose release does not open here (an old Pool Key, or a share bound to another key).
|
|
76
|
+
POOL_KEY_NOT_HELD_RULE: Final[str] = "pool_key_not_held"
|
|
77
|
+
LLM_KEY_RELEASE_REFUSED_RULE: Final[str] = "llm_key_release_refused"
|
|
78
|
+
LLM_KEY_UNOPENABLE_RULE: Final[str] = "llm_key_unopenable"
|
|
72
79
|
LLM_ACCESS_UNKNOWN_RULE: Final[str] = "llm_access_unknown"
|
|
73
80
|
# `claude setup-token` mints an OAuth bearer under this prefix; an Anthropic API key is
|
|
74
81
|
# `sk-ant-api…`. The sealed wire carries no mode, so the value's own shape is what the
|
|
@@ -154,13 +161,16 @@ def require_named_access(
|
|
|
154
161
|
subscription: SubscriptionState,
|
|
155
162
|
subscription_right: SubscriptionRight | None,
|
|
156
163
|
runtime_modes: Collection[AuthMode],
|
|
164
|
+
pool_key_held: bool,
|
|
157
165
|
) -> AuthMode:
|
|
158
166
|
"""The mode the named access runs in, or a refusal naming what is missing.
|
|
159
167
|
|
|
160
168
|
``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
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
proxy; ``subscription`` the Contract's harness root for the Directive's kind;
|
|
170
|
+
``pool_key_held`` whether this Runner was given a Pool Key, without which none of the
|
|
171
|
+
pool's LLM Keys opens here. A subscription runs only on its person's own Runner (rev 3:
|
|
172
|
+
never on a pool Runner), and only while it is signed in, unexpired and not at its
|
|
173
|
+
usage limit.
|
|
164
174
|
|
|
165
175
|
``subscription_right`` is what the last heartbeat ack said of the Contract's own
|
|
166
176
|
subscription (``llm_contracts``), so a misrouted Directive is refused here as well as
|
|
@@ -210,11 +220,14 @@ def require_named_access(
|
|
|
210
220
|
)
|
|
211
221
|
return AuthMode.SUBSCRIPTION
|
|
212
222
|
if access.kind == LlmAccessKind.LLM_KEY:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
if not pool_key_held:
|
|
224
|
+
raise AuthModeRefusedError(
|
|
225
|
+
POOL_KEY_NOT_HELD_RULE,
|
|
226
|
+
"this Directive was routed to one of the pool's LLM Keys, and this Runner "
|
|
227
|
+
"holds no Pool Key; give it the one on the Runners page as "
|
|
228
|
+
"AGENTIC_RUNNER_POOL_KEY",
|
|
229
|
+
)
|
|
230
|
+
return AuthMode.API_KEY
|
|
218
231
|
raise AuthModeRefusedError(
|
|
219
232
|
LLM_ACCESS_UNKNOWN_RULE, f"this Runner does not know the LLM access {access.kind!r}"
|
|
220
233
|
)
|
|
@@ -54,6 +54,7 @@ from agentic_runner.callback import (
|
|
|
54
54
|
)
|
|
55
55
|
from agentic_runner.config import load as load_config
|
|
56
56
|
from agentic_runner.host_store import open_workstation_store
|
|
57
|
+
from agentic_runner.pool_key import POOL_KEY_ENV, PoolKeyError
|
|
57
58
|
from agentic_runner.recipient_key_secret import ensure_recipient_key, in_cluster
|
|
58
59
|
from agentic_runner.registration import RunnerRegistrationError, can_separate_uids, load_state
|
|
59
60
|
from agentic_runner.sealed_box import RECIPIENT_KEY_FILENAME
|
|
@@ -184,6 +185,12 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
184
185
|
help="comma-separated cli_kinds served through the pinned ACP bridge (codex_cli, "
|
|
185
186
|
"claude_code); omitted keeps the previous install's, '' turns ACP off",
|
|
186
187
|
)
|
|
188
|
+
install.add_argument(
|
|
189
|
+
"--pool-key",
|
|
190
|
+
action="store_true",
|
|
191
|
+
help=f"ask for the Pool Key of this Runner's pool (or set {POOL_KEY_ENV}), to open the "
|
|
192
|
+
"pool's LLM Keys",
|
|
193
|
+
)
|
|
187
194
|
install.add_argument("--no-start", action="store_true", help="write the agent, do not start")
|
|
188
195
|
install.add_argument("--root", default=None, type=Path, help="state root (default per OS)")
|
|
189
196
|
for service_verb in ("start", "stop"):
|
|
@@ -503,6 +510,13 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
|
|
|
503
510
|
"Runner Registration Token (yours from /me/runners, or the Organisation Admin's for "
|
|
504
511
|
"an Organisation Runner): "
|
|
505
512
|
)
|
|
513
|
+
# Pasted too, never a flag value, for the token's reason. Taken out of the environment so
|
|
514
|
+
# the Runner this install starts reads it from the state directory, where it is kept.
|
|
515
|
+
pool_key = os.environ.pop(POOL_KEY_ENV, "").strip() or (
|
|
516
|
+
getpass.getpass("Pool Key (shown once on the Organisation's Runners page): ")
|
|
517
|
+
if arguments.pool_key
|
|
518
|
+
else None
|
|
519
|
+
)
|
|
506
520
|
settings = workstation.WorkstationSettings(
|
|
507
521
|
org=paths.org,
|
|
508
522
|
control_plane_url=arguments.control_plane,
|
|
@@ -515,10 +529,19 @@ def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
|
|
|
515
529
|
try:
|
|
516
530
|
lines = asyncio.run(
|
|
517
531
|
workstation.install(
|
|
518
|
-
paths,
|
|
532
|
+
paths,
|
|
533
|
+
settings,
|
|
534
|
+
registration_token=token,
|
|
535
|
+
pool_key=pool_key,
|
|
536
|
+
start_service=not arguments.no_start,
|
|
519
537
|
)
|
|
520
538
|
)
|
|
521
|
-
except (
|
|
539
|
+
except (
|
|
540
|
+
workstation.CliMissingError,
|
|
541
|
+
RunnerRegistrationError,
|
|
542
|
+
PoolKeyError,
|
|
543
|
+
RuntimeError,
|
|
544
|
+
) as error:
|
|
522
545
|
print(f"install refused: {error}", file=sys.stderr)
|
|
523
546
|
return 1
|
|
524
547
|
for line in lines:
|
|
@@ -580,6 +580,10 @@ class AttemptHandle:
|
|
|
580
580
|
host_key_only: bool = False
|
|
581
581
|
# A pool Runner's Directive that names no access (`SlotStore.resolve`).
|
|
582
582
|
delivered_only: bool = False
|
|
583
|
+
# The pool's LLM Key released for this Directive (Spec: Runners rev 3): held here and
|
|
584
|
+
# nowhere else, so it is spent by this attempt only and dropped with it. Never in the
|
|
585
|
+
# slot store, which serves every attempt and is reported on the heartbeat.
|
|
586
|
+
llm_key: CredentialSlot | None = field(default=None, repr=False)
|
|
583
587
|
|
|
584
588
|
def env(self, cli_kind: str) -> dict[str, str]:
|
|
585
589
|
return attempt_env(cli_kind, base_url=self.base_url, token=self.token)
|
|
@@ -676,6 +680,7 @@ class LlmProxy:
|
|
|
676
680
|
provider: str | None = None,
|
|
677
681
|
host_key_only: bool = False,
|
|
678
682
|
delivered_only: bool = False,
|
|
683
|
+
llm_key: CredentialSlot | None = None,
|
|
679
684
|
) -> AsyncIterator[AttemptHandle]:
|
|
680
685
|
"""Admit one Directive attempt for as long as it runs, and no longer.
|
|
681
686
|
|
|
@@ -697,6 +702,7 @@ class LlmProxy:
|
|
|
697
702
|
provider=provider,
|
|
698
703
|
host_key_only=host_key_only,
|
|
699
704
|
delivered_only=delivered_only,
|
|
705
|
+
llm_key=llm_key,
|
|
700
706
|
)
|
|
701
707
|
self._attempts[attempt_id] = handle
|
|
702
708
|
try:
|
|
@@ -738,7 +744,7 @@ class LlmProxy:
|
|
|
738
744
|
await write_json(writer, 404, {"detail": "no such proxy route"})
|
|
739
745
|
return
|
|
740
746
|
|
|
741
|
-
slot = self.slots.resolve(
|
|
747
|
+
slot = attempt.llm_key or self.slots.resolve(
|
|
742
748
|
attempt.contract_id,
|
|
743
749
|
attempt.provider,
|
|
744
750
|
host_key_only=attempt.host_key_only,
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""The pool's Pool Key on this Runner, and the LLM Keys it opens (Spec: Runners rev 3).
|
|
2
|
+
|
|
3
|
+
An Organisation's Admin creates the Pool Key in the console, which shows its private half
|
|
4
|
+
once, and gives it to every Runner of the pool at install: one chart Secret for all
|
|
5
|
+
replicas, a Docker env file, or ``agentic-runner install --pool-key`` on a workstation.
|
|
6
|
+
The Runner reads it once at start and holds it in memory.
|
|
7
|
+
|
|
8
|
+
It is kept apart from the Runner's credential store on purpose. Every value in that store
|
|
9
|
+
can be named by a Contract's credential manifest and handed to a verb seam or an MCP
|
|
10
|
+
server; the Pool Key must reach nothing but :class:`LlmKeyOpener`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import binascii
|
|
17
|
+
import os
|
|
18
|
+
from collections.abc import Mapping, MutableMapping
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Final
|
|
22
|
+
|
|
23
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
|
|
24
|
+
|
|
25
|
+
from agentic_runner.llm_proxy import LLM_SLOT_REFERENCES, CredentialSlot, LlmEndpoint
|
|
26
|
+
from agentic_runner.private_state import private_read, private_write
|
|
27
|
+
from agentic_runner.sealed_box import RecipientKeyStore, SealedCredentialError, open_sealed
|
|
28
|
+
from agentic_runner_contracts.llm_access import PROVIDER_KEY_NAMES
|
|
29
|
+
from agentic_runner_contracts.llm_key import LlmKeyRelease, join_llm_key
|
|
30
|
+
from agentic_runner_contracts.sealed_credential import key_fingerprint
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"POOL_KEY_ENV",
|
|
34
|
+
"POOL_KEY_FILENAME",
|
|
35
|
+
"LlmKeyOpener",
|
|
36
|
+
"LlmKeyUnopenableError",
|
|
37
|
+
"PoolKey",
|
|
38
|
+
"PoolKeyError",
|
|
39
|
+
"load_pool_key",
|
|
40
|
+
"parse_pool_key",
|
|
41
|
+
"save_pool_key",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
POOL_KEY_ENV: Final[str] = "AGENTIC_RUNNER_POOL_KEY"
|
|
45
|
+
# Where a workstation install keeps it, 0600 beside the Runner's identity.
|
|
46
|
+
POOL_KEY_FILENAME: Final[str] = "pool-key"
|
|
47
|
+
|
|
48
|
+
_KEY_BYTES: Final[int] = 32
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PoolKeyError(ValueError):
|
|
52
|
+
"""The Pool Key given at install is not one."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LlmKeyUnopenableError(ValueError):
|
|
56
|
+
"""A released LLM Key does not open on this Runner."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class PoolKey:
|
|
61
|
+
private_key: str = field(repr=False)
|
|
62
|
+
public_key: str
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def fingerprint(self) -> str:
|
|
66
|
+
return key_fingerprint(self.public_key)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_pool_key(value: str) -> PoolKey:
|
|
70
|
+
"""The X25519 private half the console showed, base64, as a :class:`PoolKey`."""
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
raw = base64.b64decode(value.strip(), validate=True)
|
|
74
|
+
except (binascii.Error, ValueError) as error:
|
|
75
|
+
raise PoolKeyError(f"{POOL_KEY_ENV} must be the base64 Pool Key the console showed") from (
|
|
76
|
+
error
|
|
77
|
+
)
|
|
78
|
+
if len(raw) != _KEY_BYTES:
|
|
79
|
+
raise PoolKeyError(f"{POOL_KEY_ENV} must be a 32-byte Pool Key")
|
|
80
|
+
public = X25519PrivateKey.from_private_bytes(raw).public_key().public_bytes_raw()
|
|
81
|
+
return PoolKey(
|
|
82
|
+
private_key=base64.b64encode(raw).decode("ascii"),
|
|
83
|
+
public_key=base64.b64encode(public).decode("ascii"),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_pool_key(
|
|
88
|
+
state_dir: Path | None, environ: MutableMapping[str, str] = os.environ
|
|
89
|
+
) -> PoolKey | None:
|
|
90
|
+
"""The Pool Key from the environment, else the workstation install's file; None if
|
|
91
|
+
the Runner was given none.
|
|
92
|
+
|
|
93
|
+
Taken out of the environment as it is read: a child process started without an
|
|
94
|
+
explicit environment would otherwise inherit it.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
value = environ.pop(POOL_KEY_ENV, "").strip()
|
|
98
|
+
if not value and state_dir is not None:
|
|
99
|
+
stored = private_read(state_dir / POOL_KEY_FILENAME)
|
|
100
|
+
value = stored.decode("utf-8").strip() if stored is not None else ""
|
|
101
|
+
return parse_pool_key(value) if value else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def save_pool_key(state_dir: Path, value: str) -> PoolKey:
|
|
105
|
+
"""Check and keep a workstation's Pool Key, for every later start of its Runner."""
|
|
106
|
+
|
|
107
|
+
pool_key = parse_pool_key(value)
|
|
108
|
+
private_write(state_dir / POOL_KEY_FILENAME, pool_key.private_key.encode("ascii"))
|
|
109
|
+
return pool_key
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class LlmKeyOpener:
|
|
113
|
+
"""Opens a released LLM Key into one proxy slot (Spec: Runners rev 3).
|
|
114
|
+
|
|
115
|
+
The slot is handed to one Directive's proxy attempt and to nothing else, so the key
|
|
116
|
+
lives exactly as long as that attempt: never in the proxy's slot store, never on
|
|
117
|
+
disk, never in the Agent Runtime's environment.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
*,
|
|
123
|
+
pool_key: PoolKey | None,
|
|
124
|
+
recipient_keys: RecipientKeyStore,
|
|
125
|
+
endpoints: Mapping[str, LlmEndpoint] = LLM_SLOT_REFERENCES,
|
|
126
|
+
) -> None:
|
|
127
|
+
self._pool_key = pool_key
|
|
128
|
+
self._recipient_keys = recipient_keys
|
|
129
|
+
self._endpoints = endpoints
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def fingerprint(self) -> str | None:
|
|
133
|
+
return self._pool_key.fingerprint if self._pool_key is not None else None
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def holds_pool_key(self) -> bool:
|
|
137
|
+
return self._pool_key is not None
|
|
138
|
+
|
|
139
|
+
def open(self, release: LlmKeyRelease) -> CredentialSlot:
|
|
140
|
+
"""The key the two shares make, as a slot for the release's provider.
|
|
141
|
+
|
|
142
|
+
One error for every way a release fails to open, as :func:`open_sealed` does: a
|
|
143
|
+
share sealed to an older Pool Key, to another Runner or bound to another key are
|
|
144
|
+
the same answer to the holder, and telling them apart would be an oracle.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
unopenable = f"LLM Key {release.llm_key_id} does not open with this Runner's keys"
|
|
148
|
+
if self._pool_key is None or release.pool_key_fingerprint != self._pool_key.fingerprint:
|
|
149
|
+
raise LlmKeyUnopenableError(unopenable)
|
|
150
|
+
try:
|
|
151
|
+
pool_share = open_sealed(
|
|
152
|
+
private_key=self._pool_key.private_key,
|
|
153
|
+
binding=release.pool_share_binding(),
|
|
154
|
+
ciphertext=release.pool_share,
|
|
155
|
+
)
|
|
156
|
+
platform_share = open_sealed(
|
|
157
|
+
private_key=self._recipient_private_key(release.recipient_key_id),
|
|
158
|
+
binding=release.platform_share_binding(),
|
|
159
|
+
ciphertext=release.platform_share,
|
|
160
|
+
)
|
|
161
|
+
value = join_llm_key(pool_share, platform_share)
|
|
162
|
+
except (SealedCredentialError, ValueError) as error:
|
|
163
|
+
raise LlmKeyUnopenableError(unopenable) from error
|
|
164
|
+
key_name = PROVIDER_KEY_NAMES[release.provider]
|
|
165
|
+
return self._endpoints[key_name].slot(
|
|
166
|
+
reference=key_name, key_id=release.llm_key_id, value=value
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def _recipient_private_key(self, recipient_key_id: str) -> str:
|
|
170
|
+
# The previous key too: the control plane may have sealed its share just before a
|
|
171
|
+
# renewal.
|
|
172
|
+
for pair in (self._recipient_keys.current(), self._recipient_keys.previous()):
|
|
173
|
+
if pair is not None and pair.key_id == recipient_key_id:
|
|
174
|
+
return pair.private_key
|
|
175
|
+
raise SealedCredentialError("the platform share is sealed to another Recipient Key")
|
|
@@ -85,6 +85,14 @@ from agentic_runner.llm_proxy import (
|
|
|
85
85
|
from agentic_runner.mcp import ToolServerHealthLog
|
|
86
86
|
from agentic_runner.message_store import MESSAGES_DIR, MessageStore, TemporalWorkflowSignaller
|
|
87
87
|
from agentic_runner.oauth_connectors import OAuthConnectors
|
|
88
|
+
from agentic_runner.pool_key import (
|
|
89
|
+
POOL_KEY_ENV,
|
|
90
|
+
POOL_KEY_FILENAME,
|
|
91
|
+
LlmKeyOpener,
|
|
92
|
+
PoolKey,
|
|
93
|
+
PoolKeyError,
|
|
94
|
+
load_pool_key,
|
|
95
|
+
)
|
|
88
96
|
from agentic_runner.private_state import (
|
|
89
97
|
UnsafeStateError,
|
|
90
98
|
ensure_private_dir,
|
|
@@ -160,6 +168,7 @@ from agentic_runner_contracts.runner_registration import (
|
|
|
160
168
|
HostAttestation,
|
|
161
169
|
IsolationMode,
|
|
162
170
|
LifecycleKind,
|
|
171
|
+
PoolTerms,
|
|
163
172
|
ProviderKeyReport,
|
|
164
173
|
RecipientKey,
|
|
165
174
|
ResourcePressure,
|
|
@@ -258,6 +267,7 @@ _PRIVATE_STATE_FILES = (
|
|
|
258
267
|
LIFECYCLE_FILENAME,
|
|
259
268
|
UID_MAP_FILENAME,
|
|
260
269
|
LLM_CONTRACTS_FILENAME,
|
|
270
|
+
POOL_KEY_FILENAME,
|
|
261
271
|
)
|
|
262
272
|
|
|
263
273
|
|
|
@@ -819,6 +829,10 @@ class ControlPlaneStream:
|
|
|
819
829
|
# kept for `agentic-runner sign-in`.
|
|
820
830
|
install: RunnerInstall | None = None
|
|
821
831
|
state_dir: Path | None = None
|
|
832
|
+
# Rev 3: the fingerprint of the Pool Key this Runner was given; None when it was given
|
|
833
|
+
# none. Reported only once an ack named the pool (`_pool`), which a control plane on
|
|
834
|
+
# 3.26 or later does.
|
|
835
|
+
pool_key_fingerprint: str | None = None
|
|
822
836
|
# Console-v2 issue 30: the OAuth Connectors whose code, tokens and verifier stay here.
|
|
823
837
|
oauth: OAuthConnectors | None = None
|
|
824
838
|
# ADR-0022 §57: the Runner Limit read at start and its posture; None while it is empty.
|
|
@@ -834,6 +848,7 @@ class ControlPlaneStream:
|
|
|
834
848
|
# Rev 3: the Contracts the last ack said this Runner serves; None until an ack from a
|
|
835
849
|
# control plane on 3.25 named them, and the LLM access report is sent only after.
|
|
836
850
|
_llm_contracts: list[ContractLlmTerms] | None = None
|
|
851
|
+
_pool: PoolTerms | None = None
|
|
837
852
|
_last_seen: tuple[float, datetime] | None = None
|
|
838
853
|
|
|
839
854
|
def _observe_sleep(self, now: datetime) -> None:
|
|
@@ -1023,6 +1038,7 @@ class ControlPlaneStream:
|
|
|
1023
1038
|
runner_limit=self.runner_limit,
|
|
1024
1039
|
llm_access=self._llm_access_report(),
|
|
1025
1040
|
install=self.install if self._llm_contracts is not None else None,
|
|
1041
|
+
pool_key_fingerprint=self.pool_key_fingerprint if self._pool is not None else None,
|
|
1026
1042
|
)
|
|
1027
1043
|
ack = await self.client.heartbeat(self.state, envelope)
|
|
1028
1044
|
if self.sources is not None:
|
|
@@ -1048,6 +1064,7 @@ class ControlPlaneStream:
|
|
|
1048
1064
|
self._sync_delivered()
|
|
1049
1065
|
self._manifests = list(ack.credential_manifests)
|
|
1050
1066
|
self._take_llm_contracts(ack.llm_contracts)
|
|
1067
|
+
self._take_pool(ack.pool)
|
|
1051
1068
|
await self._sync_llm_slots()
|
|
1052
1069
|
if ack.runner_token is not None:
|
|
1053
1070
|
self.runner_token = ack.runner_token
|
|
@@ -1106,6 +1123,25 @@ class ControlPlaneStream:
|
|
|
1106
1123
|
if self.state_dir is not None:
|
|
1107
1124
|
save_llm_contracts(self.state_dir, terms)
|
|
1108
1125
|
|
|
1126
|
+
def _take_pool(self, pool: PoolTerms | None) -> None:
|
|
1127
|
+
"""Keep the ack's pool. An ack without one (a personal Runner, or a control plane
|
|
1128
|
+
below 3.26) leaves what is held."""
|
|
1129
|
+
|
|
1130
|
+
if pool is None or pool == self._pool:
|
|
1131
|
+
return
|
|
1132
|
+
self._pool = pool
|
|
1133
|
+
current = pool.pool_key_fingerprint
|
|
1134
|
+
if current is not None and current != self.pool_key_fingerprint:
|
|
1135
|
+
# The operator's signal: this Runner opens none of the pool's LLM Keys until
|
|
1136
|
+
# its install carries the Pool Key now on the Runners page.
|
|
1137
|
+
_logger.warning(
|
|
1138
|
+
"this Runner holds Pool Key %s, not the pool's current %s; it opens none of "
|
|
1139
|
+
"the pool's LLM Keys until %s is updated",
|
|
1140
|
+
self.pool_key_fingerprint or "none",
|
|
1141
|
+
current,
|
|
1142
|
+
POOL_KEY_ENV,
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1109
1145
|
async def _sync_host_keys(self) -> None:
|
|
1110
1146
|
"""The host store's keys, by the provider's key name, become the proxy's host slots
|
|
1111
1147
|
(Spec: Runners rev 3), serving every Contract this Runner runs.
|
|
@@ -1367,6 +1403,13 @@ async def _run(
|
|
|
1367
1403
|
print(f"refusing to start (runner_limit): {error}", flush=True)
|
|
1368
1404
|
return 1
|
|
1369
1405
|
state_dir = config.state_dir
|
|
1406
|
+
try:
|
|
1407
|
+
pool_key = load_pool_key(state_dir)
|
|
1408
|
+
except PoolKeyError as error:
|
|
1409
|
+
# Refused rather than started without it: the pool's LLM Keys would then fail
|
|
1410
|
+
# every Directive routed to them, far from the typo that caused it.
|
|
1411
|
+
print(f"refusing to start (pool_key_invalid): {error}", flush=True)
|
|
1412
|
+
return 1
|
|
1370
1413
|
|
|
1371
1414
|
readiness = readiness or Readiness(int(os.environ.get(READINESS_PORT_ENV, "").strip() or 0))
|
|
1372
1415
|
await readiness.start()
|
|
@@ -1388,6 +1431,7 @@ async def _run(
|
|
|
1388
1431
|
attestation=attestation,
|
|
1389
1432
|
install=install,
|
|
1390
1433
|
runner_limit=runner_limit,
|
|
1434
|
+
pool_key=pool_key,
|
|
1391
1435
|
)
|
|
1392
1436
|
finally:
|
|
1393
1437
|
await readiness.close()
|
|
@@ -1411,6 +1455,7 @@ async def _serve(
|
|
|
1411
1455
|
attestation: HostAttestation | None,
|
|
1412
1456
|
install: RunnerInstall | None = None,
|
|
1413
1457
|
runner_limit: AppliedRunnerLimit = EMPTY_RUNNER_LIMIT,
|
|
1458
|
+
pool_key: PoolKey | None = None,
|
|
1414
1459
|
) -> int:
|
|
1415
1460
|
limit_report = runner_limit_report(
|
|
1416
1461
|
runner_limit,
|
|
@@ -1480,6 +1525,7 @@ async def _serve(
|
|
|
1480
1525
|
install=install,
|
|
1481
1526
|
lifecycle=lifecycle,
|
|
1482
1527
|
runner_limit=runner_limit,
|
|
1528
|
+
pool_key=pool_key,
|
|
1483
1529
|
)
|
|
1484
1530
|
except _StoppedBeforeStartError:
|
|
1485
1531
|
# No beat with a 10 s timeout on the way out (`_report_stop`): the control plane
|
|
@@ -1512,6 +1558,7 @@ async def _serve_registered(
|
|
|
1512
1558
|
lifecycle: LifecycleOutbox,
|
|
1513
1559
|
install: RunnerInstall | None = None,
|
|
1514
1560
|
runner_limit: AppliedRunnerLimit = EMPTY_RUNNER_LIMIT,
|
|
1561
|
+
pool_key: PoolKey | None = None,
|
|
1515
1562
|
) -> int:
|
|
1516
1563
|
load = _LoadInterceptor()
|
|
1517
1564
|
proxy = LlmProxy(slots=SlotStore(), ceilings=CeilingStore(), outbox=UsageOutbox())
|
|
@@ -1535,23 +1582,28 @@ async def _serve_registered(
|
|
|
1535
1582
|
usage_windows = build_usage_windows(
|
|
1536
1583
|
host_party=state.host_party, runtimes=runtimes, isolation=isolation, in_flight=in_flight
|
|
1537
1584
|
)
|
|
1585
|
+
# One store for both: it caches the key file, and a renewal through the stream must
|
|
1586
|
+
# be the key the LLM Key opener reads too.
|
|
1587
|
+
recipient_keys = RecipientKeyStore(state_dir)
|
|
1588
|
+
llm_slots = llm_slot_references(
|
|
1589
|
+
{
|
|
1590
|
+
name: url
|
|
1591
|
+
for name, url in (
|
|
1592
|
+
("openai", config.openai_base_url),
|
|
1593
|
+
("anthropic", config.anthropic_base_url),
|
|
1594
|
+
)
|
|
1595
|
+
if url
|
|
1596
|
+
}
|
|
1597
|
+
)
|
|
1598
|
+
llm_keys = LlmKeyOpener(pool_key=pool_key, recipient_keys=recipient_keys, endpoints=llm_slots)
|
|
1538
1599
|
stream = ControlPlaneStream(
|
|
1539
1600
|
client=RunnerRegistrationClient(base_url=control_plane, client=http_client),
|
|
1540
1601
|
state=state,
|
|
1541
1602
|
isolation=mode,
|
|
1542
1603
|
proxy=proxy,
|
|
1543
|
-
sealed=SealedCredentialStream(
|
|
1604
|
+
sealed=SealedCredentialStream(recipient_keys),
|
|
1544
1605
|
credentials=credentials,
|
|
1545
|
-
llm_slots=
|
|
1546
|
-
{
|
|
1547
|
-
name: url
|
|
1548
|
-
for name, url in (
|
|
1549
|
-
("openai", config.openai_base_url),
|
|
1550
|
-
("anthropic", config.anthropic_base_url),
|
|
1551
|
-
)
|
|
1552
|
-
if url
|
|
1553
|
-
}
|
|
1554
|
-
),
|
|
1606
|
+
llm_slots=llm_slots,
|
|
1555
1607
|
load=load,
|
|
1556
1608
|
egress_posture=egress_posture,
|
|
1557
1609
|
attestation=attestation,
|
|
@@ -1564,6 +1616,7 @@ async def _serve_registered(
|
|
|
1564
1616
|
runner_limit=runner_limit_report(runner_limit, runtimes, egress_posture=egress_posture),
|
|
1565
1617
|
install=install,
|
|
1566
1618
|
state_dir=state_dir,
|
|
1619
|
+
pool_key_fingerprint=llm_keys.fingerprint,
|
|
1567
1620
|
)
|
|
1568
1621
|
stream.sources = UserSourcePoller(
|
|
1569
1622
|
stream=SignedIntakeStream(stream.client, state),
|
|
@@ -1631,6 +1684,7 @@ async def _serve_registered(
|
|
|
1631
1684
|
usage_windows=usage_windows,
|
|
1632
1685
|
runner_limit=runner_limit,
|
|
1633
1686
|
llm_contracts=lambda: stream.llm_contracts,
|
|
1687
|
+
llm_keys=llm_keys,
|
|
1634
1688
|
# The wake signal rides the connection this process polls with: one
|
|
1635
1689
|
# namespace, the Organisation's own (PRD issue 52, map ticket 15 §2).
|
|
1636
1690
|
signaller=TemporalWorkflowSignaller(client),
|
|
@@ -1829,6 +1883,7 @@ def _activities(
|
|
|
1829
1883
|
usage_windows: UsageWindows | None = None,
|
|
1830
1884
|
runner_limit: AppliedRunnerLimit | None = None,
|
|
1831
1885
|
llm_contracts: Callable[[], list[ContractLlmTerms] | None] | None = None,
|
|
1886
|
+
llm_keys: LlmKeyOpener | None = None,
|
|
1832
1887
|
) -> list[Callable[..., Any]]:
|
|
1833
1888
|
github_client = GitHubAppClient(
|
|
1834
1889
|
app_id=os.getenv("GITHUB_APP_ID"),
|
|
@@ -1879,6 +1934,7 @@ def _activities(
|
|
|
1879
1934
|
usage_windows=usage_windows,
|
|
1880
1935
|
runner_limit=runner_limit,
|
|
1881
1936
|
llm_contracts=llm_contracts,
|
|
1937
|
+
llm_keys=llm_keys,
|
|
1882
1938
|
)
|
|
1883
1939
|
return [
|
|
1884
1940
|
*ralph.activity_callables(),
|
|
@@ -14,9 +14,10 @@ personal Runner serves one Contract, which the last heartbeat ack named
|
|
|
14
14
|
``codex login --device-auth`` with the code shown here. No code or token passes through
|
|
15
15
|
the control plane.
|
|
16
16
|
|
|
17
|
-
It refuses on a pool Runner, which uses only its pool's keys. It
|
|
18
|
-
when the ack says the Contract
|
|
19
|
-
|
|
17
|
+
It refuses on a pool Runner, which uses only its pool's keys. It offers a subscription
|
|
18
|
+
only when the ack says the Contract allows it; otherwise it says which of the right or
|
|
19
|
+
the person's acknowledgment is missing, names the console page that turns it on, and
|
|
20
|
+
offers the key (QTS-1776).
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
from __future__ import annotations
|
|
@@ -92,14 +93,21 @@ class SignIn:
|
|
|
92
93
|
|
|
93
94
|
async def _one(self, cli_kind: str) -> int:
|
|
94
95
|
label = _LABELS[cli_kind]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
offer = self._subscription_offer()
|
|
97
|
+
if isinstance(offer, str):
|
|
98
|
+
# Offering `[s]` and then refusing it is what sent an Organisation Admin looking
|
|
99
|
+
# for a switch nothing named (QTS-1776).
|
|
100
|
+
self.say(f"{label}: {offer}")
|
|
101
|
+
answer = self.ask(f"{label}: store an API [k]ey, or skip? [k/N] ")
|
|
102
|
+
else:
|
|
103
|
+
answer = self.ask(
|
|
104
|
+
f"{label}: sign in with your [s]ubscription, store an API [k]ey, or skip? [s/k/N] "
|
|
105
|
+
)
|
|
98
106
|
choice = answer.strip().lower()[:1]
|
|
99
107
|
if choice == "k":
|
|
100
108
|
return self._key(cli_kind)
|
|
101
|
-
if choice == "s":
|
|
102
|
-
return await self._subscription(cli_kind)
|
|
109
|
+
if choice == "s" and not isinstance(offer, str):
|
|
110
|
+
return await self._subscription(offer, cli_kind)
|
|
103
111
|
self.say(f"{label}: skipped")
|
|
104
112
|
return 0
|
|
105
113
|
|
|
@@ -118,39 +126,38 @@ class SignIn:
|
|
|
118
126
|
)
|
|
119
127
|
return 0
|
|
120
128
|
|
|
121
|
-
async def _subscription(self, cli_kind: str) -> int:
|
|
122
|
-
contract = self._contract()
|
|
123
|
-
if contract is None:
|
|
124
|
-
return 1
|
|
125
|
-
if contract.subscription == SubscriptionRight.NO_RIGHT:
|
|
126
|
-
self.say(
|
|
127
|
-
"refusing a subscription: your Contract does not allow your own subscription. "
|
|
128
|
-
"Ask an Organisation Admin to turn it on, or store an API key instead"
|
|
129
|
-
)
|
|
130
|
-
return 1
|
|
131
|
-
if contract.subscription == SubscriptionRight.NOT_ACKNOWLEDGED:
|
|
132
|
-
self.say(
|
|
133
|
-
"refusing a subscription: you have not acknowledged the subscription terms. "
|
|
134
|
-
"Acknowledge them on your Contract in the console, then run this again"
|
|
135
|
-
)
|
|
136
|
-
return 1
|
|
129
|
+
async def _subscription(self, contract: ContractLlmTerms, cli_kind: str) -> int:
|
|
137
130
|
returncode = await self.subscription(str(contract.contract_id), cli_kind)
|
|
138
131
|
if returncode != 0:
|
|
139
132
|
self.say(f"{_LABELS[cli_kind]}: the sign-in exited {returncode}; nothing changed")
|
|
140
133
|
return returncode
|
|
141
134
|
|
|
142
|
-
def
|
|
135
|
+
def _subscription_offer(self) -> ContractLlmTerms | str:
|
|
136
|
+
"""The one Contract a subscription signs in, or why the ack offers none."""
|
|
137
|
+
|
|
143
138
|
if self.contracts is None:
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
139
|
+
return (
|
|
140
|
+
"no subscription yet: this Runner has not yet been told its Contract. Start "
|
|
141
|
+
"it, wait for its first heartbeat, and run this again (the control plane must "
|
|
142
|
+
"be on contracts 3.25 or later)"
|
|
148
143
|
)
|
|
149
|
-
return None
|
|
150
144
|
if len(self.contracts) != 1:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
f"
|
|
145
|
+
return (
|
|
146
|
+
"no subscription: a personal Runner serves one Contract, and the control "
|
|
147
|
+
f"plane named {len(self.contracts)}"
|
|
148
|
+
)
|
|
149
|
+
contract = self.contracts[0]
|
|
150
|
+
if contract.subscription == SubscriptionRight.NO_RIGHT:
|
|
151
|
+
# TODO(QTS-1744): name the Contract editor once the right moves onto the Contract.
|
|
152
|
+
return (
|
|
153
|
+
"your own subscription is off for this Runner. An Organisation Admin turns it "
|
|
154
|
+
"on in the console with Allow subscription sign-in, on this Runner under "
|
|
155
|
+
"Personal Runners on /orgs/<organisation>/runners"
|
|
156
|
+
)
|
|
157
|
+
if contract.subscription == SubscriptionRight.NOT_ACKNOWLEDGED:
|
|
158
|
+
return (
|
|
159
|
+
"your own subscription is on for this Runner, and you have not acknowledged "
|
|
160
|
+
"the subscription terms. Acknowledge them in the console on My Contracts "
|
|
161
|
+
"(/me/contracts), then run this again"
|
|
154
162
|
)
|
|
155
|
-
|
|
156
|
-
return self.contracts[0]
|
|
163
|
+
return contract
|
|
@@ -26,6 +26,9 @@ Contracts 3.25 (Spec: Runners rev 3): every ack names the one Contract this fake
|
|
|
26
26
|
``llm_contracts``, with ``subscription`` as its right, so a Runner reports the LLM access it
|
|
27
27
|
holds for it; :attr:`FakeControlPlane.llm_access` keeps each report. ``subscription``
|
|
28
28
|
``None`` answers as a control plane below 3.25 does: no Contract terms at all.
|
|
29
|
+
|
|
30
|
+
Contracts 3.26: :attr:`FakeControlPlane.pool` is the ack's pool and its current Pool Key;
|
|
31
|
+
None, the default, answers for a personal Runner or a control plane below 3.26.
|
|
29
32
|
"""
|
|
30
33
|
|
|
31
34
|
from __future__ import annotations
|
|
@@ -69,6 +72,7 @@ from agentic_runner_contracts.runner_registration import (
|
|
|
69
72
|
FloorState,
|
|
70
73
|
HeartbeatAck,
|
|
71
74
|
HeartbeatEnvelope,
|
|
75
|
+
PoolTerms,
|
|
72
76
|
RunnerIdentityMaterial,
|
|
73
77
|
SubscriptionRight,
|
|
74
78
|
)
|
|
@@ -179,6 +183,7 @@ class FakeControlPlane:
|
|
|
179
183
|
# The ack's word on whether the person may sign this Contract in with their own
|
|
180
184
|
# subscription; None leaves `llm_contracts` out, as a control plane below 3.25 does.
|
|
181
185
|
subscription: SubscriptionRight | None = SubscriptionRight.NO_RIGHT
|
|
186
|
+
pool: PoolTerms | None = None
|
|
182
187
|
|
|
183
188
|
bootstraps: list[BootstrapRequest] = field(default_factory=list)
|
|
184
189
|
runner_ids: list[UUID] = field(default_factory=list)
|
|
@@ -459,6 +464,7 @@ class FakeControlPlane:
|
|
|
459
464
|
ContractLlmTerms(contract_id=self.contract_id, subscription=self.subscription)
|
|
460
465
|
]
|
|
461
466
|
),
|
|
467
|
+
pool=self.pool,
|
|
462
468
|
)
|
|
463
469
|
return _Answer(200, ack.model_dump(mode="json"))
|
|
464
470
|
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/fastapi_client.py
RENAMED
|
@@ -176,6 +176,20 @@ class RunnerFastApiClient:
|
|
|
176
176
|
records = result.get("records")
|
|
177
177
|
return records if isinstance(records, list) else []
|
|
178
178
|
|
|
179
|
+
async def release_llm_key(
|
|
180
|
+
self, work_record_id: str, llm_key_id: str, payload: Mapping[str, Any]
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
"""The two sealed shares of the LLM Key a Directive's ``llm_access`` names (Spec:
|
|
183
|
+
Runners rev 3). ``payload`` is ``LlmKeyReleaseRequest``'s shape; the Directive
|
|
184
|
+
token says which Runner and Directive ask, which is what the control plane releases
|
|
185
|
+
against."""
|
|
186
|
+
|
|
187
|
+
return await self._post(
|
|
188
|
+
f"{self._prefix}/work-records/{_path_segment(work_record_id)}"
|
|
189
|
+
f"/llm-keys/{_path_segment(llm_key_id)}/release",
|
|
190
|
+
payload,
|
|
191
|
+
)
|
|
192
|
+
|
|
179
193
|
async def append_evidence(
|
|
180
194
|
self,
|
|
181
195
|
work_record_id: str,
|
|
@@ -49,6 +49,7 @@ from agentic_runner.cli_floor import CLI_FLOORS, CLI_NAMES, meets_floor, parse_c
|
|
|
49
49
|
from agentic_runner.config import CONFIG_FILE_ENV
|
|
50
50
|
from agentic_runner.host_store import open_workstation_store
|
|
51
51
|
from agentic_runner.lifecycle import LifecycleOutbox
|
|
52
|
+
from agentic_runner.pool_key import parse_pool_key, save_pool_key
|
|
52
53
|
from agentic_runner.registration import load_state
|
|
53
54
|
from agentic_runner.service import HEARTBEAT_STAMP_NAME, PIDFILE_NAME
|
|
54
55
|
from agentic_runner.workers.acp_runtime import ACP_BRIDGES
|
|
@@ -637,6 +638,7 @@ async def install(
|
|
|
637
638
|
settings: WorkstationSettings,
|
|
638
639
|
*,
|
|
639
640
|
registration_token: str,
|
|
641
|
+
pool_key: str | None = None,
|
|
640
642
|
platform: str = sys.platform,
|
|
641
643
|
home: Path | None = None,
|
|
642
644
|
start_service: bool = True,
|
|
@@ -650,9 +652,16 @@ async def install(
|
|
|
650
652
|
prompt, never stored: the identity bootstrap hands back is what every later start uses,
|
|
651
653
|
and a lost state directory means a reinstall and a re-delivery of any sealed value
|
|
652
654
|
(22 A4), by design.
|
|
655
|
+
|
|
656
|
+
``pool_key`` is the Organisation's Pool Key, for a Runner of its pool that opens the
|
|
657
|
+
pool's LLM Keys (Spec: Runners rev 3). Unlike the token it is kept, 0600 in the state
|
|
658
|
+
directory, because every start of the Runner needs it.
|
|
653
659
|
"""
|
|
654
660
|
|
|
655
661
|
require_any_cli(settings.path)
|
|
662
|
+
if pool_key:
|
|
663
|
+
# Checked before anything registers; kept only once something has.
|
|
664
|
+
parse_pool_key(pool_key)
|
|
656
665
|
paths.create()
|
|
657
666
|
settings.save(paths)
|
|
658
667
|
already_registered = load_state(paths.state_dir) is not None
|
|
@@ -664,6 +673,8 @@ async def install(
|
|
|
664
673
|
registration_token=registration_token,
|
|
665
674
|
isolation=IsolationMode.NONE,
|
|
666
675
|
)
|
|
676
|
+
if pool_key:
|
|
677
|
+
save_pool_key(paths.state_dir, pool_key)
|
|
667
678
|
LifecycleOutbox(paths.state_dir).add(LifecycleKind.INSTALL, datetime.now(UTC))
|
|
668
679
|
definition = service_definition(paths, settings, platform=platform, home=home)
|
|
669
680
|
definition.path.parent.mkdir(parents=True, exist_ok=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/contracts.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/evidence.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/git/workspace.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/github/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/github/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/integrations/github/gh_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.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
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.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
|
{agentic_runner-3.25.1 → agentic_runner-3.26.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.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/_runtime_support.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/claude_runtime.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/claude_sign_in.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/command_policy.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/contract_isolation.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/harness_logins.py
RENAMED
|
File without changes
|
{agentic_runner-3.25.1 → agentic_runner-3.26.1}/src/agentic_runner/workers/harness_outcome.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|