pulse-coding-agent 0.1.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 (104) hide show
  1. pulse/__init__.py +5 -0
  2. pulse/__main__.py +4 -0
  3. pulse/agent.py +270 -0
  4. pulse/agent_manager.py +335 -0
  5. pulse/audit.py +70 -0
  6. pulse/auth.py +670 -0
  7. pulse/ci/github_client.py +66 -0
  8. pulse/ci/runner.py +28 -0
  9. pulse/cli.py +1075 -0
  10. pulse/cli_ui.py +977 -0
  11. pulse/config.py +167 -0
  12. pulse/context.py +960 -0
  13. pulse/conversations/__init__.py +8 -0
  14. pulse/conversations/manager.py +312 -0
  15. pulse/core/agent.py +188 -0
  16. pulse/core/planner.py +105 -0
  17. pulse/core/protocols.py +37 -0
  18. pulse/edits.py +65 -0
  19. pulse/episodic.py +93 -0
  20. pulse/eval/__init__.py +8 -0
  21. pulse/eval/trajectory_logger.py +91 -0
  22. pulse/eval/verifier.py +133 -0
  23. pulse/execution/__init__.py +5 -0
  24. pulse/execution/remote_task.py +76 -0
  25. pulse/git.py +162 -0
  26. pulse/interactive.py +234 -0
  27. pulse/mcp/__init__.py +4 -0
  28. pulse/mcp/client.py +215 -0
  29. pulse/mcp/local_tools.py +105 -0
  30. pulse/memory.py +212 -0
  31. pulse/mutations.py +283 -0
  32. pulse/orchestration/__init__.py +3 -0
  33. pulse/orchestration/orchestrator.py +162 -0
  34. pulse/patch.py +129 -0
  35. pulse/planner/__init__.py +3 -0
  36. pulse/planner/dag_planner.py +85 -0
  37. pulse/planner/execution_loop.py +159 -0
  38. pulse/production.py +235 -0
  39. pulse/provider.py +59 -0
  40. pulse/provider_keys.py +278 -0
  41. pulse/providers/__init__.py +26 -0
  42. pulse/providers/anthropic.py +65 -0
  43. pulse/providers/base.py +251 -0
  44. pulse/providers/deepseek.py +10 -0
  45. pulse/providers/failover.py +32 -0
  46. pulse/providers/gemini.py +66 -0
  47. pulse/providers/groq.py +10 -0
  48. pulse/providers/manager.py +262 -0
  49. pulse/providers/openai.py +40 -0
  50. pulse/providers/openrouter.py +20 -0
  51. pulse/py.typed +1 -0
  52. pulse/reasoning.py +570 -0
  53. pulse/refactor/__init__.py +3 -0
  54. pulse/refactor/impact_analyzer.py +44 -0
  55. pulse/repository.py +209 -0
  56. pulse/rpc.py +249 -0
  57. pulse/rule_synthesizer.py +54 -0
  58. pulse/runtime.py +217 -0
  59. pulse/safety/__init__.py +3 -0
  60. pulse/safety/safety_manager.py +97 -0
  61. pulse/sandbox/SECURITY.md +57 -0
  62. pulse/sandbox/__init__.py +57 -0
  63. pulse/sandbox/api.py +594 -0
  64. pulse/sandbox/audit.py +153 -0
  65. pulse/sandbox/backend/__init__.py +7 -0
  66. pulse/sandbox/backend/base.py +72 -0
  67. pulse/sandbox/backend/docker.py +498 -0
  68. pulse/sandbox/backend/host.py +140 -0
  69. pulse/sandbox/backend/remote.py +224 -0
  70. pulse/sandbox/errors.py +106 -0
  71. pulse/sandbox/filesystem.py +476 -0
  72. pulse/sandbox/git_safe.py +50 -0
  73. pulse/sandbox/lifecycle.py +88 -0
  74. pulse/sandbox/network.py +205 -0
  75. pulse/sandbox/path_validator.py +280 -0
  76. pulse/sandbox/policy.py +209 -0
  77. pulse/sandbox/process.py +331 -0
  78. pulse/sandbox/project.py +158 -0
  79. pulse/sandbox/python_safe.py +62 -0
  80. pulse/sandbox/remote/__init__.py +1 -0
  81. pulse/sandbox/remote/client.py +389 -0
  82. pulse/sandbox/remote/models.py +167 -0
  83. pulse/sandbox/remote/protocol.py +65 -0
  84. pulse/sandbox/remote/server.py +984 -0
  85. pulse/sandbox/remote/worker.py +175 -0
  86. pulse/sandbox/resources.py +236 -0
  87. pulse/sandbox/secrets.py +241 -0
  88. pulse/session_manager.py +365 -0
  89. pulse/software_engineer.py +189 -0
  90. pulse/storage.py +140 -0
  91. pulse/streaming.py +385 -0
  92. pulse/subprocesses.py +79 -0
  93. pulse/task_manager.py +2005 -0
  94. pulse/telemetry/__init__.py +25 -0
  95. pulse/telemetry/cost_tracker.py +95 -0
  96. pulse/telemetry/logger.py +110 -0
  97. pulse/tool_policy.py +197 -0
  98. pulse/tool_registry.py +163 -0
  99. pulse/tools.py +372 -0
  100. pulse/verification.py +118 -0
  101. pulse_coding_agent-0.1.0.dist-info/METADATA +211 -0
  102. pulse_coding_agent-0.1.0.dist-info/RECORD +104 -0
  103. pulse_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  104. pulse_coding_agent-0.1.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,224 @@
1
+ """Remote execution backend for Sandbox."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import typing
7
+ import uuid
8
+ from pathlib import Path
9
+
10
+ from pulse.sandbox.errors import SandboxUnavailableError
11
+ from pulse.sandbox.network import NetworkEnforcementLevel, NetworkMode, NetworkPolicy
12
+ from pulse.sandbox.process import ProcessEnforcementLevel, ProcessResult
13
+ from pulse.sandbox.remote.client import RemoteClient
14
+ from pulse.sandbox.remote.models import SubmitExecutionRequest
15
+ from pulse.sandbox.resources import ResourceLimits
16
+ from pulse.sandbox.secrets import SecretEnforcementLevel, SecretPolicy
17
+ from pulse.telemetry import get_correlation_id
18
+
19
+
20
+ class RemoteSandboxBackend:
21
+ """Production-grade remote container execution backend.
22
+
23
+ Communicates securely with a remote worker that enforces
24
+ strong container isolation identical to the local DockerBackend.
25
+ """
26
+
27
+ def __init__(self, endpoint_url: str | None = None, auth_token: str | None = None) -> None:
28
+ self.endpoint_url = endpoint_url
29
+ self.auth_token = auth_token
30
+ self._client: RemoteClient | None = None
31
+
32
+ @property
33
+ def client(self) -> RemoteClient:
34
+ if not self._client:
35
+ if not self.endpoint_url or not self.auth_token:
36
+ raise SandboxUnavailableError("Remote backend is not fully configured (missing endpoint or token).")
37
+ self._client = RemoteClient(self.endpoint_url, self.auth_token)
38
+ return self._client
39
+
40
+ @property
41
+ def name(self) -> str:
42
+ return "remote"
43
+
44
+ async def is_available(self) -> bool:
45
+ """Return True if remote endpoint is configured and reachable."""
46
+ if not self.endpoint_url or not self.auth_token:
47
+ return False
48
+
49
+ # Try to establish a connection to check health
50
+ try:
51
+ client = self.client
52
+ await client.connect()
53
+ await client.disconnect()
54
+ return True
55
+ except (OSError, ConnectionError, TimeoutError, ValueError, RuntimeError):
56
+ return False
57
+
58
+ async def execute(
59
+ self,
60
+ command: str | list[str],
61
+ workspace_root: Path,
62
+ cwd: Path | None = None,
63
+ env: dict[str, str] | None = None,
64
+ limits: ResourceLimits | None = None,
65
+ network_policy: NetworkPolicy | None = None,
66
+ secret_policy: SecretPolicy | None = None,
67
+ execution_id: str | None = None,
68
+ output_callback: typing.Callable[[str, bytes], typing.Awaitable[None]] | None = None,
69
+ ) -> ProcessResult:
70
+
71
+ exec_id = execution_id or str(uuid.uuid4())
72
+
73
+ # Calculate relative working directory
74
+ rel_cwd = None
75
+ if cwd:
76
+ try:
77
+ rel_cwd = cwd.relative_to(workspace_root).as_posix()
78
+ except ValueError:
79
+ pass
80
+
81
+ # Convert limits to policy format
82
+ from pulse.sandbox.resources import ResourcePolicy
83
+ res_policy = limits if isinstance(limits, ResourcePolicy) else (limits.to_policy() if limits else None)
84
+
85
+ req = SubmitExecutionRequest(
86
+ protocol_version="1.0",
87
+ execution_id=exec_id,
88
+ idempotency_key=str(uuid.uuid4()),
89
+ command=command,
90
+ correlation_id=get_correlation_id(),
91
+ working_directory=rel_cwd,
92
+ env=env,
93
+ resource_policy_dict=res_policy.to_dict() if res_policy else None,
94
+ network_policy_dict=network_policy.to_dict() if network_policy else None,
95
+ secret_policy_dict=secret_policy.to_dict() if secret_policy else None,
96
+ )
97
+
98
+ client = self.client
99
+
100
+ import io
101
+ import tarfile
102
+ import tempfile
103
+
104
+ # Phase 6: Upload artifacts
105
+ bio = io.BytesIO()
106
+ with tarfile.open(fileobj=bio, mode="w:gz") as tar:
107
+ if workspace_root.exists():
108
+ for item in workspace_root.iterdir():
109
+ # Skip internal sandbox tracking directories to save space
110
+ if item.name == ".agent":
111
+ continue
112
+ tar.add(item, arcname=item.name)
113
+
114
+ await client.upload_artifact(exec_id, bio.getvalue())
115
+
116
+ # Submit execution
117
+ await client.submit(req)
118
+
119
+ # Process streaming output in the background
120
+ async def handle_stream() -> None:
121
+ if output_callback:
122
+ async for stream_type, chunk in client.stream_output(exec_id):
123
+ await output_callback(stream_type, chunk.encode("utf-8", errors="replace"))
124
+ else:
125
+ # Still consume it so the queue doesn't back up
126
+ async for _ in client.stream_output(exec_id):
127
+ pass
128
+
129
+ stream_task = asyncio.create_task(handle_stream())
130
+
131
+ try:
132
+ # Wait for completion
133
+ res_model = await client.get_result(exec_id)
134
+ except asyncio.CancelledError:
135
+ await client.cancel(exec_id)
136
+ raise
137
+ finally:
138
+ await stream_task
139
+
140
+ # Phase 6: Download artifacts
141
+ overlay_bytes = await client.download_artifact(exec_id)
142
+ local_overlay_path = None
143
+ if overlay_bytes:
144
+ local_overlay_path = Path(tempfile.gettempdir()) / f"pulse_remote_overlay_{exec_id}"
145
+ local_overlay_path.mkdir(parents=True, exist_ok=True)
146
+ try:
147
+ with tarfile.open(fileobj=io.BytesIO(overlay_bytes), mode="r:gz") as tar:
148
+ import os
149
+ max_size = 50 * 1024 * 1024
150
+ current_size = 0
151
+ for member in tar.getmembers():
152
+ if member.issym() or member.islnk():
153
+ from pulse.sandbox.errors import SandboxSecurityError
154
+ raise SandboxSecurityError("Symlinks are not allowed in remote artifacts")
155
+ current_size += member.size
156
+ if current_size > max_size:
157
+ raise SandboxSecurityError(f"Artifact size exceeded limit of {max_size} bytes")
158
+
159
+ member_path = os.path.join(str(local_overlay_path), member.name)
160
+ if not os.path.abspath(member_path).startswith(os.path.abspath(str(local_overlay_path))):
161
+ raise SandboxSecurityError("Path traversal detected in download_artifact")
162
+ if hasattr(tarfile, 'data_filter'):
163
+ tar.extractall(path=local_overlay_path, filter='data')
164
+ else:
165
+ tar.extractall(path=local_overlay_path)
166
+ except (OSError, ValueError, RuntimeError) as e:
167
+ from pulse.sandbox.errors import SandboxSecurityError
168
+ # Raise an explicit security error on path traversal
169
+ raise SandboxSecurityError(
170
+ f"Remote execution compromised: Malformed or malicious workspace overlay. Details: {e}",
171
+ operation="download_artifact",
172
+ path=str(local_overlay_path),
173
+ )
174
+
175
+ return ProcessResult(
176
+ command=res_model.command,
177
+ exit_code=res_model.exit_code,
178
+ stdout=res_model.stdout,
179
+ stderr=res_model.stderr,
180
+ duration_ms=res_model.duration_ms,
181
+ timed_out=res_model.timed_out,
182
+ truncated=res_model.truncated,
183
+ pid=None, # Remote PID not exposed securely
184
+ overlay_path=local_overlay_path,
185
+ termination_reason=res_model.termination_reason,
186
+ )
187
+
188
+ def get_network_enforcement_capability(self, policy: NetworkPolicy) -> NetworkEnforcementLevel:
189
+ """Determine if this backend can strongly enforce the requested network policy."""
190
+ if not policy or policy.mode == NetworkMode.ALLOW_ALL:
191
+ return NetworkEnforcementLevel.STRONGLY_ENFORCED
192
+
193
+ if policy.mode in (NetworkMode.DENY_ALL, NetworkMode.LOCALHOST_ONLY):
194
+ return NetworkEnforcementLevel.STRONGLY_ENFORCED
195
+
196
+ return NetworkEnforcementLevel.UNSUPPORTED
197
+
198
+ def get_secret_enforcement_capability(self, policy: SecretPolicy) -> SecretEnforcementLevel:
199
+ """Determine if this backend can strongly enforce the requested secret isolation policy."""
200
+ from pulse.sandbox.secrets import SecretMode
201
+ if not policy or policy.mode == SecretMode.ALLOW_ALL:
202
+ return SecretEnforcementLevel.STRONGLY_ENFORCED
203
+
204
+ if policy.mode in (SecretMode.DENY_ALL, SecretMode.ALLOW_EXPLICIT):
205
+ return SecretEnforcementLevel.STRONGLY_ENFORCED
206
+
207
+ return SecretEnforcementLevel.UNSUPPORTED
208
+
209
+ def get_process_containment_capability(self) -> ProcessEnforcementLevel:
210
+ """Determine if this backend provides strong process containment.
211
+
212
+ Delegated to remote DockerBackend.
213
+ """
214
+ return ProcessEnforcementLevel.STRONGLY_ENFORCED
215
+
216
+ async def cleanup(self) -> None:
217
+ """Reap temporary remote execution artifacts."""
218
+ if self._client:
219
+ await self._client.disconnect()
220
+
221
+ async def reconcile(self) -> None:
222
+ """Clean up orphaned remote backend resources."""
223
+ if self._client:
224
+ await self._client.reconcile()
@@ -0,0 +1,106 @@
1
+ """Custom exception hierarchy for Pulse sandbox security failures.
2
+
3
+ Provides structured, catchable error types for sandbox unavailability,
4
+ security boundary violations, and resource limit breaches.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+
10
+ class SandboxUnavailableError(RuntimeError):
11
+ """No secure execution backend (Docker/Podman) is available.
12
+
13
+ Raised when the sandbox cannot find a container engine and the caller
14
+ has NOT explicitly opted into unsafe host execution.
15
+
16
+ Security rationale:
17
+ Silent fallback to host execution is a catastrophic isolation failure.
18
+ This exception forces callers to make a conscious, auditable decision
19
+ about running untrusted code directly on the host.
20
+ """
21
+
22
+ def __init__(self, message: str | None = None) -> None:
23
+ super().__init__(
24
+ message
25
+ or (
26
+ "No secure container backend (Docker/Podman) is available. "
27
+ "Set unsafe_host_execution=True to explicitly allow host execution "
28
+ "(NOT recommended for untrusted code)."
29
+ )
30
+ )
31
+
32
+
33
+ class SandboxUnsupportedPolicyError(SandboxUnavailableError):
34
+ """The requested security policy cannot be strongly enforced by the active backend.
35
+
36
+ Security rationale:
37
+ Fail-closed behavior is mandatory. If a restrictive network or isolation policy
38
+ is requested but the backend lacks the OS-level capability to enforce it
39
+ (e.g., trying to use ALLOWLIST in rootless Docker without egress filtering,
40
+ or DENY_ALL in HostBackend), execution must be rejected rather than silently
41
+ downgraded to advisory enforcement.
42
+ """
43
+
44
+
45
+ class SandboxSecurityError(RuntimeError):
46
+ """A sandbox security boundary has been violated.
47
+
48
+ Raised on TOCTOU detection, symlink escape attempts, path traversal
49
+ after validation, or any operation that breaches isolation invariants.
50
+
51
+ Security rationale:
52
+ Hard failure prevents partial-state exploitation. Every security
53
+ violation is terminal for the current operation.
54
+ """
55
+
56
+ def __init__(self, message: str, *, operation: str = "", path: str = "") -> None:
57
+ self.operation = operation
58
+ self.path = path
59
+ detail = f" [op={operation}]" if operation else ""
60
+ detail += f" [path={path}]" if path else ""
61
+ super().__init__(f"{message}{detail}")
62
+
63
+
64
+ class SandboxResourceError(RuntimeError):
65
+ """A sandbox resource limit has been exceeded.
66
+
67
+ Raised when file size, output size, or memory constraints are breached.
68
+ """
69
+
70
+ def __init__(self, message: str, *, limit_name: str = "", limit_value: int = 0) -> None:
71
+ self.limit_name = limit_name
72
+ self.limit_value = limit_value
73
+ super().__init__(message)
74
+
75
+
76
+ class SandboxConcurrentModificationError(RuntimeError):
77
+ """A target file was modified externally during a CoW transaction.
78
+
79
+ Raised when ``commit_transaction()`` detects that a file's identity,
80
+ size, mtime, or content has changed since the transaction first staged
81
+ it. The transaction is NOT destroyed when this error is raised, so
82
+ callers may retry or inspect staged changes.
83
+
84
+ Attributes:
85
+ path: Relative workspace path of the conflicting file.
86
+ reason: Human-readable description of the detected change.
87
+ """
88
+
89
+ def __init__(self, message: str, *, path: str = "", reason: str = "") -> None:
90
+ self.path = path
91
+ self.reason = reason
92
+ super().__init__(message)
93
+
94
+
95
+ class SandboxRecoveryError(RuntimeError):
96
+ """A fatal error occurred during CoW transaction recovery.
97
+
98
+ Raised when a WAL replay detects path traversal attempts, malformed data,
99
+ or irreconcilable concurrency conflicts (where a file was modified externally
100
+ while the system was offline).
101
+ """
102
+
103
+ def __init__(self, message: str, *, path: str = "", reason: str = "") -> None:
104
+ self.path = path
105
+ self.reason = reason
106
+ super().__init__(message)