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,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."""