diff --git a/internal/server/run_process_events_test.go b/internal/server/run_process_events_test.go index 76d0533..62de1a5 100644 --- a/internal/server/run_process_events_test.go +++ b/internal/server/run_process_events_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "agent-system-backend/internal/store" core "agent-system-core" ) @@ -17,10 +18,6 @@ func TestRunProcessEventsAreOrderedAndBoundToConversation(t *testing.T) { _, otherConvID := setupConversation(t, h) runID := postMessage(t, h, convID, "整理材料") waitRunTerminal(t, h, runID, 5*time.Second) - last, err := st.LastEventSeq(convID) - if err != nil { - t.Fatal(err) - } events := []core.StreamEvent{ {RunID: runID, Type: core.EvThinkingDelta, Payload: json.RawMessage(`{"index":0,"text":"先核对日期"}`)}, {RunID: runID, Type: core.EvToolOutput, Payload: json.RawMessage(`{"callId":"call-1","stream":"stdout","text":"完整工具输出"}`)}, @@ -28,7 +25,9 @@ func TestRunProcessEventsAreOrderedAndBoundToConversation(t *testing.T) { if _, err := st.AppendEvents(convID, events); err != nil { t.Fatal(err) } - path := "/api/conversations/" + convID + "/runs/" + runID + "/events?after=" + strconv.FormatInt(last, 10) + "&limit=1" + // The runner may persist its terminal event after its status becomes terminal. + // Anchor paging to the events this test appended, not an earlier snapshot. + path := "/api/conversations/" + convID + "/runs/" + runID + "/events?after=" + strconv.FormatInt(events[0].Seq-1, 10) + "&limit=1" code, first := doJSON(t, h, http.MethodGet, path, nil) if code != http.StatusOK { t.Fatalf("first page: %d %v", code, first) @@ -79,3 +78,28 @@ func TestRunProcessEventsAreOrderedAndBoundToConversation(t *testing.T) { t.Fatalf("mismatched conversation exposed run: %d", code) } } + +func TestRunProcessEventsRejectsDisabledImplicitLocal(t *testing.T) { + srv, st := newTestServer(t) + if err := st.CreateAccount("admin", "", "", true); err != nil { + t.Fatal(err) + } + h := srv.Handler() + _, convID := setupConversation(t, h) + runID := postMessage(t, h, convID, "整理材料") + waitRunTerminal(t, h, runID, 5*time.Second) + if err := st.EnsureUser(store.OwnerDefault, ""); err != nil { + t.Fatal(err) + } + account, err := st.GetAccountByOwner(store.OwnerDefault) + if err != nil { + t.Fatal(err) + } + if _, err := st.SetAccountEnabled("admin", account.ID, account.StateVersion, false); err != nil { + t.Fatal(err) + } + code, _ := doJSON(t, h, http.MethodGet, "/api/conversations/"+convID+"/runs/"+runID+"/events", nil) + if code != http.StatusForbidden { + t.Fatalf("disabled local account read process events: %d", code) + } +} diff --git a/internal/server/server.go b/internal/server/server.go index d7a9cc3..c183f7c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -917,6 +917,10 @@ func (s *Server) conversationDetail(w http.ResponseWriter, r *http.Request) { func (s *Server) runProcessEvents(w http.ResponseWriter, r *http.Request) { id, runID := r.PathValue("id"), r.PathValue("runId") owner := s.owner(r) + if err := s.checkActorBeforeExternalEffect(owner); err != nil { + writeStoreErr(w, err) + return + } if _, err := s.st.GetConversation(owner, id); err != nil { writeStoreErr(w, err) return @@ -951,6 +955,12 @@ func (s *Server) runProcessEvents(w http.ResponseWriter, r *http.Request) { writeStoreErr(w, err) return } + // The authenticated account can be disabled while stored events are read. + // Recheck immediately before returning private process output. + if err := s.checkActorBeforeExternalEffect(owner); err != nil { + writeStoreErr(w, err) + return + } writeJSON(w, http.StatusOK, map[string]any{"events": events}) }