flowly-code 1.0.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.
- flowly_code/__init__.py +30 -0
- flowly_code/__main__.py +8 -0
- flowly_code/activity/__init__.py +1 -0
- flowly_code/activity/bus.py +91 -0
- flowly_code/activity/events.py +40 -0
- flowly_code/agent/__init__.py +8 -0
- flowly_code/agent/context.py +485 -0
- flowly_code/agent/loop.py +1349 -0
- flowly_code/agent/memory.py +109 -0
- flowly_code/agent/skills.py +259 -0
- flowly_code/agent/subagent.py +249 -0
- flowly_code/agent/tools/__init__.py +6 -0
- flowly_code/agent/tools/base.py +55 -0
- flowly_code/agent/tools/delegate.py +194 -0
- flowly_code/agent/tools/dispatch.py +840 -0
- flowly_code/agent/tools/docker.py +609 -0
- flowly_code/agent/tools/filesystem.py +280 -0
- flowly_code/agent/tools/mcp.py +85 -0
- flowly_code/agent/tools/message.py +235 -0
- flowly_code/agent/tools/registry.py +257 -0
- flowly_code/agent/tools/screenshot.py +444 -0
- flowly_code/agent/tools/shell.py +166 -0
- flowly_code/agent/tools/spawn.py +65 -0
- flowly_code/agent/tools/system.py +917 -0
- flowly_code/agent/tools/trello.py +420 -0
- flowly_code/agent/tools/web.py +139 -0
- flowly_code/agent/tools/x.py +399 -0
- flowly_code/bus/__init__.py +6 -0
- flowly_code/bus/events.py +37 -0
- flowly_code/bus/queue.py +81 -0
- flowly_code/channels/__init__.py +6 -0
- flowly_code/channels/base.py +121 -0
- flowly_code/channels/manager.py +135 -0
- flowly_code/channels/telegram.py +1132 -0
- flowly_code/cli/__init__.py +1 -0
- flowly_code/cli/commands.py +1831 -0
- flowly_code/cli/setup.py +1356 -0
- flowly_code/compaction/__init__.py +39 -0
- flowly_code/compaction/estimator.py +88 -0
- flowly_code/compaction/pruning.py +223 -0
- flowly_code/compaction/service.py +297 -0
- flowly_code/compaction/summarizer.py +384 -0
- flowly_code/compaction/types.py +71 -0
- flowly_code/config/__init__.py +6 -0
- flowly_code/config/loader.py +102 -0
- flowly_code/config/schema.py +324 -0
- flowly_code/exec/__init__.py +39 -0
- flowly_code/exec/approvals.py +288 -0
- flowly_code/exec/executor.py +184 -0
- flowly_code/exec/safety.py +247 -0
- flowly_code/exec/types.py +88 -0
- flowly_code/gateway/__init__.py +5 -0
- flowly_code/gateway/server.py +103 -0
- flowly_code/heartbeat/__init__.py +5 -0
- flowly_code/heartbeat/service.py +130 -0
- flowly_code/multiagent/README.md +248 -0
- flowly_code/multiagent/__init__.py +1 -0
- flowly_code/multiagent/invoke.py +210 -0
- flowly_code/multiagent/orchestrator.py +156 -0
- flowly_code/multiagent/router.py +156 -0
- flowly_code/multiagent/setup.py +171 -0
- flowly_code/pairing/__init__.py +21 -0
- flowly_code/pairing/store.py +343 -0
- flowly_code/providers/__init__.py +6 -0
- flowly_code/providers/base.py +69 -0
- flowly_code/providers/litellm_provider.py +178 -0
- flowly_code/providers/transcription.py +64 -0
- flowly_code/session/__init__.py +5 -0
- flowly_code/session/manager.py +249 -0
- flowly_code/skills/README.md +24 -0
- flowly_code/skills/compact/SKILL.md +27 -0
- flowly_code/skills/github/SKILL.md +48 -0
- flowly_code/skills/skill-creator/SKILL.md +371 -0
- flowly_code/skills/summarize/SKILL.md +67 -0
- flowly_code/skills/tmux/SKILL.md +121 -0
- flowly_code/skills/tmux/scripts/find-sessions.sh +112 -0
- flowly_code/skills/tmux/scripts/wait-for-text.sh +83 -0
- flowly_code/skills/weather/SKILL.md +49 -0
- flowly_code/utils/__init__.py +5 -0
- flowly_code/utils/helpers.py +91 -0
- flowly_code-1.0.0.dist-info/METADATA +724 -0
- flowly_code-1.0.0.dist-info/RECORD +86 -0
- flowly_code-1.0.0.dist-info/WHEEL +4 -0
- flowly_code-1.0.0.dist-info/entry_points.txt +2 -0
- flowly_code-1.0.0.dist-info/licenses/LICENSE +191 -0
- flowly_code-1.0.0.dist-info/licenses/NOTICE +74 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Secure shell execution tool with approval system."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Callable, Awaitable
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from flowly_code.agent.tools.base import Tool
|
|
9
|
+
from flowly_code.exec import (
|
|
10
|
+
ExecConfig,
|
|
11
|
+
ExecRequest,
|
|
12
|
+
ExecResult,
|
|
13
|
+
ExecApprovalStore,
|
|
14
|
+
ExecApprovalDecision,
|
|
15
|
+
analyze_command,
|
|
16
|
+
execute_command,
|
|
17
|
+
)
|
|
18
|
+
from flowly_code.exec.types import PendingApproval
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SecureExecTool(Tool):
|
|
22
|
+
"""
|
|
23
|
+
Secure shell command execution tool.
|
|
24
|
+
|
|
25
|
+
Features:
|
|
26
|
+
- Security modes: deny, allowlist, full
|
|
27
|
+
- Ask modes: off, on-miss, always
|
|
28
|
+
- Command analysis for dangerous patterns
|
|
29
|
+
- Safe bins (jq, grep, etc.) always allowed
|
|
30
|
+
- Allowlist with glob pattern matching
|
|
31
|
+
- Approval system via callback (Telegram)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
config: ExecConfig,
|
|
37
|
+
approval_callback: Callable[[PendingApproval], Awaitable[ExecApprovalDecision | None]] | None = None,
|
|
38
|
+
working_dir: str | None = None,
|
|
39
|
+
):
|
|
40
|
+
self.config = config
|
|
41
|
+
self.working_dir = working_dir
|
|
42
|
+
self._store = ExecApprovalStore()
|
|
43
|
+
self._store.load()
|
|
44
|
+
|
|
45
|
+
if approval_callback:
|
|
46
|
+
self._store.set_approval_callback(approval_callback)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def name(self) -> str:
|
|
50
|
+
return "exec"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def description(self) -> str:
|
|
54
|
+
if not self.config.enabled:
|
|
55
|
+
return "Execute shell commands. CURRENTLY DISABLED."
|
|
56
|
+
|
|
57
|
+
security = self._store.config.security
|
|
58
|
+
ask = self._store.config.ask
|
|
59
|
+
|
|
60
|
+
desc = "Execute a shell command and return its output.\n\n"
|
|
61
|
+
desc += f"Security: {security}, Ask: {ask}\n"
|
|
62
|
+
|
|
63
|
+
if security == "deny":
|
|
64
|
+
desc += "WARNING: Command execution is currently denied."
|
|
65
|
+
elif security == "allowlist":
|
|
66
|
+
desc += "Only allowlisted commands and safe bins (grep, jq, etc.) are permitted.\n"
|
|
67
|
+
desc += "Other commands require user approval."
|
|
68
|
+
elif security == "full":
|
|
69
|
+
desc += "Full access mode - all commands allowed."
|
|
70
|
+
|
|
71
|
+
return desc
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def parameters(self) -> dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"properties": {
|
|
78
|
+
"command": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "The shell command to execute"
|
|
81
|
+
},
|
|
82
|
+
"working_dir": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "Optional working directory for the command"
|
|
85
|
+
},
|
|
86
|
+
"timeout": {
|
|
87
|
+
"type": "integer",
|
|
88
|
+
"description": f"Optional timeout in seconds (default: {self.config.timeout_seconds})"
|
|
89
|
+
},
|
|
90
|
+
"background": {
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"description": "Run in background and return immediately with PID. Use for long-running processes (servers, tunnels, watchers). Stop later with: exec(command='kill <PID>')"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"required": ["command"]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def set_approval_callback(
|
|
99
|
+
self,
|
|
100
|
+
callback: Callable[[PendingApproval], Awaitable[ExecApprovalDecision | None]]
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Set the approval callback (for Telegram integration)."""
|
|
103
|
+
self._store.set_approval_callback(callback)
|
|
104
|
+
|
|
105
|
+
async def execute(
|
|
106
|
+
self,
|
|
107
|
+
command: str,
|
|
108
|
+
working_dir: str | None = None,
|
|
109
|
+
timeout: int | None = None,
|
|
110
|
+
background: bool = False,
|
|
111
|
+
session_key: str | None = None,
|
|
112
|
+
**kwargs: Any
|
|
113
|
+
) -> str:
|
|
114
|
+
"""Execute a command with full security checks."""
|
|
115
|
+
|
|
116
|
+
# Create request
|
|
117
|
+
request = ExecRequest(
|
|
118
|
+
command=command.strip(),
|
|
119
|
+
cwd=working_dir or self.working_dir,
|
|
120
|
+
timeout=timeout,
|
|
121
|
+
session_key=session_key,
|
|
122
|
+
background=background,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Analyze command first (for logging)
|
|
126
|
+
analysis = analyze_command(command)
|
|
127
|
+
logger.info(f"Exec request: {command[:50]}... (safe_bin={analysis.is_safe_bin}, resolved={analysis.resolved_path})")
|
|
128
|
+
|
|
129
|
+
# Execute with security checks
|
|
130
|
+
result = await execute_command(request, self.config, self._store)
|
|
131
|
+
|
|
132
|
+
# Format result
|
|
133
|
+
if result.denied:
|
|
134
|
+
return f"❌ Command denied: {result.error}"
|
|
135
|
+
|
|
136
|
+
if result.pid is not None and background:
|
|
137
|
+
return f"✅ Background process started (PID: {result.pid}). To stop: exec(command='kill {result.pid}')"
|
|
138
|
+
|
|
139
|
+
if result.timed_out:
|
|
140
|
+
return f"⏰ Command timed out after {timeout or self.config.timeout_seconds} seconds"
|
|
141
|
+
|
|
142
|
+
if result.error:
|
|
143
|
+
return f"❌ Error: {result.error}"
|
|
144
|
+
|
|
145
|
+
# Build output
|
|
146
|
+
output_parts = []
|
|
147
|
+
|
|
148
|
+
if result.stdout:
|
|
149
|
+
output_parts.append(result.stdout)
|
|
150
|
+
|
|
151
|
+
if result.stderr:
|
|
152
|
+
output_parts.append(f"STDERR:\n{result.stderr}")
|
|
153
|
+
|
|
154
|
+
if result.exit_code is not None and result.exit_code != 0:
|
|
155
|
+
output_parts.append(f"\nExit code: {result.exit_code}")
|
|
156
|
+
|
|
157
|
+
return "\n".join(output_parts) if output_parts else "(no output)"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def store(self) -> ExecApprovalStore:
|
|
161
|
+
"""Get the approval store for external management."""
|
|
162
|
+
return self._store
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Keep backward compatibility alias
|
|
166
|
+
ExecTool = SecureExecTool
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Spawn tool for creating background subagents."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from flowly_code.agent.tools.base import Tool
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from flowly_code.agent.subagent import SubagentManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpawnTool(Tool):
|
|
12
|
+
"""
|
|
13
|
+
Tool to spawn a subagent for background task execution.
|
|
14
|
+
|
|
15
|
+
The subagent runs asynchronously and announces its result back
|
|
16
|
+
to the main agent when complete.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, manager: "SubagentManager"):
|
|
20
|
+
self._manager = manager
|
|
21
|
+
self._origin_channel = "cli"
|
|
22
|
+
self._origin_chat_id = "direct"
|
|
23
|
+
|
|
24
|
+
def set_context(self, channel: str, chat_id: str) -> None:
|
|
25
|
+
"""Set the origin context for subagent announcements."""
|
|
26
|
+
self._origin_channel = channel
|
|
27
|
+
self._origin_chat_id = chat_id
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
return "spawn"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def description(self) -> str:
|
|
35
|
+
return (
|
|
36
|
+
"Spawn a subagent to handle a task in the background. "
|
|
37
|
+
"Use this for complex or time-consuming tasks that can run independently. "
|
|
38
|
+
"The subagent will complete the task and report back when done."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def parameters(self) -> dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"task": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "The task for the subagent to complete",
|
|
49
|
+
},
|
|
50
|
+
"label": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Optional short label for the task (for display)",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
"required": ["task"],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async def execute(self, task: str, label: str | None = None, **kwargs: Any) -> str:
|
|
59
|
+
"""Spawn a subagent to execute the given task."""
|
|
60
|
+
return await self._manager.spawn(
|
|
61
|
+
task=task,
|
|
62
|
+
label=label,
|
|
63
|
+
origin_channel=self._origin_channel,
|
|
64
|
+
origin_chat_id=self._origin_chat_id,
|
|
65
|
+
)
|