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
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
+ ...