celltype-cli 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.
- celltype_cli-0.1.0.dist-info/METADATA +267 -0
- celltype_cli-0.1.0.dist-info/RECORD +89 -0
- celltype_cli-0.1.0.dist-info/WHEEL +4 -0
- celltype_cli-0.1.0.dist-info/entry_points.txt +2 -0
- celltype_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ct/__init__.py +3 -0
- ct/agent/__init__.py +0 -0
- ct/agent/case_studies.py +426 -0
- ct/agent/config.py +523 -0
- ct/agent/doctor.py +544 -0
- ct/agent/knowledge.py +523 -0
- ct/agent/loop.py +99 -0
- ct/agent/mcp_server.py +478 -0
- ct/agent/orchestrator.py +733 -0
- ct/agent/runner.py +656 -0
- ct/agent/sandbox.py +481 -0
- ct/agent/session.py +145 -0
- ct/agent/system_prompt.py +186 -0
- ct/agent/trace_store.py +228 -0
- ct/agent/trajectory.py +169 -0
- ct/agent/types.py +182 -0
- ct/agent/workflows.py +462 -0
- ct/api/__init__.py +1 -0
- ct/api/app.py +211 -0
- ct/api/config.py +120 -0
- ct/api/engine.py +124 -0
- ct/cli.py +1448 -0
- ct/data/__init__.py +0 -0
- ct/data/compute_providers.json +59 -0
- ct/data/cro_database.json +395 -0
- ct/data/downloader.py +238 -0
- ct/data/loaders.py +252 -0
- ct/kb/__init__.py +5 -0
- ct/kb/benchmarks.py +147 -0
- ct/kb/governance.py +106 -0
- ct/kb/ingest.py +415 -0
- ct/kb/reasoning.py +129 -0
- ct/kb/schema_monitor.py +162 -0
- ct/kb/substrate.py +387 -0
- ct/models/__init__.py +0 -0
- ct/models/llm.py +370 -0
- ct/tools/__init__.py +195 -0
- ct/tools/_compound_resolver.py +297 -0
- ct/tools/biomarker.py +368 -0
- ct/tools/cellxgene.py +282 -0
- ct/tools/chemistry.py +1371 -0
- ct/tools/claude.py +390 -0
- ct/tools/clinical.py +1153 -0
- ct/tools/clue.py +249 -0
- ct/tools/code.py +1069 -0
- ct/tools/combination.py +397 -0
- ct/tools/compute.py +402 -0
- ct/tools/cro.py +413 -0
- ct/tools/data_api.py +2114 -0
- ct/tools/design.py +295 -0
- ct/tools/dna.py +575 -0
- ct/tools/experiment.py +604 -0
- ct/tools/expression.py +655 -0
- ct/tools/files.py +957 -0
- ct/tools/genomics.py +1387 -0
- ct/tools/http_client.py +146 -0
- ct/tools/imaging.py +319 -0
- ct/tools/intel.py +223 -0
- ct/tools/literature.py +743 -0
- ct/tools/network.py +422 -0
- ct/tools/notification.py +111 -0
- ct/tools/omics.py +3330 -0
- ct/tools/ops.py +1230 -0
- ct/tools/parity.py +649 -0
- ct/tools/pk.py +245 -0
- ct/tools/protein.py +678 -0
- ct/tools/regulatory.py +643 -0
- ct/tools/remote_data.py +179 -0
- ct/tools/report.py +181 -0
- ct/tools/repurposing.py +376 -0
- ct/tools/safety.py +1280 -0
- ct/tools/shell.py +178 -0
- ct/tools/singlecell.py +533 -0
- ct/tools/statistics.py +552 -0
- ct/tools/structure.py +882 -0
- ct/tools/target.py +901 -0
- ct/tools/translational.py +123 -0
- ct/tools/viability.py +218 -0
- ct/ui/__init__.py +0 -0
- ct/ui/markdown.py +31 -0
- ct/ui/status.py +258 -0
- ct/ui/suggestions.py +567 -0
- ct/ui/terminal.py +1456 -0
- ct/ui/traces.py +112 -0
ct/tools/shell.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell execution tool for ct.
|
|
3
|
+
|
|
4
|
+
Run shell commands from within ct research workflows with safety restrictions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ct.tools import registry
|
|
13
|
+
|
|
14
|
+
# Commands/patterns that are never allowed
|
|
15
|
+
_BLOCKED_PATTERNS = (
|
|
16
|
+
"sudo ",
|
|
17
|
+
"rm -rf /",
|
|
18
|
+
"rm -rf /*",
|
|
19
|
+
"chmod 777",
|
|
20
|
+
":(){ :|:& };:", # fork bomb
|
|
21
|
+
"mkfs.",
|
|
22
|
+
"dd if=",
|
|
23
|
+
"> /dev/sd",
|
|
24
|
+
"shutdown",
|
|
25
|
+
"reboot",
|
|
26
|
+
"init 0",
|
|
27
|
+
"init 6",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_BLOCKED_BINARIES = {
|
|
31
|
+
"sudo", "su", "rm", "rmdir", "mkfs", "dd", "shutdown", "reboot", "poweroff",
|
|
32
|
+
"halt", "init", "chown", "chgrp", "chmod", "useradd", "userdel", "groupadd",
|
|
33
|
+
"groupdel", "passwd", "mount", "umount",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_UNSAFE_SHELL_SYNTAX = re.compile(r"(\|\|)|(&&)|[;<>`]|[$][(]")
|
|
37
|
+
|
|
38
|
+
# Safe commands that may appear on the right side of a pipe
|
|
39
|
+
_SAFE_PIPE_RHS = {
|
|
40
|
+
"head", "tail", "grep", "wc", "sort", "uniq", "cut", "awk", "sed",
|
|
41
|
+
"cat", "less", "more", "tr", "tee", "xargs",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _is_blocked(command: str) -> str | None:
|
|
46
|
+
"""Return a reason string if the command is blocked, else None."""
|
|
47
|
+
cmd = (command or "").strip()
|
|
48
|
+
if not cmd:
|
|
49
|
+
return "Empty command"
|
|
50
|
+
|
|
51
|
+
# Check for unsafe syntax (excluding single pipe |)
|
|
52
|
+
if _UNSAFE_SHELL_SYNTAX.search(cmd):
|
|
53
|
+
return "Shell operators/redirection are not allowed; run a single command only"
|
|
54
|
+
|
|
55
|
+
# Allow single-pipe commands where the RHS is a safe utility
|
|
56
|
+
if "|" in cmd:
|
|
57
|
+
parts = cmd.split("|")
|
|
58
|
+
if len(parts) > 3:
|
|
59
|
+
return "Too many pipes; simplify the command"
|
|
60
|
+
for part in parts[1:]: # Check RHS commands
|
|
61
|
+
part_stripped = part.strip()
|
|
62
|
+
if not part_stripped:
|
|
63
|
+
return "Empty pipe segment"
|
|
64
|
+
try:
|
|
65
|
+
first_token = shlex.split(part_stripped, posix=True)[0]
|
|
66
|
+
rhs_name = Path(first_token).name.lower()
|
|
67
|
+
except (ValueError, IndexError):
|
|
68
|
+
return f"Invalid pipe segment: {part_stripped[:50]}"
|
|
69
|
+
if rhs_name not in _SAFE_PIPE_RHS:
|
|
70
|
+
return f"Pipe to '{rhs_name}' not allowed; only safe utilities permitted after pipe"
|
|
71
|
+
|
|
72
|
+
cmd_lower = cmd.lower()
|
|
73
|
+
for pattern in _BLOCKED_PATTERNS:
|
|
74
|
+
if pattern.lower() in cmd_lower:
|
|
75
|
+
return f"Blocked command pattern: {pattern}"
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
tokens = shlex.split(cmd, posix=True)
|
|
79
|
+
except ValueError as e:
|
|
80
|
+
return f"Invalid command syntax: {e}"
|
|
81
|
+
|
|
82
|
+
if not tokens:
|
|
83
|
+
return "Empty command"
|
|
84
|
+
|
|
85
|
+
command_name = Path(tokens[0]).name.lower()
|
|
86
|
+
if command_name in _BLOCKED_BINARIES:
|
|
87
|
+
return f"Blocked command: {command_name}"
|
|
88
|
+
|
|
89
|
+
if command_name in {"python", "python3", "bash", "sh", "zsh", "node", "perl", "ruby"}:
|
|
90
|
+
if any(tok in {"-c", "-e"} for tok in tokens[1:]):
|
|
91
|
+
return f"Blocked inline script execution for {command_name}"
|
|
92
|
+
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@registry.register(
|
|
97
|
+
name="shell.run",
|
|
98
|
+
description="Run a shell command in the current working directory",
|
|
99
|
+
category="shell",
|
|
100
|
+
parameters={
|
|
101
|
+
"command": "Shell command to execute",
|
|
102
|
+
"timeout": "Timeout in seconds (default 30, max 300)",
|
|
103
|
+
},
|
|
104
|
+
usage_guide=(
|
|
105
|
+
"Use to run a single shell command: scripts, data processing, git, pip, etc. "
|
|
106
|
+
"Commands run in the current working directory. Dangerous commands and shell "
|
|
107
|
+
"operators (pipes/redirection/chaining) are blocked for safety."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
def shell_run(command: str, timeout: int = 30, **kwargs) -> dict:
|
|
111
|
+
"""Run a shell command and return stdout/stderr."""
|
|
112
|
+
# Safety check
|
|
113
|
+
blocked = _is_blocked(command)
|
|
114
|
+
if blocked:
|
|
115
|
+
return {"summary": f"Command blocked: {blocked}", "error": "blocked_command"}
|
|
116
|
+
|
|
117
|
+
# Cap timeout
|
|
118
|
+
timeout = min(max(timeout, 1), 300)
|
|
119
|
+
|
|
120
|
+
# Use shell=True for pipe commands, shell=False otherwise
|
|
121
|
+
use_shell = "|" in command
|
|
122
|
+
if use_shell:
|
|
123
|
+
run_args = command
|
|
124
|
+
else:
|
|
125
|
+
try:
|
|
126
|
+
run_args = shlex.split(command, posix=True)
|
|
127
|
+
except ValueError as e:
|
|
128
|
+
return {
|
|
129
|
+
"summary": f"Invalid command syntax: {e}",
|
|
130
|
+
"error": "invalid_command",
|
|
131
|
+
"exit_code": -1,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
result = subprocess.run(
|
|
136
|
+
run_args,
|
|
137
|
+
shell=use_shell,
|
|
138
|
+
cwd=str(Path.cwd()),
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
timeout=timeout,
|
|
142
|
+
)
|
|
143
|
+
except subprocess.TimeoutExpired:
|
|
144
|
+
return {
|
|
145
|
+
"summary": f"Command timed out after {timeout}s: {command[:80]}",
|
|
146
|
+
"error": "timeout",
|
|
147
|
+
"exit_code": -1,
|
|
148
|
+
}
|
|
149
|
+
except Exception as e:
|
|
150
|
+
return {
|
|
151
|
+
"summary": f"Command failed: {e}",
|
|
152
|
+
"error": str(e),
|
|
153
|
+
"exit_code": -1,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
stdout = result.stdout
|
|
157
|
+
stderr = result.stderr
|
|
158
|
+
|
|
159
|
+
# Truncate large output
|
|
160
|
+
if len(stdout) > 10000:
|
|
161
|
+
stdout = stdout[:10000] + f"\n... [truncated, total {len(result.stdout)} chars]"
|
|
162
|
+
if len(stderr) > 5000:
|
|
163
|
+
stderr = stderr[:5000] + f"\n... [truncated, total {len(result.stderr)} chars]"
|
|
164
|
+
|
|
165
|
+
if result.returncode == 0:
|
|
166
|
+
output_preview = stdout.strip()[:200] if stdout.strip() else "(no output)"
|
|
167
|
+
summary = f"Command succeeded (exit 0): {output_preview}"
|
|
168
|
+
else:
|
|
169
|
+
err_preview = stderr.strip()[:200] if stderr.strip() else stdout.strip()[:200]
|
|
170
|
+
summary = f"Command failed (exit {result.returncode}): {err_preview}"
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
"summary": summary,
|
|
174
|
+
"exit_code": result.returncode,
|
|
175
|
+
"stdout": stdout,
|
|
176
|
+
"stderr": stderr,
|
|
177
|
+
"command": command,
|
|
178
|
+
}
|