matrixcloud / api /handlers_matrixshell.go
github-actions[bot]
Deploy from 4dfd54f2
857a91b
Raw
History Blame Contribute Delete
2.93 kB
package api
import (
"context"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/agent-matrix/matrix-runtime/internal/jobs"
"github.com/agent-matrix/matrix-runtime/internal/matrixshell"
)
// requireMatrixShell rejects requests when MatrixShell is disabled. It executes
// commands in a local sandbox, so it is off by default in production modes.
func (s *Server) requireMatrixShell(w http.ResponseWriter) bool {
if !s.cfg.MatrixShellEnabled {
writeError(w, http.StatusForbidden, "MatrixShell is disabled (set MATRIX_SHELL_ENABLED=true to enable)")
return false
}
return true
}
// handleMatrixShellStatus reports whether MatrixShell is installed in the local
// Python sandbox, with its version and paths.
func (s *Server) handleMatrixShellStatus(w http.ResponseWriter, r *http.Request) {
if !s.requireMatrixShell(w) {
return
}
ctx, cancel := context.WithTimeout(r.Context(), 8*time.Second)
defer cancel()
writeJSON(w, http.StatusOK, matrixshell.GetStatus(ctx, s.cfg.DataDir))
}
// handleMatrixShellInstall starts a job that creates the sandbox venv and
// installs MatrixShell from git, streaming real output over SSE.
func (s *Server) handleMatrixShellInstall(w http.ResponseWriter, _ *http.Request) {
if !s.requireMatrixShell(w) {
return
}
job, err := s.manager.Create(jobs.CreateRequest{Type: jobs.TypeMatrixShellInstall, TTLSeconds: 600})
if err != nil {
writeError(w, http.StatusUnprocessableEntity, err.Error())
return
}
writeJSON(w, http.StatusAccepted, map[string]any{
"job_id": job.ID,
"events_url": "/v1/jobs/" + job.ID + "/events",
})
}
// handleMatrixShellExec runs a command inside the local MatrixShell sandbox
// (real execution with the venv on PATH) and returns stdout/stderr/exit.
func (s *Server) handleMatrixShellExec(w http.ResponseWriter, r *http.Request) {
if !s.requireMatrixShell(w) {
return
}
var req struct {
Command string `json:"command"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
wsID, actor := "", "operator"
if u, ok := s.currentUser(r); ok {
wsID, actor = u.WorkspaceID, u.ID
}
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()
res, err := matrixshell.Exec(ctx, s.cfg.DataDir, req.Command)
if err != nil {
if errors.Is(err, matrixshell.ErrBlocked) {
s.audit(r, wsID, actor, "matrixshell.exec", truncate(req.Command, 200), "failure", map[string]any{"reason": "denylist"})
writeError(w, http.StatusForbidden, "refused by safety denylist")
return
}
writeError(w, http.StatusUnprocessableEntity, err.Error())
return
}
s.audit(r, wsID, actor, "matrixshell.exec", truncate(req.Command, 200), "success", map[string]any{"exit_code": res.ExitCode})
writeJSON(w, http.StatusOK, map[string]any{
"command": req.Command, "stdout": res.Stdout, "stderr": res.Stderr, "exit_code": res.ExitCode,
})
}