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,314 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Self
5
+
6
+ from pydantic import Field, HttpUrl, field_validator, model_validator
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+
9
+ from agentic_runner_contracts import public_metadata
10
+ from agentic_runner_contracts.activity_io import DEFAULT_BUDGET, Budget
11
+
12
+
13
+ class WorkerSettings(BaseSettings):
14
+ """Runtime configuration for worker-only processes."""
15
+
16
+ TEMPORAL_ADDRESS: str = Field(..., description="Temporal server address in host:port format")
17
+ TEMPORAL_NAMESPACE: str = Field(default="default", description="Temporal namespace name")
18
+ PLATFORM_WORKER_SHARD_INDEX: int = Field(
19
+ default=0,
20
+ ge=0,
21
+ description="Which Platform Worker shard this Deployment serves (PRD issue 37).",
22
+ )
23
+ PLATFORM_WORKER_SHARD_COUNT: int = Field(
24
+ default=1,
25
+ ge=1,
26
+ description="How many Platform Worker shards exist; 1 = every replica polls every org.",
27
+ )
28
+ PLATFORM_WORKER_MAX_ORGANISATIONS: int = Field(
29
+ default=1_000,
30
+ ge=1,
31
+ description=("Organisations one shard polls before refusing to start (map ticket 27 §2)."),
32
+ )
33
+ PLATFORM_WORKER_RECONCILE_SECONDS: float = Field(
34
+ default=60.0,
35
+ gt=0,
36
+ description=(
37
+ "How often a Platform Worker replica re-reads the Organisation list and opens "
38
+ "or closes Worker pairs (ADR-0013 §1)."
39
+ ),
40
+ )
41
+ INTERNAL_FASTAPI_BASE_URL: HttpUrl = Field(..., description="Internal FastAPI HTTP(S) base URL")
42
+ WORKSPACE_ROOT: Path = Field(
43
+ default=Path("/var/lib/agentic-os/workspaces"),
44
+ description="Worker-local root for isolated git workspaces",
45
+ )
46
+ CODEX_HOME: Path = Field(
47
+ default=Path("/var/lib/agentic-os/codex"),
48
+ description="Worker-local Codex CLI config directory mounted from PVC or pod environment",
49
+ )
50
+ WORKER_STATE_DIR: Path = Field(
51
+ default=Path("/var/lib/agentic-os/state"),
52
+ description=(
53
+ "Runner-owned state directory (PRD issue 30). Holds the Contract->uid map, so a "
54
+ "restart reuses the uid a Contract's files are already owned by. Never readable "
55
+ "by a Contract uid."
56
+ ),
57
+ )
58
+ CONTRACT_UID_MIN: int = Field(
59
+ default=60_000,
60
+ ge=1,
61
+ description="Low end of the Runner-local uid range Contracts are allocated from",
62
+ )
63
+ CONTRACT_UID_MAX: int = Field(
64
+ default=60_999,
65
+ ge=1,
66
+ description="High end of the Runner-local uid range Contracts are allocated from",
67
+ )
68
+ CONTRACT_MAX_PROCESSES: int = Field(
69
+ default=512,
70
+ ge=1,
71
+ description=(
72
+ "RLIMIT_NPROC applied per Contract uid at every spawn (ADR-0011 s12 sandbox "
73
+ "floor, map ticket 17 A7): the noisy-neighbour floor, not attenuable by a Grant."
74
+ ),
75
+ )
76
+ CONTRACT_MEMORY_LIMIT_BYTES: int = Field(
77
+ default=4 * 1024**3,
78
+ ge=1,
79
+ description=(
80
+ "Default per-process memory ceiling for a Contract's spawns. An Agent Runtime "
81
+ "Profile's `command_policy.memory_limit_bytes` overrides it per Profile."
82
+ ),
83
+ )
84
+ CODEX_CLI_PATH: str = Field(
85
+ default="codex",
86
+ description=(
87
+ "Codex CLI executable, resolved on PATH by default. Mirrors AppSettings."
88
+ "codex_cli_path so the fleet status probe (issue 06) spawns the same binary "
89
+ "the FastAPI pod does."
90
+ ),
91
+ )
92
+ CODEX_CLI_TIMEOUT_SECONDS: int = Field(
93
+ default=900,
94
+ ge=1,
95
+ description="Maximum Codex CLI execution duration in seconds",
96
+ )
97
+ CODEX_CLI_OUTPUT_LIMIT_BYTES: int = Field(
98
+ default=65_536,
99
+ ge=1,
100
+ description="Maximum retained stdout and stderr bytes from Codex CLI evidence",
101
+ )
102
+ CODEX_SANDBOX_MODE: str = Field(
103
+ default="",
104
+ description="Explicit Codex sandbox mode required before worker-local execution",
105
+ )
106
+ CODEX_ASK_FOR_APPROVAL: str = Field(
107
+ default="",
108
+ description="Explicit Codex approval mode required before worker-local execution",
109
+ )
110
+ CODEX_STRICT_CONFIG: bool = Field(
111
+ default=False,
112
+ description="Require Codex strict config mode before worker-local execution",
113
+ )
114
+ CODEX_POLICY_HOOK_CONFIGURED: bool = Field(
115
+ default=False,
116
+ description="Worker assertion that Codex policy hook or permission profile is configured",
117
+ )
118
+ ANTHROPIC_API_KEY: str = Field(
119
+ default="",
120
+ description="Claude Code runtime API key (api_key auth model; redacted from evidence)",
121
+ )
122
+ CLAUDE_MODEL: str = Field(
123
+ default="claude-opus-4-8",
124
+ description="Model id the Claude Code runtime runs with",
125
+ )
126
+ CLAUDE_CLI_TIMEOUT_SECONDS: int = Field(
127
+ default=900,
128
+ ge=1,
129
+ description="Maximum Claude Code CLI execution duration in seconds",
130
+ )
131
+ CLAUDE_CLI_OUTPUT_LIMIT_BYTES: int = Field(
132
+ default=65_536,
133
+ ge=1,
134
+ description="Maximum retained stdout and stderr bytes from Claude Code CLI evidence",
135
+ )
136
+ CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS: bool = Field(
137
+ default=False,
138
+ description=(
139
+ "Worker assertion that the container sandbox makes autonomous Claude Code edits safe; "
140
+ "fail-closed — the Claude runtime refuses until this is explicitly set"
141
+ ),
142
+ )
143
+ TRIAGE_MODEL: str = Field(
144
+ default="openai/gpt-5-mini",
145
+ description="Model the Triage Directive's one-turn classification call runs on (issue 29)",
146
+ )
147
+ TRIAGE_DIRECTIVE_TIMEOUT_SECONDS: float = Field(
148
+ default=30.0,
149
+ gt=0,
150
+ description=(
151
+ "How long the Triage Directive waits on the proxy before falling back to the "
152
+ "keyword classifier (PRD issue 29: a hang is treated exactly like a malformed "
153
+ "answer, never an Incident)."
154
+ ),
155
+ )
156
+ BUDGET_MAX_DIRECTIVES: int = Field(
157
+ default=DEFAULT_BUDGET.max_directives,
158
+ ge=1,
159
+ description="Default per-Work-Record ceiling on Directives in a Ralph Loop",
160
+ )
161
+ BUDGET_MAX_WALL_CLOCK_SECONDS: float = Field(
162
+ default=DEFAULT_BUDGET.max_wall_clock_seconds,
163
+ gt=0,
164
+ description="Default per-Work-Record ceiling on cumulative Directive runtime, in seconds",
165
+ )
166
+ BUDGET_MAX_WALL_CLOCK_SECONDS_USER_HOSTED: float = Field(
167
+ # 23 item 4 asks for "a longer default" and fixes no value; twice the ordinary
168
+ # default is this release's reading, and an operator's to change.
169
+ default=DEFAULT_BUDGET.max_wall_clock_seconds * 2,
170
+ gt=0,
171
+ description=(
172
+ "Default wall-clock ceiling for a Work Record whose Contract names "
173
+ "`runner_host: user` (PRD issue 47): a workstation's sleep is an outage the "
174
+ "wall-clock keeps counting through"
175
+ ),
176
+ )
177
+ BUDGET_MAX_TOKENS: int = Field(
178
+ default=DEFAULT_BUDGET.max_tokens,
179
+ ge=0,
180
+ description=(
181
+ "Default per-Work-Record token Budget (map ticket 12 B1: tokens are the unit). "
182
+ "0 means no token ceiling. A Work Record whose `budget_max_tokens` column is "
183
+ "set overrides this; null means this default applies."
184
+ ),
185
+ )
186
+
187
+ model_config = SettingsConfigDict(env_prefix="", frozen=True)
188
+
189
+ @property
190
+ def default_budget(self) -> Budget:
191
+ """The per-Work-Record Budget the worker assigns when assembling context."""
192
+ return Budget(
193
+ max_directives=self.BUDGET_MAX_DIRECTIVES,
194
+ max_wall_clock_seconds=self.BUDGET_MAX_WALL_CLOCK_SECONDS,
195
+ max_tokens=self.BUDGET_MAX_TOKENS,
196
+ )
197
+
198
+ @field_validator("TEMPORAL_ADDRESS")
199
+ @classmethod
200
+ def validate_temporal_address(cls, value: str) -> str:
201
+ """Validate host:port shape for Temporal server address."""
202
+
203
+ stripped_value = value.strip()
204
+ if stripped_value.count(":") < 1:
205
+ raise ValueError("TEMPORAL_ADDRESS must follow host:port")
206
+
207
+ host, port_text = stripped_value.rsplit(":", 1)
208
+ if not host:
209
+ raise ValueError("TEMPORAL_ADDRESS host may not be empty")
210
+ if host.startswith("[") and host.endswith("]"):
211
+ host = host[1:-1]
212
+ if not host:
213
+ raise ValueError("TEMPORAL_ADDRESS host may not be empty")
214
+
215
+ if ":" in host:
216
+ raise ValueError("TEMPORAL_ADDRESS host may not contain unescaped ':'")
217
+
218
+ try:
219
+ port = int(port_text)
220
+ except ValueError as exception:
221
+ raise ValueError("TEMPORAL_ADDRESS port must be an integer") from exception
222
+
223
+ if not (1 <= port <= 65535):
224
+ raise ValueError("TEMPORAL_ADDRESS port must be in range 1-65535")
225
+
226
+ return stripped_value
227
+
228
+ @field_validator("TEMPORAL_NAMESPACE")
229
+ @classmethod
230
+ def strip_required_text(cls, value: str) -> str:
231
+ """Trim worker identifiers and reject empty values."""
232
+
233
+ stripped_value = value.strip()
234
+ if not stripped_value:
235
+ raise ValueError("worker setting must not be empty")
236
+ return stripped_value
237
+
238
+ @field_validator("TEMPORAL_NAMESPACE")
239
+ @classmethod
240
+ def validate_temporal_namespace(cls, value: str) -> str:
241
+ """Accept only the pre-cut `default` or an Organisation namespace the builder makes.
242
+
243
+ Mirrors ``AppSettings``: the Release D cut (PRD issue 16) sets this side and the
244
+ backend's in one commit, and a worker pointed at a namespace nobody provisioned
245
+ polls an empty queue in silence. Failing at startup is the loud half.
246
+ """
247
+ if value != "default" and not public_metadata.is_namespace(value):
248
+ raise ValueError(
249
+ "TEMPORAL_NAMESPACE must be 'default' or an Organisation namespace "
250
+ "built by agentic_runner_contracts.public_metadata.namespace()"
251
+ )
252
+ return value
253
+
254
+ @field_validator("WORKSPACE_ROOT", "CODEX_HOME", "WORKER_STATE_DIR")
255
+ @classmethod
256
+ def validate_worker_local_path(cls, value: Path) -> Path:
257
+ """Require absolute worker-local filesystem paths for worker-only mounts."""
258
+
259
+ if not value.is_absolute():
260
+ raise ValueError("worker-local paths must be absolute")
261
+ return value
262
+
263
+ @field_validator("CODEX_SANDBOX_MODE", "CODEX_ASK_FOR_APPROVAL")
264
+ @classmethod
265
+ def strip_codex_guard_text(cls, value: str) -> str:
266
+ """Trim optional Codex guard settings while allowing empty fail-closed defaults."""
267
+
268
+ return value.strip()
269
+
270
+ @model_validator(mode="after")
271
+ def validate_contract_uid_range(self) -> Self:
272
+ """A Runner that hands two Contracts the same uid has no isolation at all."""
273
+
274
+ if self.CONTRACT_UID_MAX < self.CONTRACT_UID_MIN:
275
+ raise ValueError("CONTRACT_UID_MAX must not be below CONTRACT_UID_MIN")
276
+ return self
277
+
278
+ @model_validator(mode="after")
279
+ def validate_worker_state_dir_isolated_from_workspaces(self) -> Self:
280
+ """The Contract->uid map must not sit inside a tree a Contract uid can read."""
281
+
282
+ if _paths_overlap(
283
+ self.WORKSPACE_ROOT.resolve(strict=False), self.WORKER_STATE_DIR.resolve(strict=False)
284
+ ):
285
+ raise ValueError("WORKER_STATE_DIR must not overlap WORKSPACE_ROOT")
286
+ return self
287
+
288
+ @model_validator(mode="after")
289
+ def validate_codex_home_isolated_from_workspaces(self) -> Self:
290
+ """Prevent Codex auth/config state from overlapping editable workspaces."""
291
+
292
+ workspace_root = self.WORKSPACE_ROOT.resolve(strict=False)
293
+ codex_home = self.CODEX_HOME.resolve(strict=False)
294
+ if _paths_overlap(workspace_root, codex_home):
295
+ raise ValueError("CODEX_HOME must not overlap WORKSPACE_ROOT")
296
+ return self
297
+
298
+
299
+ def get_worker_settings() -> WorkerSettings:
300
+ """Create and return worker settings from the current environment."""
301
+
302
+ return WorkerSettings()
303
+
304
+
305
+ def _paths_overlap(left: Path, right: Path) -> bool:
306
+ return _is_relative_to(left, right) or _is_relative_to(right, left)
307
+
308
+
309
+ def _is_relative_to(path: Path, root: Path) -> bool:
310
+ try:
311
+ path.relative_to(root)
312
+ except ValueError:
313
+ return False
314
+ return True