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.
- pulse/__init__.py +5 -0
- pulse/__main__.py +4 -0
- pulse/agent.py +270 -0
- pulse/agent_manager.py +335 -0
- pulse/audit.py +70 -0
- pulse/auth.py +670 -0
- pulse/ci/github_client.py +66 -0
- pulse/ci/runner.py +28 -0
- pulse/cli.py +1075 -0
- pulse/cli_ui.py +977 -0
- pulse/config.py +167 -0
- pulse/context.py +960 -0
- pulse/conversations/__init__.py +8 -0
- pulse/conversations/manager.py +312 -0
- pulse/core/agent.py +188 -0
- pulse/core/planner.py +105 -0
- pulse/core/protocols.py +37 -0
- pulse/edits.py +65 -0
- pulse/episodic.py +93 -0
- pulse/eval/__init__.py +8 -0
- pulse/eval/trajectory_logger.py +91 -0
- pulse/eval/verifier.py +133 -0
- pulse/execution/__init__.py +5 -0
- pulse/execution/remote_task.py +76 -0
- pulse/git.py +162 -0
- pulse/interactive.py +234 -0
- pulse/mcp/__init__.py +4 -0
- pulse/mcp/client.py +215 -0
- pulse/mcp/local_tools.py +105 -0
- pulse/memory.py +212 -0
- pulse/mutations.py +283 -0
- pulse/orchestration/__init__.py +3 -0
- pulse/orchestration/orchestrator.py +162 -0
- pulse/patch.py +129 -0
- pulse/planner/__init__.py +3 -0
- pulse/planner/dag_planner.py +85 -0
- pulse/planner/execution_loop.py +159 -0
- pulse/production.py +235 -0
- pulse/provider.py +59 -0
- pulse/provider_keys.py +278 -0
- pulse/providers/__init__.py +26 -0
- pulse/providers/anthropic.py +65 -0
- pulse/providers/base.py +251 -0
- pulse/providers/deepseek.py +10 -0
- pulse/providers/failover.py +32 -0
- pulse/providers/gemini.py +66 -0
- pulse/providers/groq.py +10 -0
- pulse/providers/manager.py +262 -0
- pulse/providers/openai.py +40 -0
- pulse/providers/openrouter.py +20 -0
- pulse/py.typed +1 -0
- pulse/reasoning.py +570 -0
- pulse/refactor/__init__.py +3 -0
- pulse/refactor/impact_analyzer.py +44 -0
- pulse/repository.py +209 -0
- pulse/rpc.py +249 -0
- pulse/rule_synthesizer.py +54 -0
- pulse/runtime.py +217 -0
- pulse/safety/__init__.py +3 -0
- pulse/safety/safety_manager.py +97 -0
- pulse/sandbox/SECURITY.md +57 -0
- pulse/sandbox/__init__.py +57 -0
- pulse/sandbox/api.py +594 -0
- pulse/sandbox/audit.py +153 -0
- pulse/sandbox/backend/__init__.py +7 -0
- pulse/sandbox/backend/base.py +72 -0
- pulse/sandbox/backend/docker.py +498 -0
- pulse/sandbox/backend/host.py +140 -0
- pulse/sandbox/backend/remote.py +224 -0
- pulse/sandbox/errors.py +106 -0
- pulse/sandbox/filesystem.py +476 -0
- pulse/sandbox/git_safe.py +50 -0
- pulse/sandbox/lifecycle.py +88 -0
- pulse/sandbox/network.py +205 -0
- pulse/sandbox/path_validator.py +280 -0
- pulse/sandbox/policy.py +209 -0
- pulse/sandbox/process.py +331 -0
- pulse/sandbox/project.py +158 -0
- pulse/sandbox/python_safe.py +62 -0
- pulse/sandbox/remote/__init__.py +1 -0
- pulse/sandbox/remote/client.py +389 -0
- pulse/sandbox/remote/models.py +167 -0
- pulse/sandbox/remote/protocol.py +65 -0
- pulse/sandbox/remote/server.py +984 -0
- pulse/sandbox/remote/worker.py +175 -0
- pulse/sandbox/resources.py +236 -0
- pulse/sandbox/secrets.py +241 -0
- pulse/session_manager.py +365 -0
- pulse/software_engineer.py +189 -0
- pulse/storage.py +140 -0
- pulse/streaming.py +385 -0
- pulse/subprocesses.py +79 -0
- pulse/task_manager.py +2005 -0
- pulse/telemetry/__init__.py +25 -0
- pulse/telemetry/cost_tracker.py +95 -0
- pulse/telemetry/logger.py +110 -0
- pulse/tool_policy.py +197 -0
- pulse/tool_registry.py +163 -0
- pulse/tools.py +372 -0
- pulse/verification.py +118 -0
- pulse_coding_agent-0.1.0.dist-info/METADATA +211 -0
- pulse_coding_agent-0.1.0.dist-info/RECORD +104 -0
- pulse_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- pulse_coding_agent-0.1.0.dist-info/entry_points.txt +4 -0
pulse/sandbox/audit.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Structured JSON Lines audit logging system.
|
|
2
|
+
|
|
3
|
+
Records timestamped audit entries with automatic secret redaction, policy decisions,
|
|
4
|
+
exit codes, duration, and container execution telemetry.
|
|
5
|
+
|
|
6
|
+
Security hardening:
|
|
7
|
+
- JSON values sanitized against control character injection.
|
|
8
|
+
- isolation_level field added to track execution security tier.
|
|
9
|
+
- Log writes are atomic (single write per entry).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import asdict, dataclass
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from pulse.sandbox.secrets import SecretScrubber
|
|
22
|
+
|
|
23
|
+
# Pattern to match control characters that could corrupt JSONL format.
|
|
24
|
+
# Includes \n (\x0a) and \r (\x0d) which are the primary log injection vectors.
|
|
25
|
+
# Preserves \t (\x09) as it's harmless in JSON (encoded as \t by json.dumps).
|
|
26
|
+
_CONTROL_CHARS = re.compile(r"[\x00-\x08\x0a-\x0c\x0e-\x1f]")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _sanitize_log_value(value: str) -> str:
|
|
30
|
+
"""Remove control characters that could corrupt JSONL log format.
|
|
31
|
+
|
|
32
|
+
Security rationale:
|
|
33
|
+
An attacker could inject \\n followed by a crafted JSON object into
|
|
34
|
+
a log target string, causing log injection (CWE-117). Stripping
|
|
35
|
+
control characters prevents injected newlines from splitting entries.
|
|
36
|
+
"""
|
|
37
|
+
return _CONTROL_CHARS.sub("", value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class StructuredAuditEntry:
|
|
42
|
+
"""Telemetry audit entry recorded in JSON Lines format."""
|
|
43
|
+
|
|
44
|
+
timestamp: str
|
|
45
|
+
action: str
|
|
46
|
+
target: str
|
|
47
|
+
decision: str
|
|
48
|
+
exit_code: int | None = None
|
|
49
|
+
duration_ms: float | None = None
|
|
50
|
+
container_id: str | None = None
|
|
51
|
+
isolation_level: str = "container" # "container", "host_unsafe", "unavailable"
|
|
52
|
+
enforcement_level: str | None = None
|
|
53
|
+
redacted: bool = False
|
|
54
|
+
detail: str = ""
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict[str, Any]:
|
|
57
|
+
return asdict(self)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class StructuredAuditLogger:
|
|
61
|
+
"""Thread-safe JSON Lines audit logger with automatic secret scrubbing."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
log_path: Path,
|
|
66
|
+
scrubber: SecretScrubber | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
self.log_path = log_path
|
|
69
|
+
self.scrubber = scrubber or SecretScrubber()
|
|
70
|
+
self._entries: list[StructuredAuditEntry] = []
|
|
71
|
+
|
|
72
|
+
def record(
|
|
73
|
+
self,
|
|
74
|
+
action: str,
|
|
75
|
+
target: str = "",
|
|
76
|
+
decision: str = "allow",
|
|
77
|
+
*,
|
|
78
|
+
exit_code: int | None = None,
|
|
79
|
+
duration_ms: float | None = None,
|
|
80
|
+
container_id: str | None = None,
|
|
81
|
+
isolation_level: str = "container",
|
|
82
|
+
enforcement_level: str | None = None,
|
|
83
|
+
redacted: bool = False,
|
|
84
|
+
detail: str = "",
|
|
85
|
+
) -> StructuredAuditEntry:
|
|
86
|
+
"""Create, sanitize, and record an audit entry."""
|
|
87
|
+
raw_detail = detail
|
|
88
|
+
raw_target = target
|
|
89
|
+
|
|
90
|
+
clean_detail = self.scrubber.redact(raw_detail)
|
|
91
|
+
clean_target = self.scrubber.redact(raw_target)
|
|
92
|
+
was_redacted = (clean_detail != raw_detail) or (clean_target != raw_target)
|
|
93
|
+
|
|
94
|
+
# Sanitize against log injection (CWE-117)
|
|
95
|
+
clean_detail = _sanitize_log_value(clean_detail)
|
|
96
|
+
clean_target = _sanitize_log_value(clean_target)
|
|
97
|
+
clean_action = _sanitize_log_value(action)
|
|
98
|
+
|
|
99
|
+
entry = StructuredAuditEntry(
|
|
100
|
+
timestamp=datetime.now(UTC).isoformat(),
|
|
101
|
+
action=clean_action,
|
|
102
|
+
target=clean_target,
|
|
103
|
+
decision=decision.lower(),
|
|
104
|
+
exit_code=exit_code,
|
|
105
|
+
duration_ms=duration_ms,
|
|
106
|
+
container_id=container_id,
|
|
107
|
+
isolation_level=isolation_level,
|
|
108
|
+
enforcement_level=enforcement_level,
|
|
109
|
+
redacted=was_redacted,
|
|
110
|
+
detail=clean_detail,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._entries.append(entry)
|
|
114
|
+
self._write_entry(entry)
|
|
115
|
+
return entry
|
|
116
|
+
|
|
117
|
+
def log_network(
|
|
118
|
+
self,
|
|
119
|
+
destination: str,
|
|
120
|
+
port: int | None,
|
|
121
|
+
protocol: str,
|
|
122
|
+
decision: str,
|
|
123
|
+
backend: str,
|
|
124
|
+
enforcement_level: str | None = None,
|
|
125
|
+
detail: str = "",
|
|
126
|
+
) -> StructuredAuditEntry:
|
|
127
|
+
"""Log a network access policy decision."""
|
|
128
|
+
target = f"{destination}:{port}" if port else destination
|
|
129
|
+
target = f"{protocol}://{target}" if protocol else target
|
|
130
|
+
|
|
131
|
+
return self.record(
|
|
132
|
+
action="network",
|
|
133
|
+
target=target,
|
|
134
|
+
decision=decision,
|
|
135
|
+
container_id=backend,
|
|
136
|
+
enforcement_level=enforcement_level,
|
|
137
|
+
detail=detail,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _write_entry(self, entry: StructuredAuditEntry) -> None:
|
|
141
|
+
try:
|
|
142
|
+
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
with self.log_path.open("a", encoding="utf-8") as handle:
|
|
144
|
+
handle.write(json.dumps(entry.to_dict(), separators=(",", ":")) + "\n")
|
|
145
|
+
except OSError:
|
|
146
|
+
pass # Fallback gracefully if disk access fails
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def entries(self) -> list[StructuredAuditEntry]:
|
|
150
|
+
return list(self._entries)
|
|
151
|
+
|
|
152
|
+
def last_entry(self) -> StructuredAuditEntry | None:
|
|
153
|
+
return self._entries[-1] if self._entries else None
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Container backend abstractions for Pulse sandbox execution."""
|
|
2
|
+
|
|
3
|
+
from pulse.sandbox.backend.base import ContainerBackend
|
|
4
|
+
from pulse.sandbox.backend.docker import DockerBackend
|
|
5
|
+
from pulse.sandbox.backend.host import HostBackend
|
|
6
|
+
|
|
7
|
+
__all__ = ["ContainerBackend", "DockerBackend", "HostBackend"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Structural protocol for sandbox container execution backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typing
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from pulse.sandbox.network import NetworkEnforcementLevel, NetworkPolicy
|
|
10
|
+
from pulse.sandbox.process import ProcessEnforcementLevel, ProcessResult
|
|
11
|
+
from pulse.sandbox.resources import ResourceLimits
|
|
12
|
+
from pulse.sandbox.secrets import SecretEnforcementLevel, SecretPolicy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class ContainerBackend(Protocol):
|
|
17
|
+
"""Abstract interface satisfied by Docker, Podman, and Host backends."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
|
|
21
|
+
async def is_available(self) -> bool:
|
|
22
|
+
"""Return True if this container backend engine is installed and operational."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
async def execute(
|
|
26
|
+
self,
|
|
27
|
+
command: str | list[str],
|
|
28
|
+
workspace_root: Path,
|
|
29
|
+
cwd: Path | None = None,
|
|
30
|
+
env: dict[str, str] | None = None,
|
|
31
|
+
limits: ResourceLimits | None = None,
|
|
32
|
+
network_policy: NetworkPolicy | None = None,
|
|
33
|
+
secret_policy: SecretPolicy | None = None,
|
|
34
|
+
execution_id: str | None = None,
|
|
35
|
+
output_callback: typing.Callable[[str, bytes], typing.Awaitable[None]] | None = None,
|
|
36
|
+
) -> ProcessResult:
|
|
37
|
+
"""Run a command inside the isolated backend environment.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
command: Command string or argument list to run.
|
|
41
|
+
workspace_root: Absolute host path to the workspace root directory.
|
|
42
|
+
cwd: Working directory (must be inside workspace_root).
|
|
43
|
+
env: Environment variable overrides.
|
|
44
|
+
limits: Process resource limits.
|
|
45
|
+
network_policy: Execution network policy configuration.
|
|
46
|
+
secret_policy: Execution secret policy configuration.
|
|
47
|
+
execution_id: Optional UUID identifying this lifecycle execution.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
ProcessResult object containing execution status and output.
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def get_network_enforcement_capability(self, policy: NetworkPolicy) -> NetworkEnforcementLevel:
|
|
55
|
+
"""Determine if this backend can strongly enforce the requested network policy."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def get_secret_enforcement_capability(self, policy: SecretPolicy) -> SecretEnforcementLevel:
|
|
59
|
+
"""Determine if this backend can strongly enforce the requested secret isolation policy."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
def get_process_containment_capability(self) -> ProcessEnforcementLevel:
|
|
63
|
+
"""Determine if this backend provides strong process containment."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
async def cleanup(self) -> None:
|
|
67
|
+
"""Reap temporary volumes, containers, or process artifacts."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
async def reconcile(self) -> None:
|
|
71
|
+
"""Clean up orphaned backend resources (e.g., leaked containers)."""
|
|
72
|
+
...
|