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,226 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import re
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Final
7
+
8
+ from agentic_runner.workers._runtime_support import (
9
+ SubprocessResult,
10
+ apply_extra_env,
11
+ bound_text,
12
+ command_policy_refusal,
13
+ hash_command,
14
+ is_relative_to,
15
+ redact_match,
16
+ run_subprocess_exec,
17
+ workspace_id,
18
+ )
19
+ from agentic_runner.workers.agent_runtime import (
20
+ AgentRuntime,
21
+ AuthModel,
22
+ DirectiveEvidence,
23
+ DirectiveRequest,
24
+ DirectiveResult,
25
+ )
26
+ from agentic_runner.workers.mcp_config import claude_mcp_config
27
+ from agentic_runner.workers.settings import WorkerSettings
28
+
29
+ __all__ = ["ClaudeRuntime"]
30
+
31
+ _SECRET_VALUE_PATTERNS: Final[tuple[re.Pattern[str], ...]] = (
32
+ re.compile(
33
+ r"(?i)((?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|credential)\s*=\s*)([^\s,}]+)"
34
+ ),
35
+ re.compile(
36
+ r"(?i)((?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|credential)\s*:\s*)([^\s,}]+)"
37
+ ),
38
+ re.compile(
39
+ r"(?i)((?:\"(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|credential)\"\s*:\s*\"))([^\"]+)(\")"
40
+ ),
41
+ re.compile(r"(?i)((?:--password|--token|--secret|--api-key)\s+)([^\s]+)"),
42
+ re.compile(r"(?i)(authorization:\s*bearer\s+)([^\s]+)"),
43
+ re.compile(r"(?i)(bearer\s+)([A-Za-z0-9._~+/-]{8,})"),
44
+ re.compile(r"(?i)(sk-[A-Za-z0-9_-]{8,})"),
45
+ re.compile(r"(?i)(?:~|/[^\s]*)/\.claude(?:/[^\s-]+)?"),
46
+ re.compile(r"(?is)-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----"),
47
+ re.compile(r"(?i)-----BEGIN [A-Z ]*PRIVATE KEY-----"),
48
+ )
49
+
50
+
51
+ class ClaudeRuntime(AgentRuntime):
52
+ """Worker-local Claude Code CLI runtime.
53
+
54
+ Authenticates with an API key (``ANTHROPIC_API_KEY``) rather than a device-login session,
55
+ so an unattended Persona can run without an expiring browser session (ADR-0006). Like the
56
+ Codex runtime it confines execution to the supplied workspace, refuses fail-closed unless the
57
+ autonomous-permission guard is configured, and returns bounded, redacted evidence safe for
58
+ control-plane storage — the API key never appears in returned text.
59
+ """
60
+
61
+ auth_model = AuthModel.API_KEY
62
+
63
+ def __init__(
64
+ self,
65
+ *,
66
+ settings: WorkerSettings,
67
+ runner: Callable[..., Awaitable[SubprocessResult]] | None = None,
68
+ ) -> None:
69
+ self._settings = settings
70
+ self._runner = runner or run_subprocess_exec
71
+
72
+ async def execute_directive(self, request: DirectiveRequest) -> DirectiveResult:
73
+ workspace_path = request.workspace_path.resolve(strict=False)
74
+ workspace_root = self._settings.WORKSPACE_ROOT.resolve(strict=False)
75
+ if not is_relative_to(workspace_path, workspace_root):
76
+ raise ValueError("workspace_path must be under WORKSPACE_ROOT")
77
+
78
+ directive_workspace_id = workspace_id(workspace_path, workspace_root)
79
+ guard_mode = _guard_mode(self._settings)
80
+ if guard_mode is None:
81
+ return _refused_result(
82
+ workspace_id=directive_workspace_id,
83
+ base_branch=request.base_branch,
84
+ work_branch=request.work_branch,
85
+ evidence_limit_bytes=self._settings.CLAUDE_CLI_OUTPUT_LIMIT_BYTES,
86
+ )
87
+
88
+ argv = _claude_argv(self._settings)
89
+ if request.mcp_servers is not None:
90
+ # Only what the Grant let through: `--strict-mcp-config` ignores every other
91
+ # MCP source, so nothing in the harness root can add an ungranted server.
92
+ if request.mcp_servers:
93
+ argv += ["--mcp-config", claude_mcp_config(request.mcp_servers)]
94
+ argv.append("--strict-mcp-config")
95
+ floor_refusal = command_policy_refusal(
96
+ argv=argv,
97
+ program="claude",
98
+ workspace_path=workspace_path,
99
+ workspace_root=workspace_root,
100
+ base_branch=request.base_branch,
101
+ work_branch=request.work_branch,
102
+ )
103
+ if floor_refusal is not None:
104
+ return _refused_result(
105
+ workspace_id=directive_workspace_id,
106
+ base_branch=request.base_branch,
107
+ work_branch=request.work_branch,
108
+ evidence_limit_bytes=self._settings.CLAUDE_CLI_OUTPUT_LIMIT_BYTES,
109
+ guard_mode=f"refused: command policy ({floor_refusal})",
110
+ )
111
+ command_hash = hash_command(argv)
112
+ # Per-Contract CLAUDE_CONFIG_DIR, never a fleet-wide one (ADR-0015 §4).
113
+ sandbox = request.sandbox
114
+ env = {"ANTHROPIC_API_KEY": self._settings.ANTHROPIC_API_KEY}
115
+ if sandbox is not None:
116
+ env["CLAUDE_CONFIG_DIR"] = str(sandbox.harness_config_dir)
117
+ env["HOME"] = str(sandbox.home_dir)
118
+ env["TMPDIR"] = str(sandbox.tmp_dir)
119
+ apply_extra_env(env, request.extra_env)
120
+ notes: list[str] = []
121
+
122
+ try:
123
+ raw_result = await self._runner(
124
+ argv=argv,
125
+ cwd=workspace_path,
126
+ env=env,
127
+ stdin=request.prompt,
128
+ timeout_seconds=self._settings.CLAUDE_CLI_TIMEOUT_SECONDS,
129
+ output_limit_bytes=self._settings.CLAUDE_CLI_OUTPUT_LIMIT_BYTES,
130
+ sandbox=sandbox,
131
+ )
132
+ error = ""
133
+ except asyncio.CancelledError:
134
+ raise
135
+ except TimeoutError as exception:
136
+ raw_result = SubprocessResult(exit_code=124, stdout="", stderr="")
137
+ error = str(exception) or "Claude Code CLI timed out"
138
+ except OSError as exception:
139
+ raw_result = SubprocessResult(exit_code=127, stdout="", stderr="")
140
+ error = str(exception)
141
+
142
+ limit_bytes = self._settings.CLAUDE_CLI_OUTPUT_LIMIT_BYTES
143
+ stdout = bound_text(_redact_text(raw_result.stdout), limit_bytes, notes, "stdout")
144
+ if raw_result.stdout_truncated:
145
+ notes.append(f"stdout truncated to {limit_bytes} bytes")
146
+ stderr = bound_text(_redact_text(raw_result.stderr), limit_bytes, notes, "stderr")
147
+ if raw_result.stderr_truncated:
148
+ notes.append(f"stderr truncated to {limit_bytes} bytes")
149
+ redacted_error = bound_text(_redact_text(error), limit_bytes, notes, "error")
150
+
151
+ evidence = DirectiveEvidence(
152
+ workspace_id=_redact_text(directive_workspace_id),
153
+ base_branch=_redact_evidence_field(request.base_branch, limit_bytes),
154
+ work_branch=_redact_evidence_field(request.work_branch, limit_bytes),
155
+ command_hash=command_hash,
156
+ guard_mode=guard_mode,
157
+ notes=notes,
158
+ )
159
+ return DirectiveResult(
160
+ exit_code=raw_result.exit_code,
161
+ stdout=stdout,
162
+ stderr=stderr,
163
+ error=redacted_error,
164
+ command_hash=command_hash,
165
+ evidence=evidence,
166
+ )
167
+
168
+
169
+ def _guard_mode(settings: WorkerSettings) -> str | None:
170
+ if settings.ANTHROPIC_API_KEY and settings.CLAUDE_DANGEROUSLY_SKIP_PERMISSIONS:
171
+ return "auth=api-key; permission=skip-in-sandbox; output=json"
172
+ return None
173
+
174
+
175
+ def _claude_argv(settings: WorkerSettings) -> list[str]:
176
+ return [
177
+ "claude",
178
+ "--print",
179
+ "--bare",
180
+ "--output-format",
181
+ "json",
182
+ "--model",
183
+ settings.CLAUDE_MODEL,
184
+ "--dangerously-skip-permissions",
185
+ ]
186
+
187
+
188
+ def _refused_result(
189
+ *,
190
+ workspace_id: str,
191
+ base_branch: str,
192
+ work_branch: str,
193
+ evidence_limit_bytes: int,
194
+ guard_mode: str = "refused: missing API key or autonomous permission configuration",
195
+ error: str = (
196
+ "Claude Code API key and autonomous permission configuration are required "
197
+ "before worker-local execution"
198
+ ),
199
+ ) -> DirectiveResult:
200
+ command_hash = hash_command(["claude", "refused"])
201
+ return DirectiveResult(
202
+ exit_code=126,
203
+ stdout="",
204
+ stderr="",
205
+ error=error,
206
+ command_hash=command_hash,
207
+ evidence=DirectiveEvidence(
208
+ workspace_id=_redact_text(workspace_id),
209
+ command_hash=command_hash,
210
+ guard_mode=guard_mode,
211
+ base_branch=_redact_evidence_field(base_branch, evidence_limit_bytes),
212
+ work_branch=_redact_evidence_field(work_branch, evidence_limit_bytes),
213
+ notes=[],
214
+ ),
215
+ )
216
+
217
+
218
+ def _redact_text(value: str) -> str:
219
+ redacted = value
220
+ for pattern in _SECRET_VALUE_PATTERNS:
221
+ redacted = pattern.sub(redact_match, redacted)
222
+ return redacted
223
+
224
+
225
+ def _redact_evidence_field(value: str, limit_bytes: int) -> str:
226
+ return bound_text(_redact_text(value), limit_bytes, [], "evidence")
@@ -0,0 +1,311 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import re
6
+ from collections.abc import Awaitable, Callable
7
+ from pathlib import Path
8
+ from typing import Final
9
+
10
+ from agentic_runner.workers._runtime_support import (
11
+ AsyncSubprocessRunner,
12
+ SubprocessResult,
13
+ apply_extra_env,
14
+ bound_text,
15
+ command_policy_refusal,
16
+ hash_command,
17
+ is_relative_to,
18
+ redact_match,
19
+ run_subprocess_exec,
20
+ workspace_id,
21
+ )
22
+ from agentic_runner.workers.agent_runtime import (
23
+ AgentRuntime,
24
+ AuthModel,
25
+ DirectiveEvidence,
26
+ DirectiveRequest,
27
+ DirectiveResult,
28
+ )
29
+ from agentic_runner.workers.mcp_config import codex_mcp_argv
30
+ from agentic_runner.workers.settings import WorkerSettings
31
+
32
+ __all__ = [
33
+ "AsyncSubprocessRunner",
34
+ "CodexRuntime",
35
+ "SubprocessResult",
36
+ "build_codex_subprocess_env",
37
+ ]
38
+
39
+ # The Dockerfile installs `codex` (and its `node` interpreter) here; used as a PATH fallback
40
+ # so a bare `codex` still resolves even if the parent PATH is somehow unset.
41
+ _CODEX_INSTALL_DIR: Final[str] = "/usr/local/bin"
42
+
43
+
44
+ def build_codex_subprocess_env(codex_home: Path) -> dict[str, str]:
45
+ """Codex child env for the worker runtime: CODEX_HOME + a resolvable PATH.
46
+
47
+ An env with only CODEX_HOME cannot resolve the bare ``codex``
48
+ in ``argv[0]`` (Python falls back to ``os.defpath`` = ``/bin:/usr/bin``, but codex is at
49
+ ``/usr/local/bin/codex``), so every worker Directive exited 127. Inherit the parent PATH
50
+ (which reaches ``/usr/local/bin`` in the pod) so both ``codex`` and its
51
+ ``#!/usr/bin/env node`` shebang resolve, and pass HOME when set; add no parent secrets.
52
+ """
53
+
54
+ inherited_path = os.environ.get("PATH") or ""
55
+ path = inherited_path or os.pathsep.join((_CODEX_INSTALL_DIR, os.defpath))
56
+ env = {"CODEX_HOME": str(codex_home), "PATH": path}
57
+ home = os.environ.get("HOME")
58
+ if home:
59
+ env["HOME"] = home
60
+ return env
61
+
62
+
63
+ _SECRET_VALUE_PATTERNS: Final[tuple[re.Pattern[str], ...]] = (
64
+ re.compile(
65
+ r"(?i)((?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|credential)\s*=\s*)([^\s,}]+)"
66
+ ),
67
+ re.compile(
68
+ r"(?i)((?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|credential)\s*:\s*)([^\s,}]+)"
69
+ ),
70
+ re.compile(
71
+ r"(?i)((?:\"(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password|credential)\"\s*:\s*\"))([^\"]+)(\")"
72
+ ),
73
+ re.compile(r"(?i)((?:--password|--token|--secret|--api-key)\s+)([^\s]+)"),
74
+ re.compile(r"(?i)(authorization:\s*bearer\s+)([^\s]+)"),
75
+ re.compile(r"(?i)(bearer\s+)([A-Za-z0-9._~+/-]{8,})"),
76
+ re.compile(r"(?i)(sk-[A-Za-z0-9_-]{8,})"),
77
+ re.compile(r"(?i)(?:~|/[^\s]*)/\.codex(?:/[^\s-]+)?"),
78
+ re.compile(r"(?is)-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----"),
79
+ re.compile(r"(?i)-----BEGIN [A-Z ]*PRIVATE KEY-----"),
80
+ )
81
+
82
+
83
+ class CodexRuntime(AgentRuntime):
84
+ """Worker-local Codex CLI runtime.
85
+
86
+ The runtime does not accept caller-supplied secrets. It constructs the Codex environment from
87
+ worker settings only, keeps execution inside the supplied workspace, and returns bounded
88
+ redacted evidence suitable for control-plane storage. Codex authenticates via a device-login
89
+ session on a worker-local PVC (CODEX_HOME).
90
+ """
91
+
92
+ auth_model = AuthModel.DEVICE_LOGIN
93
+
94
+ def __init__(
95
+ self,
96
+ *,
97
+ settings: WorkerSettings,
98
+ runner: Callable[..., Awaitable[SubprocessResult]] | None = None,
99
+ ) -> None:
100
+ self._settings = settings
101
+ self._runner = runner or run_subprocess_exec
102
+
103
+ async def execute_directive(self, request: DirectiveRequest) -> DirectiveResult:
104
+ workspace_path = request.workspace_path.resolve(strict=False)
105
+ workspace_root = self._settings.WORKSPACE_ROOT.resolve(strict=False)
106
+ if not is_relative_to(workspace_path, workspace_root):
107
+ raise ValueError("workspace_path must be under WORKSPACE_ROOT")
108
+
109
+ directive_workspace_id = workspace_id(workspace_path, workspace_root)
110
+ allowlisted = bool(request.egress_allow_list)
111
+ guard_mode = _guard_mode(self._settings, allowlisted=allowlisted)
112
+ if guard_mode is None:
113
+ return _refused_result(
114
+ workspace_id=directive_workspace_id,
115
+ base_branch=request.base_branch,
116
+ work_branch=request.work_branch,
117
+ evidence_limit_bytes=self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
118
+ )
119
+
120
+ argv = _codex_argv(self._settings, allowlisted=allowlisted)
121
+ # Locked with no server bound too: otherwise a `[mcp_servers.*]` table in the
122
+ # Workspace or harness-root config.toml starts a process before any model turn (LA-19b).
123
+ argv[-1:-1] = codex_mcp_argv(request.mcp_servers or (), workspace_path)
124
+ floor_refusal = command_policy_refusal(
125
+ argv=argv,
126
+ program="codex",
127
+ workspace_path=workspace_path,
128
+ workspace_root=workspace_root,
129
+ base_branch=request.base_branch,
130
+ work_branch=request.work_branch,
131
+ )
132
+ if floor_refusal is not None:
133
+ return _refused_result(
134
+ workspace_id=directive_workspace_id,
135
+ base_branch=request.base_branch,
136
+ work_branch=request.work_branch,
137
+ evidence_limit_bytes=self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
138
+ guard_mode=f"refused: command policy ({floor_refusal})",
139
+ )
140
+ command_hash = hash_command(argv)
141
+ # The Contract's own harness config root, never the fleet-wide one (ADR-0015 §4):
142
+ # a device login materialised into it is readable only by that Contract's uid.
143
+ # The shared CODEX_HOME survives only as the no-isolation fallback.
144
+ sandbox = request.sandbox
145
+ env = build_codex_subprocess_env(
146
+ self._settings.CODEX_HOME if sandbox is None else sandbox.harness_config_dir
147
+ )
148
+ if sandbox is not None:
149
+ env["HOME"] = str(sandbox.home_dir)
150
+ env["TMPDIR"] = str(sandbox.tmp_dir)
151
+ apply_extra_env(env, request.extra_env)
152
+ notes: list[str] = []
153
+
154
+ try:
155
+ raw_result = await self._runner(
156
+ argv=argv,
157
+ cwd=workspace_path,
158
+ env=env,
159
+ stdin=request.prompt,
160
+ timeout_seconds=self._settings.CODEX_CLI_TIMEOUT_SECONDS,
161
+ output_limit_bytes=self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
162
+ sandbox=sandbox,
163
+ )
164
+ error = ""
165
+ except asyncio.CancelledError:
166
+ raise
167
+ except TimeoutError as exception:
168
+ raw_result = SubprocessResult(exit_code=124, stdout="", stderr="")
169
+ error = str(exception) or "Codex CLI timed out"
170
+ except OSError as exception:
171
+ raw_result = SubprocessResult(exit_code=127, stdout="", stderr="")
172
+ error = str(exception)
173
+
174
+ stdout = bound_text(
175
+ _redact_text(raw_result.stdout),
176
+ self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
177
+ notes,
178
+ "stdout",
179
+ )
180
+ if raw_result.stdout_truncated:
181
+ notes.append(f"stdout truncated to {self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES} bytes")
182
+ stderr = bound_text(
183
+ _redact_text(raw_result.stderr),
184
+ self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
185
+ notes,
186
+ "stderr",
187
+ )
188
+ if raw_result.stderr_truncated:
189
+ notes.append(f"stderr truncated to {self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES} bytes")
190
+ redacted_error = bound_text(
191
+ _redact_text(error),
192
+ self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
193
+ notes,
194
+ "error",
195
+ )
196
+
197
+ evidence = DirectiveEvidence(
198
+ workspace_id=_redact_text(directive_workspace_id),
199
+ base_branch=_redact_evidence_field(
200
+ request.base_branch,
201
+ self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
202
+ ),
203
+ work_branch=_redact_evidence_field(
204
+ request.work_branch,
205
+ self._settings.CODEX_CLI_OUTPUT_LIMIT_BYTES,
206
+ ),
207
+ command_hash=command_hash,
208
+ guard_mode=guard_mode,
209
+ notes=notes,
210
+ )
211
+ return DirectiveResult(
212
+ exit_code=raw_result.exit_code,
213
+ stdout=stdout,
214
+ stderr=stderr,
215
+ error=redacted_error,
216
+ command_hash=command_hash,
217
+ evidence=evidence,
218
+ )
219
+
220
+
221
+ def _guard_mode(settings: WorkerSettings, *, allowlisted: bool = False) -> str | None:
222
+ if not (
223
+ settings.CODEX_ASK_FOR_APPROVAL == "never"
224
+ and settings.CODEX_STRICT_CONFIG
225
+ and settings.CODEX_POLICY_HOOK_CONFIGURED
226
+ ):
227
+ return None
228
+ if settings.CODEX_SANDBOX_MODE == "workspace-write":
229
+ # The Profile's egress list replaced the old all-or-nothing `CODEX_NETWORK_ACCESS`
230
+ # toggle (PRD issue 58): with a list, Codex's sandbox lets traffic out and the
231
+ # attempt's egress proxy decides where it may go; without one, none leaves.
232
+ network = "allowlisted" if allowlisted else "disabled"
233
+ return (
234
+ "sandbox=workspace-write; approval=never; strict-config; "
235
+ f"network={network}; policy-hook=configured"
236
+ )
237
+ if settings.CODEX_SANDBOX_MODE == "danger-full-access":
238
+ # Containerized posture: Codex's own OS sandbox cannot run inside the worker
239
+ # pod (bwrap needs to create user namespaces, which the container runtime
240
+ # denies — every exec/apply_patch failed with "bwrap: No permissions to
241
+ # create a new namespace"). The pod is the isolation boundary instead:
242
+ # dedicated container, PVC-scoped workspaces, cluster network policy.
243
+ return (
244
+ "sandbox=danger-full-access (pod-isolated); approval=never; strict-config; "
245
+ "policy-hook=configured"
246
+ )
247
+ return None
248
+
249
+
250
+ def _codex_argv(settings: WorkerSettings, *, allowlisted: bool = False) -> list[str]:
251
+ argv = [
252
+ "codex",
253
+ "exec",
254
+ # JSONL event stream (research/29 §1.6) -- without it stdout is human prose and
255
+ # `harness_usage.extract_codex_turn_usage` never finds a `turn.completed` line, so
256
+ # a device-login Directive's harness Usage Record (PRD issue 31, 17 A9) is silent.
257
+ "--json",
258
+ "--sandbox",
259
+ settings.CODEX_SANDBOX_MODE,
260
+ # `codex exec` is non-interactive and rejects `--ask-for-approval`
261
+ # (codex-cli 0.141.x exits 2 on it); the approval policy is expressed as a
262
+ # config override instead.
263
+ "--config",
264
+ f"approval_policy={settings.CODEX_ASK_FOR_APPROVAL}",
265
+ "--strict-config",
266
+ ]
267
+ if settings.CODEX_SANDBOX_MODE == "workspace-write":
268
+ network_access = "true" if allowlisted else "false"
269
+ argv += ["--config", f"sandbox_workspace_write.network_access={network_access}"]
270
+ argv.append("-")
271
+ return argv
272
+
273
+
274
+ def _refused_result(
275
+ *,
276
+ workspace_id: str,
277
+ base_branch: str,
278
+ work_branch: str,
279
+ evidence_limit_bytes: int,
280
+ guard_mode: str = "refused: missing sandbox/policy configuration",
281
+ error: str = (
282
+ "Codex sandbox/policy guard configuration is required before worker-local execution"
283
+ ),
284
+ ) -> DirectiveResult:
285
+ command_hash = hash_command(["codex", "exec", "refused"])
286
+ return DirectiveResult(
287
+ exit_code=126,
288
+ stdout="",
289
+ stderr="",
290
+ error=error,
291
+ command_hash=command_hash,
292
+ evidence=DirectiveEvidence(
293
+ workspace_id=_redact_text(workspace_id),
294
+ command_hash=command_hash,
295
+ guard_mode=guard_mode,
296
+ base_branch=_redact_evidence_field(base_branch, evidence_limit_bytes),
297
+ work_branch=_redact_evidence_field(work_branch, evidence_limit_bytes),
298
+ notes=[],
299
+ ),
300
+ )
301
+
302
+
303
+ def _redact_text(value: str) -> str:
304
+ redacted = value
305
+ for pattern in _SECRET_VALUE_PATTERNS:
306
+ redacted = pattern.sub(redact_match, redacted)
307
+ return redacted
308
+
309
+
310
+ def _redact_evidence_field(value: str, limit_bytes: int) -> str:
311
+ return bound_text(_redact_text(value), limit_bytes, [], "evidence")