Spaces:
Running
Running
Update openspace/dashboard_server.py
Browse files
openspace/dashboard_server.py
CHANGED
|
@@ -71,7 +71,12 @@ def create_app() -> Flask:
|
|
| 71 |
expected_key = os.environ.get("OPENSPACE_API_KEY")
|
| 72 |
if expected_key:
|
| 73 |
auth_header = request.headers.get("Authorization")
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
abort(401, description="Unauthorized: Invalid or missing API Key")
|
| 76 |
|
| 77 |
@app.route(f"{API_PREFIX}/health", methods=["GET"])
|
|
@@ -182,6 +187,95 @@ def create_app() -> Flask:
|
|
| 182 |
abort(404, description=f"Unknown skill_id: {skill_id}")
|
| 183 |
return jsonify(_load_skill_source(record))
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
@app.route(f"{API_PREFIX}/workflows", methods=["GET"])
|
| 186 |
def list_workflows() -> Any:
|
| 187 |
items = [_build_workflow_summary(path) for path in _discover_workflow_dirs()]
|
|
|
|
| 71 |
expected_key = os.environ.get("OPENSPACE_API_KEY")
|
| 72 |
if expected_key:
|
| 73 |
auth_header = request.headers.get("Authorization")
|
| 74 |
+
x_api_key = request.headers.get("X-API-Key")
|
| 75 |
+
|
| 76 |
+
is_valid_auth = auth_header and auth_header == f"Bearer {expected_key}"
|
| 77 |
+
is_valid_x = x_api_key and x_api_key == expected_key
|
| 78 |
+
|
| 79 |
+
if not (is_valid_auth or is_valid_x):
|
| 80 |
abort(401, description="Unauthorized: Invalid or missing API Key")
|
| 81 |
|
| 82 |
@app.route(f"{API_PREFIX}/health", methods=["GET"])
|
|
|
|
| 187 |
abort(404, description=f"Unknown skill_id: {skill_id}")
|
| 188 |
return jsonify(_load_skill_source(record))
|
| 189 |
|
| 190 |
+
@app.route(f"{API_PREFIX}/artifacts/stage", methods=["POST"])
|
| 191 |
+
def stage_artifact() -> Any:
|
| 192 |
+
if 'files' not in request.files:
|
| 193 |
+
abort(400, description="No files part in the request")
|
| 194 |
+
|
| 195 |
+
files = request.files.getlist('files')
|
| 196 |
+
if not files:
|
| 197 |
+
abort(400, description="No files selected for uploading")
|
| 198 |
+
|
| 199 |
+
import uuid
|
| 200 |
+
artifact_id = f"art_{uuid.uuid4().hex[:8]}"
|
| 201 |
+
stage_dir = PROJECT_ROOT / "skills" / artifact_id
|
| 202 |
+
stage_dir.mkdir(parents=True, exist_ok=True)
|
| 203 |
+
|
| 204 |
+
saved_count = 0
|
| 205 |
+
for file in files:
|
| 206 |
+
if file.filename:
|
| 207 |
+
# Need to handle nested paths like sub/file.txt
|
| 208 |
+
# request.files filename could contain slashes if using webkitRelativePath
|
| 209 |
+
# But typically multipart form-data filename is just the basename
|
| 210 |
+
# However client.py sets: filename="{rel_path}"
|
| 211 |
+
safe_path = (stage_dir / file.filename).resolve()
|
| 212 |
+
if stage_dir not in safe_path.parents and safe_path != stage_dir:
|
| 213 |
+
continue
|
| 214 |
+
safe_path.parent.mkdir(parents=True, exist_ok=True)
|
| 215 |
+
file.save(str(safe_path))
|
| 216 |
+
saved_count += 1
|
| 217 |
+
|
| 218 |
+
return jsonify({
|
| 219 |
+
"artifact_id": artifact_id,
|
| 220 |
+
"stats": {"file_count": saved_count}
|
| 221 |
+
})
|
| 222 |
+
|
| 223 |
+
@app.route(f"{API_PREFIX}/records", methods=["POST"])
|
| 224 |
+
def create_record() -> Any:
|
| 225 |
+
data = request.json
|
| 226 |
+
if not data:
|
| 227 |
+
abort(400, description="Invalid JSON payload")
|
| 228 |
+
|
| 229 |
+
artifact_id = data.get("artifact_id")
|
| 230 |
+
if not artifact_id:
|
| 231 |
+
abort(400, description="Missing artifact_id")
|
| 232 |
+
|
| 233 |
+
stage_dir = PROJECT_ROOT / "skills" / artifact_id
|
| 234 |
+
if not stage_dir.exists():
|
| 235 |
+
abort(404, description="Artifact not found")
|
| 236 |
+
|
| 237 |
+
# The skill folder name should ideally be the skill name
|
| 238 |
+
skill_name = data.get("name", artifact_id)
|
| 239 |
+
final_dir = PROJECT_ROOT / "skills" / skill_name
|
| 240 |
+
|
| 241 |
+
# If final_dir exists, we might need to overwrite or abort
|
| 242 |
+
if final_dir.exists() and final_dir != stage_dir:
|
| 243 |
+
import shutil
|
| 244 |
+
shutil.rmtree(final_dir, ignore_errors=True)
|
| 245 |
+
|
| 246 |
+
try:
|
| 247 |
+
stage_dir.rename(final_dir)
|
| 248 |
+
except OSError:
|
| 249 |
+
import shutil
|
| 250 |
+
shutil.copytree(stage_dir, final_dir, dirs_exist_ok=True)
|
| 251 |
+
shutil.rmtree(stage_dir, ignore_errors=True)
|
| 252 |
+
|
| 253 |
+
# Register the skill using SkillRegistry
|
| 254 |
+
from openspace.skill_engine import SkillRegistry
|
| 255 |
+
registry = SkillRegistry([PROJECT_ROOT / "skills"])
|
| 256 |
+
meta = registry.register_skill_dir(final_dir)
|
| 257 |
+
|
| 258 |
+
if not meta:
|
| 259 |
+
abort(400, description="Failed to register skill (missing SKILL.md or invalid)")
|
| 260 |
+
|
| 261 |
+
store = _get_store()
|
| 262 |
+
|
| 263 |
+
# Sync to DB
|
| 264 |
+
import asyncio
|
| 265 |
+
loop = asyncio.new_event_loop()
|
| 266 |
+
asyncio.set_event_loop(loop)
|
| 267 |
+
try:
|
| 268 |
+
loop.run_until_complete(store.sync_from_registry([meta]))
|
| 269 |
+
finally:
|
| 270 |
+
loop.close()
|
| 271 |
+
|
| 272 |
+
return jsonify({
|
| 273 |
+
"status": "success",
|
| 274 |
+
"skill_id": meta.skill_id,
|
| 275 |
+
"name": meta.name,
|
| 276 |
+
"local_path": str(final_dir)
|
| 277 |
+
})
|
| 278 |
+
|
| 279 |
@app.route(f"{API_PREFIX}/workflows", methods=["GET"])
|
| 280 |
def list_workflows() -> Any:
|
| 281 |
items = [_build_workflow_summary(path) for path in _discover_workflow_dirs()]
|