Marvin Wiesner commited on
Commit
4e5f741
·
verified ·
1 Parent(s): 967c121

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -0
app.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shlex
3
+ import subprocess
4
+ from typing import Tuple
5
+
6
+ from flask import Flask, render_template_string, request
7
+
8
+ app = Flask(__name__)
9
+
10
+ ALLOWED_COMMANDS = {
11
+ "status": ["openclaw", "status"],
12
+ "onboard": ["openclaw", "onboard"],
13
+ "gateway": ["openclaw", "gateway"],
14
+ "tui": ["openclaw", "tui"],
15
+ "dashboard": ["openclaw", "dashboard"],
16
+ }
17
+
18
+ HTML = """
19
+ <!doctype html>
20
+ <html>
21
+ <head>
22
+ <meta charset="utf-8">
23
+ <title>OpenClaw Docker Space</title>
24
+ <meta name="viewport" content="width=device-width, initial-scale=1">
25
+ <style>
26
+ body {
27
+ font-family: Arial, sans-serif;
28
+ max-width: 900px;
29
+ margin: 40px auto;
30
+ padding: 0 16px;
31
+ }
32
+ h1 { margin-bottom: 8px; }
33
+ .muted { color: #666; margin-bottom: 24px; }
34
+ form { margin-bottom: 24px; }
35
+ button {
36
+ margin: 6px 8px 6px 0;
37
+ padding: 10px 14px;
38
+ cursor: pointer;
39
+ }
40
+ pre {
41
+ background: #111;
42
+ color: #eee;
43
+ padding: 16px;
44
+ border-radius: 8px;
45
+ overflow-x: auto;
46
+ white-space: pre-wrap;
47
+ }
48
+ .row {
49
+ display: flex;
50
+ gap: 12px;
51
+ align-items: center;
52
+ flex-wrap: wrap;
53
+ margin-bottom: 16px;
54
+ }
55
+ input[type=text] {
56
+ flex: 1;
57
+ min-width: 280px;
58
+ padding: 10px;
59
+ }
60
+ .warn {
61
+ background: #fff4e5;
62
+ border: 1px solid #f0c36d;
63
+ padding: 12px;
64
+ border-radius: 8px;
65
+ margin-bottom: 20px;
66
+ }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <h1>OpenClaw Docker Space</h1>
71
+ <div class="muted">
72
+ Runs supported <code>openclaw</code> commands inside this Hugging Face Docker Space.
73
+ </div>
74
+
75
+ <div class="warn">
76
+ Only a fixed allowlist of commands can be run. Arbitrary shell execution is disabled.
77
+ </div>
78
+
79
+ <form method="post">
80
+ <div>
81
+ {% for key in commands %}
82
+ <button type="submit" name="command" value="{{ key }}">{{ key }}</button>
83
+ {% endfor %}
84
+ </div>
85
+ </form>
86
+
87
+ <form method="post">
88
+ <div class="row">
89
+ <input type="text" name="custom_command" placeholder="Example: openclaw status" />
90
+ <button type="submit">Run custom command</button>
91
+ </div>
92
+ </form>
93
+
94
+ {% if executed %}
95
+ <h3>Executed</h3>
96
+ <pre>{{ executed }}</pre>
97
+
98
+ <h3>Exit code</h3>
99
+ <pre>{{ returncode }}</pre>
100
+
101
+ <h3>Output</h3>
102
+ <pre>{{ output }}</pre>
103
+ {% endif %}
104
+ </body>
105
+ </html>
106
+ """
107
+
108
+ def normalize_custom_command(text: str):
109
+ text = (text or "").strip()
110
+ if not text:
111
+ return None
112
+
113
+ try:
114
+ parts = shlex.split(text)
115
+ except ValueError:
116
+ return None
117
+
118
+ for allowed in ALLOWED_COMMANDS.values():
119
+ if parts == allowed:
120
+ return allowed
121
+
122
+ return None
123
+
124
+ def run_command(cmd: list[str]) -> Tuple[str, int]:
125
+ env = os.environ.copy()
126
+ env["PYTHONUNBUFFERED"] = "1"
127
+
128
+ try:
129
+ result = subprocess.run(
130
+ cmd,
131
+ stdout=subprocess.PIPE,
132
+ stderr=subprocess.STDOUT,
133
+ text=True,
134
+ timeout=120,
135
+ env=env,
136
+ cwd="/app",
137
+ )
138
+ return result.stdout, result.returncode
139
+ except FileNotFoundError:
140
+ return "Error: `openclaw` is not installed in the container.", 127
141
+ except subprocess.TimeoutExpired:
142
+ return "Error: command timed out after 120 seconds.", 124
143
+ except Exception as e:
144
+ return f"Unexpected error: {e}", 1
145
+
146
+ @app.route("/", methods=["GET", "POST"])
147
+ def index():
148
+ executed = ""
149
+ output = ""
150
+ returncode = ""
151
+
152
+ if request.method == "POST":
153
+ selected = request.form.get("command", "").strip()
154
+ custom = request.form.get("custom_command", "").strip()
155
+
156
+ cmd = None
157
+ if selected in ALLOWED_COMMANDS:
158
+ cmd = ALLOWED_COMMANDS[selected]
159
+ elif custom:
160
+ cmd = normalize_custom_command(custom)
161
+
162
+ if cmd is None:
163
+ executed = custom or selected or "(none)"
164
+ output = (
165
+ "Rejected command.\n\n"
166
+ "Allowed commands are:\n"
167
+ + "\n".join(" ".join(v) for v in ALLOWED_COMMANDS.values())
168
+ )
169
+ returncode = 400
170
+ else:
171
+ executed = " ".join(cmd)
172
+ output, returncode = run_command(cmd)
173
+
174
+ return render_template_string(
175
+ HTML,
176
+ commands=ALLOWED_COMMANDS.keys(),
177
+ executed=executed,
178
+ output=output,
179
+ returncode=returncode,
180
+ )
181
+
182
+ if __name__ == "__main__":
183
+ port = int(os.environ.get("PORT", "7860"))
184
+ app.run(host="0.0.0.0", port=port, debug=False)