agentic-runner 3.8.0__tar.gz → 3.9.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.8.0 → agentic_runner-3.9.0}/PKG-INFO +1 -1
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/activities.py +53 -1
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/service.py +38 -3
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/_runtime_support.py +1 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/acp_runtime.py +123 -19
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/agent_runtime.py +3 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/settings.py +10 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/.gitignore +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/LICENSE +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/README.md +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/pyproject.toml +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/auth_mode.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/cli.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/credentials.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/device_login_activities.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/heartbeat_link.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/llm_proxy.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/oauth_connectors.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/sealed_box.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/testing/control_plane.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/testing/test_conformance.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/usage_windows.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/contract_device_login.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/skills.py +0 -0
- {agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workstation.py +0 -0
|
@@ -124,6 +124,7 @@ from agentic_runner.workers._runtime_support import (
|
|
|
124
124
|
bound_text,
|
|
125
125
|
bound_text_tail,
|
|
126
126
|
)
|
|
127
|
+
from agentic_runner.workers.acp_runtime import ACP_BRIDGES, AcpRuntime
|
|
127
128
|
from agentic_runner.workers.agent_runtime import (
|
|
128
129
|
AgentRuntime,
|
|
129
130
|
AuthMode,
|
|
@@ -595,6 +596,8 @@ class _RuntimeContextState:
|
|
|
595
596
|
# PRD decision 12: what a runtime asked per command does when the command policy has
|
|
596
597
|
# no answer. Carried on the same `command_policy` blob as the ceiling above.
|
|
597
598
|
permission_fallback: PermissionFallback = PermissionFallback.DENY
|
|
599
|
+
# Local-agents 18: the Profile-named ACP command, for a kind with no pinned bridge.
|
|
600
|
+
acp_command: tuple[str, ...] = ()
|
|
598
601
|
# Pushed on change and applied on receipt (ADR-0011 §11, PRD issue 44): the evaluator
|
|
599
602
|
# is local, so a narrowing lands on the next verb this state is read for.
|
|
600
603
|
grant_snapshot: GrantSnapshot = UNENFORCED_SNAPSHOT
|
|
@@ -2053,6 +2056,7 @@ class RunnerRalphActivities:
|
|
|
2053
2056
|
egress_allow_list=attempt.egress_allow_list,
|
|
2054
2057
|
auth_mode=attempt.auth_mode,
|
|
2055
2058
|
permission_fallback=runtime_state.permission_fallback,
|
|
2059
|
+
acp_command=runtime_state.acp_command,
|
|
2056
2060
|
prompt=attempt.skills_preamble
|
|
2057
2061
|
+ _directive_prompt(
|
|
2058
2062
|
completion_criteria=runtime_state.completion_criteria,
|
|
@@ -3008,10 +3012,55 @@ class RunnerRalphActivities:
|
|
|
3008
3012
|
Fails closed (local-agents 01): a kind this process does not serve fails the
|
|
3009
3013
|
Directive non-retryably, with Evidence naming the kind and the Runner, and never
|
|
3010
3014
|
falls back to another runtime -- routing should not have sent it here, and a
|
|
3011
|
-
retry on this Runner would find the same registry.
|
|
3015
|
+
retry on this Runner would find the same registry. So does a Profile-named ACP
|
|
3016
|
+
command for a kind whose bridge the Runner pins (local-agents 18): the platform
|
|
3017
|
+
never sends one, and the pinned bridge is the only thing that runs for that kind --
|
|
3018
|
+
and, for any other kind, a command whose ``argv[0]`` is not the executable the host
|
|
3019
|
+
pinned for it.
|
|
3012
3020
|
"""
|
|
3013
3021
|
|
|
3022
|
+
if state.acp_command and state.cli_kind in ACP_BRIDGES:
|
|
3023
|
+
await _raise_recorded(
|
|
3024
|
+
ApplicationError(
|
|
3025
|
+
f"the Runner pins the ACP bridge for {state.cli_kind!r}; a Profile-named "
|
|
3026
|
+
"ACP command is refused for it",
|
|
3027
|
+
non_retryable=True,
|
|
3028
|
+
),
|
|
3029
|
+
self._fastapi_client,
|
|
3030
|
+
work_record_id,
|
|
3031
|
+
source=AGENT_RUNTIME_EVIDENCE_SOURCE,
|
|
3032
|
+
actor=self._actor(),
|
|
3033
|
+
payload={
|
|
3034
|
+
"event": "directive.acp_command_refused",
|
|
3035
|
+
"cli_kind": state.cli_kind,
|
|
3036
|
+
# The executable's name only: its arguments may carry a secret.
|
|
3037
|
+
"executable": redact_secret_like_text(Path(state.acp_command[0]).name),
|
|
3038
|
+
},
|
|
3039
|
+
)
|
|
3014
3040
|
runtime = self._agent_runtimes.get(state.cli_kind)
|
|
3041
|
+
command_refusal = (
|
|
3042
|
+
runtime.acp_command_refusal(state.acp_command)
|
|
3043
|
+
if isinstance(runtime, AcpRuntime)
|
|
3044
|
+
else None
|
|
3045
|
+
)
|
|
3046
|
+
if command_refusal is not None:
|
|
3047
|
+
await _raise_recorded(
|
|
3048
|
+
ApplicationError(command_refusal, non_retryable=True),
|
|
3049
|
+
self._fastapi_client,
|
|
3050
|
+
work_record_id,
|
|
3051
|
+
source=AGENT_RUNTIME_EVIDENCE_SOURCE,
|
|
3052
|
+
actor=self._actor(),
|
|
3053
|
+
payload={
|
|
3054
|
+
"event": "directive.acp_command_refused",
|
|
3055
|
+
"cli_kind": state.cli_kind,
|
|
3056
|
+
"executable": (
|
|
3057
|
+
redact_secret_like_text(Path(state.acp_command[0]).name)
|
|
3058
|
+
if state.acp_command
|
|
3059
|
+
else None
|
|
3060
|
+
),
|
|
3061
|
+
"reason": command_refusal,
|
|
3062
|
+
},
|
|
3063
|
+
)
|
|
3015
3064
|
if runtime is not None:
|
|
3016
3065
|
return runtime
|
|
3017
3066
|
served = sorted(self._agent_runtimes)
|
|
@@ -3425,6 +3474,7 @@ class RunnerRalphActivities:
|
|
|
3425
3474
|
model=runtime_context.model,
|
|
3426
3475
|
memory_limit_bytes=_profile_memory_limit_bytes(runtime_context.command_policy),
|
|
3427
3476
|
permission_fallback=_profile_permission_fallback(runtime_context.command_policy),
|
|
3477
|
+
acp_command=tuple(runtime_context.acp_command or ()),
|
|
3428
3478
|
grant_snapshot=snapshot,
|
|
3429
3479
|
grant_snapshot_payload=snapshot_payload,
|
|
3430
3480
|
persona_slug=runtime_context.persona_slug,
|
|
@@ -4699,6 +4749,7 @@ class RunnerRalphActivities:
|
|
|
4699
4749
|
egress_allow_list=attempt.egress_allow_list,
|
|
4700
4750
|
auth_mode=attempt.auth_mode,
|
|
4701
4751
|
permission_fallback=state.permission_fallback,
|
|
4752
|
+
acp_command=state.acp_command,
|
|
4702
4753
|
prompt=attempt.skills_preamble
|
|
4703
4754
|
+ _learning_prompt(
|
|
4704
4755
|
request,
|
|
@@ -4960,6 +5011,7 @@ class RunnerRalphActivities:
|
|
|
4960
5011
|
egress_allow_list=attempt.egress_allow_list,
|
|
4961
5012
|
auth_mode=attempt.auth_mode,
|
|
4962
5013
|
permission_fallback=state.permission_fallback,
|
|
5014
|
+
acp_command=state.acp_command,
|
|
4963
5015
|
prompt=attempt.skills_preamble + await kind.prompt(state, prompt_notes),
|
|
4964
5016
|
base_branch=request.base_ref,
|
|
4965
5017
|
work_branch=state.work_branch,
|
|
@@ -107,7 +107,7 @@ from agentic_runner.usage_windows import (
|
|
|
107
107
|
UsageWindows,
|
|
108
108
|
)
|
|
109
109
|
from agentic_runner.user_sources import ProxyTriage, UserSourcePoller
|
|
110
|
-
from agentic_runner.workers.acp_runtime import ACP_BRIDGES, AcpRuntime
|
|
110
|
+
from agentic_runner.workers.acp_runtime import ACP_BRIDGES, AcpRuntime, profile_acp_executables
|
|
111
111
|
from agentic_runner.workers.agent_runtime import AgentRuntime
|
|
112
112
|
from agentic_runner.workers.claude_runtime import ClaudeRuntime
|
|
113
113
|
from agentic_runner.workers.claude_sign_in import ClaudeSignIns
|
|
@@ -165,6 +165,7 @@ __all__ = [
|
|
|
165
165
|
"run",
|
|
166
166
|
"run_runner_lifecycle_hook",
|
|
167
167
|
"run_worker_until_terminated",
|
|
168
|
+
"with_profile_acp_cli_kinds",
|
|
168
169
|
]
|
|
169
170
|
|
|
170
171
|
_logger = logging.getLogger(__name__)
|
|
@@ -406,20 +407,49 @@ def build_agent_runtimes(
|
|
|
406
407
|
|
|
407
408
|
Which runtime serves a kind -- its pinned ACP bridge or the per-CLI one -- is this
|
|
408
409
|
Runner's setting (`ACP_CLI_KINDS`), never the payload's (local-agents 12 items 6, 7).
|
|
410
|
+
A kind the host opted in to run a Profile-named command for (`ACP_PROFILE_CLI_KINDS`,
|
|
411
|
+
local-agents 18) reaches `clis` through :func:`with_profile_acp_cli_kinds`.
|
|
409
412
|
"""
|
|
410
413
|
|
|
411
414
|
acp_kinds = {kind.strip() for kind in settings.ACP_CLI_KINDS.split(",") if kind.strip()}
|
|
415
|
+
profile_kinds = set(profile_acp_executables(settings))
|
|
412
416
|
return {
|
|
413
417
|
cli.cli_kind: (
|
|
414
418
|
AcpRuntime(cli_kind=cli.cli_kind, settings=settings)
|
|
415
|
-
if cli.cli_kind in
|
|
419
|
+
if cli.cli_kind in profile_kinds
|
|
420
|
+
or (cli.cli_kind in acp_kinds and cli.cli_kind in ACP_BRIDGES)
|
|
416
421
|
else _RUNTIMES[cli.cli_kind](settings)
|
|
417
422
|
)
|
|
418
423
|
for cli in clis
|
|
419
|
-
if cli.meets_floor and cli.cli_kind in _RUNTIMES
|
|
424
|
+
if cli.meets_floor and (cli.cli_kind in _RUNTIMES or cli.cli_kind in profile_kinds)
|
|
420
425
|
}
|
|
421
426
|
|
|
422
427
|
|
|
428
|
+
def with_profile_acp_cli_kinds(
|
|
429
|
+
attestation: HostAttestation | None, settings: WorkerSettings
|
|
430
|
+
) -> HostAttestation | None:
|
|
431
|
+
"""The attestation, plus each kind the host opted in to run a Profile-named ACP command
|
|
432
|
+
for, so routing sends that kind here and the registry serves it (local-agents 18).
|
|
433
|
+
|
|
434
|
+
The Runner does not start the host's executable just to ask its version, so the
|
|
435
|
+
version reported is the Runner's own: what serves the kind is this build's ACP runtime.
|
|
436
|
+
No attestation stays none -- such a Runner is routed nothing at all.
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
if attestation is None:
|
|
440
|
+
return None
|
|
441
|
+
found = {cli.cli_kind for cli in attestation.clis}
|
|
442
|
+
added = [
|
|
443
|
+
CliVersion(cli_kind=kind, version=runner_version, meets_floor=True)
|
|
444
|
+
for kind in profile_acp_executables(settings)
|
|
445
|
+
if kind not in found
|
|
446
|
+
]
|
|
447
|
+
# Validated, not copied: the attestation's cap on `clis` holds for these too.
|
|
448
|
+
return HostAttestation.model_validate(
|
|
449
|
+
{**attestation.model_dump(), "clis": [*attestation.clis, *added]}
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
423
453
|
async def run_runner_lifecycle_hook(hooks: HookRunner, name: HookName) -> None:
|
|
424
454
|
"""``runner_startup`` / ``runner_shutdown``: the two slots outside a Directive.
|
|
425
455
|
|
|
@@ -927,6 +957,11 @@ async def run(
|
|
|
927
957
|
# with, unless the operator points them at an internal address explicitly.
|
|
928
958
|
os.environ.setdefault("INTERNAL_FASTAPI_BASE_URL", control_plane)
|
|
929
959
|
settings = get_worker_settings()
|
|
960
|
+
try:
|
|
961
|
+
attestation = with_profile_acp_cli_kinds(attestation, settings)
|
|
962
|
+
except ValueError as error:
|
|
963
|
+
print(f"refusing to start: {error}", flush=True)
|
|
964
|
+
return 1
|
|
930
965
|
state_dir = config.state_dir
|
|
931
966
|
|
|
932
967
|
readiness = readiness or Readiness(int(os.environ.get(READINESS_PORT_ENV, "").strip() or 0))
|
|
@@ -35,6 +35,17 @@ Facts this module stands on (the LA-02 spike, re-read against the pinned bridge
|
|
|
35
35
|
(LA-19): nothing may load that the Runner did not choose.
|
|
36
36
|
- claude-agent-acp loads the Workspace's settings, hooks, ``.mcp.json`` and ``CLAUDE.md``
|
|
37
37
|
unless ``session/new`` carries the three options in ``_CLAUDE_SESSION_OPTIONS``.
|
|
38
|
+
|
|
39
|
+
A ``cli_kind`` with no pinned bridge (local-agents 18) runs the command its Agent Runtime
|
|
40
|
+
Profile names, and only on a Runner whose host pinned that kind's executable in
|
|
41
|
+
``ACP_PROFILE_CLI_KINDS`` (``gemini_cli=/usr/local/bin/gemini``). The Profile supplies the
|
|
42
|
+
arguments, never the program: an ``argv[0]`` that is not exactly the host-pinned path is
|
|
43
|
+
refused, or whoever edits the Profile would choose what runs on the host -- a shell, or a
|
|
44
|
+
binary the repository ships. None of the facts above are known for that harness. It gets
|
|
45
|
+
exactly a pinned bridge's isolation -- the Contract's uid,
|
|
46
|
+
``DirectiveSandbox``, the reserved env, the granted MCP servers, the egress proxy and the
|
|
47
|
+
permission seam -- with the harness root as ``XDG_CONFIG_HOME``, the one config-root
|
|
48
|
+
convention the Runner can assume of a harness it does not know.
|
|
38
49
|
"""
|
|
39
50
|
|
|
40
51
|
from __future__ import annotations
|
|
@@ -86,9 +97,10 @@ from agentic_runner.workers.command_policy import CommandPolicy, evaluate_comman
|
|
|
86
97
|
from agentic_runner.workers.contract_isolation import DirectiveSandbox
|
|
87
98
|
from agentic_runner.workers.mcp_config import McpServerEntry
|
|
88
99
|
from agentic_runner.workers.settings import WorkerSettings
|
|
100
|
+
from agentic_runner_contracts.redaction import redact_secret_like_text
|
|
89
101
|
from agentic_runner_contracts.runner_registration import UsageWindow
|
|
90
102
|
|
|
91
|
-
__all__ = ["ACP_BRIDGES", "AcpBridge", "AcpRuntime"]
|
|
103
|
+
__all__ = ["ACP_BRIDGES", "AcpBridge", "AcpRuntime", "profile_acp_executables"]
|
|
92
104
|
|
|
93
105
|
|
|
94
106
|
@dataclass(frozen=True, slots=True)
|
|
@@ -127,6 +139,41 @@ ACP_BRIDGES: Final[Mapping[str, AcpBridge]] = {
|
|
|
127
139
|
),
|
|
128
140
|
}
|
|
129
141
|
|
|
142
|
+
_CLI_KIND_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[a-z][a-z0-9_]{0,31}$")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def profile_acp_executables(settings: WorkerSettings) -> Mapping[str, str]:
|
|
146
|
+
"""Each kind this host opted in to run a Profile-named ACP command for, to the absolute
|
|
147
|
+
path of the one executable it trusts for that kind (local-agents 18).
|
|
148
|
+
|
|
149
|
+
A pinned kind, a malformed one, a kind named twice or a path that is not absolute is a
|
|
150
|
+
ValueError, so a typo stops the Runner starting instead of leaving a kind silently
|
|
151
|
+
unserved or served by whatever a relative name resolves to.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
executables: dict[str, str] = {}
|
|
155
|
+
for entry in (part.strip() for part in settings.ACP_PROFILE_CLI_KINDS.split(",")):
|
|
156
|
+
if not entry:
|
|
157
|
+
continue
|
|
158
|
+
kind, separator, executable = (part.strip() for part in entry.partition("="))
|
|
159
|
+
if kind in ACP_BRIDGES:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"ACP_PROFILE_CLI_KINDS names {kind!r}, whose ACP bridge the Runner pins; "
|
|
162
|
+
"a Profile never names the command for it"
|
|
163
|
+
)
|
|
164
|
+
if not _CLI_KIND_PATTERN.fullmatch(kind):
|
|
165
|
+
raise ValueError(f"ACP_PROFILE_CLI_KINDS names {kind!r}, which is not a cli_kind")
|
|
166
|
+
if not separator or not Path(executable).is_absolute():
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"ACP_PROFILE_CLI_KINDS must pin {kind!r} to an absolute executable path "
|
|
169
|
+
f"({kind}=/path/to/executable)"
|
|
170
|
+
)
|
|
171
|
+
if kind in executables:
|
|
172
|
+
raise ValueError(f"ACP_PROFILE_CLI_KINDS names {kind!r} twice")
|
|
173
|
+
executables[kind] = executable
|
|
174
|
+
return dict(sorted(executables.items()))
|
|
175
|
+
|
|
176
|
+
|
|
130
177
|
_ACP_PROTOCOL_VERSION: Final[int] = 1
|
|
131
178
|
# ACP frames are single JSON lines; a tool call carrying a file diff runs far past
|
|
132
179
|
# asyncio's 64 KiB default line limit.
|
|
@@ -224,7 +271,8 @@ class _Turn:
|
|
|
224
271
|
|
|
225
272
|
|
|
226
273
|
class AcpRuntime:
|
|
227
|
-
"""An Agent Runtime that serves one ``cli_kind`` through its pinned ACP bridge
|
|
274
|
+
"""An Agent Runtime that serves one ``cli_kind`` through its pinned ACP bridge, or, for
|
|
275
|
+
a kind with none, through the command the Directive's Profile names."""
|
|
228
276
|
|
|
229
277
|
auth_modes = frozenset({AuthMode.API_KEY, AuthMode.SUBSCRIPTION})
|
|
230
278
|
# Never a key of its own: an `api_key` Directive runs on the attempt's proxy pair.
|
|
@@ -239,7 +287,10 @@ class AcpRuntime:
|
|
|
239
287
|
ask: Ask = ask_over_callback,
|
|
240
288
|
) -> None:
|
|
241
289
|
self._cli_kind = cli_kind
|
|
242
|
-
self._bridge = ACP_BRIDGES
|
|
290
|
+
self._bridge = ACP_BRIDGES.get(cli_kind)
|
|
291
|
+
self._profile_executable = (
|
|
292
|
+
None if self._bridge is not None else profile_acp_executables(settings).get(cli_kind)
|
|
293
|
+
)
|
|
243
294
|
self._settings = settings
|
|
244
295
|
# Tests drive a fake ACP agent through this; production runs the pinned bridge.
|
|
245
296
|
self._bridge_argv = list(bridge_argv) if bridge_argv is not None else None
|
|
@@ -249,6 +300,37 @@ class AcpRuntime:
|
|
|
249
300
|
def _is_codex(self) -> bool:
|
|
250
301
|
return self._cli_kind == "codex_cli"
|
|
251
302
|
|
|
303
|
+
@property
|
|
304
|
+
def _is_claude(self) -> bool:
|
|
305
|
+
return self._cli_kind == "claude_code"
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def _executable(self) -> str:
|
|
309
|
+
return self._bridge.executable if self._bridge is not None else self._cli_kind
|
|
310
|
+
|
|
311
|
+
def acp_command_refusal(self, acp_command: Sequence[str]) -> str | None:
|
|
312
|
+
"""Why this runtime will not run the Profile-named ``acp_command``, or None.
|
|
313
|
+
|
|
314
|
+
None for a pinned kind: its bridge runs and the Profile's command is never read
|
|
315
|
+
(the activity refuses one sent for it). For any other kind the host's pinned
|
|
316
|
+
executable must be exactly ``argv[0]``; the Profile chooses only the arguments.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
if self._bridge is not None:
|
|
320
|
+
return None
|
|
321
|
+
if not acp_command:
|
|
322
|
+
return f"the Agent Runtime Profile names no ACP command for {self._cli_kind}"
|
|
323
|
+
if self._profile_executable is None:
|
|
324
|
+
return f"this Runner's host pinned no ACP executable for {self._cli_kind}"
|
|
325
|
+
if not Path(acp_command[0]).is_absolute():
|
|
326
|
+
return f"the Profile's ACP command for {self._cli_kind} is not an absolute path"
|
|
327
|
+
if acp_command[0] != self._profile_executable:
|
|
328
|
+
return (
|
|
329
|
+
f"the Profile's ACP command for {self._cli_kind} is not the executable this "
|
|
330
|
+
"Runner's host pinned for it"
|
|
331
|
+
)
|
|
332
|
+
return None
|
|
333
|
+
|
|
252
334
|
@property
|
|
253
335
|
def _timeout_seconds(self) -> int:
|
|
254
336
|
if self._is_codex:
|
|
@@ -285,8 +367,10 @@ class AcpRuntime:
|
|
|
285
367
|
return self._refused(request, directive_workspace_id, guard_mode, error)
|
|
286
368
|
|
|
287
369
|
extra_env = dict(request.extra_env)
|
|
370
|
+
# The name `llm_proxy.attempt_env` gave this kind its endpoint under.
|
|
288
371
|
proxy_url = extra_env.get(
|
|
289
|
-
|
|
372
|
+
"ANTHROPIC_BASE_URL" if self._cli_kind.startswith("claude") else PROXY_BASE_URL_ENV,
|
|
373
|
+
"",
|
|
290
374
|
)
|
|
291
375
|
if request.auth_mode == AuthMode.API_KEY and not proxy_url:
|
|
292
376
|
# Without the proxy pair the only credential left for the harness is a login in
|
|
@@ -306,7 +390,11 @@ class AcpRuntime:
|
|
|
306
390
|
if config_refusal:
|
|
307
391
|
return refuse("refused: Codex config file", config_refusal)
|
|
308
392
|
|
|
309
|
-
|
|
393
|
+
# The activity refuses this first, with Evidence; checked again where it runs.
|
|
394
|
+
command_refusal = self.acp_command_refusal(request.acp_command)
|
|
395
|
+
if command_refusal is not None:
|
|
396
|
+
return refuse("refused: Profile ACP command", command_refusal)
|
|
397
|
+
argv = self._argv(request)
|
|
310
398
|
command_hash = hash_command(argv)
|
|
311
399
|
floor_refusal = command_policy_refusal(
|
|
312
400
|
argv=argv,
|
|
@@ -319,18 +407,26 @@ class AcpRuntime:
|
|
|
319
407
|
if floor_refusal is not None:
|
|
320
408
|
return refuse(f"refused: command policy ({floor_refusal})", floor_refusal)
|
|
321
409
|
|
|
410
|
+
if self._bridge is not None:
|
|
411
|
+
runs = f"bridge={self._bridge.package}@{self._bridge.version}"
|
|
412
|
+
else:
|
|
413
|
+
# The executable's name and nothing of its arguments here; the note below
|
|
414
|
+
# carries them, scrubbed (local-agents 18 item 4).
|
|
415
|
+
command = redact_secret_like_text(Path(argv[0]).name)
|
|
416
|
+
runs = f"cli_kind={self._cli_kind}; command={command}"
|
|
322
417
|
guard_mode = (
|
|
323
|
-
f"runtime=acp;
|
|
324
|
-
f"
|
|
325
|
-
f"fallback={request.permission_fallback.value}"
|
|
418
|
+
f"runtime=acp; {runs}; auth={request.auth_mode.value}; "
|
|
419
|
+
f"permission=command-policy; fallback={request.permission_fallback.value}"
|
|
326
420
|
)
|
|
327
421
|
turn = _Turn()
|
|
422
|
+
if self._bridge is None:
|
|
423
|
+
turn.notes.append(f"profile ACP command: {redact_secret_like_text(shlex.join(argv))}")
|
|
328
424
|
timed_out = False
|
|
329
425
|
async with contextlib.AsyncExitStack() as stack:
|
|
330
426
|
env = self._env(request, harness_root)
|
|
331
427
|
if self._is_codex and request.auth_mode == AuthMode.API_KEY:
|
|
332
428
|
assert harness_root is not None # Codex always has one: see _harness_root
|
|
333
|
-
env[
|
|
429
|
+
env[ACP_BRIDGES["codex_cli"].config_root_env] = str(
|
|
334
430
|
stack.enter_context(_attempt_codex_home(proxy_url, harness_root, sandbox))
|
|
335
431
|
)
|
|
336
432
|
apply_extra_env(env, request.extra_env)
|
|
@@ -343,7 +439,7 @@ class AcpRuntime:
|
|
|
343
439
|
except TimeoutError:
|
|
344
440
|
timed_out = True
|
|
345
441
|
stop_reason, model_usage, stderr = "", (), ""
|
|
346
|
-
error = f"{self.
|
|
442
|
+
error = f"{self._executable} timed out after {self._timeout_seconds}s"
|
|
347
443
|
except OSError as exception:
|
|
348
444
|
return self._result(
|
|
349
445
|
request,
|
|
@@ -377,9 +473,13 @@ class AcpRuntime:
|
|
|
377
473
|
model_usage=model_usage,
|
|
378
474
|
)
|
|
379
475
|
|
|
380
|
-
def _argv(self,
|
|
476
|
+
def _argv(self, request: DirectiveRequest) -> list[str]:
|
|
477
|
+
if self._bridge is None:
|
|
478
|
+
# Only for a kind with no pinned bridge: for a pinned one the activity refuses a
|
|
479
|
+
# Profile-named command before it gets here, and this never reads it.
|
|
480
|
+
return list(request.acp_command)
|
|
381
481
|
argv = list(self._bridge_argv) if self._bridge_argv else [self._bridge.executable]
|
|
382
|
-
if
|
|
482
|
+
if self._is_claude and request.auth_mode == AuthMode.API_KEY:
|
|
383
483
|
# The bridge's own guard: refuse any turn that would bill a claude.ai login.
|
|
384
484
|
argv.append("--hide-claude-auth")
|
|
385
485
|
return argv
|
|
@@ -395,12 +495,16 @@ class AcpRuntime:
|
|
|
395
495
|
path = os.environ.get("PATH") or os.defpath
|
|
396
496
|
env = {"PATH": path}
|
|
397
497
|
if harness_root is not None:
|
|
398
|
-
|
|
498
|
+
config_root_env = (
|
|
499
|
+
self._bridge.config_root_env if self._bridge is not None else "XDG_CONFIG_HOME"
|
|
500
|
+
)
|
|
501
|
+
env[config_root_env] = str(harness_root)
|
|
399
502
|
if request.sandbox is not None:
|
|
400
503
|
env["HOME"] = str(request.sandbox.home_dir)
|
|
401
504
|
env["TMPDIR"] = str(request.sandbox.tmp_dir)
|
|
402
|
-
|
|
403
|
-
|
|
505
|
+
if self._bridge is not None:
|
|
506
|
+
program = self._bridge.harness_program
|
|
507
|
+
env[self._bridge.harness_path_env] = shutil.which(program, path=path) or program
|
|
404
508
|
if self._is_codex:
|
|
405
509
|
env["INITIAL_AGENT_MODE"] = "workspace-write"
|
|
406
510
|
env["NO_BROWSER"] = "1"
|
|
@@ -473,7 +577,7 @@ class AcpRuntime:
|
|
|
473
577
|
"cwd": str(workspace_path),
|
|
474
578
|
"mcpServers": acp_mcp_servers(request.mcp_servers or (), extra_env),
|
|
475
579
|
}
|
|
476
|
-
if
|
|
580
|
+
if self._is_claude:
|
|
477
581
|
session_params["_meta"] = {
|
|
478
582
|
"claudeCode": {
|
|
479
583
|
"options": {**_CLAUDE_SESSION_OPTIONS, "model": self._settings.CLAUDE_MODEL}
|
|
@@ -481,7 +585,7 @@ class AcpRuntime:
|
|
|
481
585
|
}
|
|
482
586
|
session = await connection.request("session/new", session_params)
|
|
483
587
|
session_id = str(session.get("sessionId") or "")
|
|
484
|
-
if
|
|
588
|
+
if self._is_claude and _offers_mode(session, _CLAUDE_MODE):
|
|
485
589
|
await connection.request(
|
|
486
590
|
"session/set_mode", {"sessionId": session_id, "modeId": _CLAUDE_MODE}
|
|
487
591
|
)
|
|
@@ -492,7 +596,7 @@ class AcpRuntime:
|
|
|
492
596
|
except AcpError as exception:
|
|
493
597
|
return "", (), str(exception)
|
|
494
598
|
except ConnectionError as exception:
|
|
495
|
-
return "", (), str(exception) or f"{self.
|
|
599
|
+
return "", (), str(exception) or f"{self._executable} closed its stdout"
|
|
496
600
|
return str(response.get("stopReason") or ""), _model_usage(response), ""
|
|
497
601
|
|
|
498
602
|
def _handler(
|
|
@@ -561,7 +665,7 @@ class AcpRuntime:
|
|
|
561
665
|
guard_mode: str,
|
|
562
666
|
error: str,
|
|
563
667
|
) -> DirectiveResult:
|
|
564
|
-
command_hash = hash_command([self.
|
|
668
|
+
command_hash = hash_command([self._executable, "refused"])
|
|
565
669
|
return DirectiveResult(
|
|
566
670
|
exit_code=126,
|
|
567
671
|
stdout="",
|
|
@@ -103,6 +103,9 @@ class DirectiveRequest:
|
|
|
103
103
|
auth_mode: AuthMode = AuthMode.API_KEY
|
|
104
104
|
# Read only by a runtime the harness asks per command (the ACP runtime).
|
|
105
105
|
permission_fallback: PermissionFallback = PermissionFallback.DENY
|
|
106
|
+
# The Profile-named ACP command (local-agents 18). Read only by the ACP runtime, and
|
|
107
|
+
# only for a `cli_kind` it pins no bridge for.
|
|
108
|
+
acp_command: tuple[str, ...] = ()
|
|
106
109
|
|
|
107
110
|
|
|
108
111
|
@dataclass(frozen=True, slots=True)
|
|
@@ -140,6 +140,16 @@ class WorkerSettings(BaseSettings):
|
|
|
140
140
|
"the pinned ACP bridge instead of the per-CLI runtime (local-agents 12)."
|
|
141
141
|
),
|
|
142
142
|
)
|
|
143
|
+
ACP_PROFILE_CLI_KINDS: str = Field(
|
|
144
|
+
default="",
|
|
145
|
+
description=(
|
|
146
|
+
"Comma-separated `cli_kind=/absolute/executable` pairs, kinds other than "
|
|
147
|
+
"`codex_cli` and `claude_code`, for which this Runner's host opts in to run the "
|
|
148
|
+
"ACP command an Agent Runtime Profile names (local-agents 18). The Profile "
|
|
149
|
+
"supplies only the arguments: a command whose argv[0] is not exactly the pinned "
|
|
150
|
+
"executable is refused. Empty: no Profile-named command ever runs here."
|
|
151
|
+
),
|
|
152
|
+
)
|
|
143
153
|
CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS: bool = Field(
|
|
144
154
|
default=False,
|
|
145
155
|
description=(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/contracts.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/evidence.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/fake_workspace.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/git/workspace.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/github/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/github/auth.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/github/fake_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/github/gh_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/oauth/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/integrations/oauth/fake_provider.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
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/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
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/contract_device_login.py
RENAMED
|
File without changes
|
{agentic_runner-3.8.0 → agentic_runner-3.9.0}/src/agentic_runner/workers/contract_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|