agentic-runner 3.6.0__tar.gz → 3.8.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.
Files changed (74) hide show
  1. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/PKG-INFO +1 -1
  2. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/__init__.py +1 -1
  3. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/activities.py +12 -0
  4. agentic_runner-3.8.0/src/agentic_runner/integrations/oauth/__init__.py +68 -0
  5. agentic_runner-3.8.0/src/agentic_runner/integrations/oauth/fake_provider.py +134 -0
  6. agentic_runner-3.8.0/src/agentic_runner/oauth_connectors.py +592 -0
  7. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/sealed_box.py +95 -1
  8. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/service.py +97 -2
  9. agentic_runner-3.8.0/src/agentic_runner/usage_windows.py +529 -0
  10. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/acp_runtime.py +15 -1
  11. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/agent_runtime.py +4 -0
  12. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/harness_outcome.py +5 -5
  13. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/.gitignore +0 -0
  14. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/LICENSE +0 -0
  15. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/README.md +0 -0
  16. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/pyproject.toml +0 -0
  17. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/attempts.py +0 -0
  18. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/auth_mode.py +0 -0
  19. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/build.py +0 -0
  20. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/callback.py +0 -0
  21. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/child_watcher.py +0 -0
  22. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/cli.py +0 -0
  23. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/cli_floor.py +0 -0
  24. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/config.py +0 -0
  25. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/credentials.py +0 -0
  26. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/device_login_activities.py +0 -0
  27. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/egress.py +0 -0
  28. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/harness_self_test.py +0 -0
  29. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/heartbeat_link.py +0 -0
  30. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/hooks.py +0 -0
  31. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/host_store.py +0 -0
  32. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/__init__.py +0 -0
  33. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/git/__init__.py +0 -0
  34. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/git/contracts.py +0 -0
  35. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/git/evidence.py +0 -0
  36. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/git/fake_workspace.py +0 -0
  37. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/git/workspace.py +0 -0
  38. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/github/__init__.py +0 -0
  39. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/github/auth.py +0 -0
  40. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/github/fake_client.py +0 -0
  41. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/integrations/github/gh_client.py +0 -0
  42. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/lifecycle.py +0 -0
  43. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/llm_proxy.py +0 -0
  44. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/mcp.py +0 -0
  45. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/message_store.py +0 -0
  46. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/private_state.py +0 -0
  47. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/py.typed +0 -0
  48. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/recipient_key_secret.py +0 -0
  49. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/registration.py +0 -0
  50. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/runtime/__init__.py +0 -0
  51. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/runtime/verifier_command.py +0 -0
  52. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/testing/__init__.py +0 -0
  53. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/testing/__main__.py +0 -0
  54. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/testing/control_plane.py +0 -0
  55. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/testing/plugin.py +0 -0
  56. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/testing/test_conformance.py +0 -0
  57. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/testing/workflow.py +0 -0
  58. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/tiny_http.py +0 -0
  59. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/triage_activities.py +0 -0
  60. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/user_sources.py +0 -0
  61. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/__init__.py +0 -0
  62. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/_runtime_support.py +0 -0
  63. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/claude_runtime.py +0 -0
  64. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/claude_sign_in.py +0 -0
  65. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/codex_runtime.py +0 -0
  66. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/command_policy.py +0 -0
  67. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/contract_device_login.py +0 -0
  68. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/contract_isolation.py +0 -0
  69. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/fastapi_client.py +0 -0
  70. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/harness_usage.py +0 -0
  71. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/mcp_config.py +0 -0
  72. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/settings.py +0 -0
  73. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workers/skills.py +0 -0
  74. {agentic_runner-3.6.0 → agentic_runner-3.8.0}/src/agentic_runner/workstation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentic-runner
3
- Version: 3.6.0
3
+ Version: 3.8.0
4
4
  Summary: The Agentic OS Runner: an activity-only Temporal executor for workspaces, Agent Runtimes and verb seams
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -7,6 +7,6 @@ stay on the platform; this package imports none of them, and a CI partition test
7
7
  (``tests/test_package_partition.py``) fails the build if that changes.
8
8
  """
9
9
 
10
- __version__ = "3.6.0"
10
+ __version__ = "3.8.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -118,6 +118,7 @@ from agentic_runner.message_store import MessageStore, WorkflowSignaller
118
118
  from agentic_runner.registration import RunnerRegistrationError
119
119
  from agentic_runner.runtime import verifier_command
120
120
  from agentic_runner.runtime.verifier_command import CommandValidationError
121
+ from agentic_runner.usage_windows import UsageWindows
121
122
  from agentic_runner.workers._runtime_support import (
122
123
  RESERVED_DIRECTIVE_ENV,
123
124
  bound_text,
@@ -174,6 +175,7 @@ from agentic_runner_contracts.activity_io import (
174
175
  GitHubCallError,
175
176
  GitHubCallFailure,
176
177
  HarnessHold,
178
+ HarnessHoldKind,
177
179
  LearningDirectiveInput,
178
180
  LearningDirectiveOutput,
179
181
  MemberDirectiveInput,
@@ -1616,10 +1618,13 @@ class RunnerRalphActivities:
1616
1618
  tool_server_health: ToolServerHealthLog | None = None,
1617
1619
  attempt_records: AttemptRecords | None = None,
1618
1620
  directives_in_flight: DirectivesInFlight | None = None,
1621
+ usage_windows: UsageWindows | None = None,
1619
1622
  ) -> None:
1620
1623
  self._fastapi_client = fastapi_client
1621
1624
  # Read by the harness self-test, which never runs beside a Contract's Directive.
1622
1625
  self._directives_in_flight = directives_in_flight or DirectivesInFlight()
1626
+ # Local-agents 10: None off a user-hosted Runner, which reports no usage windows.
1627
+ self._usage_windows = usage_windows
1623
1628
  # Wall-clock source for per-Directive runtime, accrued against the Budget's
1624
1629
  # wall-clock dimension (ADR-0007). Activities run outside the Temporal sandbox,
1625
1630
  # so a real monotonic clock is allowed here (it must never be used in the
@@ -5273,6 +5278,13 @@ class RunnerRalphActivities:
5273
5278
  error=result.error,
5274
5279
  now=datetime.now(UTC),
5275
5280
  )
5281
+ if self._usage_windows is not None and runtime_state.contract_id:
5282
+ self._usage_windows.after_directive(
5283
+ runtime_state.contract_id,
5284
+ runtime_state.cli_kind,
5285
+ result.usage_windows,
5286
+ usage_limit=hold is not None and hold.kind == HarnessHoldKind.USAGE_LIMIT,
5287
+ )
5276
5288
  if hold is None:
5277
5289
  return None
5278
5290
  await self._fastapi_client.append_evidence(
@@ -0,0 +1,68 @@
1
+ """The OAuth provider port: one form POST to a token or revocation endpoint (issue 30).
2
+
3
+ RFC 6749 §4.1.3 / §6 and RFC 7009 are each a form POST answered with JSON, so the port is
4
+ exactly that and the protocol lives in :mod:`agentic_runner.oauth_connectors`, where a
5
+ fake provider exercises all of it. ``HttpOAuthProvider`` is the Runner's own outbound HTTPS
6
+ -- the exchange never touches the control plane.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Mapping
12
+ from dataclasses import dataclass, field
13
+ from typing import Final, Protocol
14
+
15
+ import httpx
16
+
17
+ __all__ = [
18
+ "HttpOAuthProvider",
19
+ "OAuthProvider",
20
+ "OAuthProviderError",
21
+ "ProviderResponse",
22
+ ]
23
+
24
+ # One beat's budget: the exchange and a refresh run on the heartbeat, so a provider that
25
+ # hangs must cost a failed step and not a stale link.
26
+ _TIMEOUT: Final[float] = 10.0
27
+
28
+
29
+ class OAuthProviderError(RuntimeError):
30
+ """The provider could not be reached or did not answer HTTP. Never carries a body."""
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ProviderResponse:
35
+ status: int
36
+ # repr=False: a token response is the one body that must never reach a log line.
37
+ body: Mapping[str, object] = field(default_factory=dict, repr=False)
38
+
39
+
40
+ class OAuthProvider(Protocol):
41
+ async def post_form(self, url: str, form: Mapping[str, str]) -> ProviderResponse: ...
42
+
43
+
44
+ class HttpOAuthProvider:
45
+ """The real provider: HTTPS only, no redirects, JSON asked for.
46
+
47
+ Redirects are not followed because a 30x on a token endpoint would re-send the code
48
+ and the verifier to wherever it points. ``Accept: application/json`` because GitHub,
49
+ among others, answers form-encoded unless asked.
50
+ """
51
+
52
+ async def post_form(self, url: str, form: Mapping[str, str]) -> ProviderResponse:
53
+ try:
54
+ async with httpx.AsyncClient(timeout=_TIMEOUT, follow_redirects=False) as client:
55
+ response = await client.post(
56
+ url, data=dict(form), headers={"Accept": "application/json"}
57
+ )
58
+ except httpx.HTTPError as error:
59
+ # The type only: an httpx error's text can carry the request URL, and a
60
+ # refresh's form is in no URL, but nothing here needs more than the class.
61
+ raise OAuthProviderError(type(error).__name__) from None
62
+ try:
63
+ body = response.json()
64
+ except ValueError:
65
+ body = {}
66
+ return ProviderResponse(
67
+ status=response.status_code, body=body if isinstance(body, dict) else {}
68
+ )
@@ -0,0 +1,134 @@
1
+ """A fake OAuth provider that enforces what a real one does (issue 30's Runner tests).
2
+
3
+ Strict on purpose: a code is single-use and bound to its client, redirect and S256
4
+ challenge; a refresh token rotates and the old one stops working; revocation ends both.
5
+ A Runner that sent the wrong verifier, replayed a code or kept an old refresh token fails
6
+ here the way it would fail against a provider.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import hashlib
13
+ import secrets
14
+ from collections.abc import Mapping
15
+ from dataclasses import dataclass, field
16
+ from urllib.parse import parse_qs, urlsplit
17
+
18
+ from agentic_runner.integrations.oauth import ProviderResponse
19
+
20
+ __all__ = ["FakeOAuthProvider"]
21
+
22
+ AUTHORIZE = "https://provider.test/authorize"
23
+ TOKEN = "https://provider.test/token"
24
+ REVOKE = "https://provider.test/revoke"
25
+
26
+
27
+ @dataclass
28
+ class _Grant:
29
+ client_id: str
30
+ redirect_uri: str
31
+ challenge: str
32
+ scope: str
33
+
34
+
35
+ @dataclass
36
+ class FakeOAuthProvider:
37
+ client_id: str = "client-123"
38
+ client_secret: str | None = None
39
+ expires_in: int | None = 3600
40
+ authorize_endpoint: str = AUTHORIZE
41
+ token_endpoint: str = TOKEN
42
+ revocation_endpoint: str = REVOKE
43
+ requests: list[tuple[str, dict[str, str]]] = field(default_factory=list)
44
+ issued: list[str] = field(default_factory=list)
45
+ revoked: set[str] = field(default_factory=set)
46
+ _codes: dict[str, _Grant] = field(default_factory=dict)
47
+ # token -> the grant it belongs to; a refresh token also keeps the grant's scope.
48
+ _access: dict[str, str] = field(default_factory=dict)
49
+ _refresh: dict[str, tuple[str, str]] = field(default_factory=dict)
50
+
51
+ def consent(self, authorize_url: str) -> tuple[str, str]:
52
+ """The person approves: the ``(code, state)`` the callback URL would carry."""
53
+
54
+ query = {key: values[0] for key, values in parse_qs(urlsplit(authorize_url).query).items()}
55
+ assert query["response_type"] == "code"
56
+ assert query["code_challenge_method"] == "S256"
57
+ code = f"code-{secrets.token_hex(8)}"
58
+ self._codes[code] = _Grant(
59
+ client_id=query["client_id"],
60
+ redirect_uri=query["redirect_uri"],
61
+ challenge=query["code_challenge"],
62
+ scope=query.get("scope", ""),
63
+ )
64
+ return code, query["state"]
65
+
66
+ def access_valid(self, token: str) -> bool:
67
+ return token in self._access and token not in self.revoked
68
+
69
+ async def post_form(self, url: str, form: Mapping[str, str]) -> ProviderResponse:
70
+ self.requests.append((url, dict(form)))
71
+ if url == self.revocation_endpoint:
72
+ return self._revoke(form)
73
+ if url != self.token_endpoint:
74
+ return ProviderResponse(404)
75
+ if not self._client_ok(form):
76
+ return _error(401, "invalid_client")
77
+ if form.get("grant_type") == "authorization_code":
78
+ return self._exchange(form)
79
+ if form.get("grant_type") == "refresh_token":
80
+ return self._rotate(form)
81
+ return _error(400, "unsupported_grant_type")
82
+
83
+ def _client_ok(self, form: Mapping[str, str]) -> bool:
84
+ if form.get("client_id") != self.client_id:
85
+ return False
86
+ return self.client_secret is None or form.get("client_secret") == self.client_secret
87
+
88
+ def _exchange(self, form: Mapping[str, str]) -> ProviderResponse:
89
+ grant = self._codes.pop(form.get("code", ""), None)
90
+ if grant is None or grant.redirect_uri != form.get("redirect_uri"):
91
+ return _error(400, "invalid_grant")
92
+ verifier = form.get("code_verifier", "")
93
+ digest = hashlib.sha256(verifier.encode("ascii")).digest()
94
+ if base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") != grant.challenge:
95
+ return _error(400, "invalid_grant")
96
+ return self._tokens(f"grant-{secrets.token_hex(8)}", grant.scope)
97
+
98
+ def _rotate(self, form: Mapping[str, str]) -> ProviderResponse:
99
+ held = self._refresh.pop(form.get("refresh_token", ""), None)
100
+ if held is None:
101
+ return _error(400, "invalid_grant")
102
+ return self._tokens(*held)
103
+
104
+ def _tokens(self, grant_id: str, scope: str) -> ProviderResponse:
105
+ access = f"at-{secrets.token_hex(16)}"
106
+ refresh = f"rt-{secrets.token_hex(16)}"
107
+ self._access[access] = grant_id
108
+ self._refresh[refresh] = (grant_id, scope)
109
+ self.issued += [access, refresh]
110
+ body: dict[str, object] = {
111
+ "access_token": access,
112
+ "refresh_token": refresh,
113
+ "token_type": "Bearer",
114
+ "scope": scope,
115
+ }
116
+ if self.expires_in is not None:
117
+ body["expires_in"] = self.expires_in
118
+ return ProviderResponse(200, body)
119
+
120
+ def _revoke(self, form: Mapping[str, str]) -> ProviderResponse:
121
+ if not self._client_ok(form):
122
+ return _error(401, "invalid_client")
123
+ token = form.get("token", "")
124
+ self.revoked.add(token)
125
+ # RFC 7009 §2.1: revoking a refresh token may revoke the grant's access tokens;
126
+ # this provider does, so the test can see the access token stop working.
127
+ held = self._refresh.pop(token, None)
128
+ if held is not None:
129
+ self.revoked.update(t for t, grant_id in self._access.items() if grant_id == held[0])
130
+ return ProviderResponse(200)
131
+
132
+
133
+ def _error(status: int, code: str) -> ProviderResponse:
134
+ return ProviderResponse(status, {"error": code, "error_description": "fake provider"})