agentic-runner 3.13.0__tar.gz → 3.14.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.13.0 → agentic_runner-3.14.0}/PKG-INFO +1 -1
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/__init__.py +1 -1
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/cli.py +43 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/device_login_activities.py +6 -10
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/sealed_box.py +1 -12
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/service.py +41 -38
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/skills/agentic-runner-setup/SKILL.md +7 -4
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/claude_sign_in.py +145 -252
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/contract_device_login.py +3 -17
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/.gitignore +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/LICENSE +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/README.md +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/pyproject.toml +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/activities.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/attempts.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/auth_mode.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/build.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/callback.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/child_watcher.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/cli_floor.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/config.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/credentials.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/egress.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/harness_self_test.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/heartbeat_link.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/hooks.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/host_store.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/__init__.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/github/auth.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/oauth/__init__.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/oauth/fake_provider.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/lifecycle.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/llm_proxy.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/mcp.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/message_store.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/oauth_connectors.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/private_state.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/py.typed +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/recipient_key_secret.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/registration.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/runtime/__init__.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/skill.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/testing/__init__.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/testing/__main__.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/testing/control_plane.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/testing/plugin.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/testing/test_conformance.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/testing/workflow.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/tiny_http.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/triage_activities.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/usage_windows.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/user_sources.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/__init__.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/acp_runtime.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/agent_runtime.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/command_policy.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/harness_outcome.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/harness_usage.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/mcp_config.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/settings.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/skills.py +0 -0
- {agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workstation.py +0 -0
|
@@ -15,6 +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 --contract <id>`` signs one Contract's Claude Code in with Anthropic's own flow,
|
|
19
|
+
in this terminal, as that Contract's uid in its harness root (QTS-1448). No code or token
|
|
20
|
+
passes through the control plane.
|
|
21
|
+
|
|
18
22
|
``skill install | print`` hands the setup skill to the person's own Claude Code or Codex
|
|
19
23
|
(``skill.py``, console-v2 issue 21).
|
|
20
24
|
|
|
@@ -36,6 +40,7 @@ import os
|
|
|
36
40
|
import sys
|
|
37
41
|
from collections.abc import Sequence
|
|
38
42
|
from pathlib import Path
|
|
43
|
+
from uuid import UUID
|
|
39
44
|
|
|
40
45
|
from agentic_runner import __version__ as runner_version
|
|
41
46
|
from agentic_runner import service, skill, workstation
|
|
@@ -59,6 +64,7 @@ from agentic_runner.service import (
|
|
|
59
64
|
TAGS_ENV,
|
|
60
65
|
parse_tags,
|
|
61
66
|
)
|
|
67
|
+
from agentic_runner.workers.claude_sign_in import OAUTH_TOKEN, SIGN_IN_METHODS
|
|
62
68
|
from agentic_runner_contracts import __version__ as contracts_version
|
|
63
69
|
from agentic_runner_contracts.runner_registration import (
|
|
64
70
|
HostAttestation,
|
|
@@ -198,6 +204,21 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
198
204
|
status.add_argument("org", nargs="?", help="a workstation Organisation (issue 47)")
|
|
199
205
|
status.add_argument("--root", default=None, type=Path)
|
|
200
206
|
|
|
207
|
+
sign_in = subcommands.add_parser(
|
|
208
|
+
"sign-in",
|
|
209
|
+
help="sign a Contract's Claude Code in with Anthropic's own flow, in this terminal",
|
|
210
|
+
)
|
|
211
|
+
sign_in.add_argument("--contract", required=True, help="the Contract's id")
|
|
212
|
+
sign_in.add_argument(
|
|
213
|
+
"--method",
|
|
214
|
+
default=OAUTH_TOKEN,
|
|
215
|
+
choices=SIGN_IN_METHODS,
|
|
216
|
+
help="oauth_token: a long-lived token (claude setup-token); claude_ai: a Claude.ai "
|
|
217
|
+
"login (claude auth login --claudeai)",
|
|
218
|
+
)
|
|
219
|
+
sign_in.add_argument("--org", default=None, help="a workstation Organisation (issue 47)")
|
|
220
|
+
sign_in.add_argument("--root", default=None, type=Path, help=argparse.SUPPRESS)
|
|
221
|
+
|
|
201
222
|
annotate = subcommands.add_parser("annotate", help="attach an annotation to this Work Record")
|
|
202
223
|
annotate.add_argument("--context", default="default")
|
|
203
224
|
annotate.add_argument(
|
|
@@ -303,6 +324,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
303
324
|
for line in lines:
|
|
304
325
|
print(line)
|
|
305
326
|
return 0
|
|
327
|
+
if arguments.command == "sign-in":
|
|
328
|
+
return _sign_in(arguments)
|
|
306
329
|
if arguments.command == "skill":
|
|
307
330
|
if arguments.action == "print":
|
|
308
331
|
sys.stdout.write(skill.text())
|
|
@@ -366,6 +389,26 @@ def _image_attestation() -> HostAttestation:
|
|
|
366
389
|
)
|
|
367
390
|
|
|
368
391
|
|
|
392
|
+
def _sign_in(arguments: argparse.Namespace) -> int:
|
|
393
|
+
try:
|
|
394
|
+
contract_id = str(UUID(arguments.contract))
|
|
395
|
+
except ValueError:
|
|
396
|
+
print(f"--contract expects a Contract id, got {arguments.contract!r}", file=sys.stderr)
|
|
397
|
+
return 2
|
|
398
|
+
if not (os.isatty(0) and os.isatty(1)):
|
|
399
|
+
print(
|
|
400
|
+
"sign-in is interactive: run it in a terminal (`kubectl exec -it`, `docker exec -it`)",
|
|
401
|
+
file=sys.stderr,
|
|
402
|
+
)
|
|
403
|
+
return 2
|
|
404
|
+
if arguments.org:
|
|
405
|
+
paths = _org_paths(arguments)
|
|
406
|
+
os.environ.update(
|
|
407
|
+
workstation.run_environment(paths, workstation.WorkstationSettings.load(paths))
|
|
408
|
+
)
|
|
409
|
+
return asyncio.run(service.sign_in_claude(contract_id, method=arguments.method))
|
|
410
|
+
|
|
411
|
+
|
|
369
412
|
def _install(arguments: argparse.Namespace, paths: workstation.OrgPaths) -> int:
|
|
370
413
|
if not arguments.control_plane:
|
|
371
414
|
print(f"--control-plane (or {CONTROL_PLANE_ENV}) is required", file=sys.stderr)
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/device_login_activities.py
RENAMED
|
@@ -14,7 +14,6 @@ from temporalio import activity
|
|
|
14
14
|
from temporalio.exceptions import ApplicationError
|
|
15
15
|
|
|
16
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
|
|
18
17
|
from agentic_runner.workers.contract_device_login import ContractDeviceLogin
|
|
19
18
|
from agentic_runner.workers.contract_isolation import ContractIsolation
|
|
20
19
|
from agentic_runner_contracts.activity_io import (
|
|
@@ -37,8 +36,9 @@ class ContractDeviceLoginActivities:
|
|
|
37
36
|
host_party: str | None = None,
|
|
38
37
|
) -> None:
|
|
39
38
|
self._contract_isolation = contract_isolation
|
|
40
|
-
# The party this process was registered as hosted by. A shared Runner
|
|
41
|
-
#
|
|
39
|
+
# The party this process was registered as hosted by. A shared Runner starts no
|
|
40
|
+
# device login and reports only Claude Code's sign-in (local-agents 04, amended by
|
|
41
|
+
# 21); a person signs Claude Code in on the Runner itself (QTS-1448).
|
|
42
42
|
self._host_party = host_party
|
|
43
43
|
# Injectable for tests; None falls through to the real vendor CLIs.
|
|
44
44
|
self._device_login = device_login or ContractDeviceLogin(contract_isolation)
|
|
@@ -53,12 +53,9 @@ class ContractDeviceLoginActivities:
|
|
|
53
53
|
async def sign_in_contract_device_login(
|
|
54
54
|
self, request: ContractDeviceLoginInput
|
|
55
55
|
) -> ContractDeviceLoginResult:
|
|
56
|
-
|
|
57
|
-
OAUTH_TOKEN
|
|
58
|
-
)
|
|
59
|
-
self._refuse_on_shared_runner(allow=long_lived)
|
|
56
|
+
self._refuse_on_shared_runner()
|
|
60
57
|
prompt = await self._device_login.sign_in(
|
|
61
|
-
request.contract_id, runtime_kind=request.runtime_kind
|
|
58
|
+
request.contract_id, runtime_kind=request.runtime_kind
|
|
62
59
|
)
|
|
63
60
|
return ContractDeviceLoginResult(
|
|
64
61
|
contract_id=prompt.contract_id,
|
|
@@ -66,7 +63,6 @@ class ContractDeviceLoginActivities:
|
|
|
66
63
|
verification_uri=prompt.verification_uri,
|
|
67
64
|
user_code=prompt.user_code,
|
|
68
65
|
expires_at=prompt.expires_at.isoformat(),
|
|
69
|
-
sign_in_id=prompt.sign_in_id,
|
|
70
66
|
)
|
|
71
67
|
|
|
72
68
|
@activity.defn(name="check_contract_device_login_status")
|
|
@@ -107,7 +103,7 @@ class ContractDeviceLoginActivities:
|
|
|
107
103
|
on here, so the failure the platform's sign-in workflow receives names it.
|
|
108
104
|
|
|
109
105
|
Only a Runner known to be the person's own signs in, except that a shared Runner
|
|
110
|
-
may
|
|
106
|
+
may report (``allow``) a Claude Code long-lived token (local-agents 21). One whose
|
|
111
107
|
state names no host party (registered before issue 42) is refused either way, as
|
|
112
108
|
`choose_auth_mode` never runs a subscription on it."""
|
|
113
109
|
|
|
@@ -63,7 +63,6 @@ from agentic_runner_contracts.sealed_credential import (
|
|
|
63
63
|
SEAL_DOMAIN,
|
|
64
64
|
OpenedCredential,
|
|
65
65
|
SealedCredential,
|
|
66
|
-
SealedSignInCode,
|
|
67
66
|
delivery_binding,
|
|
68
67
|
key_fingerprint,
|
|
69
68
|
)
|
|
@@ -554,20 +553,10 @@ class SealedCredentialStream:
|
|
|
554
553
|
if not self._awaiting:
|
|
555
554
|
self._keys.retire_previous()
|
|
556
555
|
|
|
557
|
-
def open_sign_in_code(self, sealed: SealedSignInCode) -> str | None:
|
|
558
|
-
"""A relayed sign-in code, or None when it does not open (local-agents 05).
|
|
559
|
-
|
|
560
|
-
Opened with the same Recipient Key as a slot, but never held: the caller types it
|
|
561
|
-
into the waiting CLI and drops it.
|
|
562
|
-
"""
|
|
563
|
-
|
|
564
|
-
return self._open_with(sealed.recipient_key_id, sealed.binding(), sealed.ciphertext)
|
|
565
|
-
|
|
566
556
|
def open_oauth_code(self, sealed: SealedOAuthCode) -> str | None:
|
|
567
557
|
"""A relayed OAuth code, or None when it does not open (console-v2 issue 30).
|
|
568
558
|
|
|
569
|
-
Never held
|
|
570
|
-
drops it.
|
|
559
|
+
Never held: the caller spends it on the token endpoint and drops it.
|
|
571
560
|
"""
|
|
572
561
|
|
|
573
562
|
return self._open_with(sealed.recipient_key_id, sealed.binding(), sealed.ciphertext)
|
|
@@ -24,6 +24,7 @@ import logging
|
|
|
24
24
|
import os
|
|
25
25
|
import shutil
|
|
26
26
|
import signal
|
|
27
|
+
import sys
|
|
27
28
|
import tempfile
|
|
28
29
|
import time
|
|
29
30
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
@@ -34,6 +35,7 @@ from typing import Any, Protocol
|
|
|
34
35
|
from uuid import UUID
|
|
35
36
|
|
|
36
37
|
import httpx
|
|
38
|
+
from pydantic import ValidationError
|
|
37
39
|
from temporalio import activity
|
|
38
40
|
from temporalio.client import Client
|
|
39
41
|
from temporalio.worker import (
|
|
@@ -110,7 +112,7 @@ from agentic_runner.user_sources import ProxyTriage, UserSourcePoller
|
|
|
110
112
|
from agentic_runner.workers.acp_runtime import ACP_BRIDGES, AcpRuntime, profile_acp_executables
|
|
111
113
|
from agentic_runner.workers.agent_runtime import AgentRuntime
|
|
112
114
|
from agentic_runner.workers.claude_runtime import ClaudeRuntime
|
|
113
|
-
from agentic_runner.workers.claude_sign_in import ClaudeSignIns
|
|
115
|
+
from agentic_runner.workers.claude_sign_in import ClaudeSignInError, ClaudeSignIns
|
|
114
116
|
from agentic_runner.workers.codex_runtime import CodexRuntime
|
|
115
117
|
from agentic_runner.workers.contract_device_login import ContractDeviceLogin
|
|
116
118
|
from agentic_runner.workers.contract_isolation import UID_MAP_FILENAME, ContractIsolation
|
|
@@ -135,11 +137,7 @@ from agentic_runner_contracts.runner_registration import (
|
|
|
135
137
|
ResourcePressure,
|
|
136
138
|
)
|
|
137
139
|
from agentic_runner_contracts.runtime_context import WorkerRuntimeContextResolver
|
|
138
|
-
from agentic_runner_contracts.sealed_credential import
|
|
139
|
-
SignInCodeOutcome,
|
|
140
|
-
SignInCodeRelay,
|
|
141
|
-
key_fingerprint,
|
|
142
|
-
)
|
|
140
|
+
from agentic_runner_contracts.sealed_credential import key_fingerprint
|
|
143
141
|
|
|
144
142
|
__all__ = [
|
|
145
143
|
"AGENT_TOKEN_ENV",
|
|
@@ -166,6 +164,7 @@ __all__ = [
|
|
|
166
164
|
"run",
|
|
167
165
|
"run_runner_lifecycle_hook",
|
|
168
166
|
"run_worker_until_terminated",
|
|
167
|
+
"sign_in_claude",
|
|
169
168
|
"with_profile_acp_cli_kinds",
|
|
170
169
|
]
|
|
171
170
|
|
|
@@ -657,13 +656,8 @@ class ControlPlaneStream:
|
|
|
657
656
|
usage_windows: UsageWindows | None = None
|
|
658
657
|
# Local-agents 04b: which delivered references fill the LLM proxy's slot, and where.
|
|
659
658
|
llm_slots: Mapping[str, LlmProvider] = field(default_factory=lambda: dict(LLM_SLOT_REFERENCES))
|
|
660
|
-
# Local-agents 05: the Claude Code sign-ins a relayed code is typed into.
|
|
661
|
-
sign_ins: ClaudeSignIns | None = None
|
|
662
659
|
# Console-v2 issue 30: the OAuth Connectors whose code, tokens and verifier stay here.
|
|
663
660
|
oauth: OAuthConnectors | None = None
|
|
664
|
-
# Reported once, on the beat after the relay: a lost beat loses the outcome, and the
|
|
665
|
-
# console's own wait on the sign-in is what tells the person.
|
|
666
|
-
_sign_in_outcomes: list[SignInCodeRelay] = field(default_factory=list)
|
|
667
661
|
_delivered: set[tuple[str, str]] = field(default_factory=set)
|
|
668
662
|
_offered_to_proxy: dict[tuple[str, str], str] = field(default_factory=dict)
|
|
669
663
|
_last_seen: tuple[float, datetime] | None = None
|
|
@@ -773,17 +767,14 @@ class ControlPlaneStream:
|
|
|
773
767
|
clock_skew_seconds=self._clock_skew(now),
|
|
774
768
|
source_status=self.sources.statuses() if self.sources is not None else [],
|
|
775
769
|
tool_servers=self.tool_servers.latest() if self.tool_servers is not None else [],
|
|
776
|
-
sign_in_codes=self._sign_in_outcomes,
|
|
777
770
|
oauth_authorizations=self.oauth.take_authorizations() if self.oauth else [],
|
|
778
771
|
oauth_tokens=self.oauth.uploads() if self.oauth else [],
|
|
779
772
|
oauth_outcomes=self.oauth.take_outcomes() if self.oauth else [],
|
|
780
773
|
)
|
|
781
|
-
self._sign_in_outcomes = []
|
|
782
774
|
ack = await self.client.heartbeat(self.state, envelope)
|
|
783
775
|
if self.sources is not None:
|
|
784
776
|
self.sources.assign(ack.user_sources)
|
|
785
777
|
await self._deliver_transcripts(ack)
|
|
786
|
-
await self._relay_sign_in_codes(ack)
|
|
787
778
|
if self.oauth is not None:
|
|
788
779
|
# Before the sealed values are applied: a Disconnect revokes with the tokens
|
|
789
780
|
# this Runner holds, and the same ack may already have withdrawn their ciphertext.
|
|
@@ -836,28 +827,6 @@ class ControlPlaneStream:
|
|
|
836
827
|
"transcript %s could not be delivered: %s", request.request_id, error
|
|
837
828
|
)
|
|
838
829
|
|
|
839
|
-
async def _relay_sign_in_codes(self, ack: HeartbeatAck) -> None:
|
|
840
|
-
"""Type each relayed code into the sign-in it names (local-agents 05).
|
|
841
|
-
|
|
842
|
-
The code is opened, written to that sign-in's PTY and dropped here; nothing logs
|
|
843
|
-
it and only its outcome travels back.
|
|
844
|
-
"""
|
|
845
|
-
|
|
846
|
-
for sealed in ack.sign_in_codes:
|
|
847
|
-
code = self.sealed.open_sign_in_code(sealed)
|
|
848
|
-
if code is None:
|
|
849
|
-
outcome = SignInCodeOutcome.UNOPENABLE
|
|
850
|
-
elif self.sign_ins is None:
|
|
851
|
-
outcome = SignInCodeOutcome.UNKNOWN
|
|
852
|
-
else:
|
|
853
|
-
outcome = await self.sign_ins.relay(
|
|
854
|
-
str(sealed.contract_id), sealed.sign_in_id, code
|
|
855
|
-
)
|
|
856
|
-
del code
|
|
857
|
-
self._sign_in_outcomes.append(
|
|
858
|
-
SignInCodeRelay(sign_in_id=sealed.sign_in_id, outcome=outcome)
|
|
859
|
-
)
|
|
860
|
-
|
|
861
830
|
def _sync_delivered(self) -> None:
|
|
862
831
|
"""The resolver holds exactly what the last ack let this installation open."""
|
|
863
832
|
|
|
@@ -1021,6 +990,42 @@ async def run(
|
|
|
1021
990
|
await readiness.close()
|
|
1022
991
|
|
|
1023
992
|
|
|
993
|
+
async def sign_in_claude(contract_id: str, *, method: str) -> int:
|
|
994
|
+
"""``agentic-runner sign-in``: Claude Code's own flow for one Contract, in this terminal.
|
|
995
|
+
|
|
996
|
+
Runs with the Runner's environment (``kubectl exec``, or ``--org`` on a workstation),
|
|
997
|
+
so the Contract gets the uid and the harness root its Directives already run under.
|
|
998
|
+
Nothing reaches the control plane: the console only reads the outcome back through
|
|
999
|
+
``claude auth status`` (QTS-1448).
|
|
1000
|
+
"""
|
|
1001
|
+
|
|
1002
|
+
mode = isolation_mode()
|
|
1003
|
+
can = can_separate_uids()
|
|
1004
|
+
try:
|
|
1005
|
+
require_isolation_supported(mode, can_separate_uids=can)
|
|
1006
|
+
except RunnerRegistrationError as error:
|
|
1007
|
+
print(f"refusing to sign in ({error.reason}): {error}", file=sys.stderr)
|
|
1008
|
+
return 1
|
|
1009
|
+
control_plane = os.environ.get(CONTROL_PLANE_ENV, "").strip()
|
|
1010
|
+
if control_plane:
|
|
1011
|
+
os.environ.setdefault("INTERNAL_FASTAPI_BASE_URL", control_plane)
|
|
1012
|
+
try:
|
|
1013
|
+
settings = get_worker_settings()
|
|
1014
|
+
except ValidationError:
|
|
1015
|
+
print(
|
|
1016
|
+
"refusing to sign in: run this with the Runner's environment "
|
|
1017
|
+
"(`--org <org>` on a workstation, `kubectl exec` into the Runner's pod)",
|
|
1018
|
+
file=sys.stderr,
|
|
1019
|
+
)
|
|
1020
|
+
return 2
|
|
1021
|
+
sign_ins = ClaudeSignIns(build_contract_isolation(settings, can_separate_uids=can))
|
|
1022
|
+
try:
|
|
1023
|
+
return await sign_ins.sign_in(contract_id, method=method)
|
|
1024
|
+
except ClaudeSignInError as error:
|
|
1025
|
+
print(f"sign-in failed: {error}", file=sys.stderr)
|
|
1026
|
+
return 1
|
|
1027
|
+
|
|
1028
|
+
|
|
1024
1029
|
async def _serve(
|
|
1025
1030
|
*,
|
|
1026
1031
|
config: RunnerConfig,
|
|
@@ -1157,7 +1162,6 @@ async def _serve_registered(
|
|
|
1157
1162
|
messages = MessageStore(state_dir / MESSAGES_DIR)
|
|
1158
1163
|
stream.messages = messages
|
|
1159
1164
|
sign_ins = ClaudeSignIns(build_contract_isolation(settings, can_separate_uids=can_change_uid))
|
|
1160
|
-
stream.sign_ins = sign_ins
|
|
1161
1165
|
stream.oauth = OAuthConnectors(
|
|
1162
1166
|
sealed=stream.sealed, provider=HttpOAuthProvider(), credentials=credentials
|
|
1163
1167
|
)
|
|
@@ -1243,7 +1247,6 @@ async def _serve_registered(
|
|
|
1243
1247
|
task.cancel()
|
|
1244
1248
|
with contextlib.suppress(asyncio.CancelledError):
|
|
1245
1249
|
await task
|
|
1246
|
-
await sign_ins.close()
|
|
1247
1250
|
await run_runner_lifecycle_hook(hooks, HookName.RUNNER_SHUTDOWN)
|
|
1248
1251
|
await _report_stop(link, lifecycle)
|
|
1249
1252
|
return 1 if revoked.is_set() else 0
|
|
@@ -120,11 +120,14 @@ whose LLM key or subscription the Agents spend. Ask which of the two the Contrac
|
|
|
120
120
|
For Docker and Kubernetes the value is delivered from the console, sealed to the Runner:
|
|
121
121
|
https://github.com/qtsone/agentic-runner/blob/main/docs/helm.md#the-recipient-key.
|
|
122
122
|
- **A Codex or Claude Code subscription.** This works only on a Runner the person hosts
|
|
123
|
-
themselves.
|
|
124
|
-
|
|
123
|
+
themselves. For Codex, send them to the Contract's page in the console and its
|
|
124
|
+
**Connect** button. For Claude Code, ask the person to run, in their own terminal, the
|
|
125
|
+
sign-in line for their way of installing: `agentic-runner sign-in --org <org> --contract
|
|
126
|
+
<contract-id>` on this machine, or the `docker exec -it` / `kubectl exec -it` line in
|
|
127
|
+
docker.md or helm.md. See
|
|
125
128
|
https://github.com/qtsone/agentic-runner/blob/main/docs/workstation.md#sign-in-to-codex-and-claude-code.
|
|
126
|
-
|
|
127
|
-
`claude auth login`.
|
|
129
|
+
The sign-in is the person's own, with their own Anthropic account: you never run it, and
|
|
130
|
+
you do not run `codex login` or `claude auth login`.
|
|
128
131
|
|
|
129
132
|
### 5. Wait for the Runner to come online
|
|
130
133
|
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/claude_sign_in.py
RENAMED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
"""Claude Code's
|
|
1
|
+
"""Claude Code's sign-in for one Contract, run by the person on the Runner (QTS-1448).
|
|
2
|
+
|
|
3
|
+
``agentic-runner sign-in`` runs Anthropic's own CLI flow, unmodified, as the Contract's
|
|
4
|
+
uid in its harness root and attached to the caller's terminal. No sign-in code, token or
|
|
5
|
+
session passes through the control plane: the QTS-1349 decision (controls 1 and 5)
|
|
6
|
+
retired the sealed sign-in-code relay local-agents 05 built here.
|
|
2
7
|
|
|
3
8
|
Two methods, both made in the Contract's own harness root and neither ever delivered:
|
|
4
9
|
|
|
@@ -9,14 +14,9 @@ Two methods, both made in the Contract's own harness root and neither ever deliv
|
|
|
9
14
|
* ``claude_ai``: ``claude auth login --claudeai``, a refreshing login the CLI keeps itself
|
|
10
15
|
-- ``.credentials.json`` on Linux, a Keychain item on macOS.
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
(:meth:`ClaudeSignIns.relay`). The PKCE verifier never leaves the CLI's process, so the
|
|
16
|
-
code alone signs nobody in.
|
|
17
|
-
|
|
18
|
-
A waiting sign-in is the one thing here held in the Runner's process: the PTY is the only
|
|
19
|
-
way to reach the CLI. A restart closes it and the CLI exits; the person starts again.
|
|
17
|
+
The CLI runs on a pseudo-terminal this process relays to the caller's own, byte for byte:
|
|
18
|
+
``setup-token`` prints the token to its terminal and nowhere else, so the relay is the
|
|
19
|
+
only way to read it. The person sees and types exactly what the CLI shows and reads.
|
|
20
20
|
|
|
21
21
|
macOS (the 2026-10-08 spike): Claude saves a ``claude_ai`` login with the ``security`` CLI
|
|
22
22
|
into the default keychain of the ``HOME`` it is given, and a Contract's sandbox ``HOME``
|
|
@@ -31,7 +31,6 @@ import asyncio
|
|
|
31
31
|
import contextlib
|
|
32
32
|
import fcntl
|
|
33
33
|
import json
|
|
34
|
-
import logging
|
|
35
34
|
import os
|
|
36
35
|
import pty
|
|
37
36
|
import re
|
|
@@ -40,15 +39,14 @@ import signal
|
|
|
40
39
|
import struct
|
|
41
40
|
import sys
|
|
42
41
|
import termios
|
|
43
|
-
|
|
44
|
-
from
|
|
45
|
-
from
|
|
42
|
+
import tty
|
|
43
|
+
from collections.abc import Iterator, Sequence
|
|
44
|
+
from dataclasses import dataclass
|
|
46
45
|
from pathlib import Path
|
|
47
46
|
from typing import Final
|
|
48
47
|
|
|
49
48
|
from agentic_runner.workers._runtime_support import run_subprocess_exec
|
|
50
49
|
from agentic_runner.workers.contract_isolation import ContractIsolation, DirectiveSandbox
|
|
51
|
-
from agentic_runner_contracts.sealed_credential import SignInCodeOutcome
|
|
52
50
|
|
|
53
51
|
__all__ = [
|
|
54
52
|
"CLAUDE_AI",
|
|
@@ -57,9 +55,9 @@ __all__ = [
|
|
|
57
55
|
"KEYCHAIN_PASSWORD_FILE",
|
|
58
56
|
"OAUTH_TOKEN",
|
|
59
57
|
"OAUTH_TOKEN_FILE",
|
|
58
|
+
"SIGN_IN_METHODS",
|
|
60
59
|
"ClaudeAuthStatus",
|
|
61
60
|
"ClaudeSignInError",
|
|
62
|
-
"ClaudeSignInPrompt",
|
|
63
61
|
"ClaudeSignIns",
|
|
64
62
|
"UnknownSignInMethodError",
|
|
65
63
|
"launcher_argv",
|
|
@@ -73,6 +71,7 @@ CLAUDE_SIGN_IN_ARGV: Final[dict[str, tuple[str, ...]]] = {
|
|
|
73
71
|
OAUTH_TOKEN: ("claude", "setup-token"),
|
|
74
72
|
CLAUDE_AI: ("claude", "auth", "login", "--claudeai"),
|
|
75
73
|
}
|
|
74
|
+
SIGN_IN_METHODS: Final[tuple[str, ...]] = tuple(CLAUDE_SIGN_IN_ARGV)
|
|
76
75
|
_STATUS_ARGV: Final[tuple[str, ...]] = ("claude", "auth", "status", "--json")
|
|
77
76
|
|
|
78
77
|
OAUTH_TOKEN_FILE: Final[str] = "oauth-token"
|
|
@@ -99,24 +98,16 @@ export CLAUDE_CODE_SUBPROCESS_ENV_SCRUB
|
|
|
99
98
|
exec "$@"
|
|
100
99
|
"""
|
|
101
100
|
|
|
102
|
-
#
|
|
103
|
-
# the
|
|
104
|
-
|
|
105
|
-
# Paperclip's setup-token-runner.ts:44-60: Ink reads a paste and its Enter as one chunk
|
|
106
|
-
# and drops the Enter, so the newline goes in a separate write a beat later.
|
|
107
|
-
_ENTER_DELAY_SECONDS: Final[float] = 0.15
|
|
108
|
-
# Ink wraps the authorize URL and the token at the terminal width; one line each keeps
|
|
109
|
-
# them whole.
|
|
101
|
+
# Ink wraps the authorize URL and the token at the width it is told; one line each keeps
|
|
102
|
+
# the token whole for `_printed_token`. The caller's terminal soft-wraps what it shows,
|
|
103
|
+
# which also leaves the URL copyable in one piece.
|
|
110
104
|
_PTY_COLUMNS: Final[int] = 1000
|
|
111
105
|
_PTY_ROWS: Final[int] = 50
|
|
112
|
-
_DEFAULT_PROMPT_TIMEOUT_SECONDS: Final[float] = 30.0
|
|
113
106
|
_STATUS_TIMEOUT_SECONDS: Final[int] = 30
|
|
114
107
|
_OUTPUT_LIMIT_BYTES: Final[int] = 65_536
|
|
115
108
|
|
|
116
|
-
_logger = logging.getLogger(__name__)
|
|
117
|
-
|
|
118
109
|
# Runs as the Contract's uid, never the Runner's: the Contract owns every directory down to
|
|
119
|
-
# its harness root and can swap one for a symlink while a sign-in
|
|
110
|
+
# its harness root and can swap one for a symlink while a sign-in runs. The Runner, with
|
|
120
111
|
# CAP_CHOWN and CAP_DAC_OVERRIDE, would follow it into another Contract's root; as the
|
|
121
112
|
# Contract's uid the write or unlink reaches only what that Contract already could.
|
|
122
113
|
# ``value`` None removes the file.
|
|
@@ -146,31 +137,18 @@ except BaseException:
|
|
|
146
137
|
raise
|
|
147
138
|
"""
|
|
148
139
|
|
|
149
|
-
# The URL arrives as an OSC 8 hyperlink whose target is whole even when its visible text
|
|
150
|
-
# is split; the plain form is the fallback for a CLI that prints it bare.
|
|
151
|
-
_OSC8_URL: Final[re.Pattern[str]] = re.compile(r"\x1b\]8;[^;\x07]*;(https://[^\x07\x1b]+)")
|
|
152
|
-
_PLAIN_URL: Final[re.Pattern[str]] = re.compile(r"https://\S+/oauth/authorize\?[^\s\x07\x1b]+")
|
|
153
140
|
_ANSI: Final[re.Pattern[str]] = re.compile(r"\x1b(?:\][^\x07]*\x07|\[[0-9;?>]*[A-Za-z]|[78])")
|
|
154
141
|
_TOKEN: Final[re.Pattern[str]] = re.compile(r"sk-ant-oat\d\d-[A-Za-z0-9_-]{20,}")
|
|
155
|
-
# What a browser shows the person to paste back: base64url, with `#` joining the state.
|
|
156
|
-
_CODE_SHAPE: Final[re.Pattern[str]] = re.compile(r"^[A-Za-z0-9_\-#.~]{8,512}$")
|
|
157
142
|
|
|
158
143
|
|
|
159
144
|
class ClaudeSignInError(RuntimeError):
|
|
160
|
-
"""The CLI
|
|
145
|
+
"""The CLI did not start, its status could not be read, or it printed no token."""
|
|
161
146
|
|
|
162
147
|
|
|
163
148
|
class UnknownSignInMethodError(ValueError):
|
|
164
149
|
"""A ``method`` this Runner has no Claude Code sign-in command for."""
|
|
165
150
|
|
|
166
151
|
|
|
167
|
-
@dataclass(frozen=True, slots=True)
|
|
168
|
-
class ClaudeSignInPrompt:
|
|
169
|
-
sign_in_id: str
|
|
170
|
-
verification_uri: str
|
|
171
|
-
expires_at: datetime
|
|
172
|
-
|
|
173
|
-
|
|
174
152
|
@dataclass(frozen=True, slots=True)
|
|
175
153
|
class ClaudeAuthStatus:
|
|
176
154
|
"""``claude auth status --json``'s three fields, and nothing else it reports."""
|
|
@@ -180,22 +158,6 @@ class ClaudeAuthStatus:
|
|
|
180
158
|
subscription_type: str | None
|
|
181
159
|
|
|
182
160
|
|
|
183
|
-
@dataclass(eq=False)
|
|
184
|
-
class _WaitingSignIn:
|
|
185
|
-
sign_in_id: str
|
|
186
|
-
contract_id: str
|
|
187
|
-
method: str
|
|
188
|
-
harness_root: Path
|
|
189
|
-
uid: int | None
|
|
190
|
-
process: asyncio.subprocess.Process
|
|
191
|
-
master_fd: int
|
|
192
|
-
deadline: float
|
|
193
|
-
output: bytearray = field(default_factory=bytearray)
|
|
194
|
-
changed: asyncio.Event = field(default_factory=asyncio.Event)
|
|
195
|
-
exited: asyncio.Event = field(default_factory=asyncio.Event)
|
|
196
|
-
ended: bool = False
|
|
197
|
-
|
|
198
|
-
|
|
199
161
|
def launcher_argv(harness_root: Path, argv: Sequence[str]) -> list[str]:
|
|
200
162
|
"""``argv`` behind the launcher that hands it this harness root's login."""
|
|
201
163
|
|
|
@@ -203,7 +165,7 @@ def launcher_argv(harness_root: Path, argv: Sequence[str]) -> list[str]:
|
|
|
203
165
|
|
|
204
166
|
|
|
205
167
|
class ClaudeSignIns:
|
|
206
|
-
"""
|
|
168
|
+
"""Claude Code's sign-in and sign-in status for each Contract's harness root."""
|
|
207
169
|
|
|
208
170
|
def __init__(
|
|
209
171
|
self,
|
|
@@ -211,80 +173,48 @@ class ClaudeSignIns:
|
|
|
211
173
|
*,
|
|
212
174
|
argv_by_method: dict[str, tuple[str, ...]] | None = None,
|
|
213
175
|
status_argv: Sequence[str] = _STATUS_ARGV,
|
|
214
|
-
prompt_timeout_seconds: float = _DEFAULT_PROMPT_TIMEOUT_SECONDS,
|
|
215
|
-
window: timedelta = SIGN_IN_WINDOW,
|
|
216
176
|
platform: str = sys.platform,
|
|
217
|
-
clock: Callable[[], float] | None = None,
|
|
218
177
|
) -> None:
|
|
219
178
|
self._isolation = isolation
|
|
220
179
|
self._argv_by_method = argv_by_method or CLAUDE_SIGN_IN_ARGV
|
|
221
180
|
self._status_argv = list(status_argv)
|
|
222
|
-
self._prompt_timeout_seconds = prompt_timeout_seconds
|
|
223
|
-
self._window = window
|
|
224
181
|
self._platform = platform
|
|
225
|
-
self._clock = clock
|
|
226
|
-
self._waiting: dict[str, _WaitingSignIn] = {}
|
|
227
|
-
self._watchers: set[asyncio.Task[None]] = set()
|
|
228
|
-
|
|
229
|
-
def _now(self) -> float:
|
|
230
|
-
return self._clock() if self._clock is not None else asyncio.get_running_loop().time()
|
|
231
182
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
183
|
+
async def sign_in(
|
|
184
|
+
self, contract_id: str, *, method: str = OAUTH_TOKEN, stdin_fd: int = 0, stdout_fd: int = 1
|
|
185
|
+
) -> int:
|
|
186
|
+
"""Run the method's CLI as this Contract's uid, attached to the caller's terminal
|
|
187
|
+
(``stdin_fd``/``stdout_fd``); keep what it signed in. Returns the CLI's exit code."""
|
|
235
188
|
|
|
236
|
-
async def start(self, contract_id: str, *, method: str | None) -> ClaudeSignInPrompt:
|
|
237
|
-
"""Spawn the method's CLI as this Contract's uid; return once it prints its URL."""
|
|
238
|
-
|
|
239
|
-
method = method or OAUTH_TOKEN
|
|
240
189
|
argv = self._argv_by_method.get(method)
|
|
241
190
|
if argv is None:
|
|
242
191
|
raise UnknownSignInMethodError(f"no Claude Code sign-in method {method!r}")
|
|
243
192
|
sandbox = self._isolation.sandbox(contract_id, runtime_kind=RUNTIME_KIND)
|
|
244
|
-
# One sign-in per Contract: a second start supersedes the first, whose code the
|
|
245
|
-
# person can no longer use anyway.
|
|
246
|
-
for waiting in [w for w in self._waiting.values() if w.contract_id == contract_id]:
|
|
247
|
-
await self._end(waiting)
|
|
248
193
|
if method == CLAUDE_AI and self._platform == "darwin":
|
|
249
194
|
await self._ensure_keychain(sandbox)
|
|
250
|
-
|
|
195
|
+
output = bytearray()
|
|
251
196
|
try:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
session = self._waiting.get(sign_in_id)
|
|
275
|
-
if session is None or session.contract_id != contract_id:
|
|
276
|
-
return SignInCodeOutcome.UNKNOWN
|
|
277
|
-
if session.exited.is_set() or self._now() >= session.deadline:
|
|
278
|
-
return SignInCodeOutcome.EXPIRED
|
|
279
|
-
if not _CODE_SHAPE.fullmatch(code):
|
|
280
|
-
# A newline or a control character would be keystrokes, not a code.
|
|
281
|
-
return SignInCodeOutcome.UNOPENABLE
|
|
282
|
-
os.write(session.master_fd, code.encode("ascii"))
|
|
283
|
-
await asyncio.sleep(_ENTER_DELAY_SECONDS)
|
|
284
|
-
if session.exited.is_set():
|
|
285
|
-
return SignInCodeOutcome.EXPIRED
|
|
286
|
-
os.write(session.master_fd, b"\r")
|
|
287
|
-
return SignInCodeOutcome.WRITTEN
|
|
197
|
+
returncode = await _run_attached(
|
|
198
|
+
launcher_argv(sandbox.harness_config_dir, argv),
|
|
199
|
+
sandbox,
|
|
200
|
+
output=output,
|
|
201
|
+
stdin_fd=stdin_fd,
|
|
202
|
+
stdout_fd=stdout_fd,
|
|
203
|
+
)
|
|
204
|
+
token_file = sandbox.harness_config_dir / OAUTH_TOKEN_FILE
|
|
205
|
+
if method == OAUTH_TOKEN:
|
|
206
|
+
token = _printed_token(output.decode("utf-8", errors="replace"))
|
|
207
|
+
if token is not None:
|
|
208
|
+
await _write_private(token_file, token, sandbox.uid)
|
|
209
|
+
elif returncode == 0:
|
|
210
|
+
raise ClaudeSignInError("claude setup-token exited 0 but printed no token")
|
|
211
|
+
elif returncode == 0:
|
|
212
|
+
# A finished `claude_ai` sign-in replaces a long-lived token, which the
|
|
213
|
+
# launcher would otherwise go on preferring.
|
|
214
|
+
await _write_private(token_file, None, sandbox.uid)
|
|
215
|
+
finally:
|
|
216
|
+
output.clear()
|
|
217
|
+
return returncode
|
|
288
218
|
|
|
289
219
|
async def status(self, contract_id: str) -> ClaudeAuthStatus:
|
|
290
220
|
"""Ask the CLI, as the Contract's uid, whether it is signed in.
|
|
@@ -322,134 +252,6 @@ class ClaudeSignIns:
|
|
|
322
252
|
subscription_type=plan if isinstance(plan, str) else None,
|
|
323
253
|
)
|
|
324
254
|
|
|
325
|
-
async def close(self) -> None:
|
|
326
|
-
for session in list(self._waiting.values()):
|
|
327
|
-
await self._end(session)
|
|
328
|
-
|
|
329
|
-
async def _spawn(
|
|
330
|
-
self,
|
|
331
|
-
contract_id: str,
|
|
332
|
-
method: str,
|
|
333
|
-
argv: tuple[str, ...],
|
|
334
|
-
sandbox: DirectiveSandbox,
|
|
335
|
-
) -> _WaitingSignIn:
|
|
336
|
-
master_fd, slave_fd = pty.openpty()
|
|
337
|
-
try:
|
|
338
|
-
fcntl.ioctl(
|
|
339
|
-
slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", _PTY_ROWS, _PTY_COLUMNS, 0, 0)
|
|
340
|
-
)
|
|
341
|
-
os.set_blocking(master_fd, False)
|
|
342
|
-
try:
|
|
343
|
-
process = await asyncio.create_subprocess_exec(
|
|
344
|
-
*launcher_argv(sandbox.harness_config_dir, argv),
|
|
345
|
-
cwd=sandbox.home_dir,
|
|
346
|
-
env=_harness_env(sandbox) | {"TERM": "xterm-256color"},
|
|
347
|
-
stdin=slave_fd,
|
|
348
|
-
stdout=slave_fd,
|
|
349
|
-
stderr=slave_fd,
|
|
350
|
-
start_new_session=True,
|
|
351
|
-
**sandbox.spawn_kwargs(),
|
|
352
|
-
)
|
|
353
|
-
except OSError as error:
|
|
354
|
-
raise ClaudeSignInError(
|
|
355
|
-
f"claude failed to start: {error.__class__.__name__}"
|
|
356
|
-
) from None
|
|
357
|
-
except BaseException:
|
|
358
|
-
os.close(master_fd)
|
|
359
|
-
raise
|
|
360
|
-
finally:
|
|
361
|
-
os.close(slave_fd)
|
|
362
|
-
session = _WaitingSignIn(
|
|
363
|
-
sign_in_id=f"si_{secrets.token_hex(16)}",
|
|
364
|
-
contract_id=contract_id,
|
|
365
|
-
method=method,
|
|
366
|
-
harness_root=sandbox.harness_config_dir,
|
|
367
|
-
uid=sandbox.uid,
|
|
368
|
-
process=process,
|
|
369
|
-
master_fd=master_fd,
|
|
370
|
-
deadline=self._now() + self._window.total_seconds(),
|
|
371
|
-
)
|
|
372
|
-
asyncio.get_running_loop().add_reader(master_fd, self._read, session)
|
|
373
|
-
return session
|
|
374
|
-
|
|
375
|
-
def _read(self, session: _WaitingSignIn) -> None:
|
|
376
|
-
try:
|
|
377
|
-
chunk = os.read(session.master_fd, 4096)
|
|
378
|
-
except BlockingIOError:
|
|
379
|
-
return
|
|
380
|
-
except OSError:
|
|
381
|
-
# EIO: every holder of the slave side is gone, i.e. the CLI exited.
|
|
382
|
-
chunk = b""
|
|
383
|
-
if not chunk:
|
|
384
|
-
asyncio.get_running_loop().remove_reader(session.master_fd)
|
|
385
|
-
session.exited.set()
|
|
386
|
-
session.changed.set()
|
|
387
|
-
return
|
|
388
|
-
_keep_tail(session.output, chunk)
|
|
389
|
-
session.changed.set()
|
|
390
|
-
|
|
391
|
-
async def _await_prompt(self, session: _WaitingSignIn) -> str:
|
|
392
|
-
async with asyncio.timeout(self._prompt_timeout_seconds):
|
|
393
|
-
while True:
|
|
394
|
-
url = _authorize_url(session.output.decode("utf-8", errors="replace"))
|
|
395
|
-
if url is not None:
|
|
396
|
-
return url
|
|
397
|
-
if session.exited.is_set():
|
|
398
|
-
code = await session.process.wait()
|
|
399
|
-
raise ClaudeSignInError(
|
|
400
|
-
f"claude exited before printing its sign-in URL (code {code})"
|
|
401
|
-
)
|
|
402
|
-
session.changed.clear()
|
|
403
|
-
await session.changed.wait()
|
|
404
|
-
|
|
405
|
-
async def _watch(self, session: _WaitingSignIn) -> None:
|
|
406
|
-
remaining = max(0.0, session.deadline - self._now())
|
|
407
|
-
with contextlib.suppress(TimeoutError):
|
|
408
|
-
await asyncio.wait_for(session.exited.wait(), timeout=remaining)
|
|
409
|
-
await self._end(session)
|
|
410
|
-
|
|
411
|
-
async def _end(self, session: _WaitingSignIn) -> None:
|
|
412
|
-
"""Kill the group if it is still waiting, keep what it signed in, forget the rest."""
|
|
413
|
-
|
|
414
|
-
if session.ended:
|
|
415
|
-
return
|
|
416
|
-
session.ended = True
|
|
417
|
-
loop = asyncio.get_running_loop()
|
|
418
|
-
if not session.exited.is_set():
|
|
419
|
-
loop.remove_reader(session.master_fd)
|
|
420
|
-
with contextlib.suppress(ProcessLookupError, PermissionError):
|
|
421
|
-
os.killpg(session.process.pid, signal.SIGKILL)
|
|
422
|
-
session.exited.set()
|
|
423
|
-
with contextlib.suppress(TimeoutError):
|
|
424
|
-
await asyncio.wait_for(session.process.wait(), timeout=2)
|
|
425
|
-
with contextlib.suppress(OSError):
|
|
426
|
-
# The CLI's last words can still be in the master's buffer after EOF.
|
|
427
|
-
while chunk := os.read(session.master_fd, 4096):
|
|
428
|
-
_keep_tail(session.output, chunk)
|
|
429
|
-
with contextlib.suppress(OSError):
|
|
430
|
-
os.close(session.master_fd)
|
|
431
|
-
session.master_fd = -1
|
|
432
|
-
token_file = session.harness_root / OAUTH_TOKEN_FILE
|
|
433
|
-
try:
|
|
434
|
-
if session.method == OAUTH_TOKEN:
|
|
435
|
-
token = _printed_token(session.output.decode("utf-8", errors="replace"))
|
|
436
|
-
if token is not None:
|
|
437
|
-
await _write_private(token_file, token, session.uid)
|
|
438
|
-
elif session.process.returncode == 0:
|
|
439
|
-
raise ClaudeSignInError("claude setup-token exited 0 but printed no token")
|
|
440
|
-
elif session.process.returncode == 0:
|
|
441
|
-
# A finished `claude_ai` sign-in replaces a long-lived token, which the
|
|
442
|
-
# launcher would otherwise go on preferring.
|
|
443
|
-
await _write_private(token_file, None, session.uid)
|
|
444
|
-
except ClaudeSignInError as error:
|
|
445
|
-
# Nobody awaits a sign-in's end; the person sees it as still signed out.
|
|
446
|
-
_logger.error("Claude Code sign-in %s did not complete: %s", session.sign_in_id, error)
|
|
447
|
-
finally:
|
|
448
|
-
session.output.clear()
|
|
449
|
-
# Last, so `waiting` falls to zero only once the token is on disk.
|
|
450
|
-
if self._waiting.get(session.sign_in_id) is session:
|
|
451
|
-
del self._waiting[session.sign_in_id]
|
|
452
|
-
|
|
453
255
|
async def _ensure_keychain(self, sandbox: DirectiveSandbox) -> None:
|
|
454
256
|
"""A per-Contract keychain, the default of the sandbox ``HOME`` (macOS only)."""
|
|
455
257
|
|
|
@@ -483,6 +285,104 @@ class ClaudeSignIns:
|
|
|
483
285
|
raise ClaudeSignInError(f"security {argv[1]} failed (exit {result.exit_code})")
|
|
484
286
|
|
|
485
287
|
|
|
288
|
+
async def _run_attached(
|
|
289
|
+
argv: Sequence[str],
|
|
290
|
+
sandbox: DirectiveSandbox,
|
|
291
|
+
*,
|
|
292
|
+
output: bytearray,
|
|
293
|
+
stdin_fd: int,
|
|
294
|
+
stdout_fd: int,
|
|
295
|
+
) -> int:
|
|
296
|
+
"""Run ``argv`` on a pseudo-terminal relayed to the caller's; its output's tail lands
|
|
297
|
+
in ``output``. Returns once the CLI has exited and its last output is relayed."""
|
|
298
|
+
|
|
299
|
+
master_fd, slave_fd = pty.openpty()
|
|
300
|
+
try:
|
|
301
|
+
rows = os.get_terminal_size(stdout_fd).lines if os.isatty(stdout_fd) else _PTY_ROWS
|
|
302
|
+
fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, _PTY_COLUMNS, 0, 0))
|
|
303
|
+
os.set_blocking(master_fd, False)
|
|
304
|
+
try:
|
|
305
|
+
process = await asyncio.create_subprocess_exec(
|
|
306
|
+
*argv,
|
|
307
|
+
cwd=sandbox.home_dir,
|
|
308
|
+
env=_harness_env(sandbox) | {"TERM": os.environ.get("TERM") or "xterm-256color"},
|
|
309
|
+
stdin=slave_fd,
|
|
310
|
+
stdout=slave_fd,
|
|
311
|
+
stderr=slave_fd,
|
|
312
|
+
start_new_session=True,
|
|
313
|
+
**sandbox.spawn_kwargs(),
|
|
314
|
+
)
|
|
315
|
+
except OSError as error:
|
|
316
|
+
raise ClaudeSignInError(f"claude failed to start: {error.__class__.__name__}") from None
|
|
317
|
+
except BaseException:
|
|
318
|
+
os.close(master_fd)
|
|
319
|
+
raise
|
|
320
|
+
finally:
|
|
321
|
+
os.close(slave_fd)
|
|
322
|
+
|
|
323
|
+
loop = asyncio.get_running_loop()
|
|
324
|
+
exited = asyncio.Event()
|
|
325
|
+
|
|
326
|
+
def from_cli() -> None:
|
|
327
|
+
try:
|
|
328
|
+
chunk = os.read(master_fd, 4096)
|
|
329
|
+
except BlockingIOError:
|
|
330
|
+
return
|
|
331
|
+
except OSError:
|
|
332
|
+
# EIO: every holder of the slave side is gone, i.e. the CLI exited.
|
|
333
|
+
chunk = b""
|
|
334
|
+
if not chunk:
|
|
335
|
+
loop.remove_reader(master_fd)
|
|
336
|
+
exited.set()
|
|
337
|
+
return
|
|
338
|
+
_write_all(stdout_fd, chunk)
|
|
339
|
+
_keep_tail(output, chunk)
|
|
340
|
+
|
|
341
|
+
def from_person() -> None:
|
|
342
|
+
typed = os.read(stdin_fd, 1024)
|
|
343
|
+
if not typed:
|
|
344
|
+
loop.remove_reader(stdin_fd)
|
|
345
|
+
return
|
|
346
|
+
os.write(master_fd, typed)
|
|
347
|
+
|
|
348
|
+
loop.add_reader(master_fd, from_cli)
|
|
349
|
+
loop.add_reader(stdin_fd, from_person)
|
|
350
|
+
try:
|
|
351
|
+
with _raw(stdin_fd):
|
|
352
|
+
await exited.wait()
|
|
353
|
+
return await process.wait()
|
|
354
|
+
except BaseException:
|
|
355
|
+
with contextlib.suppress(ProcessLookupError, PermissionError):
|
|
356
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
357
|
+
raise
|
|
358
|
+
finally:
|
|
359
|
+
loop.remove_reader(master_fd)
|
|
360
|
+
loop.remove_reader(stdin_fd)
|
|
361
|
+
os.close(master_fd)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@contextlib.contextmanager
|
|
365
|
+
def _raw(fd: int) -> Iterator[None]:
|
|
366
|
+
"""The caller's terminal in raw mode, so each key reaches the CLI as the CLI's own
|
|
367
|
+
terminal would deliver it (Ink reads keys, not lines), and restored after."""
|
|
368
|
+
|
|
369
|
+
if not os.isatty(fd):
|
|
370
|
+
yield
|
|
371
|
+
return
|
|
372
|
+
saved = termios.tcgetattr(fd)
|
|
373
|
+
tty.setraw(fd)
|
|
374
|
+
try:
|
|
375
|
+
yield
|
|
376
|
+
finally:
|
|
377
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, saved)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _write_all(fd: int, data: bytes) -> None:
|
|
381
|
+
view = memoryview(data)
|
|
382
|
+
while view:
|
|
383
|
+
view = view[os.write(fd, view) :]
|
|
384
|
+
|
|
385
|
+
|
|
486
386
|
def _harness_env(sandbox: DirectiveSandbox) -> dict[str, str]:
|
|
487
387
|
return {
|
|
488
388
|
"HOME": str(sandbox.home_dir),
|
|
@@ -492,13 +392,6 @@ def _harness_env(sandbox: DirectiveSandbox) -> dict[str, str]:
|
|
|
492
392
|
}
|
|
493
393
|
|
|
494
394
|
|
|
495
|
-
def _authorize_url(text: str) -> str | None:
|
|
496
|
-
if hyperlink := _OSC8_URL.search(text):
|
|
497
|
-
return hyperlink.group(1)
|
|
498
|
-
plain = _PLAIN_URL.search(_ANSI.sub("", text))
|
|
499
|
-
return plain.group(0) if plain else None
|
|
500
|
-
|
|
501
|
-
|
|
502
395
|
def _printed_token(text: str) -> str | None:
|
|
503
396
|
match = _TOKEN.search(_ANSI.sub("", text))
|
|
504
397
|
return match.group(0) if match else None
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/contract_device_login.py
RENAMED
|
@@ -54,8 +54,8 @@ _TOKEN_FILES_BY_RUNTIME: Final[dict[str, tuple[str, ...]]] = {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
# Codex's non-interactive device-code flow (research/29 §1.5, "Headless login is
|
|
57
|
-
# first-class"). Claude Code
|
|
58
|
-
#
|
|
57
|
+
# first-class"). Claude Code has none here: the person runs its own flow on the Runner,
|
|
58
|
+
# ``agentic-runner sign-in`` (``claude_sign_in``, QTS-1448), and no console starts it.
|
|
59
59
|
DEVICE_LOGIN_ARGV: Final[dict[str, tuple[str, ...]]] = {
|
|
60
60
|
"codex_cli": ("codex", "login", "--device-auth"),
|
|
61
61
|
}
|
|
@@ -97,8 +97,6 @@ class DeviceLoginPrompt:
|
|
|
97
97
|
verification_uri: str
|
|
98
98
|
user_code: str
|
|
99
99
|
expires_at: datetime
|
|
100
|
-
# Claude Code's: what a relayed code names (local-agents 05).
|
|
101
|
-
sign_in_id: str = ""
|
|
102
100
|
|
|
103
101
|
|
|
104
102
|
class ContractDeviceLogin:
|
|
@@ -119,9 +117,7 @@ class ContractDeviceLogin:
|
|
|
119
117
|
self._prompt_timeout_seconds = prompt_timeout_seconds
|
|
120
118
|
self._output_limit_bytes = output_limit_bytes
|
|
121
119
|
|
|
122
|
-
async def sign_in(
|
|
123
|
-
self, contract_id: str, *, runtime_kind: str, method: str | None = None
|
|
124
|
-
) -> DeviceLoginPrompt:
|
|
120
|
+
async def sign_in(self, contract_id: str, *, runtime_kind: str) -> DeviceLoginPrompt:
|
|
125
121
|
"""Spawn the harness CLI as this Contract's own uid; return once it has printed
|
|
126
122
|
its verification prompt, and leave it running to finish the funder's approval.
|
|
127
123
|
|
|
@@ -130,16 +126,6 @@ class ContractDeviceLogin:
|
|
|
130
126
|
harness config root no other Contract's uid can read.
|
|
131
127
|
"""
|
|
132
128
|
|
|
133
|
-
if runtime_kind == "claude_code":
|
|
134
|
-
claude = await self.claude.start(contract_id, method=method)
|
|
135
|
-
return DeviceLoginPrompt(
|
|
136
|
-
contract_id=contract_id,
|
|
137
|
-
runtime_kind=runtime_kind,
|
|
138
|
-
verification_uri=claude.verification_uri,
|
|
139
|
-
user_code="",
|
|
140
|
-
expires_at=claude.expires_at,
|
|
141
|
-
sign_in_id=claude.sign_in_id,
|
|
142
|
-
)
|
|
143
129
|
argv = self._argv_by_runtime.get(runtime_kind)
|
|
144
130
|
if argv is None:
|
|
145
131
|
raise UnknownHarnessError(f"no device-login command for runtime {runtime_kind!r}")
|
|
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.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/contracts.py
RENAMED
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/evidence.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/git/workspace.py
RENAMED
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/github/__init__.py
RENAMED
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/github/auth.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/github/gh_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/integrations/oauth/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.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.13.0 → agentic_runner-3.14.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
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/_runtime_support.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/claude_runtime.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/command_policy.py
RENAMED
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/contract_isolation.py
RENAMED
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/fastapi_client.py
RENAMED
|
File without changes
|
{agentic_runner-3.13.0 → agentic_runner-3.14.0}/src/agentic_runner/workers/harness_outcome.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|