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.
- agentic_runner/__init__.py +12 -0
- agentic_runner/activities.py +4918 -0
- agentic_runner/callback.py +342 -0
- agentic_runner/child_watcher.py +66 -0
- agentic_runner/cli.py +416 -0
- agentic_runner/config.py +105 -0
- agentic_runner/credentials.py +252 -0
- agentic_runner/device_login_activities.py +79 -0
- agentic_runner/egress.py +243 -0
- agentic_runner/heartbeat_link.py +249 -0
- agentic_runner/hooks.py +455 -0
- agentic_runner/host_store.py +295 -0
- agentic_runner/integrations/__init__.py +0 -0
- agentic_runner/integrations/git/__init__.py +1 -0
- agentic_runner/integrations/git/contracts.py +198 -0
- agentic_runner/integrations/git/evidence.py +442 -0
- agentic_runner/integrations/git/fake_workspace.py +339 -0
- agentic_runner/integrations/git/workspace.py +921 -0
- agentic_runner/integrations/github/__init__.py +53 -0
- agentic_runner/integrations/github/auth.py +171 -0
- agentic_runner/integrations/github/fake_client.py +494 -0
- agentic_runner/integrations/github/gh_client.py +944 -0
- agentic_runner/lifecycle.py +48 -0
- agentic_runner/llm_proxy.py +937 -0
- agentic_runner/mcp.py +342 -0
- agentic_runner/message_store.py +341 -0
- agentic_runner/py.typed +0 -0
- agentic_runner/recipient_key_secret.py +134 -0
- agentic_runner/registration.py +363 -0
- agentic_runner/runtime/__init__.py +0 -0
- agentic_runner/runtime/verifier_command.py +344 -0
- agentic_runner/sealed_box.py +509 -0
- agentic_runner/service.py +1068 -0
- agentic_runner/tiny_http.py +133 -0
- agentic_runner/triage_activities.py +113 -0
- agentic_runner/user_sources.py +546 -0
- agentic_runner/workers/__init__.py +1 -0
- agentic_runner/workers/_runtime_support.py +388 -0
- agentic_runner/workers/agent_runtime.py +93 -0
- agentic_runner/workers/claude_runtime.py +226 -0
- agentic_runner/workers/codex_runtime.py +311 -0
- agentic_runner/workers/command_policy.py +250 -0
- agentic_runner/workers/contract_device_login.py +211 -0
- agentic_runner/workers/contract_isolation.py +500 -0
- agentic_runner/workers/fastapi_client.py +396 -0
- agentic_runner/workers/harness_usage.py +65 -0
- agentic_runner/workers/mcp_config.py +111 -0
- agentic_runner/workers/settings.py +314 -0
- agentic_runner/workstation.py +687 -0
- agentic_runner-2.6.0.dist-info/METADATA +49 -0
- agentic_runner-2.6.0.dist-info/RECORD +54 -0
- agentic_runner-2.6.0.dist-info/WHEEL +4 -0
- agentic_runner-2.6.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|