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/runtime.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pulse.agent import ProjectAgent
|
|
7
|
+
from pulse.agent_manager import (
|
|
8
|
+
AgentManager,
|
|
9
|
+
DocumentationAgent,
|
|
10
|
+
GitAgent,
|
|
11
|
+
PlannerAgent,
|
|
12
|
+
ReviewerAgent,
|
|
13
|
+
SoftwareEngineerAgent,
|
|
14
|
+
TesterAgent,
|
|
15
|
+
)
|
|
16
|
+
from pulse.audit import AuditLog
|
|
17
|
+
from pulse.auth import AuthenticationManager
|
|
18
|
+
from pulse.config import AgentConfig, load_agent_config
|
|
19
|
+
from pulse.context import ContextManager
|
|
20
|
+
from pulse.core.planner import RequestPlanner
|
|
21
|
+
from pulse.edits import EditWorkflow
|
|
22
|
+
from pulse.git import GitIntelligence
|
|
23
|
+
from pulse.memory import LongTermMemory
|
|
24
|
+
from pulse.mutations import MutationTracker
|
|
25
|
+
from pulse.provider import ModelProvider
|
|
26
|
+
from pulse.providers.manager import ProviderManager
|
|
27
|
+
from pulse.reasoning import ReasoningEngine
|
|
28
|
+
from pulse.repository import RepositoryIndex
|
|
29
|
+
from pulse.sandbox import ProjectSandbox
|
|
30
|
+
from pulse.session_manager import SessionManager
|
|
31
|
+
from pulse.software_engineer import AutonomousSoftwareEngineer
|
|
32
|
+
from pulse.streaming import StreamingExecutionEngine
|
|
33
|
+
from pulse.task_manager import TaskManager
|
|
34
|
+
from pulse.telemetry import TelemetryLogger
|
|
35
|
+
from pulse.tool_policy import ToolPolicyEngine
|
|
36
|
+
from pulse.tool_registry import ToolInvocation, ToolRegistry
|
|
37
|
+
from pulse.tools import (
|
|
38
|
+
DoctorTool,
|
|
39
|
+
EditTool,
|
|
40
|
+
GitTool,
|
|
41
|
+
IndexTool,
|
|
42
|
+
MemoryTool,
|
|
43
|
+
MutationsTool,
|
|
44
|
+
RollbackTool,
|
|
45
|
+
SearchTool,
|
|
46
|
+
SessionTool,
|
|
47
|
+
StatusTool,
|
|
48
|
+
SymbolsTool,
|
|
49
|
+
TaskTool,
|
|
50
|
+
VerifyTool,
|
|
51
|
+
)
|
|
52
|
+
from pulse.verification import VerificationEngine
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True)
|
|
56
|
+
class AgentRuntime:
|
|
57
|
+
workspace: Path
|
|
58
|
+
config: AgentConfig
|
|
59
|
+
audit: AuditLog
|
|
60
|
+
mutations: MutationTracker
|
|
61
|
+
sandbox: ProjectSandbox
|
|
62
|
+
provider: ModelProvider
|
|
63
|
+
agent: ProjectAgent
|
|
64
|
+
edits: EditWorkflow
|
|
65
|
+
tools: ToolRegistry
|
|
66
|
+
repository: RepositoryIndex
|
|
67
|
+
verification: VerificationEngine
|
|
68
|
+
git: GitIntelligence
|
|
69
|
+
memory: LongTermMemory
|
|
70
|
+
manager: AgentManager
|
|
71
|
+
task_manager: TaskManager
|
|
72
|
+
session_manager: SessionManager
|
|
73
|
+
context_manager: ContextManager
|
|
74
|
+
reasoning_engine: ReasoningEngine
|
|
75
|
+
planner: RequestPlanner
|
|
76
|
+
streaming_engine: StreamingExecutionEngine
|
|
77
|
+
software_engineer: AutonomousSoftwareEngineer
|
|
78
|
+
auth: AuthenticationManager
|
|
79
|
+
telemetry: TelemetryLogger
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_runtime(workspace: Path, config: AgentConfig | None = None) -> AgentRuntime:
|
|
83
|
+
resolved_workspace = workspace.resolve()
|
|
84
|
+
resolved_config = config or load_agent_config(resolved_workspace)
|
|
85
|
+
audit = AuditLog(resolved_config.logging.action_log)
|
|
86
|
+
telemetry = TelemetryLogger(resolved_config.logging.telemetry_log)
|
|
87
|
+
mutations = MutationTracker(resolved_workspace)
|
|
88
|
+
sandbox = ProjectSandbox(resolved_config.sandbox, audit, mutations)
|
|
89
|
+
|
|
90
|
+
provider_manager = ProviderManager(resolved_workspace)
|
|
91
|
+
provider = provider_manager.create_provider(
|
|
92
|
+
resolved_config.model, resolved_workspace / ".env"
|
|
93
|
+
)
|
|
94
|
+
audit.add_secret(getattr(provider, "api_key", None))
|
|
95
|
+
telemetry.add_secret(getattr(provider, "api_key", None))
|
|
96
|
+
|
|
97
|
+
edits = EditWorkflow(sandbox)
|
|
98
|
+
repository = RepositoryIndex(resolved_workspace)
|
|
99
|
+
verification = VerificationEngine(resolved_workspace)
|
|
100
|
+
git = GitIntelligence(resolved_workspace)
|
|
101
|
+
memory = LongTermMemory(
|
|
102
|
+
resolved_workspace,
|
|
103
|
+
secrets=[provider.api_key] if getattr(provider, "api_key", None) else None,
|
|
104
|
+
)
|
|
105
|
+
task_manager = TaskManager(resolved_workspace, memory=memory, telemetry=telemetry)
|
|
106
|
+
session_manager = SessionManager(
|
|
107
|
+
resolved_workspace, task_manager=task_manager, telemetry=telemetry
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
async def check_permission(invocation: ToolInvocation, tool: object) -> bool:
|
|
111
|
+
return sandbox.request_project_action(
|
|
112
|
+
"run tool", getattr(tool, "name", "tool"), "This action changes the project."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
tool_capabilities = frozenset(
|
|
116
|
+
{
|
|
117
|
+
"status", "doctor", "mutations", "edit", "rollback", "git", "memory",
|
|
118
|
+
"index", "search", "symbols", "verify", "task", "tasks", "resume", "cancel",
|
|
119
|
+
"session", "sessions", "resume-session",
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
tools = ToolRegistry(
|
|
123
|
+
[
|
|
124
|
+
StatusTool(resolved_config, provider), DoctorTool(resolved_workspace, resolved_config, provider),
|
|
125
|
+
MutationsTool(mutations), EditTool(edits, git), RollbackTool(edits), GitTool(git), MemoryTool(memory),
|
|
126
|
+
],
|
|
127
|
+
permission_checker=check_permission,
|
|
128
|
+
telemetry=telemetry,
|
|
129
|
+
policy_engine=ToolPolicyEngine(
|
|
130
|
+
workspace=resolved_workspace,
|
|
131
|
+
allowed_capabilities=tool_capabilities,
|
|
132
|
+
audit_log=audit,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
tools.register(IndexTool(repository))
|
|
136
|
+
tools.register(SearchTool(repository))
|
|
137
|
+
tools.register(SymbolsTool(repository))
|
|
138
|
+
tools.register(VerifyTool(verification))
|
|
139
|
+
tools.register(TaskTool(task_manager))
|
|
140
|
+
tools.register(SessionTool(session_manager))
|
|
141
|
+
|
|
142
|
+
manager = AgentManager(
|
|
143
|
+
task_manager=task_manager,
|
|
144
|
+
agents=[
|
|
145
|
+
PlannerAgent(provider, tools),
|
|
146
|
+
SoftwareEngineerAgent(provider, tools),
|
|
147
|
+
ReviewerAgent(provider, tools),
|
|
148
|
+
TesterAgent(provider, tools),
|
|
149
|
+
DocumentationAgent(provider, tools),
|
|
150
|
+
GitAgent(provider, tools),
|
|
151
|
+
],
|
|
152
|
+
)
|
|
153
|
+
agent = ProjectAgent(
|
|
154
|
+
resolved_config.agent_name,
|
|
155
|
+
sandbox,
|
|
156
|
+
provider,
|
|
157
|
+
audit,
|
|
158
|
+
tools,
|
|
159
|
+
repository,
|
|
160
|
+
memory,
|
|
161
|
+
manager,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
context_manager = ContextManager(
|
|
165
|
+
repository=repository, memory=memory, git=git, workspace=resolved_workspace
|
|
166
|
+
)
|
|
167
|
+
reasoning_engine = ReasoningEngine(
|
|
168
|
+
provider=provider, context_manager=context_manager, tool_registry=tools
|
|
169
|
+
)
|
|
170
|
+
planner = RequestPlanner()
|
|
171
|
+
streaming_engine = StreamingExecutionEngine(
|
|
172
|
+
provider=provider,
|
|
173
|
+
tool_registry=tools,
|
|
174
|
+
reasoning_engine=reasoning_engine,
|
|
175
|
+
task_manager=task_manager,
|
|
176
|
+
telemetry=telemetry,
|
|
177
|
+
verification_engine=verification,
|
|
178
|
+
)
|
|
179
|
+
software_engineer = AutonomousSoftwareEngineer(
|
|
180
|
+
reasoning_engine=reasoning_engine,
|
|
181
|
+
planner=planner,
|
|
182
|
+
task_manager=task_manager,
|
|
183
|
+
session_manager=session_manager,
|
|
184
|
+
streaming_engine=streaming_engine,
|
|
185
|
+
verification_engine=verification,
|
|
186
|
+
context_manager=context_manager,
|
|
187
|
+
memory=memory,
|
|
188
|
+
repository=repository,
|
|
189
|
+
tool_registry=tools,
|
|
190
|
+
)
|
|
191
|
+
auth = AuthenticationManager(resolved_workspace)
|
|
192
|
+
|
|
193
|
+
return AgentRuntime(
|
|
194
|
+
workspace=resolved_workspace,
|
|
195
|
+
config=resolved_config,
|
|
196
|
+
audit=audit,
|
|
197
|
+
mutations=mutations,
|
|
198
|
+
sandbox=sandbox,
|
|
199
|
+
provider=provider,
|
|
200
|
+
agent=agent,
|
|
201
|
+
edits=edits,
|
|
202
|
+
tools=tools,
|
|
203
|
+
repository=repository,
|
|
204
|
+
verification=verification,
|
|
205
|
+
git=git,
|
|
206
|
+
memory=memory,
|
|
207
|
+
manager=manager,
|
|
208
|
+
task_manager=task_manager,
|
|
209
|
+
session_manager=session_manager,
|
|
210
|
+
context_manager=context_manager,
|
|
211
|
+
reasoning_engine=reasoning_engine,
|
|
212
|
+
planner=planner,
|
|
213
|
+
streaming_engine=streaming_engine,
|
|
214
|
+
software_engineer=software_engineer,
|
|
215
|
+
auth=auth,
|
|
216
|
+
telemetry=telemetry,
|
|
217
|
+
)
|
pulse/safety/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from pulse.audit import AuditLog
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RiskLevel(Enum):
|
|
11
|
+
LOW = "LOW" # Read-only operations
|
|
12
|
+
MEDIUM = "MEDIUM" # File edits and test executions
|
|
13
|
+
HIGH = "HIGH" # System execution, shell commands, or file deletion
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SafetyManager:
|
|
17
|
+
"""Assesses action risk levels, enforces user confirmation for HIGH risk actions, and records audit logs."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
audit_log: AuditLog | None = None,
|
|
22
|
+
confirmation_callback: Callable[[str, RiskLevel], bool | Awaitable[bool]] | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.audit_log = audit_log
|
|
25
|
+
self.confirmation_callback = confirmation_callback
|
|
26
|
+
|
|
27
|
+
def assess_risk(self, action: str, target: str = "") -> RiskLevel:
|
|
28
|
+
action_lower = action.lower()
|
|
29
|
+
target_lower = target.lower()
|
|
30
|
+
|
|
31
|
+
# HIGH risk: system command execution, shell access, deletion
|
|
32
|
+
high_keywords = {
|
|
33
|
+
"execute",
|
|
34
|
+
"system",
|
|
35
|
+
"shell",
|
|
36
|
+
"delete",
|
|
37
|
+
"remove",
|
|
38
|
+
"destroy",
|
|
39
|
+
"drop",
|
|
40
|
+
"unlink",
|
|
41
|
+
"bash",
|
|
42
|
+
"cmd",
|
|
43
|
+
"terminal",
|
|
44
|
+
"eval",
|
|
45
|
+
}
|
|
46
|
+
if any(kw in action_lower for kw in high_keywords) or "delete" in target_lower or "remove" in target_lower:
|
|
47
|
+
return RiskLevel.HIGH
|
|
48
|
+
|
|
49
|
+
# MEDIUM risk: edits, code modifications, writing files, running test suites
|
|
50
|
+
medium_keywords = {
|
|
51
|
+
"edit",
|
|
52
|
+
"modify",
|
|
53
|
+
"write",
|
|
54
|
+
"update",
|
|
55
|
+
"patch",
|
|
56
|
+
"test",
|
|
57
|
+
"pytest",
|
|
58
|
+
"mutate",
|
|
59
|
+
"create",
|
|
60
|
+
}
|
|
61
|
+
if any(kw in action_lower for kw in medium_keywords):
|
|
62
|
+
return RiskLevel.MEDIUM
|
|
63
|
+
|
|
64
|
+
return RiskLevel.LOW
|
|
65
|
+
|
|
66
|
+
async def authorize(self, action: str, target: str = "", detail: str = "") -> bool:
|
|
67
|
+
risk = self.assess_risk(action, target)
|
|
68
|
+
authorized = True
|
|
69
|
+
|
|
70
|
+
if risk is RiskLevel.HIGH:
|
|
71
|
+
if self.confirmation_callback is not None:
|
|
72
|
+
res = self.confirmation_callback(action, risk)
|
|
73
|
+
if asyncio.iscoroutine(res) or hasattr(res, "__await__"):
|
|
74
|
+
authorized = bool(await res)
|
|
75
|
+
else:
|
|
76
|
+
authorized = bool(res)
|
|
77
|
+
else:
|
|
78
|
+
authorized = False
|
|
79
|
+
|
|
80
|
+
if self.audit_log:
|
|
81
|
+
status = "APPROVED" if authorized else "REJECTED"
|
|
82
|
+
self.audit_log.record(
|
|
83
|
+
action=f"safety-{risk.value.lower()}-{status.lower()}",
|
|
84
|
+
file=target or ".",
|
|
85
|
+
detail=f"Action: {action} | Risk: {risk.value} | Authorized: {authorized} | {detail}".strip(" |"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return authorized
|
|
89
|
+
|
|
90
|
+
def log_audit(self, action: str, target: str, detail: str, risk: RiskLevel | None = None) -> None:
|
|
91
|
+
if self.audit_log:
|
|
92
|
+
risk_label = risk.value if risk else "UNKNOWN"
|
|
93
|
+
self.audit_log.record(
|
|
94
|
+
action=action,
|
|
95
|
+
file=target or ".",
|
|
96
|
+
detail=f"[{risk_label}] {detail}",
|
|
97
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Pulse Secure Sandbox — Security Architecture
|
|
2
|
+
|
|
3
|
+
## Isolation Models
|
|
4
|
+
|
|
5
|
+
The Pulse Sandbox provides multiple layers of defense to execute untrusted code safely.
|
|
6
|
+
We categorize security controls into Prevention, Detection, and Redaction.
|
|
7
|
+
|
|
8
|
+
### Prevention (Isolation)
|
|
9
|
+
- **Tool Authorization**: Model-produced tool arguments are validated against
|
|
10
|
+
typed schemas before execution. `ToolPolicyEngine` evaluates a per-runtime
|
|
11
|
+
capability allowlist, workspace scope, risk level, and approval requirement.
|
|
12
|
+
Unknown capabilities, malformed arguments, and paths outside the configured
|
|
13
|
+
workspace are rejected before a tool receives them.
|
|
14
|
+
- **Containerization**: `DockerBackend` enforces process isolation, filesystem isolation, and network isolation using namespaces and cgroups.
|
|
15
|
+
- **Remote Execution**: `RemoteSandboxBackend` enables untrusted code to run on a dedicated remote host via the `pulse-remote` worker daemon. This physical/network separation prevents any host escape from affecting the local development machine, with communication secured via authenticated WSS.
|
|
16
|
+
- **Secret Injection**: Secrets are explicitly injected into the container environment via temporary `--env-file` structures stored outside the workspace. This prevents argv leakage (e.g. `ps aux`) and ensures robust lifecycle cleanup.
|
|
17
|
+
- **Copy-on-Write Filesystem (CoW)**: Changes are strictly staged via CoW transactions. If explicitly authorized secrets are detected in staged files, the commit is outright rejected by the security engine to prevent secret persistence in snapshots.
|
|
18
|
+
- **Path Validation**: TOCTOU-safe path verification prevents directory traversal and symlink escapes.
|
|
19
|
+
|
|
20
|
+
### Detection (Auditing)
|
|
21
|
+
- **Structured Audit Logging**: All security boundary interactions (reads, writes, network connections, execution) are logged securely in `audit.jsonl` with their resolved `isolation_level`.
|
|
22
|
+
- **Policy Decision Logging**: Every central tool-policy decision records the
|
|
23
|
+
subject, capability, risk, and reason. Raw tool argument values are excluded
|
|
24
|
+
so audit records cannot become a second secret store.
|
|
25
|
+
|
|
26
|
+
### Redaction (Scrubber)
|
|
27
|
+
- **SecretScrubber**: In-memory regex and exact-value matching redaction engine scrub sensitive keys from standard output, errors, and audit logs. All regex patterns are optimized to prevent ReDoS.
|
|
28
|
+
|
|
29
|
+
## Known Limitations & Unsupported Scopes
|
|
30
|
+
|
|
31
|
+
- **Host Execution Fallback**: Running `HostBackend` is intrinsically unsafe for untrusted code execution. By default, the sandbox requires an explicit opt-in (`unsafe_host_execution=True`) which is audited and warned against.
|
|
32
|
+
- **Process Group Termination Escapes**: On POSIX, a child process running directly on the host can escape `killpg()` termination by calling `setsid()`. This underscores the mandatory requirement for container-level isolation boundaries.
|
|
33
|
+
- **Scoped Secret Access**: Providing per-network-request secret injection (Scoped Secrets) is **currently unsupported**. Given that network isolation and environment variable capabilities are decoupled, attempting to implement scoped secrets without strong OS/ebpf-level enforcement can lead to a false sense of security. Secrets injected into a container are accessible to any process within that container.
|
|
34
|
+
|
|
35
|
+
## Threat Model and Incident Response
|
|
36
|
+
|
|
37
|
+
Pulse treats provider output, tool arguments, downloaded artifacts, and remote
|
|
38
|
+
worker messages as untrusted. The intended deployment boundary is a dedicated
|
|
39
|
+
container or remote worker; host execution is development-only and explicitly
|
|
40
|
+
unsafe. The system is designed to contain path traversal, shell injection,
|
|
41
|
+
archive attacks, unrestricted network egress, credential leakage, stale remote
|
|
42
|
+
results, and cross-tenant remote execution access.
|
|
43
|
+
|
|
44
|
+
If a remote token, client certificate, or provider credential may be exposed:
|
|
45
|
+
|
|
46
|
+
1. Revoke the affected credential at its issuer and replace the configured
|
|
47
|
+
`PULSE_REMOTE_TOKEN` or TLS material on both worker and clients.
|
|
48
|
+
2. Restart remote workers to terminate active sessions; inspect the durable
|
|
49
|
+
execution store and `audit.jsonl` for the affected tenant and execution IDs.
|
|
50
|
+
3. Quarantine ambiguous tasks through recovery rather than retrying them with a
|
|
51
|
+
new external ID, then rotate any secrets mounted into the affected worker.
|
|
52
|
+
4. Preserve redacted audit logs and the worker version/configuration for
|
|
53
|
+
incident review. Do not copy raw execution output into tickets.
|
|
54
|
+
|
|
55
|
+
Remote workers must use `wss://` with mTLS outside loopback, have a dedicated
|
|
56
|
+
tenant-isolated workspace volume, enforce a finite execution retention period,
|
|
57
|
+
and expose health checks only on an authenticated operations network.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Secure sandbox subsystem public exports.
|
|
2
|
+
|
|
3
|
+
The package keeps these exports lazy so importing a narrow helper such as
|
|
4
|
+
``pulse.sandbox.secrets`` does not initialize the full sandbox stack.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from importlib import import_module
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
_EXPORTS = {
|
|
13
|
+
"ActionType": "pulse.sandbox.policy",
|
|
14
|
+
"CoWFilesystem": "pulse.sandbox.filesystem",
|
|
15
|
+
"CoWTransaction": "pulse.sandbox.filesystem",
|
|
16
|
+
"ContainerBackend": "pulse.sandbox.backend",
|
|
17
|
+
"DockerBackend": "pulse.sandbox.backend",
|
|
18
|
+
"ExecutionMetrics": "pulse.sandbox.resources",
|
|
19
|
+
"HostBackend": "pulse.sandbox.backend",
|
|
20
|
+
"PathValidationError": "pulse.sandbox.path_validator",
|
|
21
|
+
"PathValidator": "pulse.sandbox.path_validator",
|
|
22
|
+
"PolicyDecision": "pulse.sandbox.policy",
|
|
23
|
+
"PolicyRule": "pulse.sandbox.policy",
|
|
24
|
+
"ProcessManager": "pulse.sandbox.process",
|
|
25
|
+
"ProcessResult": "pulse.sandbox.process",
|
|
26
|
+
"ProjectSandbox": "pulse.sandbox.project",
|
|
27
|
+
"ResourceController": "pulse.sandbox.resources",
|
|
28
|
+
"ResourceLimitExceeded": "pulse.sandbox.resources",
|
|
29
|
+
"ResourceLimiter": "pulse.sandbox.resources",
|
|
30
|
+
"ResourceLimits": "pulse.sandbox.resources",
|
|
31
|
+
"ResourceMonitor": "pulse.sandbox.resources",
|
|
32
|
+
"ResourcePolicy": "pulse.sandbox.resources",
|
|
33
|
+
"SafeGit": "pulse.sandbox.git_safe",
|
|
34
|
+
"SafePython": "pulse.sandbox.python_safe",
|
|
35
|
+
"Sandbox": "pulse.sandbox.api",
|
|
36
|
+
"SandboxConcurrentModificationError": "pulse.sandbox.errors",
|
|
37
|
+
"SandboxPolicy": "pulse.sandbox.policy",
|
|
38
|
+
"SandboxResourceError": "pulse.sandbox.errors",
|
|
39
|
+
"SandboxSecurityError": "pulse.sandbox.errors",
|
|
40
|
+
"SandboxSession": "pulse.sandbox.api",
|
|
41
|
+
"SandboxUnavailableError": "pulse.sandbox.errors",
|
|
42
|
+
"SecretScrubber": "pulse.sandbox.secrets",
|
|
43
|
+
"StructuredAuditEntry": "pulse.sandbox.audit",
|
|
44
|
+
"StructuredAuditLogger": "pulse.sandbox.audit",
|
|
45
|
+
"TimeoutExceeded": "pulse.sandbox.resources",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
__all__ = tuple(sorted(_EXPORTS))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __getattr__(name: str) -> Any:
|
|
52
|
+
if name not in _EXPORTS:
|
|
53
|
+
raise AttributeError(name)
|
|
54
|
+
module = import_module(_EXPORTS[name])
|
|
55
|
+
value = getattr(module, name)
|
|
56
|
+
globals()[name] = value
|
|
57
|
+
return value
|