agentic-runner 3.4.2__tar.gz → 3.6.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.4.2 → agentic_runner-3.6.0}/PKG-INFO +1 -1
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/activities.py +92 -30
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/device_login_activities.py +30 -12
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/sealed_box.py +15 -4
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/service.py +55 -3
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/testing/control_plane.py +2 -2
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/_runtime_support.py +23 -6
- agentic_runner-3.6.0/src/agentic_runner/workers/acp_runtime.py +938 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/agent_runtime.py +32 -0
- agentic_runner-3.6.0/src/agentic_runner/workers/claude_sign_in.py +534 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/contract_device_login.py +32 -14
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/settings.py +7 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/.gitignore +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/LICENSE +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/README.md +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/pyproject.toml +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/auth_mode.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/cli.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/credentials.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/heartbeat_link.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/llm_proxy.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/testing/test_conformance.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/skills.py +0 -0
- {agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workstation.py +0 -0
|
@@ -29,7 +29,7 @@ import shlex
|
|
|
29
29
|
import shutil
|
|
30
30
|
import tempfile
|
|
31
31
|
import time
|
|
32
|
-
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
|
|
32
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping, Sequence
|
|
33
33
|
from dataclasses import asdict, dataclass, field, fields, replace
|
|
34
34
|
from datetime import UTC, datetime
|
|
35
35
|
from pathlib import Path
|
|
@@ -128,6 +128,8 @@ from agentic_runner.workers.agent_runtime import (
|
|
|
128
128
|
AuthMode,
|
|
129
129
|
DirectiveRequest,
|
|
130
130
|
DirectiveResult,
|
|
131
|
+
ModelUsage,
|
|
132
|
+
PermissionFallback,
|
|
131
133
|
ResumableAgentRuntime,
|
|
132
134
|
)
|
|
133
135
|
from agentic_runner.workers.contract_device_login import login_file_counts
|
|
@@ -588,6 +590,9 @@ class _RuntimeContextState:
|
|
|
588
590
|
# on the Profile's `command_policy` blob — the sandbox floor already lives there, and
|
|
589
591
|
# a ceiling is exactly a floor rule. None means the Runner's own default applies.
|
|
590
592
|
memory_limit_bytes: int | None = None
|
|
593
|
+
# PRD decision 12: what a runtime asked per command does when the command policy has
|
|
594
|
+
# no answer. Carried on the same `command_policy` blob as the ceiling above.
|
|
595
|
+
permission_fallback: PermissionFallback = PermissionFallback.DENY
|
|
591
596
|
# Pushed on change and applied on receipt (ADR-0011 §11, PRD issue 44): the evaluator
|
|
592
597
|
# is local, so a narrowing lands on the next verb this state is read for.
|
|
593
598
|
grant_snapshot: GrantSnapshot = UNENFORCED_SNAPSHOT
|
|
@@ -1296,6 +1301,56 @@ def _profile_memory_limit_bytes(command_policy: Mapping[str, object]) -> int | N
|
|
|
1296
1301
|
return None
|
|
1297
1302
|
|
|
1298
1303
|
|
|
1304
|
+
def _model_usage_reports(
|
|
1305
|
+
cli_kind: str, model_usage: Sequence[ModelUsage]
|
|
1306
|
+
) -> list[tuple[dict[str, Any], str | None]]:
|
|
1307
|
+
"""A harness's per-model usage in the shapes the harness usage route already prices.
|
|
1308
|
+
|
|
1309
|
+
Claude Code: one ``result`` message whose ``modelUsage`` names every model. Codex: one
|
|
1310
|
+
``turn.completed.usage`` per model, whose ``input_tokens`` includes the cached reads,
|
|
1311
|
+
with the model the harness itself named rather than the Agent's configured one.
|
|
1312
|
+
"""
|
|
1313
|
+
|
|
1314
|
+
if cli_kind == "claude_code":
|
|
1315
|
+
return [
|
|
1316
|
+
(
|
|
1317
|
+
{
|
|
1318
|
+
"type": "result",
|
|
1319
|
+
"modelUsage": {
|
|
1320
|
+
row.model: {
|
|
1321
|
+
"inputTokens": row.input_tokens,
|
|
1322
|
+
"outputTokens": row.output_tokens,
|
|
1323
|
+
"cacheReadInputTokens": row.cached_read_tokens,
|
|
1324
|
+
"cacheCreationInputTokens": row.cached_write_tokens,
|
|
1325
|
+
}
|
|
1326
|
+
for row in model_usage
|
|
1327
|
+
},
|
|
1328
|
+
},
|
|
1329
|
+
None,
|
|
1330
|
+
)
|
|
1331
|
+
]
|
|
1332
|
+
return [
|
|
1333
|
+
(
|
|
1334
|
+
{
|
|
1335
|
+
"input_tokens": row.input_tokens + row.cached_read_tokens,
|
|
1336
|
+
"cached_input_tokens": row.cached_read_tokens,
|
|
1337
|
+
"output_tokens": row.output_tokens,
|
|
1338
|
+
"reasoning_output_tokens": row.reasoning_output_tokens,
|
|
1339
|
+
},
|
|
1340
|
+
row.model,
|
|
1341
|
+
)
|
|
1342
|
+
for row in model_usage
|
|
1343
|
+
]
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
def _profile_permission_fallback(command_policy: Mapping[str, object]) -> PermissionFallback:
|
|
1347
|
+
"""The Profile's `permission_fallback`; anything but exactly ``hold`` is ``deny``."""
|
|
1348
|
+
|
|
1349
|
+
if command_policy.get("permission_fallback") == PermissionFallback.HOLD.value:
|
|
1350
|
+
return PermissionFallback.HOLD
|
|
1351
|
+
return PermissionFallback.DENY
|
|
1352
|
+
|
|
1353
|
+
|
|
1299
1354
|
def _contract_workspace_path(
|
|
1300
1355
|
*,
|
|
1301
1356
|
workspace_root: Path,
|
|
@@ -1992,6 +2047,7 @@ class RunnerRalphActivities:
|
|
|
1992
2047
|
mcp_servers=attempt.mcp_servers,
|
|
1993
2048
|
egress_allow_list=attempt.egress_allow_list,
|
|
1994
2049
|
auth_mode=attempt.auth_mode,
|
|
2050
|
+
permission_fallback=runtime_state.permission_fallback,
|
|
1995
2051
|
prompt=attempt.skills_preamble
|
|
1996
2052
|
+ _directive_prompt(
|
|
1997
2053
|
completion_criteria=runtime_state.completion_criteria,
|
|
@@ -3363,6 +3419,7 @@ class RunnerRalphActivities:
|
|
|
3363
3419
|
cli_kind=runtime_context.cli_kind,
|
|
3364
3420
|
model=runtime_context.model,
|
|
3365
3421
|
memory_limit_bytes=_profile_memory_limit_bytes(runtime_context.command_policy),
|
|
3422
|
+
permission_fallback=_profile_permission_fallback(runtime_context.command_policy),
|
|
3366
3423
|
grant_snapshot=snapshot,
|
|
3367
3424
|
grant_snapshot_payload=snapshot_payload,
|
|
3368
3425
|
persona_slug=runtime_context.persona_slug,
|
|
@@ -4636,6 +4693,7 @@ class RunnerRalphActivities:
|
|
|
4636
4693
|
mcp_servers=attempt.mcp_servers,
|
|
4637
4694
|
egress_allow_list=attempt.egress_allow_list,
|
|
4638
4695
|
auth_mode=attempt.auth_mode,
|
|
4696
|
+
permission_fallback=state.permission_fallback,
|
|
4639
4697
|
prompt=attempt.skills_preamble
|
|
4640
4698
|
+ _learning_prompt(
|
|
4641
4699
|
request,
|
|
@@ -4896,6 +4954,7 @@ class RunnerRalphActivities:
|
|
|
4896
4954
|
mcp_servers=attempt.mcp_servers,
|
|
4897
4955
|
egress_allow_list=attempt.egress_allow_list,
|
|
4898
4956
|
auth_mode=attempt.auth_mode,
|
|
4957
|
+
permission_fallback=state.permission_fallback,
|
|
4899
4958
|
prompt=attempt.skills_preamble + await kind.prompt(state, prompt_notes),
|
|
4900
4959
|
base_branch=request.base_ref,
|
|
4901
4960
|
work_branch=state.work_branch,
|
|
@@ -5155,38 +5214,41 @@ class RunnerRalphActivities:
|
|
|
5155
5214
|
if auth_mode == AuthMode.API_KEY:
|
|
5156
5215
|
return
|
|
5157
5216
|
directive = directive_id(work_record_id=work_record_id, directive_number=directive_number)
|
|
5158
|
-
|
|
5217
|
+
reports: list[tuple[dict[str, Any], str | None]]
|
|
5218
|
+
if result.model_usage:
|
|
5219
|
+
reports = _model_usage_reports(runtime_state.cli_kind, result.model_usage)
|
|
5220
|
+
elif runtime_state.cli_kind == "claude_code":
|
|
5159
5221
|
usage_event = extract_claude_result(result.stdout)
|
|
5160
|
-
|
|
5222
|
+
reports = [(usage_event, None)] if usage_event else []
|
|
5161
5223
|
else:
|
|
5162
5224
|
usage_event = extract_codex_turn_usage(result.stdout)
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5225
|
+
reports = [(usage_event, runtime_state.model or None)] if usage_event else []
|
|
5226
|
+
for usage_event, model_name in reports:
|
|
5227
|
+
if runtime_state.cli_kind != "claude_code" and model_name is None:
|
|
5228
|
+
# The backend 422s a Codex harness report with no model
|
|
5229
|
+
# (`report_harness_usage` requires one to price it), and the `suppress`
|
|
5230
|
+
# below swallows that -- an Agent with no model configured would otherwise
|
|
5231
|
+
# lose this Usage Record with no trace.
|
|
5232
|
+
activity.logger.warning(
|
|
5233
|
+
"harness usage unreported: Agent %s has no model configured for a Codex "
|
|
5234
|
+
"harness Directive (work record %s)",
|
|
5235
|
+
runtime_state.agent_id,
|
|
5236
|
+
work_record_id,
|
|
5237
|
+
)
|
|
5238
|
+
with contextlib.suppress(Exception): # noqa: BLE001 - metering never fails a Directive
|
|
5239
|
+
await self._fastapi_client.report_harness_usage(
|
|
5240
|
+
{
|
|
5241
|
+
"work_record_id": work_record_id,
|
|
5242
|
+
"agent_id": runtime_state.agent_id,
|
|
5243
|
+
"contract_id": runtime_state.contract_id,
|
|
5244
|
+
"directive_id": directive,
|
|
5245
|
+
"runner_id": None,
|
|
5246
|
+
"runtime_kind": runtime_state.cli_kind,
|
|
5247
|
+
"provider_name": runtime_state.cli_kind,
|
|
5248
|
+
"model_name": model_name,
|
|
5249
|
+
"usage_event": usage_event,
|
|
5250
|
+
}
|
|
5251
|
+
)
|
|
5190
5252
|
|
|
5191
5253
|
async def _harness_hold(
|
|
5192
5254
|
self,
|
|
@@ -13,7 +13,8 @@ from datetime import UTC, datetime
|
|
|
13
13
|
from temporalio import activity
|
|
14
14
|
from temporalio.exceptions import ApplicationError
|
|
15
15
|
|
|
16
|
-
from agentic_runner.auth_mode import SHARED_RUNNER_SIGN_IN_RULE
|
|
16
|
+
from agentic_runner.auth_mode import SHARED_RUNNER_SIGN_IN_RULE, is_shared_runner
|
|
17
|
+
from agentic_runner.workers.claude_sign_in import OAUTH_TOKEN
|
|
17
18
|
from agentic_runner.workers.contract_device_login import ContractDeviceLogin
|
|
18
19
|
from agentic_runner.workers.contract_isolation import ContractIsolation
|
|
19
20
|
from agentic_runner_contracts.activity_io import (
|
|
@@ -36,10 +37,10 @@ class ContractDeviceLoginActivities:
|
|
|
36
37
|
host_party: str | None = None,
|
|
37
38
|
) -> None:
|
|
38
39
|
self._contract_isolation = contract_isolation
|
|
39
|
-
# The party this process was registered as hosted by. A shared Runner
|
|
40
|
-
#
|
|
40
|
+
# The party this process was registered as hosted by. A shared Runner signs in only
|
|
41
|
+
# a Claude Code long-lived token (local-agents 04, amended by 21).
|
|
41
42
|
self._host_party = host_party
|
|
42
|
-
# Injectable for tests; None falls through to the real
|
|
43
|
+
# Injectable for tests; None falls through to the real vendor CLIs.
|
|
43
44
|
self._device_login = device_login or ContractDeviceLogin(contract_isolation)
|
|
44
45
|
|
|
45
46
|
def activity_callables(self) -> list[Callable[..., object]]:
|
|
@@ -52,9 +53,12 @@ class ContractDeviceLoginActivities:
|
|
|
52
53
|
async def sign_in_contract_device_login(
|
|
53
54
|
self, request: ContractDeviceLoginInput
|
|
54
55
|
) -> ContractDeviceLoginResult:
|
|
55
|
-
|
|
56
|
+
long_lived = request.runtime_kind == "claude_code" and (request.method or OAUTH_TOKEN) == (
|
|
57
|
+
OAUTH_TOKEN
|
|
58
|
+
)
|
|
59
|
+
self._refuse_on_shared_runner(allow=long_lived)
|
|
56
60
|
prompt = await self._device_login.sign_in(
|
|
57
|
-
request.contract_id, runtime_kind=request.runtime_kind
|
|
61
|
+
request.contract_id, runtime_kind=request.runtime_kind, method=request.method
|
|
58
62
|
)
|
|
59
63
|
return ContractDeviceLoginResult(
|
|
60
64
|
contract_id=prompt.contract_id,
|
|
@@ -62,12 +66,24 @@ class ContractDeviceLoginActivities:
|
|
|
62
66
|
verification_uri=prompt.verification_uri,
|
|
63
67
|
user_code=prompt.user_code,
|
|
64
68
|
expires_at=prompt.expires_at.isoformat(),
|
|
69
|
+
sign_in_id=prompt.sign_in_id,
|
|
65
70
|
)
|
|
66
71
|
|
|
67
72
|
@activity.defn(name="check_contract_device_login_status")
|
|
68
73
|
async def check_contract_device_login_status(
|
|
69
74
|
self, request: ContractDeviceLoginStatusInput
|
|
70
75
|
) -> ContractDeviceLoginStatusResult:
|
|
76
|
+
if request.runtime_kind == "claude_code":
|
|
77
|
+
self._refuse_on_shared_runner(allow=True)
|
|
78
|
+
status = await self._device_login.claude.status(request.contract_id)
|
|
79
|
+
return ContractDeviceLoginStatusResult(
|
|
80
|
+
contract_id=request.contract_id,
|
|
81
|
+
runtime_kind=request.runtime_kind,
|
|
82
|
+
token_present=status.logged_in,
|
|
83
|
+
delivered_at=None,
|
|
84
|
+
auth_method=status.auth_method,
|
|
85
|
+
subscription_type=status.subscription_type,
|
|
86
|
+
)
|
|
71
87
|
self._refuse_on_shared_runner()
|
|
72
88
|
present = self._device_login.token_present(
|
|
73
89
|
request.contract_id, runtime_kind=request.runtime_kind
|
|
@@ -86,18 +102,20 @@ class ContractDeviceLoginActivities:
|
|
|
86
102
|
delivered_at=delivered_at,
|
|
87
103
|
)
|
|
88
104
|
|
|
89
|
-
def _refuse_on_shared_runner(self) -> None:
|
|
105
|
+
def _refuse_on_shared_runner(self, *, allow: bool = False) -> None:
|
|
90
106
|
"""Non-retryable, and typed by the rule: there is no Work Record to write Evidence
|
|
91
107
|
on here, so the failure the platform's sign-in workflow receives names it.
|
|
92
108
|
|
|
93
|
-
Only a Runner known to be the person's own signs in
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
Only a Runner known to be the person's own signs in, except that a shared Runner
|
|
110
|
+
may make (``allow``) a Claude Code long-lived token (local-agents 21). One whose
|
|
111
|
+
state names no host party (registered before issue 42) is refused either way, as
|
|
112
|
+
`choose_auth_mode` never runs a subscription on it."""
|
|
96
113
|
|
|
97
|
-
if self._host_party != "user":
|
|
114
|
+
if self._host_party != "user" and not (allow and is_shared_runner(self._host_party)):
|
|
98
115
|
raise ApplicationError(
|
|
99
116
|
f"this Runner is hosted by {self._host_party!r}; only a person's own Runner "
|
|
100
|
-
"signs in, a shared Runner runs API keys
|
|
117
|
+
"signs in, a shared Runner runs API keys and Claude Code long-lived tokens "
|
|
118
|
+
"only",
|
|
101
119
|
type=SHARED_RUNNER_SIGN_IN_RULE,
|
|
102
120
|
non_retryable=True,
|
|
103
121
|
)
|
|
@@ -61,6 +61,7 @@ from agentic_runner_contracts.sealed_credential import (
|
|
|
61
61
|
SEAL_DOMAIN,
|
|
62
62
|
OpenedCredential,
|
|
63
63
|
SealedCredential,
|
|
64
|
+
SealedSignInCode,
|
|
64
65
|
delivery_binding,
|
|
65
66
|
key_fingerprint,
|
|
66
67
|
)
|
|
@@ -516,15 +517,25 @@ class SealedCredentialStream:
|
|
|
516
517
|
if not self._awaiting:
|
|
517
518
|
self._keys.retire_previous()
|
|
518
519
|
|
|
520
|
+
def open_sign_in_code(self, sealed: SealedSignInCode) -> str | None:
|
|
521
|
+
"""A relayed sign-in code, or None when it does not open (local-agents 05).
|
|
522
|
+
|
|
523
|
+
Opened with the same Recipient Key as a slot, but never held: the caller types it
|
|
524
|
+
into the waiting CLI and drops it.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
return self._open_with(sealed.recipient_key_id, sealed.binding(), sealed.ciphertext)
|
|
528
|
+
|
|
519
529
|
def _open(self, sealed: SealedCredential) -> str | None:
|
|
530
|
+
return self._open_with(sealed.recipient_key_id, sealed.binding(), sealed.ciphertext)
|
|
531
|
+
|
|
532
|
+
def _open_with(self, recipient_key_id: str, binding: bytes, ciphertext: str) -> str | None:
|
|
520
533
|
for pair in (self._keys.current(), self._keys.previous()):
|
|
521
|
-
if pair is None or pair.key_id !=
|
|
534
|
+
if pair is None or pair.key_id != recipient_key_id:
|
|
522
535
|
continue
|
|
523
536
|
try:
|
|
524
537
|
return open_sealed(
|
|
525
|
-
private_key=pair.private_key,
|
|
526
|
-
binding=sealed.binding(),
|
|
527
|
-
ciphertext=sealed.ciphertext,
|
|
538
|
+
private_key=pair.private_key, binding=binding, ciphertext=ciphertext
|
|
528
539
|
)
|
|
529
540
|
except SealedCredentialError:
|
|
530
541
|
return None
|
|
@@ -99,9 +99,12 @@ from agentic_runner.sealed_box import (
|
|
|
99
99
|
from agentic_runner.tiny_http import HttpRequest, read_request, write_json
|
|
100
100
|
from agentic_runner.triage_activities import RunnerTriageActivities
|
|
101
101
|
from agentic_runner.user_sources import ProxyTriage, UserSourcePoller
|
|
102
|
+
from agentic_runner.workers.acp_runtime import ACP_BRIDGES, AcpRuntime
|
|
102
103
|
from agentic_runner.workers.agent_runtime import AgentRuntime
|
|
103
104
|
from agentic_runner.workers.claude_runtime import ClaudeRuntime
|
|
105
|
+
from agentic_runner.workers.claude_sign_in import ClaudeSignIns
|
|
104
106
|
from agentic_runner.workers.codex_runtime import CodexRuntime
|
|
107
|
+
from agentic_runner.workers.contract_device_login import ContractDeviceLogin
|
|
105
108
|
from agentic_runner.workers.contract_isolation import UID_MAP_FILENAME, ContractIsolation
|
|
106
109
|
from agentic_runner.workers.fastapi_client import DirectiveTokenSource, RunnerFastApiClient
|
|
107
110
|
from agentic_runner.workers.settings import WorkerSettings, get_worker_settings
|
|
@@ -123,7 +126,11 @@ from agentic_runner_contracts.runner_registration import (
|
|
|
123
126
|
ResourcePressure,
|
|
124
127
|
)
|
|
125
128
|
from agentic_runner_contracts.runtime_context import WorkerRuntimeContextResolver
|
|
126
|
-
from agentic_runner_contracts.sealed_credential import
|
|
129
|
+
from agentic_runner_contracts.sealed_credential import (
|
|
130
|
+
SignInCodeOutcome,
|
|
131
|
+
SignInCodeRelay,
|
|
132
|
+
key_fingerprint,
|
|
133
|
+
)
|
|
127
134
|
|
|
128
135
|
__all__ = [
|
|
129
136
|
"AGENT_TOKEN_ENV",
|
|
@@ -388,10 +395,18 @@ def build_agent_runtimes(
|
|
|
388
395
|
picks from here by the Work Record's `cli_kind` (local-agents 01) -- there is no
|
|
389
396
|
process-wide choice. The same `clis` ride the heartbeat's attestation, so what this
|
|
390
397
|
Runner serves and what routing believes it serves are one list.
|
|
398
|
+
|
|
399
|
+
Which runtime serves a kind -- its pinned ACP bridge or the per-CLI one -- is this
|
|
400
|
+
Runner's setting (`ACP_CLI_KINDS`), never the payload's (local-agents 12 items 6, 7).
|
|
391
401
|
"""
|
|
392
402
|
|
|
403
|
+
acp_kinds = {kind.strip() for kind in settings.ACP_CLI_KINDS.split(",") if kind.strip()}
|
|
393
404
|
return {
|
|
394
|
-
cli.cli_kind:
|
|
405
|
+
cli.cli_kind: (
|
|
406
|
+
AcpRuntime(cli_kind=cli.cli_kind, settings=settings)
|
|
407
|
+
if cli.cli_kind in acp_kinds and cli.cli_kind in ACP_BRIDGES
|
|
408
|
+
else _RUNTIMES[cli.cli_kind](settings)
|
|
409
|
+
)
|
|
395
410
|
for cli in clis
|
|
396
411
|
if cli.meets_floor and cli.cli_kind in _RUNTIMES
|
|
397
412
|
}
|
|
@@ -568,6 +583,11 @@ class ControlPlaneStream:
|
|
|
568
583
|
self_tests: HarnessSelfTests | None = None
|
|
569
584
|
# Local-agents 04b: which delivered references fill the LLM proxy's slot, and where.
|
|
570
585
|
llm_slots: Mapping[str, LlmProvider] = field(default_factory=lambda: dict(LLM_SLOT_REFERENCES))
|
|
586
|
+
# Local-agents 05: the Claude Code sign-ins a relayed code is typed into.
|
|
587
|
+
sign_ins: ClaudeSignIns | None = None
|
|
588
|
+
# Reported once, on the beat after the relay: a lost beat loses the outcome, and the
|
|
589
|
+
# console's own wait on the sign-in is what tells the person.
|
|
590
|
+
_sign_in_outcomes: list[SignInCodeRelay] = field(default_factory=list)
|
|
571
591
|
_delivered: set[tuple[str, str]] = field(default_factory=set)
|
|
572
592
|
_offered_to_proxy: dict[tuple[str, str], str] = field(default_factory=dict)
|
|
573
593
|
_last_seen: tuple[float, datetime] | None = None
|
|
@@ -670,11 +690,14 @@ class ControlPlaneStream:
|
|
|
670
690
|
clock_skew_seconds=self._clock_skew(now),
|
|
671
691
|
source_status=self.sources.statuses() if self.sources is not None else [],
|
|
672
692
|
tool_servers=self.tool_servers.latest() if self.tool_servers is not None else [],
|
|
693
|
+
sign_in_codes=self._sign_in_outcomes,
|
|
673
694
|
)
|
|
695
|
+
self._sign_in_outcomes = []
|
|
674
696
|
ack = await self.client.heartbeat(self.state, envelope)
|
|
675
697
|
if self.sources is not None:
|
|
676
698
|
self.sources.assign(ack.user_sources)
|
|
677
699
|
await self._deliver_transcripts(ack)
|
|
700
|
+
await self._relay_sign_in_codes(ack)
|
|
678
701
|
if self.lifecycle is not None:
|
|
679
702
|
self.lifecycle.acknowledge(lifecycle)
|
|
680
703
|
if self.heartbeat_stamp is not None:
|
|
@@ -721,6 +744,28 @@ class ControlPlaneStream:
|
|
|
721
744
|
"transcript %s could not be delivered: %s", request.request_id, error
|
|
722
745
|
)
|
|
723
746
|
|
|
747
|
+
async def _relay_sign_in_codes(self, ack: HeartbeatAck) -> None:
|
|
748
|
+
"""Type each relayed code into the sign-in it names (local-agents 05).
|
|
749
|
+
|
|
750
|
+
The code is opened, written to that sign-in's PTY and dropped here; nothing logs
|
|
751
|
+
it and only its outcome travels back.
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
for sealed in ack.sign_in_codes:
|
|
755
|
+
code = self.sealed.open_sign_in_code(sealed)
|
|
756
|
+
if code is None:
|
|
757
|
+
outcome = SignInCodeOutcome.UNOPENABLE
|
|
758
|
+
elif self.sign_ins is None:
|
|
759
|
+
outcome = SignInCodeOutcome.UNKNOWN
|
|
760
|
+
else:
|
|
761
|
+
outcome = await self.sign_ins.relay(
|
|
762
|
+
str(sealed.contract_id), sealed.sign_in_id, code
|
|
763
|
+
)
|
|
764
|
+
del code
|
|
765
|
+
self._sign_in_outcomes.append(
|
|
766
|
+
SignInCodeRelay(sign_in_id=sealed.sign_in_id, outcome=outcome)
|
|
767
|
+
)
|
|
768
|
+
|
|
724
769
|
def _sync_delivered(self) -> None:
|
|
725
770
|
"""The resolver holds exactly what the last ack let this installation open."""
|
|
726
771
|
|
|
@@ -1010,6 +1055,8 @@ async def _serve_registered(
|
|
|
1010
1055
|
)
|
|
1011
1056
|
messages = MessageStore(state_dir / MESSAGES_DIR)
|
|
1012
1057
|
stream.messages = messages
|
|
1058
|
+
sign_ins = ClaudeSignIns(build_contract_isolation(settings, can_separate_uids=can_change_uid))
|
|
1059
|
+
stream.sign_ins = sign_ins
|
|
1013
1060
|
tool_servers = ToolServerHealthLog()
|
|
1014
1061
|
stream.tool_servers = tool_servers
|
|
1015
1062
|
link = HeartbeatLink(stream, ceilings=proxy.ceilings)
|
|
@@ -1056,6 +1103,7 @@ async def _serve_registered(
|
|
|
1056
1103
|
token_source=stream,
|
|
1057
1104
|
messages=messages,
|
|
1058
1105
|
tool_servers=tool_servers,
|
|
1106
|
+
sign_ins=sign_ins,
|
|
1059
1107
|
# The wake signal rides the connection this process polls with: one
|
|
1060
1108
|
# namespace, the Organisation's own (PRD issue 52, map ticket 15 §2).
|
|
1061
1109
|
signaller=TemporalWorkflowSignaller(client),
|
|
@@ -1083,6 +1131,7 @@ async def _serve_registered(
|
|
|
1083
1131
|
task.cancel()
|
|
1084
1132
|
with contextlib.suppress(asyncio.CancelledError):
|
|
1085
1133
|
await task
|
|
1134
|
+
await sign_ins.close()
|
|
1086
1135
|
await run_runner_lifecycle_hook(hooks, HookName.RUNNER_SHUTDOWN)
|
|
1087
1136
|
await _report_stop(link, lifecycle)
|
|
1088
1137
|
return 1 if revoked.is_set() else 0
|
|
@@ -1195,6 +1244,7 @@ def _activities(
|
|
|
1195
1244
|
messages: MessageStore | None = None,
|
|
1196
1245
|
signaller: TemporalWorkflowSignaller | None = None,
|
|
1197
1246
|
tool_servers: ToolServerHealthLog | None = None,
|
|
1247
|
+
sign_ins: ClaudeSignIns | None = None,
|
|
1198
1248
|
) -> list[Callable[..., Any]]:
|
|
1199
1249
|
github_client = GitHubAppClient(
|
|
1200
1250
|
app_id=os.getenv("GITHUB_APP_ID"),
|
|
@@ -1246,7 +1296,9 @@ def _activities(
|
|
|
1246
1296
|
return [
|
|
1247
1297
|
*ralph.activity_callables(),
|
|
1248
1298
|
*ContractDeviceLoginActivities(
|
|
1249
|
-
contract_isolation=isolation,
|
|
1299
|
+
contract_isolation=isolation,
|
|
1300
|
+
device_login=ContractDeviceLogin(isolation, claude=sign_ins),
|
|
1301
|
+
host_party=state.host_party or None,
|
|
1250
1302
|
).activity_callables(),
|
|
1251
1303
|
*RunnerTriageActivities(
|
|
1252
1304
|
proxy=proxy,
|
|
@@ -37,7 +37,7 @@ from dataclasses import dataclass, field
|
|
|
37
37
|
from datetime import UTC, datetime, timedelta
|
|
38
38
|
from email.utils import formatdate
|
|
39
39
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
40
|
-
from typing import Any
|
|
40
|
+
from typing import Any
|
|
41
41
|
from uuid import UUID, uuid4
|
|
42
42
|
|
|
43
43
|
import httpx
|
|
@@ -166,7 +166,7 @@ class FakeControlPlane:
|
|
|
166
166
|
contracts_floor: str = contracts_version
|
|
167
167
|
contract_id: UUID = DEFAULT_CONTRACT_ID
|
|
168
168
|
agent_id: UUID = DEFAULT_AGENT_ID
|
|
169
|
-
cli_kind:
|
|
169
|
+
cli_kind: str = "codex_cli"
|
|
170
170
|
|
|
171
171
|
bootstraps: list[BootstrapRequest] = field(default_factory=list)
|
|
172
172
|
runner_ids: list[UUID] = field(default_factory=list)
|
{agentic_runner-3.4.2 → agentic_runner-3.6.0}/src/agentic_runner/workers/_runtime_support.py
RENAMED
|
@@ -42,7 +42,10 @@ from agentic_runner.workers.contract_isolation import DirectiveSandbox
|
|
|
42
42
|
# the funder anyway, or route it round the Profile's egress allow-list. The two Claude Code
|
|
43
43
|
# bearers are here for the last reason too: either would authenticate the harness on a
|
|
44
44
|
# credential the Runner never chose, around the proxy and the mode it decided
|
|
45
|
-
# (local-agents 04; research 01 §4 gap 6).
|
|
45
|
+
# (local-agents 04; research 01 §4 gap 6). The ACP bridges' control variables are here
|
|
46
|
+
# for the same reason (local-agents 12): each swaps the vendor binary, hands approvals
|
|
47
|
+
# back to the harness's own reviewer, authenticates the bridge, or re-enables the MCP
|
|
48
|
+
# filtering that drops a granted server.
|
|
46
49
|
RESERVED_DIRECTIVE_ENV: Final[frozenset[str]] = frozenset(
|
|
47
50
|
{
|
|
48
51
|
"HOME",
|
|
@@ -53,6 +56,20 @@ RESERVED_DIRECTIVE_ENV: Final[frozenset[str]] = frozenset(
|
|
|
53
56
|
"ANTHROPIC_API_KEY",
|
|
54
57
|
"ANTHROPIC_AUTH_TOKEN",
|
|
55
58
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
59
|
+
"CODEX_PATH",
|
|
60
|
+
"CODEX_CONFIG",
|
|
61
|
+
"CODEX_API_KEY",
|
|
62
|
+
"MODEL_PROVIDER",
|
|
63
|
+
"DEFAULT_AUTH_REQUEST",
|
|
64
|
+
"INITIAL_AGENT_MODE",
|
|
65
|
+
"NO_BROWSER",
|
|
66
|
+
"APP_SERVER_LOGS",
|
|
67
|
+
"DISABLE_MCP_CONFIG_FILTERING",
|
|
68
|
+
"CLAUDE_CODE_EXECUTABLE",
|
|
69
|
+
"CLAUDE_MODEL_CONFIG",
|
|
70
|
+
"CLAUDE_AGENT_LOGS",
|
|
71
|
+
"MAX_THINKING_TOKENS",
|
|
72
|
+
"ANTHROPIC_MODEL",
|
|
56
73
|
CALLBACK_SOCKET_ENV,
|
|
57
74
|
CALLBACK_TOKEN_ENV,
|
|
58
75
|
*PROXY_ENV_NAMES,
|
|
@@ -154,10 +171,10 @@ async def run_subprocess_exec(
|
|
|
154
171
|
timeout=timeout_seconds,
|
|
155
172
|
)
|
|
156
173
|
except (TimeoutError, asyncio.CancelledError):
|
|
157
|
-
await
|
|
174
|
+
await terminate_process_tree(process)
|
|
158
175
|
raise
|
|
159
176
|
except PriorAttemptAliveError:
|
|
160
|
-
await
|
|
177
|
+
await terminate_process_tree(process)
|
|
161
178
|
raise
|
|
162
179
|
|
|
163
180
|
return SubprocessResult(
|
|
@@ -240,7 +257,7 @@ async def _read_stream_limited(
|
|
|
240
257
|
return bytes(tail), total_bytes > limit_bytes
|
|
241
258
|
|
|
242
259
|
|
|
243
|
-
async def
|
|
260
|
+
async def terminate_process_tree(process: asyncio.subprocess.Process) -> None:
|
|
244
261
|
if process.returncode is not None:
|
|
245
262
|
return
|
|
246
263
|
with contextlib.suppress(ProcessLookupError, PermissionError):
|
|
@@ -379,7 +396,7 @@ async def run_subprocess_launch_and_detach(
|
|
|
379
396
|
raise RuntimeError(f"process failed to start: {error.__class__.__name__}") from None
|
|
380
397
|
|
|
381
398
|
if process.stdout is None:
|
|
382
|
-
await
|
|
399
|
+
await terminate_process_tree(process)
|
|
383
400
|
raise RuntimeError("process stdout stream was unavailable")
|
|
384
401
|
|
|
385
402
|
loop = asyncio.get_running_loop()
|
|
@@ -408,7 +425,7 @@ async def run_subprocess_launch_and_detach(
|
|
|
408
425
|
_detach(process)
|
|
409
426
|
return text
|
|
410
427
|
except BaseException:
|
|
411
|
-
await
|
|
428
|
+
await terminate_process_tree(process)
|
|
412
429
|
raise
|
|
413
430
|
|
|
414
431
|
|