agentic-runner 2.6.0__py3-none-any.whl

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 (54) hide show
  1. agentic_runner/__init__.py +12 -0
  2. agentic_runner/activities.py +4918 -0
  3. agentic_runner/callback.py +342 -0
  4. agentic_runner/child_watcher.py +66 -0
  5. agentic_runner/cli.py +416 -0
  6. agentic_runner/config.py +105 -0
  7. agentic_runner/credentials.py +252 -0
  8. agentic_runner/device_login_activities.py +79 -0
  9. agentic_runner/egress.py +243 -0
  10. agentic_runner/heartbeat_link.py +249 -0
  11. agentic_runner/hooks.py +455 -0
  12. agentic_runner/host_store.py +295 -0
  13. agentic_runner/integrations/__init__.py +0 -0
  14. agentic_runner/integrations/git/__init__.py +1 -0
  15. agentic_runner/integrations/git/contracts.py +198 -0
  16. agentic_runner/integrations/git/evidence.py +442 -0
  17. agentic_runner/integrations/git/fake_workspace.py +339 -0
  18. agentic_runner/integrations/git/workspace.py +921 -0
  19. agentic_runner/integrations/github/__init__.py +53 -0
  20. agentic_runner/integrations/github/auth.py +171 -0
  21. agentic_runner/integrations/github/fake_client.py +494 -0
  22. agentic_runner/integrations/github/gh_client.py +944 -0
  23. agentic_runner/lifecycle.py +48 -0
  24. agentic_runner/llm_proxy.py +937 -0
  25. agentic_runner/mcp.py +342 -0
  26. agentic_runner/message_store.py +341 -0
  27. agentic_runner/py.typed +0 -0
  28. agentic_runner/recipient_key_secret.py +134 -0
  29. agentic_runner/registration.py +363 -0
  30. agentic_runner/runtime/__init__.py +0 -0
  31. agentic_runner/runtime/verifier_command.py +344 -0
  32. agentic_runner/sealed_box.py +509 -0
  33. agentic_runner/service.py +1068 -0
  34. agentic_runner/tiny_http.py +133 -0
  35. agentic_runner/triage_activities.py +113 -0
  36. agentic_runner/user_sources.py +546 -0
  37. agentic_runner/workers/__init__.py +1 -0
  38. agentic_runner/workers/_runtime_support.py +388 -0
  39. agentic_runner/workers/agent_runtime.py +93 -0
  40. agentic_runner/workers/claude_runtime.py +226 -0
  41. agentic_runner/workers/codex_runtime.py +311 -0
  42. agentic_runner/workers/command_policy.py +250 -0
  43. agentic_runner/workers/contract_device_login.py +211 -0
  44. agentic_runner/workers/contract_isolation.py +500 -0
  45. agentic_runner/workers/fastapi_client.py +396 -0
  46. agentic_runner/workers/harness_usage.py +65 -0
  47. agentic_runner/workers/mcp_config.py +111 -0
  48. agentic_runner/workers/settings.py +314 -0
  49. agentic_runner/workstation.py +687 -0
  50. agentic_runner-2.6.0.dist-info/METADATA +49 -0
  51. agentic_runner-2.6.0.dist-info/RECORD +54 -0
  52. agentic_runner-2.6.0.dist-info/WHEEL +4 -0
  53. agentic_runner-2.6.0.dist-info/entry_points.txt +2 -0
  54. agentic_runner-2.6.0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,339 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from agentic_runner.integrations.git.contracts import (
7
+ AdoptWorkspaceRequest,
8
+ CheckoutWorkBranchRequest,
9
+ CheckoutWorkBranchResult,
10
+ CloneWorkspaceRequest,
11
+ CloneWorkspaceResult,
12
+ CommitAllRequest,
13
+ CommitAllResult,
14
+ FetchBranchRequest,
15
+ FetchBranchResult,
16
+ PrepareVerifierWorkspaceRequest,
17
+ PrepareVerifierWorkspaceResult,
18
+ PushBranchRequest,
19
+ PushBranchResult,
20
+ )
21
+ from agentic_runner.integrations.git.evidence import GitEvidence, build_git_evidence
22
+ from agentic_runner.integrations.git.workspace import (
23
+ GitWorkspacePolicyError,
24
+ build_push_refspec,
25
+ resolve_workspace_path,
26
+ validate_base_branch,
27
+ validate_commit_sha,
28
+ validate_push_branch,
29
+ validate_work_branch,
30
+ )
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class FakeGitWorkspaceCall:
35
+ """Recorded fake git operation for deterministic assertions."""
36
+
37
+ operation: str
38
+ repo_full_name: str
39
+ workspace_path: Path
40
+
41
+
42
+ @dataclass
43
+ class _WorkspaceRecord:
44
+ repo_full_name: str
45
+ remote_url: str
46
+ workspace_root: Path
47
+ workspace_path: Path
48
+ base_branch: str | None = None
49
+ current_branch: str | None = None
50
+ current_branch_base_commit_id: str | None = None
51
+ latest_commit_id: str | None = None
52
+ verifier_head_sha: str | None = None
53
+ verifier_status_summary: str = ""
54
+
55
+
56
+ class FakeGitWorkspace:
57
+ """Deterministic in-memory git workspace adapter for tests and workflow rehearsal."""
58
+
59
+ def __init__(self, *, status_evidence: str = "", diff_evidence: str = "") -> None:
60
+ self._workspaces: dict[Path, _WorkspaceRecord] = {}
61
+ self._remote_branch_heads: dict[tuple[str, str], str] = {}
62
+ self._calls: list[FakeGitWorkspaceCall] = []
63
+ self._next_commit_number = 1
64
+ self._status_evidence = status_evidence
65
+ self._diff_evidence = diff_evidence
66
+
67
+ @property
68
+ def calls(self) -> tuple[FakeGitWorkspaceCall, ...]:
69
+ return tuple(self._calls)
70
+
71
+ def clone_repository(self, request: CloneWorkspaceRequest) -> CloneWorkspaceResult:
72
+ workspace_path = resolve_workspace_path(request.workspace_root, request.workspace_path)
73
+ workspace_root = request.workspace_root.resolve()
74
+ existing_record = self._workspaces.get(workspace_path)
75
+ if existing_record is not None:
76
+ self._require_same_repo(existing_record, request.repo_full_name)
77
+ if existing_record.remote_url != request.remote_url:
78
+ raise GitWorkspacePolicyError(
79
+ "Existing fake workspace origin does not match expected origin"
80
+ )
81
+ self._record_call("clone_repository", request.repo_full_name, workspace_path)
82
+ return CloneWorkspaceResult(
83
+ repo_full_name=existing_record.repo_full_name,
84
+ remote_url=existing_record.remote_url,
85
+ workspace_root=existing_record.workspace_root,
86
+ workspace_path=existing_record.workspace_path,
87
+ )
88
+
89
+ record = _WorkspaceRecord(
90
+ repo_full_name=request.repo_full_name,
91
+ remote_url=request.remote_url,
92
+ workspace_root=workspace_root,
93
+ workspace_path=workspace_path,
94
+ )
95
+ # The real adapter's clone materializes the directory; consumers (e.g. the
96
+ # fix Directive's workspace-exists check) rely on that part of the contract.
97
+ workspace_path.mkdir(parents=True, exist_ok=True)
98
+ self._workspaces[workspace_path] = record
99
+ self._record_call("clone_repository", request.repo_full_name, workspace_path)
100
+ return CloneWorkspaceResult(
101
+ repo_full_name=request.repo_full_name,
102
+ remote_url=request.remote_url,
103
+ workspace_root=workspace_root,
104
+ workspace_path=workspace_path,
105
+ )
106
+
107
+ def adopt_existing_clone(self, request: AdoptWorkspaceRequest) -> CloneWorkspaceResult:
108
+ validate_work_branch(request.work_branch)
109
+ workspace_path = resolve_workspace_path(request.workspace_root, request.workspace_path)
110
+ if not workspace_path.exists():
111
+ raise GitWorkspacePolicyError(f"Adopted workspace '{workspace_path}' does not exist")
112
+ record = self._workspaces.get(workspace_path)
113
+ if record is None:
114
+ record = _WorkspaceRecord(
115
+ repo_full_name=request.repo_full_name,
116
+ remote_url=request.remote_url,
117
+ workspace_root=request.workspace_root.resolve(),
118
+ workspace_path=workspace_path,
119
+ )
120
+ self._workspaces[workspace_path] = record
121
+ self._require_same_repo(record, request.repo_full_name)
122
+ # The real adapter reads these off the adopted checkout; the fake is told them,
123
+ # so commit/push see the same "work branch is checked out" state a clone leaves.
124
+ record.current_branch = request.work_branch
125
+ record.current_branch_base_commit_id = self._remote_branch_heads.get(
126
+ (request.repo_full_name, request.work_branch)
127
+ )
128
+ record.latest_commit_id = record.current_branch_base_commit_id
129
+ self._record_call("adopt_existing_clone", request.repo_full_name, workspace_path)
130
+ return CloneWorkspaceResult(
131
+ repo_full_name=record.repo_full_name,
132
+ remote_url=record.remote_url,
133
+ workspace_root=record.workspace_root,
134
+ workspace_path=record.workspace_path,
135
+ )
136
+
137
+ def fetch_base_branch(self, request: FetchBranchRequest) -> FetchBranchResult:
138
+ validate_base_branch(request.base_branch)
139
+ record = self._get_workspace(request.workspace_path)
140
+ self._require_same_repo(record, request.repo_full_name)
141
+ record.base_branch = request.base_branch
142
+ self._record_call("fetch_base_branch", request.repo_full_name, record.workspace_path)
143
+ return FetchBranchResult(
144
+ repo_full_name=request.repo_full_name,
145
+ workspace_path=record.workspace_path,
146
+ base_branch=request.base_branch,
147
+ )
148
+
149
+ def checkout_work_branch(
150
+ self,
151
+ request: CheckoutWorkBranchRequest,
152
+ ) -> CheckoutWorkBranchResult:
153
+ validate_base_branch(request.base_branch)
154
+ validate_work_branch(request.work_branch)
155
+ record = self._get_workspace(request.workspace_path)
156
+ self._require_same_repo(record, request.repo_full_name)
157
+ record.base_branch = request.base_branch
158
+ record.current_branch = request.work_branch
159
+ record.current_branch_base_commit_id = self._remote_branch_heads.get(
160
+ (request.repo_full_name, request.work_branch)
161
+ )
162
+ record.latest_commit_id = record.current_branch_base_commit_id
163
+ self._record_call("checkout_work_branch", request.repo_full_name, record.workspace_path)
164
+ return CheckoutWorkBranchResult(
165
+ repo_full_name=request.repo_full_name,
166
+ workspace_path=record.workspace_path,
167
+ base_branch=request.base_branch,
168
+ work_branch=request.work_branch,
169
+ )
170
+
171
+ def commit_all(self, request: CommitAllRequest) -> CommitAllResult:
172
+ validate_work_branch(request.work_branch)
173
+ record = self._get_workspace(request.workspace_path)
174
+ self._require_same_repo(record, request.repo_full_name)
175
+ if record.current_branch != request.work_branch:
176
+ raise GitWorkspacePolicyError(
177
+ f"Cannot commit branch '{request.work_branch}' while current branch is "
178
+ f"'{record.current_branch}'"
179
+ )
180
+ if (
181
+ record.current_branch_base_commit_id is not None
182
+ and record.latest_commit_id == record.current_branch_base_commit_id
183
+ ):
184
+ self._record_call("commit_all", request.repo_full_name, record.workspace_path)
185
+ return CommitAllResult(
186
+ repo_full_name=request.repo_full_name,
187
+ workspace_path=record.workspace_path,
188
+ work_branch=request.work_branch,
189
+ commit_message=request.commit_message,
190
+ author_name=request.author_name,
191
+ author_email=request.author_email,
192
+ commit_id=record.current_branch_base_commit_id,
193
+ )
194
+
195
+ commit_id = validate_commit_sha(f"{self._next_commit_number:040x}")
196
+ self._next_commit_number += 1
197
+ record.latest_commit_id = commit_id
198
+ self._record_call("commit_all", request.repo_full_name, record.workspace_path)
199
+ return CommitAllResult(
200
+ repo_full_name=request.repo_full_name,
201
+ workspace_path=record.workspace_path,
202
+ work_branch=request.work_branch,
203
+ commit_message=request.commit_message,
204
+ author_name=request.author_name,
205
+ author_email=request.author_email,
206
+ commit_id=commit_id,
207
+ )
208
+
209
+ def push_branch(self, request: PushBranchRequest) -> PushBranchResult:
210
+ validate_push_branch(
211
+ work_branch=request.work_branch,
212
+ base_branch=request.base_branch,
213
+ protected_branches=request.protected_branches,
214
+ )
215
+ record = self._get_workspace(request.workspace_path)
216
+ self._require_same_repo(record, request.repo_full_name)
217
+ if record.current_branch != request.work_branch:
218
+ raise GitWorkspacePolicyError(
219
+ f"Cannot push branch '{request.work_branch}' while current branch is "
220
+ f"'{record.current_branch}'"
221
+ )
222
+ if record.latest_commit_id is None:
223
+ raise GitWorkspacePolicyError("Cannot push before committing workspace changes")
224
+
225
+ refspec = build_push_refspec(request.work_branch)
226
+ self._remote_branch_heads[(request.repo_full_name, request.work_branch)] = (
227
+ record.latest_commit_id
228
+ )
229
+ self._record_call("push_branch", request.repo_full_name, record.workspace_path)
230
+ return PushBranchResult(
231
+ repo_full_name=request.repo_full_name,
232
+ workspace_path=record.workspace_path,
233
+ work_branch=request.work_branch,
234
+ refspec=refspec,
235
+ commit_id=record.latest_commit_id,
236
+ )
237
+
238
+ def prepare_verifier_workspace(
239
+ self,
240
+ request: PrepareVerifierWorkspaceRequest,
241
+ ) -> PrepareVerifierWorkspaceResult:
242
+ work_branch = validate_work_branch(request.work_branch)
243
+ approved_head_sha = validate_commit_sha(request.approved_head_sha)
244
+ workspace_path = resolve_workspace_path(request.workspace_root, request.workspace_path)
245
+ record = self._get_workspace(workspace_path)
246
+ self._require_same_repo(record, request.repo_full_name)
247
+
248
+ remote_head_sha = self._remote_branch_heads.get((request.repo_full_name, work_branch))
249
+ if remote_head_sha != approved_head_sha:
250
+ fetched_head_sha = remote_head_sha or "<missing>"
251
+ raise GitWorkspacePolicyError(
252
+ f"Fetched work branch head '{fetched_head_sha}' does not match "
253
+ f"approved head '{approved_head_sha}'"
254
+ )
255
+
256
+ attested_head_sha = record.verifier_head_sha or approved_head_sha
257
+ if attested_head_sha != approved_head_sha:
258
+ raise GitWorkspacePolicyError(
259
+ f"Verifier workspace HEAD '{attested_head_sha}' does not match "
260
+ f"approved head '{approved_head_sha}'"
261
+ )
262
+ if record.verifier_status_summary:
263
+ raise GitWorkspacePolicyError(
264
+ "Verifier workspace is dirty immediately before verifier execution: "
265
+ f"{record.verifier_status_summary!r}"
266
+ )
267
+
268
+ record.current_branch = work_branch
269
+ record.verifier_head_sha = approved_head_sha
270
+ self._record_call(
271
+ "prepare_verifier_workspace",
272
+ request.repo_full_name,
273
+ record.workspace_path,
274
+ )
275
+ return PrepareVerifierWorkspaceResult(
276
+ repo_full_name=request.repo_full_name,
277
+ workspace_root=record.workspace_root,
278
+ workspace_path=record.workspace_path,
279
+ work_branch=work_branch,
280
+ approved_head_sha=approved_head_sha,
281
+ attested_head_sha=attested_head_sha,
282
+ status_summary=record.verifier_status_summary,
283
+ )
284
+
285
+ def collect_git_evidence(
286
+ self,
287
+ workspace_path: Path,
288
+ output_limit_bytes: int | None = None,
289
+ ) -> GitEvidence:
290
+ record = self._get_workspace(workspace_path)
291
+ self._record_call("collect_git_evidence", record.repo_full_name, record.workspace_path)
292
+ return build_git_evidence(
293
+ workspace_path=record.workspace_path,
294
+ status=self._status_evidence,
295
+ diff=self._diff_evidence,
296
+ output_limit_bytes=output_limit_bytes or 4096,
297
+ )
298
+
299
+ def set_verifier_workspace_head(self, workspace_path: Path, head_sha: str) -> None:
300
+ record = self._get_workspace(workspace_path)
301
+ record.verifier_head_sha = validate_commit_sha(head_sha)
302
+
303
+ def set_verifier_workspace_status(self, workspace_path: Path, status_summary: str) -> None:
304
+ record = self._get_workspace(workspace_path)
305
+ record.verifier_status_summary = status_summary
306
+
307
+ def set_remote_work_branch_head(
308
+ self,
309
+ *,
310
+ repo_full_name: str,
311
+ work_branch: str,
312
+ head_sha: str,
313
+ ) -> None:
314
+ self._remote_branch_heads[(repo_full_name, validate_work_branch(work_branch))] = (
315
+ validate_commit_sha(head_sha)
316
+ )
317
+
318
+ def _record_call(self, operation: str, repo_full_name: str, workspace_path: Path) -> None:
319
+ self._calls.append(
320
+ FakeGitWorkspaceCall(
321
+ operation=operation,
322
+ repo_full_name=repo_full_name,
323
+ workspace_path=workspace_path,
324
+ )
325
+ )
326
+
327
+ def _get_workspace(self, workspace_path: Path) -> _WorkspaceRecord:
328
+ resolved_path = workspace_path.resolve()
329
+ try:
330
+ return self._workspaces[resolved_path]
331
+ except KeyError as error:
332
+ raise KeyError(f"Git workspace '{resolved_path}' was not cloned") from error
333
+
334
+ def _require_same_repo(self, record: _WorkspaceRecord, repo_full_name: str) -> None:
335
+ if record.repo_full_name != repo_full_name:
336
+ raise GitWorkspacePolicyError(
337
+ f"Workspace repo '{record.repo_full_name}' does not match request repo "
338
+ f"'{repo_full_name}'"
339
+ )