feat(intercept): 增加审批记录详情与执行审计
This commit is contained in:
1 parent
8dae851b9b
commit
9ad323dc4c
22 files changed
+1687
-535
No files matched your search
+30
-3
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -35,15 +36,15 @@ func (s *Server) wireInterceptReviewer() {
|
||||
}
|
||||
prov, _, ok := s.providerForProfile(profileID)
|
||||
if !ok {
|
||||
return intercept.Decision{}, fmt.Errorf("裁判模型 profile %d 不可用", profileID)
|
||||
return intercept.Decision{ProfileID: profileID}, fmt.Errorf("裁判模型 profile %d 不可用", profileID)
|
||||
}
|
||||
user := fmt.Sprintf("Tool: %s\nArguments:\n%s", tool, command)
|
||||
text, err := streamCollectText(ctx, prov, prompt, user)
|
||||
if err != nil {
|
||||
return intercept.Decision{}, err
|
||||
return intercept.Decision{ProfileID: profileID}, err
|
||||
}
|
||||
v := intercept.ParseVerdict(text)
|
||||
return intercept.Decision{Action: v.Action, Message: v.Reason}, nil
|
||||
return intercept.Decision{Action: v.Action, Message: v.Reason, ProfileID: profileID}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,6 +279,10 @@ func (s *Server) interceptDecide(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if err := s.m.interceptor.Decide(id, req.Decision == "allowed"); err != nil {
|
||||
if errors.Is(err, intercept.ErrAlreadyDecided) {
|
||||
writeErr(w, 409, err.Error())
|
||||
return
|
||||
}
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -396,3 +401,25 @@ func validateInterceptRuleReq(req interceptRuleReq) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) interceptDetail(w http.ResponseWriter, r *http.Request) {
|
||||
pg := s.pg(w)
|
||||
if pg == nil {
|
||||
return
|
||||
}
|
||||
id, ok := pathInt(r, "id")
|
||||
if !ok || id <= 0 {
|
||||
writeErr(w, 400, "bad approval id")
|
||||
return
|
||||
}
|
||||
detail, err := pg.GetInterceptDetail(id)
|
||||
if err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
if detail == nil {
|
||||
writeErr(w, 404, "not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, detail)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
"github.com/Autumn-27/artex/intercept"
|
||||
)
|
||||
|
||||
func TestInterceptDetailHTTP(t *testing.T) {
|
||||
dsn, _, err := db.DSN()
|
||||
if err != nil {
|
||||
t.Skip("no test database configured")
|
||||
}
|
||||
d, err := db.Open(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = d.Close() })
|
||||
// Exercise the authenticated HTTP surface without starting unrelated task
|
||||
// schedulers or mutating the process-global tool assembly used by other tests.
|
||||
m := &Manager{pg: d, interceptor: intercept.New(d)}
|
||||
s := &Server{m: m, jwtKey: []byte("approval-http-test-signing-key")}
|
||||
h := s.Handler()
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id, err := m.pg.CreateInterceptPending(0, 0, "approval-http", "test", "Write", []byte(`{}`), "[模型] 确认", &db.InterceptAudit{InitialAction: "ask", UserMessage: "snapshot", ExecutionStatus: "not_started"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _, _ = m.pg.Exec(`DELETE FROM intercept_pending WHERE id=$1`, id) })
|
||||
do := func(method, path, body string, authenticated bool) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||
if authenticated {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
path := fmt.Sprintf("/api/intercept/history/%d", id)
|
||||
if r := do(http.MethodGet, path, "", false); r.Code != 401 {
|
||||
t.Fatalf("unprotected detail: %d", r.Code)
|
||||
}
|
||||
r := do(http.MethodGet, path, "", true)
|
||||
var detail db.InterceptDetail
|
||||
if r.Code != 200 || json.Unmarshal(r.Body.Bytes(), &detail) != nil || detail.Audit == nil || detail.Audit.UserMessage != "snapshot" {
|
||||
t.Fatalf("detail: %d %s", r.Code, r.Body.String())
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
code int
|
||||
}{{"/api/intercept/history/not-a-number", 400}, {"/api/intercept/history/0", 400}, {"/api/intercept/history/9223372036854775807", 404}} {
|
||||
if r := do(http.MethodGet, tc.path, "", true); r.Code != tc.code {
|
||||
t.Fatalf("%s: %d", tc.path, r.Code)
|
||||
}
|
||||
}
|
||||
decisionPath := fmt.Sprintf("/api/intercept/pending/%d/decide", id)
|
||||
if r := do(http.MethodPost, decisionPath, `{"decision":"denied"}`, true); r.Code != 200 {
|
||||
t.Fatalf("decide: %d %s", r.Code, r.Body.String())
|
||||
}
|
||||
if r := do(http.MethodPost, decisionPath, `{"decision":"allowed"}`, true); r.Code != 409 {
|
||||
t.Fatalf("duplicate decision: %d", r.Code)
|
||||
}
|
||||
}
|
||||
@@ -949,6 +949,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/intercept/pending/{id}", s.interceptGetOne)
|
||||
mux.HandleFunc("POST /api/intercept/pending/{id}/decide", s.interceptDecide)
|
||||
mux.HandleFunc("GET /api/intercept/history", s.interceptHistory)
|
||||
mux.HandleFunc("GET /api/intercept/history/{id}", s.interceptDetail)
|
||||
mux.HandleFunc("GET /api/intercept/task/{taskID}", s.interceptListTaskItems)
|
||||
mux.HandleFunc("GET /api/intercept/tool-config", s.interceptGetToolConfig)
|
||||
mux.HandleFunc("PUT /api/intercept/tool-config", s.interceptSetToolConfig)
|
||||
|
||||
Reference in new issue
Block a user