Files
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

164 lines
5.4 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import functools
import json
from typing import Any
from fastapi import APIRouter, HTTPException, Query, Request, Response
from pydantic import BaseModel
from mirage.server.io_serde import io_result_to_dict
from mirage.server.jobs import JobEntry, JobStatus
router = APIRouter(prefix="/v1/workspaces/{workspace_id}/execute")
class ExecuteRequest(BaseModel):
command: str
session_id: str | None = None
provision: bool = False
agent_id: str | None = None
class BackgroundResponse(BaseModel):
job_id: str
workspace_id: str
submitted_at: float
def _require_entry(request: Request, workspace_id: str):
registry = request.app.state.registry
if workspace_id not in registry:
raise HTTPException(status_code=404, detail="workspace not found")
return registry.get(workspace_id)
def _build_execute_kwargs(req: ExecuteRequest, stdin: bytes | None) -> dict:
kwargs: dict[str, Any] = {
"command": req.command,
"provision": req.provision,
}
if req.session_id is not None:
kwargs["session_id"] = req.session_id
if req.agent_id is not None:
kwargs["agent_id"] = req.agent_id
if stdin is not None:
kwargs["stdin"] = stdin
return kwargs
def _make_coro_factory(runner, kwargs: dict):
return functools.partial(_invoke_execute, runner, kwargs)
async def _invoke_execute(runner, kwargs: dict):
return await runner.ws.execute(**kwargs)
def _schedule_on_runner(runner, coro):
return asyncio.run_coroutine_threadsafe(coro, runner.loop)
def _job_to_dict(entry: JobEntry,
result_dict: dict | None = None) -> dict[str, Any]:
return {
"job_id": entry.id,
"workspace_id": entry.workspace_id,
"command": entry.command,
"status": entry.status.value,
"submitted_at": entry.submitted_at,
"started_at": entry.started_at,
"finished_at": entry.finished_at,
"result": result_dict,
"error": entry.error,
}
@router.post("")
async def execute(
workspace_id: str,
request: Request,
background: bool = Query(False),
) -> Response:
entry = _require_entry(request, workspace_id)
job_table = request.app.state.jobs
content_type = request.headers.get("content-type", "")
req_obj, stdin_bytes = await _parse_execute_body(request, content_type)
schedule = functools.partial(_schedule_on_runner, entry.runner)
job = job_table.submit(
workspace_id=workspace_id,
command=req_obj.command,
schedule=schedule,
coro_factory=_make_coro_factory(
entry.runner,
_build_execute_kwargs(req_obj, stdin_bytes),
),
)
if background:
return Response(
content=BackgroundResponse(
job_id=job.id,
workspace_id=workspace_id,
submitted_at=job.submitted_at,
).model_dump_json(),
media_type="application/json",
status_code=202,
headers={"X-Mirage-Job-Id": job.id},
)
await job_table.wait(job.id)
if job.status == JobStatus.CANCELED:
raise HTTPException(status_code=499, detail="job canceled")
if job.status == JobStatus.FAILED:
raise HTTPException(status_code=500,
detail=job.error or "execute failed")
result_dict = await io_result_to_dict(job.result)
return Response(
content=json.dumps(result_dict),
media_type="application/json",
status_code=200,
headers={"X-Mirage-Job-Id": job.id},
)
async def _parse_execute_body(
request: Request,
content_type: str) -> tuple[ExecuteRequest, bytes | None]:
if content_type.startswith("multipart/"):
form = await request.form()
request_part = form.get("request")
if request_part is None:
raise HTTPException(status_code=400,
detail="multipart body missing 'request' part")
if hasattr(request_part, "read"):
req_text = (await request_part.read()).decode("utf-8")
else:
req_text = str(request_part)
try:
req_obj = ExecuteRequest.model_validate(json.loads(req_text))
except (json.JSONDecodeError, ValueError) as e:
raise HTTPException(status_code=400,
detail=f"bad request part: {e}")
stdin_part = form.get("stdin")
stdin_bytes: bytes | None = None
if stdin_part is not None:
if hasattr(stdin_part, "read"):
stdin_bytes = await stdin_part.read()
else:
stdin_bytes = str(stdin_part).encode("utf-8")
return req_obj, stdin_bytes
body = await request.json()
return ExecuteRequest.model_validate(body), None