agentic-runner 3.23.1__tar.gz → 3.25.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/PKG-INFO +2 -2
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/pyproject.toml +1 -1
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/activities.py +72 -15
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/auth_mode.py +74 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/cli.py +92 -18
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/credentials.py +27 -36
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/heartbeat_link.py +0 -1
- agentic_runner-3.25.0/src/agentic_runner/llm_contracts.py +41 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/llm_proxy.py +168 -81
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/service.py +273 -139
- agentic_runner-3.25.0/src/agentic_runner/sign_in.py +156 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +23 -26
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/testing/control_plane.py +23 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/testing/test_conformance.py +8 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/claude_runtime.py +63 -22
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/claude_sign_in.py +12 -6
- agentic_runner-3.25.0/src/agentic_runner/workers/harness_logins.py +172 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/skills.py +4 -2
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workstation.py +5 -1
- agentic_runner-3.23.1/src/agentic_runner/device_login_activities.py +0 -119
- agentic_runner-3.23.1/src/agentic_runner/workers/contract_device_login.py +0 -241
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/.gitignore +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/LICENSE +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/README.md +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/launch.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/oauth_connectors.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/runner_limit.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/sealed_box.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/skill.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/usage_windows.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/acp_runtime.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.23.1 → agentic_runner-3.25.0}/src/agentic_runner/workers/settings.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.0
|
|
4
4
|
Summary: The Agentic OS Runner: an activity-only Temporal executor for workspaces, Agent Runtimes and verb seams
|
|
5
5
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.12
|
|
8
|
-
Requires-Dist: agentic-runner-contracts==3.
|
|
8
|
+
Requires-Dist: agentic-runner-contracts==3.25.0
|
|
9
9
|
Requires-Dist: cryptography<51,>=50
|
|
10
10
|
Requires-Dist: httpx<1.0,>=0.27
|
|
11
11
|
Requires-Dist: pydantic-settings<3.0,>=2.0
|
|
@@ -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.0",
|
|
14
14
|
# Imported directly by `registration.py` (the Ed25519 identity that signs every
|
|
15
15
|
# heartbeat); already present transitively through pyjwt[crypto], declared because
|
|
16
16
|
# we import it.
|
|
@@ -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,7 @@ from agentic_runner_contracts.routing import (
|
|
|
286
292
|
assert_routed,
|
|
287
293
|
selector_satisfied,
|
|
288
294
|
)
|
|
295
|
+
from agentic_runner_contracts.runner_registration import SubscriptionState
|
|
289
296
|
from agentic_runner_contracts.runtime_context import (
|
|
290
297
|
EGRESS_ALLOW_LIST_KEY,
|
|
291
298
|
McpServerSpec,
|
|
@@ -2650,7 +2657,8 @@ class RunnerRalphActivities:
|
|
|
2650
2657
|
the bearer defensible under ADR-0011 §9.
|
|
2651
2658
|
"""
|
|
2652
2659
|
|
|
2653
|
-
|
|
2660
|
+
access = routing.llm_access if routing is not None else None
|
|
2661
|
+
auth_mode = await self._directive_auth_mode(work_record_id, state, access)
|
|
2654
2662
|
await self._report_login_residue(work_record_id, state)
|
|
2655
2663
|
facts = AttemptFacts(
|
|
2656
2664
|
work_record_id=work_record_id,
|
|
@@ -2669,9 +2677,7 @@ class RunnerRalphActivities:
|
|
|
2669
2677
|
)
|
|
2670
2678
|
|
|
2671
2679
|
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
|
-
)
|
|
2680
|
+
credentials = await self._resolve_credentials(work_record_id, state)
|
|
2675
2681
|
if credentials is not None:
|
|
2676
2682
|
# Names only, at the moment they resolved (22 A1's Evidence): a support read
|
|
2677
2683
|
# of "which credentials did this Directive have" must not be answerable by
|
|
@@ -2716,7 +2722,8 @@ class RunnerRalphActivities:
|
|
|
2716
2722
|
if self._llm_proxy is not None and auth_mode == AuthMode.API_KEY:
|
|
2717
2723
|
# API-key mode only (issue 43): a subscription Directive never has a slot
|
|
2718
2724
|
# to resolve, and issue 31 meters it from the harness's own output. One
|
|
2719
|
-
# bearer serves the callback socket and the proxy alike.
|
|
2725
|
+
# bearer serves the callback socket and the proxy alike. The attempt spends
|
|
2726
|
+
# its own provider's slot, and a `host_key` Directive the store's alone.
|
|
2720
2727
|
proxy_attempt = await stack.enter_async_context(
|
|
2721
2728
|
self._llm_proxy.attempt(
|
|
2722
2729
|
directive_id=facts.directive_id,
|
|
@@ -2725,6 +2732,11 @@ class RunnerRalphActivities:
|
|
|
2725
2732
|
work_record_id=_optional_uuid(work_record_id),
|
|
2726
2733
|
token=callback_env.get(CALLBACK_TOKEN_ENV),
|
|
2727
2734
|
reserve_max_tokens=reserve_max_tokens,
|
|
2735
|
+
provider=provider_for(state.cli_kind),
|
|
2736
|
+
host_key_only=(
|
|
2737
|
+
access is not None and access.kind == LlmAccessKind.HOST_KEY
|
|
2738
|
+
),
|
|
2739
|
+
delivered_only=access is None and not self._personal_runner(),
|
|
2728
2740
|
)
|
|
2729
2741
|
)
|
|
2730
2742
|
callback_env.update(proxy_attempt.env(state.cli_kind))
|
|
@@ -3081,8 +3093,6 @@ class RunnerRalphActivities:
|
|
|
3081
3093
|
self,
|
|
3082
3094
|
work_record_id: str,
|
|
3083
3095
|
state: _RuntimeContextState,
|
|
3084
|
-
*,
|
|
3085
|
-
funding_party: str | None = None,
|
|
3086
3096
|
) -> DirectiveCredentials | None:
|
|
3087
3097
|
"""Resolve this Contract's Credential References, or fail the Directive closed.
|
|
3088
3098
|
|
|
@@ -3100,7 +3110,6 @@ class RunnerRalphActivities:
|
|
|
3100
3110
|
return resolver.resolve(
|
|
3101
3111
|
contract_id=state.contract_id,
|
|
3102
3112
|
manifest=state.credential_references,
|
|
3103
|
-
funding_party=funding_party,
|
|
3104
3113
|
)
|
|
3105
3114
|
except UnresolvableCredentialReferenceError as error:
|
|
3106
3115
|
await self._fastapi_client.append_evidence(
|
|
@@ -3111,11 +3120,15 @@ class RunnerRalphActivities:
|
|
|
3111
3120
|
raise
|
|
3112
3121
|
|
|
3113
3122
|
async def _directive_auth_mode(
|
|
3114
|
-
self, work_record_id: str, state: _RuntimeContextState
|
|
3123
|
+
self, work_record_id: str, state: _RuntimeContextState, access: LlmAccess | None = None
|
|
3115
3124
|
) -> AuthMode:
|
|
3116
3125
|
"""This Directive's auth mode, or a non-retryable refusal with Evidence naming the
|
|
3117
3126
|
rule (local-agents 04).
|
|
3118
3127
|
|
|
3128
|
+
``access`` is the LLM access the control plane routed the Directive with (rev 3):
|
|
3129
|
+
the Runner runs on exactly that, or refuses and never falls back to other access.
|
|
3130
|
+
None, from a control plane below contracts 3.25, keeps the Runner's own choice.
|
|
3131
|
+
|
|
3119
3132
|
Decided from what this process can check -- the host party it was registered with,
|
|
3120
3133
|
whether a key is present for the Contract, whether the Contract runs under its own
|
|
3121
3134
|
uid here and whether its harness root holds a login (QTS-1450) -- never from the
|
|
@@ -3129,6 +3142,14 @@ class RunnerRalphActivities:
|
|
|
3129
3142
|
host_party = self._routing_identity.host_party if self._routing_identity else None
|
|
3130
3143
|
contract_uid = self._runs_under_contract_uid(state)
|
|
3131
3144
|
try:
|
|
3145
|
+
if access is not None:
|
|
3146
|
+
return require_named_access(
|
|
3147
|
+
access,
|
|
3148
|
+
host_party=host_party,
|
|
3149
|
+
host_key_held=self._host_key_held(state),
|
|
3150
|
+
subscription=self._subscription_state(state),
|
|
3151
|
+
runtime_modes=getattr(runtime, "auth_modes", frozenset({AuthMode.API_KEY})),
|
|
3152
|
+
)
|
|
3132
3153
|
return choose_auth_mode(
|
|
3133
3154
|
host_party=host_party,
|
|
3134
3155
|
key_present=self._api_key_present(state, runtime),
|
|
@@ -3148,11 +3169,16 @@ class RunnerRalphActivities:
|
|
|
3148
3169
|
source=AGENT_RUNTIME_EVIDENCE_SOURCE,
|
|
3149
3170
|
actor=self._actor(),
|
|
3150
3171
|
payload={
|
|
3151
|
-
"event":
|
|
3172
|
+
"event": (
|
|
3173
|
+
"directive.auth_mode_refused"
|
|
3174
|
+
if access is None
|
|
3175
|
+
else "directive.llm_access_refused"
|
|
3176
|
+
),
|
|
3152
3177
|
"rule": refusal.rule,
|
|
3153
3178
|
"cli_kind": state.cli_kind,
|
|
3154
3179
|
"host_party": host_party,
|
|
3155
3180
|
"contract_id": state.contract_id,
|
|
3181
|
+
**({} if access is None else {"llm_access": access.kind}),
|
|
3156
3182
|
},
|
|
3157
3183
|
)
|
|
3158
3184
|
|
|
@@ -3167,16 +3193,47 @@ class RunnerRalphActivities:
|
|
|
3167
3193
|
)
|
|
3168
3194
|
|
|
3169
3195
|
def _api_key_present(self, state: _RuntimeContextState, runtime: AgentRuntime | None) -> bool:
|
|
3170
|
-
"""A
|
|
3196
|
+
"""A slot the attempt's provider would resolve: delivered for the Contract, or the
|
|
3197
|
+
host store's key (rev 3); or a key the host configured on the runtime itself."""
|
|
3171
3198
|
|
|
3172
3199
|
if bool(getattr(runtime, "host_api_key", False)):
|
|
3173
3200
|
return True
|
|
3174
|
-
|
|
3201
|
+
return self._llm_proxy is not None and self._llm_proxy.slots.holds(
|
|
3202
|
+
_optional_uuid(state.contract_id),
|
|
3203
|
+
provider_for(state.cli_kind),
|
|
3204
|
+
delivered_only=not self._personal_runner(),
|
|
3205
|
+
)
|
|
3206
|
+
|
|
3207
|
+
def _personal_runner(self) -> bool:
|
|
3208
|
+
"""Hosted by its person. Only such a Runner spends its store's key on a Directive
|
|
3209
|
+
that names no access (a control plane below 3.25): a pool Runner's key never
|
|
3210
|
+
reached the proxy before, and that control plane caps none of its spend."""
|
|
3211
|
+
|
|
3212
|
+
return self._routing_identity is not None and self._routing_identity.host_party == "user"
|
|
3213
|
+
|
|
3214
|
+
def _host_key_held(self, state: _RuntimeContextState) -> bool:
|
|
3215
|
+
provider = provider_for(state.cli_kind)
|
|
3175
3216
|
return (
|
|
3176
3217
|
self._llm_proxy is not None
|
|
3177
|
-
and
|
|
3178
|
-
and self._llm_proxy.slots.
|
|
3218
|
+
and provider is not None
|
|
3219
|
+
and self._llm_proxy.slots.holds_host(provider.value)
|
|
3220
|
+
)
|
|
3221
|
+
|
|
3222
|
+
def _subscription_state(self, state: _RuntimeContextState) -> SubscriptionState:
|
|
3223
|
+
if self._contract_isolation is None or not state.contract_id:
|
|
3224
|
+
return SubscriptionState.NONE
|
|
3225
|
+
windows = (
|
|
3226
|
+
self._usage_windows.windows(state.contract_id, state.cli_kind)
|
|
3227
|
+
if self._usage_windows is not None
|
|
3228
|
+
else None
|
|
3179
3229
|
)
|
|
3230
|
+
return subscription_report(
|
|
3231
|
+
self._contract_isolation,
|
|
3232
|
+
state.contract_id,
|
|
3233
|
+
state.cli_kind,
|
|
3234
|
+
windows=windows,
|
|
3235
|
+
now=datetime.now(UTC),
|
|
3236
|
+
).state
|
|
3180
3237
|
|
|
3181
3238
|
async def _report_login_residue(self, work_record_id: str, state: _RuntimeContextState) -> None:
|
|
3182
3239
|
"""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,24 @@ 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 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_SIGNED_IN_RULE",
|
|
42
|
+
"SUBSCRIPTION_OFF_PERSONAL_RUNNER_RULE",
|
|
30
43
|
"SHARED_HOST_PARTIES",
|
|
31
44
|
"SIGN_IN_NEEDS_CONTRACT_UID_RULE",
|
|
32
45
|
"SUBSCRIPTION_NEEDS_CONTRACT_UID_RULE",
|
|
33
46
|
"SUBSCRIPTION_NEEDS_HARNESS_LOGIN_RULE",
|
|
34
47
|
"AuthModeRefusedError",
|
|
35
48
|
"choose_auth_mode",
|
|
49
|
+
"require_named_access",
|
|
36
50
|
"is_setup_token",
|
|
37
51
|
"is_shared_runner",
|
|
38
52
|
"runs_subscriptions",
|
|
@@ -46,6 +60,12 @@ SUBSCRIPTION_NEEDS_HARNESS_LOGIN_RULE: Final[str] = "subscription_needs_harness_
|
|
|
46
60
|
NO_RUNTIME_SUBSCRIPTION_RULE: Final[str] = "runtime_has_no_subscription_mode"
|
|
47
61
|
SIGN_IN_NEEDS_CONTRACT_UID_RULE: Final[str] = "sign_in_needs_contract_uid"
|
|
48
62
|
SETUP_TOKEN_REFUSED_RULE: Final[str] = "setup_token_refused"
|
|
63
|
+
# A Directive whose named `llm_access` this Runner no longer holds (rev 3).
|
|
64
|
+
HOST_KEY_NOT_HELD_RULE: Final[str] = "host_key_not_held"
|
|
65
|
+
SUBSCRIPTION_NOT_SIGNED_IN_RULE: Final[str] = "subscription_not_signed_in"
|
|
66
|
+
SUBSCRIPTION_OFF_PERSONAL_RUNNER_RULE: Final[str] = "subscription_off_personal_runner"
|
|
67
|
+
LLM_KEY_UNSUPPORTED_RULE: Final[str] = "llm_key_unsupported"
|
|
68
|
+
LLM_ACCESS_UNKNOWN_RULE: Final[str] = "llm_access_unknown"
|
|
49
69
|
# `claude setup-token` mints an OAuth bearer under this prefix; an Anthropic API key is
|
|
50
70
|
# `sk-ant-api…`. The sealed wire carries no mode, so the value's own shape is what the
|
|
51
71
|
# Runner can refuse on.
|
|
@@ -122,5 +142,59 @@ def choose_auth_mode(
|
|
|
122
142
|
return AuthMode.SUBSCRIPTION
|
|
123
143
|
|
|
124
144
|
|
|
145
|
+
def require_named_access(
|
|
146
|
+
access: LlmAccess,
|
|
147
|
+
*,
|
|
148
|
+
host_party: str | None,
|
|
149
|
+
host_key_held: bool,
|
|
150
|
+
subscription: SubscriptionState,
|
|
151
|
+
runtime_modes: Collection[AuthMode],
|
|
152
|
+
) -> AuthMode:
|
|
153
|
+
"""The mode the named access runs in, or a refusal naming what is missing.
|
|
154
|
+
|
|
155
|
+
``host_key_held`` is the provider's key in this Runner's own store, serving in the
|
|
156
|
+
proxy; ``subscription`` the Contract's harness root for the Directive's kind. A
|
|
157
|
+
subscription runs only on its person's own Runner (rev 3: never on a pool Runner),
|
|
158
|
+
and only while it is signed in, unexpired and not at its usage limit.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
if access.kind == LlmAccessKind.HOST_KEY:
|
|
162
|
+
if not host_key_held:
|
|
163
|
+
raise AuthModeRefusedError(
|
|
164
|
+
HOST_KEY_NOT_HELD_RULE,
|
|
165
|
+
"this Directive was routed to the provider's key in this Runner's own store, "
|
|
166
|
+
"and the store no longer holds a key the provider accepts",
|
|
167
|
+
)
|
|
168
|
+
return AuthMode.API_KEY
|
|
169
|
+
if access.kind == LlmAccessKind.SUBSCRIPTION:
|
|
170
|
+
if host_party != "user":
|
|
171
|
+
raise AuthModeRefusedError(
|
|
172
|
+
SUBSCRIPTION_OFF_PERSONAL_RUNNER_RULE,
|
|
173
|
+
f"this Runner is hosted by {host_party!r}; a subscription runs only on its "
|
|
174
|
+
"person's own Runner",
|
|
175
|
+
)
|
|
176
|
+
if AuthMode.SUBSCRIPTION not in runtime_modes:
|
|
177
|
+
raise AuthModeRefusedError(
|
|
178
|
+
NO_RUNTIME_SUBSCRIPTION_RULE,
|
|
179
|
+
"this Runner's runtime for the Directive's CLI kind runs on API keys only",
|
|
180
|
+
)
|
|
181
|
+
if subscription != SubscriptionState.SIGNED_IN:
|
|
182
|
+
raise AuthModeRefusedError(
|
|
183
|
+
SUBSCRIPTION_NOT_SIGNED_IN_RULE,
|
|
184
|
+
f"this Directive was routed to the person's subscription, and its sign-in on "
|
|
185
|
+
f"this Runner is {subscription.value}; run `agentic-runner sign-in` here",
|
|
186
|
+
)
|
|
187
|
+
return AuthMode.SUBSCRIPTION
|
|
188
|
+
if access.kind == LlmAccessKind.LLM_KEY:
|
|
189
|
+
raise AuthModeRefusedError(
|
|
190
|
+
LLM_KEY_UNSUPPORTED_RULE,
|
|
191
|
+
"this Directive was routed to one of the pool's LLM Keys, which this Runner "
|
|
192
|
+
"release cannot open; update the Runner",
|
|
193
|
+
)
|
|
194
|
+
raise AuthModeRefusedError(
|
|
195
|
+
LLM_ACCESS_UNKNOWN_RULE, f"this Runner does not know the LLM access {access.kind!r}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
125
199
|
def is_setup_token(value: str) -> bool:
|
|
126
200
|
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:
|
|
@@ -44,6 +44,7 @@ __all__ = [
|
|
|
44
44
|
"EmptyCredentialStore",
|
|
45
45
|
"FakeCredentialStore",
|
|
46
46
|
"HostCredentialStore",
|
|
47
|
+
"LayeredCredentialStore",
|
|
47
48
|
"UnresolvableCredentialReferenceError",
|
|
48
49
|
]
|
|
49
50
|
|
|
@@ -115,6 +116,25 @@ class EmptyCredentialStore:
|
|
|
115
116
|
return None
|
|
116
117
|
|
|
117
118
|
|
|
119
|
+
class LayeredCredentialStore:
|
|
120
|
+
"""Several host stores read as one, the first that holds a value winning.
|
|
121
|
+
|
|
122
|
+
A container Runner's store is a mounted Secret, read-only, so a key the person types
|
|
123
|
+
into ``agentic-runner sign-in`` goes to a store in the state directory, read first:
|
|
124
|
+
it is the person's later act on their own Runner.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, *stores: HostCredentialStore) -> None:
|
|
128
|
+
self._stores = stores
|
|
129
|
+
|
|
130
|
+
def get(self, reference: str) -> str | None:
|
|
131
|
+
for store in self._stores:
|
|
132
|
+
value = store.get(reference)
|
|
133
|
+
if value is not None:
|
|
134
|
+
return value
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
118
138
|
class UnresolvableCredentialReferenceError(RuntimeError):
|
|
119
139
|
"""A Directive named a Credential Reference it cannot have (22 A1's fail-closed half).
|
|
120
140
|
|
|
@@ -205,12 +225,9 @@ class CredentialResolver:
|
|
|
205
225
|
value is stating it is theirs to state; the host's own install is the fallback, which
|
|
206
226
|
is exactly the hand-over default.
|
|
207
227
|
|
|
208
|
-
The
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Contract in that pool brings sealed values or finds the reference unusable. A
|
|
212
|
-
personal Runner lends to every Contract it runs, because it runs only its person's
|
|
213
|
-
own (§2, §42). A blank ``host_party`` -- a Runner not yet registered -- lends nothing.
|
|
228
|
+
The host's store serves every Contract the Runner runs (Spec: Runners rev 3): the
|
|
229
|
+
Contract's hosts already say whose Runners, and so whose secrets, its work may use.
|
|
230
|
+
ADR-0022 §36's limit to the Contracts its host funds went with `funding`.
|
|
214
231
|
"""
|
|
215
232
|
|
|
216
233
|
def __init__(
|
|
@@ -218,14 +235,9 @@ class CredentialResolver:
|
|
|
218
235
|
*,
|
|
219
236
|
store: HostCredentialStore,
|
|
220
237
|
delivered: Mapping[tuple[str, str], str] | None = None,
|
|
221
|
-
host_party: str | None = None,
|
|
222
238
|
) -> None:
|
|
223
239
|
self._store = store
|
|
224
240
|
self._delivered = dict(delivered or {})
|
|
225
|
-
self._host_party = host_party
|
|
226
|
-
# The funding party the last ack named per Contract, for the readers that have no
|
|
227
|
-
# Directive to carry one: the heartbeat's report and an OAuth Connector's secret.
|
|
228
|
-
self._funding: dict[str, str] = {}
|
|
229
241
|
|
|
230
242
|
@property
|
|
231
243
|
def host_store(self) -> HostCredentialStore:
|
|
@@ -243,33 +255,22 @@ class CredentialResolver:
|
|
|
243
255
|
|
|
244
256
|
self._delivered.pop((str(contract_id), slot), None)
|
|
245
257
|
|
|
246
|
-
def fund(self, funding: Mapping[str, str]) -> None:
|
|
247
|
-
"""Replace the per-Contract funding parties with the last ack's (ADR-0022 §7)."""
|
|
248
|
-
|
|
249
|
-
self._funding = dict(funding)
|
|
250
|
-
|
|
251
258
|
def resolve(
|
|
252
259
|
self,
|
|
253
260
|
*,
|
|
254
261
|
contract_id: str | None,
|
|
255
262
|
manifest: Iterable[str],
|
|
256
|
-
funding_party: str | None = None,
|
|
257
263
|
) -> DirectiveCredentials:
|
|
258
264
|
"""Every reference the Contract declares, or the first failure, named.
|
|
259
265
|
|
|
260
|
-
``funding_party`` is the Directive's own (``DirectiveRouting.funding_party``); a
|
|
261
|
-
reader without a Directive leaves it out and the last ack's word for the Contract
|
|
262
|
-
stands in, and with neither the host store is not lent.
|
|
263
|
-
|
|
264
266
|
Fails on the *first* unresolvable reference rather than collecting them: a
|
|
265
267
|
Directive that cannot have one of its credentials is not going to run, and a
|
|
266
268
|
partial resolution left lying around is a value held for no reason.
|
|
267
269
|
"""
|
|
268
270
|
|
|
269
|
-
lends = self._lends_host_store(contract_id, funding_party)
|
|
270
271
|
values: dict[str, str] = {}
|
|
271
272
|
for reference in manifest:
|
|
272
|
-
value = self._value(contract_id, reference
|
|
273
|
+
value = self._value(contract_id, reference)
|
|
273
274
|
if value is None:
|
|
274
275
|
raise UnresolvableCredentialReferenceError(
|
|
275
276
|
reference, contract_id=contract_id, reason="not_installed"
|
|
@@ -280,18 +281,8 @@ class CredentialResolver:
|
|
|
280
281
|
def usable(self, contract_id: str, reference: str) -> bool:
|
|
281
282
|
"""Whether a Directive of this Contract would resolve ``reference`` now (§7)."""
|
|
282
283
|
|
|
283
|
-
|
|
284
|
-
return self._value(contract_id, reference, lends=lends) is not None
|
|
284
|
+
return self._value(contract_id, reference) is not None
|
|
285
285
|
|
|
286
|
-
def _value(self, contract_id: str | None, reference: str
|
|
286
|
+
def _value(self, contract_id: str | None, reference: str) -> str | None:
|
|
287
287
|
value = self._delivered.get((str(contract_id), reference))
|
|
288
|
-
if value is None
|
|
289
|
-
value = self._store.get(reference)
|
|
290
|
-
return value
|
|
291
|
-
|
|
292
|
-
def _lends_host_store(self, contract_id: str | None, funding_party: str | None) -> bool:
|
|
293
|
-
if self._host_party == "user":
|
|
294
|
-
return True
|
|
295
|
-
if funding_party is None:
|
|
296
|
-
funding_party = self._funding.get(str(contract_id), "")
|
|
297
|
-
return bool(self._host_party) and funding_party == self._host_party
|
|
288
|
+
return value if value is not None else self._store.get(reference)
|