File size: 2,933 Bytes
857a91b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
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,
	})
}