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,295 @@
|
|
|
1
|
+
"""The workstation's Credential Reference store (PRD issue 47, map ticket 23 item 5).
|
|
2
|
+
|
|
3
|
+
The OS credential store when it is reachable -- Keychain on macOS, Secret Service on
|
|
4
|
+
Linux, Credential Manager on Windows -- and a directory of ``0600`` files under the
|
|
5
|
+
state directory otherwise. Reachable is the operative word: a login agent reaches the
|
|
6
|
+
login keychain and a daemon does not (Apple TN3137), which is half the reason the
|
|
7
|
+
workstation Runner is a login agent at all.
|
|
8
|
+
|
|
9
|
+
Every backend is driven through the OS's own tool or API rather than a Python keyring
|
|
10
|
+
library: ``security`` and ``secret-tool`` ship with the OS (or its desktop), and the
|
|
11
|
+
Runner stays inside the dependency set an Organisation already audited. Values reach the
|
|
12
|
+
tools on **stdin**, never argv, because argv is readable by every process on the box.
|
|
13
|
+
|
|
14
|
+
The store holds a value per Credential Reference name, scoped by Organisation -- one
|
|
15
|
+
process per Organisation (23 item 3) means one service name per process. The Runner
|
|
16
|
+
itself only ever *reads* (``credentials.HostCredentialStore``); ``put`` is the host
|
|
17
|
+
operator's act, through ``agentic-runner credential set``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import shutil
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from collections.abc import Callable, Sequence
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Protocol
|
|
29
|
+
|
|
30
|
+
from agentic_runner.credentials import DirectoryCredentialStore
|
|
31
|
+
from agentic_runner_contracts.runner_registration import StoreKind
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"FileCredentialStore",
|
|
35
|
+
"KeychainStore",
|
|
36
|
+
"SecretServiceStore",
|
|
37
|
+
"WindowsCredentialStore",
|
|
38
|
+
"WorkstationStore",
|
|
39
|
+
"open_workstation_store",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
Runner = Callable[..., subprocess.CompletedProcess[str]]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WorkstationStore(Protocol):
|
|
46
|
+
kind: StoreKind
|
|
47
|
+
|
|
48
|
+
def get(self, reference: str) -> str | None: ...
|
|
49
|
+
def put(self, reference: str, value: str) -> None: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _single_segment(reference: str) -> str:
|
|
53
|
+
# The same rule `DirectoryCredentialStore` enforces, so a reference that works in one
|
|
54
|
+
# store works in every other.
|
|
55
|
+
if not reference or "/" in reference or "\\" in reference or reference in {".", ".."}:
|
|
56
|
+
raise ValueError(f"Credential Reference {reference!r} is not a single name")
|
|
57
|
+
return reference
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FileCredentialStore(DirectoryCredentialStore):
|
|
61
|
+
"""The fallback: one ``0600`` file per reference, in a ``0700`` directory."""
|
|
62
|
+
|
|
63
|
+
kind = StoreKind.FILE
|
|
64
|
+
|
|
65
|
+
def __init__(self, root: Path) -> None:
|
|
66
|
+
super().__init__(root)
|
|
67
|
+
self.root = root
|
|
68
|
+
|
|
69
|
+
def put(self, reference: str, value: str) -> None:
|
|
70
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
self.root.chmod(0o700)
|
|
72
|
+
path = self.root / _single_segment(reference)
|
|
73
|
+
# Created 0600 rather than chmod-ed after: there is no instant at which the value
|
|
74
|
+
# sits in a file another uid can open.
|
|
75
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
76
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
77
|
+
handle.write(value)
|
|
78
|
+
path.chmod(0o600)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class KeychainStore:
|
|
82
|
+
"""macOS: generic passwords in the login keychain, via ``security``."""
|
|
83
|
+
|
|
84
|
+
kind = StoreKind.KEYCHAIN
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self, service: str, *, keychain: Path | None = None, run: Runner = subprocess.run
|
|
88
|
+
) -> None:
|
|
89
|
+
self._service = service
|
|
90
|
+
self._keychain = [str(keychain)] if keychain is not None else []
|
|
91
|
+
self._run = run
|
|
92
|
+
|
|
93
|
+
def available(self) -> bool:
|
|
94
|
+
if shutil.which("security") is None:
|
|
95
|
+
return False
|
|
96
|
+
probe = self._run(
|
|
97
|
+
["security", "show-keychain-info", *self._keychain],
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
check=False,
|
|
101
|
+
)
|
|
102
|
+
return probe.returncode == 0
|
|
103
|
+
|
|
104
|
+
def get(self, reference: str) -> str | None:
|
|
105
|
+
found = self._run(
|
|
106
|
+
[
|
|
107
|
+
"security",
|
|
108
|
+
"find-generic-password",
|
|
109
|
+
"-s",
|
|
110
|
+
self._service,
|
|
111
|
+
"-a",
|
|
112
|
+
_single_segment(reference),
|
|
113
|
+
"-w",
|
|
114
|
+
*self._keychain,
|
|
115
|
+
],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
check=False,
|
|
119
|
+
)
|
|
120
|
+
return found.stdout.rstrip("\n") if found.returncode == 0 else None
|
|
121
|
+
|
|
122
|
+
def put(self, reference: str, value: str) -> None:
|
|
123
|
+
if "\n" in value:
|
|
124
|
+
raise ValueError("a Keychain value is a single line")
|
|
125
|
+
# `security -i` reads its command from stdin, so the value never enters argv.
|
|
126
|
+
command = " ".join(
|
|
127
|
+
[
|
|
128
|
+
"add-generic-password -U",
|
|
129
|
+
f"-s {_quote(self._service)}",
|
|
130
|
+
f"-a {_quote(_single_segment(reference))}",
|
|
131
|
+
f"-w {_quote(value)}",
|
|
132
|
+
*(_quote(item) for item in self._keychain),
|
|
133
|
+
]
|
|
134
|
+
)
|
|
135
|
+
stored = self._run(
|
|
136
|
+
["security", "-i"], input=command + "\n", capture_output=True, text=True, check=False
|
|
137
|
+
)
|
|
138
|
+
if stored.returncode != 0 or stored.stderr.strip():
|
|
139
|
+
raise RuntimeError(f"Keychain refused the value: {stored.stderr.strip()}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _quote(value: str) -> str:
|
|
143
|
+
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class SecretServiceStore:
|
|
147
|
+
"""Linux: the desktop's Secret Service (GNOME Keyring, KWallet), via ``secret-tool``."""
|
|
148
|
+
|
|
149
|
+
kind = StoreKind.SECRET_SERVICE
|
|
150
|
+
|
|
151
|
+
def __init__(self, service: str, *, run: Runner = subprocess.run) -> None:
|
|
152
|
+
self._service = service
|
|
153
|
+
self._run = run
|
|
154
|
+
|
|
155
|
+
def available(self) -> bool:
|
|
156
|
+
if shutil.which("secret-tool") is None:
|
|
157
|
+
return False
|
|
158
|
+
# A lookup that finds nothing exits 1 with nothing on stderr; one that cannot
|
|
159
|
+
# reach the bus (a headless box, a daemon) says so on stderr.
|
|
160
|
+
probe = self._run(
|
|
161
|
+
["secret-tool", "lookup", "service", self._service, "account", "-probe-"],
|
|
162
|
+
capture_output=True,
|
|
163
|
+
text=True,
|
|
164
|
+
check=False,
|
|
165
|
+
)
|
|
166
|
+
return probe.returncode == 0 or not probe.stderr.strip()
|
|
167
|
+
|
|
168
|
+
def get(self, reference: str) -> str | None:
|
|
169
|
+
found = self._run(
|
|
170
|
+
["secret-tool", "lookup", *self._attributes(reference)],
|
|
171
|
+
capture_output=True,
|
|
172
|
+
text=True,
|
|
173
|
+
check=False,
|
|
174
|
+
)
|
|
175
|
+
return found.stdout if found.returncode == 0 and found.stdout else None
|
|
176
|
+
|
|
177
|
+
def put(self, reference: str, value: str) -> None:
|
|
178
|
+
stored = self._run(
|
|
179
|
+
[
|
|
180
|
+
"secret-tool",
|
|
181
|
+
"store",
|
|
182
|
+
f"--label=agentic-runner {self._service} {reference}",
|
|
183
|
+
*self._attributes(reference),
|
|
184
|
+
],
|
|
185
|
+
input=value,
|
|
186
|
+
capture_output=True,
|
|
187
|
+
text=True,
|
|
188
|
+
check=False,
|
|
189
|
+
)
|
|
190
|
+
if stored.returncode != 0:
|
|
191
|
+
raise RuntimeError(f"Secret Service refused the value: {stored.stderr.strip()}")
|
|
192
|
+
|
|
193
|
+
def _attributes(self, reference: str) -> Sequence[str]:
|
|
194
|
+
return ["service", self._service, "account", _single_segment(reference)]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class WindowsCredentialStore:
|
|
198
|
+
"""Windows: generic credentials in the user's Credential Manager (``CredReadW``)."""
|
|
199
|
+
|
|
200
|
+
kind = StoreKind.CREDENTIAL_MANAGER
|
|
201
|
+
|
|
202
|
+
def __init__(self, service: str) -> None:
|
|
203
|
+
self._service = service
|
|
204
|
+
|
|
205
|
+
def available(self) -> bool:
|
|
206
|
+
return sys.platform == "win32"
|
|
207
|
+
|
|
208
|
+
def _target(self, reference: str) -> str:
|
|
209
|
+
return f"{self._service}/{_single_segment(reference)}"
|
|
210
|
+
|
|
211
|
+
def get(self, reference: str) -> str | None:
|
|
212
|
+
if sys.platform != "win32":
|
|
213
|
+
return None
|
|
214
|
+
return _win_read(self._target(reference))
|
|
215
|
+
|
|
216
|
+
def put(self, reference: str, value: str) -> None:
|
|
217
|
+
if sys.platform != "win32":
|
|
218
|
+
raise RuntimeError("Credential Manager exists only on Windows")
|
|
219
|
+
_win_write(self._target(reference), value)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if sys.platform == "win32": # pragma: no cover - exercised on Windows only
|
|
223
|
+
import ctypes
|
|
224
|
+
from ctypes import wintypes
|
|
225
|
+
|
|
226
|
+
_CRED_TYPE_GENERIC = 1
|
|
227
|
+
_CRED_PERSIST_LOCAL_MACHINE = 2
|
|
228
|
+
|
|
229
|
+
class _Credential(ctypes.Structure):
|
|
230
|
+
_fields_ = [
|
|
231
|
+
("Flags", wintypes.DWORD),
|
|
232
|
+
("Type", wintypes.DWORD),
|
|
233
|
+
("TargetName", wintypes.LPWSTR),
|
|
234
|
+
("Comment", wintypes.LPWSTR),
|
|
235
|
+
("LastWritten", wintypes.FILETIME),
|
|
236
|
+
("CredentialBlobSize", wintypes.DWORD),
|
|
237
|
+
("CredentialBlob", ctypes.POINTER(ctypes.c_char)),
|
|
238
|
+
("Persist", wintypes.DWORD),
|
|
239
|
+
("AttributeCount", wintypes.DWORD),
|
|
240
|
+
("Attributes", ctypes.c_void_p),
|
|
241
|
+
("TargetAlias", wintypes.LPWSTR),
|
|
242
|
+
("UserName", wintypes.LPWSTR),
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
_advapi = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
246
|
+
|
|
247
|
+
def _win_read(target: str) -> str | None:
|
|
248
|
+
pointer = ctypes.POINTER(_Credential)()
|
|
249
|
+
if not _advapi.CredReadW(target, _CRED_TYPE_GENERIC, 0, ctypes.byref(pointer)):
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
credential = pointer.contents
|
|
253
|
+
blob = ctypes.string_at(credential.CredentialBlob, credential.CredentialBlobSize)
|
|
254
|
+
return blob.decode("utf-8")
|
|
255
|
+
finally:
|
|
256
|
+
_advapi.CredFree(pointer)
|
|
257
|
+
|
|
258
|
+
def _win_write(target: str, value: str) -> None:
|
|
259
|
+
blob = value.encode("utf-8")
|
|
260
|
+
credential = _Credential()
|
|
261
|
+
credential.Type = _CRED_TYPE_GENERIC
|
|
262
|
+
credential.TargetName = target
|
|
263
|
+
credential.CredentialBlobSize = len(blob)
|
|
264
|
+
credential.CredentialBlob = ctypes.cast(
|
|
265
|
+
ctypes.create_string_buffer(blob, len(blob)), ctypes.POINTER(ctypes.c_char)
|
|
266
|
+
)
|
|
267
|
+
credential.Persist = _CRED_PERSIST_LOCAL_MACHINE
|
|
268
|
+
credential.UserName = "agentic-runner"
|
|
269
|
+
if not _advapi.CredWriteW(ctypes.byref(credential), 0):
|
|
270
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
271
|
+
|
|
272
|
+
else:
|
|
273
|
+
|
|
274
|
+
def _win_read(target: str) -> str | None:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
def _win_write(target: str, value: str) -> None:
|
|
278
|
+
raise RuntimeError("Credential Manager exists only on Windows")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def open_workstation_store(
|
|
282
|
+
org: str, *, fallback: Path, platform: str = sys.platform, run: Runner = subprocess.run
|
|
283
|
+
) -> WorkstationStore:
|
|
284
|
+
"""The OS store for this platform if it answers, else the ``0600`` file fallback."""
|
|
285
|
+
|
|
286
|
+
service = f"agentic-runner.{org}"
|
|
287
|
+
candidates: dict[str, KeychainStore | SecretServiceStore | WindowsCredentialStore] = {
|
|
288
|
+
"darwin": KeychainStore(service, run=run),
|
|
289
|
+
"linux": SecretServiceStore(service, run=run),
|
|
290
|
+
"win32": WindowsCredentialStore(service),
|
|
291
|
+
}
|
|
292
|
+
candidate = candidates.get(platform)
|
|
293
|
+
if candidate is not None and candidate.available():
|
|
294
|
+
return candidate
|
|
295
|
+
return FileCredentialStore(fallback)
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Git workspace contracts and deterministic test adapters."""
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from agentic_runner.integrations.git.evidence import GitEvidence
|
|
9
|
+
|
|
10
|
+
DEFAULT_COMMIT_AUTHOR_NAME = "QTS Agentic OS"
|
|
11
|
+
DEFAULT_COMMIT_AUTHOR_EMAIL = "agentic-os[bot]@qts.one"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class CloneWorkspaceRequest:
|
|
16
|
+
"""Request to clone a tokenless remote into a worker-local workspace path."""
|
|
17
|
+
|
|
18
|
+
repo_full_name: str
|
|
19
|
+
remote_url: str
|
|
20
|
+
workspace_root: Path
|
|
21
|
+
workspace_path: Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CloneWorkspaceResult:
|
|
26
|
+
"""Result of preparing a local repository workspace."""
|
|
27
|
+
|
|
28
|
+
repo_full_name: str
|
|
29
|
+
remote_url: str
|
|
30
|
+
workspace_root: Path
|
|
31
|
+
workspace_path: Path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class AdoptWorkspaceRequest:
|
|
36
|
+
"""A checkout produced outside this adapter, offered for adoption."""
|
|
37
|
+
|
|
38
|
+
repo_full_name: str
|
|
39
|
+
remote_url: str
|
|
40
|
+
workspace_root: Path
|
|
41
|
+
workspace_path: Path
|
|
42
|
+
work_branch: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class FetchBranchRequest:
|
|
47
|
+
"""Request to fetch a base branch into an existing workspace."""
|
|
48
|
+
|
|
49
|
+
repo_full_name: str
|
|
50
|
+
workspace_path: Path
|
|
51
|
+
base_branch: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class FetchBranchResult:
|
|
56
|
+
"""Result of fetching a base branch."""
|
|
57
|
+
|
|
58
|
+
repo_full_name: str
|
|
59
|
+
workspace_path: Path
|
|
60
|
+
base_branch: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class CheckoutWorkBranchRequest:
|
|
65
|
+
"""Request to create or reset a generated work branch from the base branch."""
|
|
66
|
+
|
|
67
|
+
repo_full_name: str
|
|
68
|
+
workspace_path: Path
|
|
69
|
+
base_branch: str
|
|
70
|
+
work_branch: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class CheckoutWorkBranchResult:
|
|
75
|
+
"""Result of checking out a generated work branch."""
|
|
76
|
+
|
|
77
|
+
repo_full_name: str
|
|
78
|
+
workspace_path: Path
|
|
79
|
+
base_branch: str
|
|
80
|
+
work_branch: str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class CommitAllRequest:
|
|
85
|
+
"""Request to commit all workspace changes with a deterministic author."""
|
|
86
|
+
|
|
87
|
+
repo_full_name: str
|
|
88
|
+
workspace_path: Path
|
|
89
|
+
work_branch: str
|
|
90
|
+
commit_message: str
|
|
91
|
+
author_name: str = DEFAULT_COMMIT_AUTHOR_NAME
|
|
92
|
+
author_email: str = DEFAULT_COMMIT_AUTHOR_EMAIL
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class CommitAllResult:
|
|
97
|
+
"""Result of committing all workspace changes."""
|
|
98
|
+
|
|
99
|
+
repo_full_name: str
|
|
100
|
+
workspace_path: Path
|
|
101
|
+
work_branch: str
|
|
102
|
+
commit_message: str
|
|
103
|
+
author_name: str
|
|
104
|
+
author_email: str
|
|
105
|
+
commit_id: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class PushBranchRequest:
|
|
110
|
+
"""Request to push the current HEAD to a generated work branch."""
|
|
111
|
+
|
|
112
|
+
repo_full_name: str
|
|
113
|
+
workspace_path: Path
|
|
114
|
+
base_branch: str
|
|
115
|
+
work_branch: str
|
|
116
|
+
protected_branches: tuple[str, ...] = ()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class PushBranchResult:
|
|
121
|
+
"""Result of pushing a generated work branch."""
|
|
122
|
+
|
|
123
|
+
repo_full_name: str
|
|
124
|
+
workspace_path: Path
|
|
125
|
+
work_branch: str
|
|
126
|
+
refspec: str
|
|
127
|
+
commit_id: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class PrepareVerifierWorkspaceRequest:
|
|
132
|
+
"""Request to attest a verifier workspace at the approved generated branch head."""
|
|
133
|
+
|
|
134
|
+
repo_full_name: str
|
|
135
|
+
workspace_root: Path
|
|
136
|
+
workspace_path: Path
|
|
137
|
+
work_branch: str
|
|
138
|
+
approved_head_sha: str
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class PrepareVerifierWorkspaceResult:
|
|
143
|
+
"""Attestation proving the verifier workspace path, HEAD, and status."""
|
|
144
|
+
|
|
145
|
+
repo_full_name: str
|
|
146
|
+
workspace_root: Path
|
|
147
|
+
workspace_path: Path
|
|
148
|
+
work_branch: str
|
|
149
|
+
approved_head_sha: str
|
|
150
|
+
attested_head_sha: str
|
|
151
|
+
status_summary: str
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@runtime_checkable
|
|
155
|
+
class GitWorkspace(Protocol):
|
|
156
|
+
"""Deep seam for local git workspace lifecycle operations."""
|
|
157
|
+
|
|
158
|
+
def clone_repository(self, request: CloneWorkspaceRequest) -> CloneWorkspaceResult:
|
|
159
|
+
"""Clone a tokenless remote into a worker-local workspace."""
|
|
160
|
+
|
|
161
|
+
def adopt_existing_clone(self, request: AdoptWorkspaceRequest) -> CloneWorkspaceResult:
|
|
162
|
+
"""Adopt a checkout this adapter did not clone (the ``checkout`` hook, issue 45).
|
|
163
|
+
|
|
164
|
+
Every other method here is keyed on a workspace this adapter cloned itself, so a
|
|
165
|
+
``checkout`` Runner Hook -- a separate process, which cannot reach the adapter --
|
|
166
|
+
has to hand its result back across the port or the Directive dies on the first
|
|
167
|
+
git call after it. Validated rather than trusted: a work tree root, ``origin`` at
|
|
168
|
+
the repository's own remote (the push refspec goes there), and the work branch
|
|
169
|
+
already checked out.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def fetch_base_branch(self, request: FetchBranchRequest) -> FetchBranchResult:
|
|
173
|
+
"""Fetch the requested base branch into a workspace."""
|
|
174
|
+
|
|
175
|
+
def checkout_work_branch(
|
|
176
|
+
self,
|
|
177
|
+
request: CheckoutWorkBranchRequest,
|
|
178
|
+
) -> CheckoutWorkBranchResult:
|
|
179
|
+
"""Checkout a generated work branch from the base branch."""
|
|
180
|
+
|
|
181
|
+
def commit_all(self, request: CommitAllRequest) -> CommitAllResult:
|
|
182
|
+
"""Commit all workspace changes."""
|
|
183
|
+
|
|
184
|
+
def push_branch(self, request: PushBranchRequest) -> PushBranchResult:
|
|
185
|
+
"""Push the work branch using a safe HEAD-to-branch refspec."""
|
|
186
|
+
|
|
187
|
+
def prepare_verifier_workspace(
|
|
188
|
+
self,
|
|
189
|
+
request: PrepareVerifierWorkspaceRequest,
|
|
190
|
+
) -> PrepareVerifierWorkspaceResult:
|
|
191
|
+
"""Prepare and attest a clean verifier workspace at the approved head."""
|
|
192
|
+
|
|
193
|
+
def collect_git_evidence(
|
|
194
|
+
self,
|
|
195
|
+
workspace_path: Path,
|
|
196
|
+
output_limit_bytes: int | None = None,
|
|
197
|
+
) -> GitEvidence:
|
|
198
|
+
"""Collect bounded, redacted status and diff evidence for a workspace."""
|