Guard source previews against content version substitution

Why: Opening a processed source must not show changed current bytes as the quoted version.

Evidence: SELF TaskSourceContent regression fails before and passes after expected-hash enforcement; artifact regressions and live compilation pass.

Impact: Optional content hash narrows the existing authorized artifact read; controlled fixtures select a genuinely provided quotation.

Refs: fast-slow-u2-u3 source-version acceptance
This commit is contained in:
2026-10-13 02:51:07 +00:00
parent 0dad4f3f70
commit c50f9f07e8
3 changed files with 57 additions and 1 deletions
+7
View File
@@ -1382,6 +1382,13 @@ func (s *Server) artifactContent(w http.ResponseWriter, r *http.Request) {
writeStoreErr(w, err)
return
}
if expected := r.URL.Query().Get("expectedHash"); expected != "" {
sum := sha256.Sum256(content)
if hex.EncodeToString(sum[:]) != expected {
http.Error(w, "来源已更新,请从文件列表查看当前版本;任务中的引用片段仍可用于历史核验", http.StatusConflict)
return
}
}
ct := contentTypeOf(a)
w.Header().Set("Content-Type", ct)
setArtifactHeaders(w, ct)
+38
View File
@@ -0,0 +1,38 @@
package server
import (
"agent-system-backend/internal/store"
core "agent-system-core"
"crypto/sha256"
"encoding/hex"
"net/http"
"net/http/httptest"
"testing"
)
func TestTaskSourceContentDoesNotSubstituteLatestVersion(t *testing.T) {
srv, st := newTestServer(t)
original := []byte("上线六月")
a := core.Artifact{Owner: store.OwnerDefault, ProjectID: "p", Name: "input.txt"}
if e := st.CreateArtifact(&a, original); e != nil {
t.Fatal(e)
}
sum := sha256.Sum256(original)
hash := hex.EncodeToString(sum[:])
get := func() *httptest.ResponseRecorder {
r := httptest.NewRequest("GET", "/api/artifacts/"+a.ID+"/content?expectedHash="+hash, nil)
r.SetPathValue("id", a.ID)
w := httptest.NewRecorder()
srv.artifactContent(w, r)
return w
}
if w := get(); w.Code != http.StatusOK || w.Body.String() != string(original) {
t.Fatal("original version not readable", w.Code, w.Body.String())
}
if e := st.UpdateArtifactContent(store.OwnerDefault, a.ID, []byte("上线七月")); e != nil {
t.Fatal(e)
}
if w := get(); w.Code != http.StatusConflict {
t.Fatal("latest content masquerades as processed version", w.Code, w.Body.String())
}
}
+12 -1
View File
@@ -69,7 +69,8 @@ func evalMechanismStream(ctx context.Context, model string, msgs []agentllm.Mess
count++
}
}
matches := regexp.MustCompile(`sourceId=(src_[a-f0-9]+) blockIndex=(\d+)`).FindAllStringSubmatch(sourceText, -1)
matches := regexp.MustCompile(`sourceId=(src_[a-f0-9]+) blockIndex=(\d+)[^\n]*\n引用定位片段:([^\n]*)`).FindAllStringSubmatch(sourceText, -1)
matches = filterCheckpointSources(matches)
if count < 2 && len(matches) > 0 {
var block int
fmt.Sscan(matches[0][2], &block)
@@ -318,3 +319,13 @@ func evalProviderStream(route map[string]any, dir string) agentllm.StreamFunc {
return out
}
}
func filterCheckpointSources(matches [][]string) [][]string {
var out [][]string
for _, match := range matches {
if len(match) > 3 && strings.Contains(match[3], "上线六月") {
out = append(out, match)
}
}
return out
}