101 lines
4.7 KiB
Go
101 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"git.cin.su/m/Assistant/contracts/capability"
|
|
"git.cin.su/m/Assistant/contracts/work"
|
|
)
|
|
|
|
const verifierIdentity = "deterministic-task-verifier-v1"
|
|
const genericVerifierIdentity = "deterministic-generic-envelope-verifier-v1"
|
|
|
|
func main() {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"runtime":"task-verifier","status":"ok"}`))
|
|
})
|
|
mux.HandleFunc("POST /v1/task-evaluations/verify", verifyHandler)
|
|
mux.HandleFunc("POST /v1/work-graphs/candidates/verify", genericCandidateVerifyHandler)
|
|
addr := ":8091"
|
|
if value := strings.TrimSpace(os.Getenv("LISTEN_ADDR")); value != "" {
|
|
addr = value
|
|
}
|
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func genericCandidateVerifyHandler(w http.ResponseWriter, r *http.Request) {
|
|
var request work.GenericCandidateVerificationRequest
|
|
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&request); err != nil || request.GraphID == "" || request.GraphRevision < 1 || request.NodeID == "" || request.AuthorRunID == "" || request.CandidateID == "" || strings.TrimSpace(request.Summary) == "" || len(request.EvidenceRefs) == 0 {
|
|
http.Error(w, "invalid generic candidate envelope", http.StatusBadRequest)
|
|
return
|
|
}
|
|
payload := fmt.Sprintf("%s|%s|%d|%s|%s|%s|%s", genericVerifierIdentity, request.GraphID, request.GraphRevision, request.NodeID, request.AuthorRunID, request.CandidateID, strings.Join(evidenceRefStrings(request.EvidenceRefs), "|"))
|
|
digest := sha256.Sum256([]byte(payload))
|
|
writeJSON(w, work.GenericCandidateVerificationResponse{Verified: true, VerificationEvidenceRef: "verification:" + hex.EncodeToString(digest[:]), VerifierIdentity: genericVerifierIdentity, Summary: "candidate attribution and evidence envelope verified; semantic content remains unverified"})
|
|
}
|
|
|
|
func evidenceRefStrings(values []work.EvidenceRef) []string {
|
|
result := make([]string, len(values))
|
|
for i, value := range values {
|
|
result[i] = string(value)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func verifyHandler(w http.ResponseWriter, r *http.Request) {
|
|
var request capability.TaskCaseVerificationRequest
|
|
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&request); err != nil || request.CaseID == "" || request.ResponseText == "" || request.EvidenceArtifactRef == "" || request.LatencyMicros < 0 || (request.Verification.Mode != capability.TaskCaseVerificationExactText && request.Verification.Mode != capability.TaskCaseVerificationJSONFieldEquals) || (request.Verification.Mode == capability.TaskCaseVerificationJSONFieldEquals && request.Verification.FieldPath == "") || request.Verification.ExpectedText == "" || request.Verification.MinimumQualityMillis < 0 || request.Verification.MinimumQualityMillis > 1000 {
|
|
http.Error(w, "invalid task verification request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
matched := matches(request)
|
|
quality := int64(0)
|
|
result := capability.EvaluationUnsupported
|
|
if matched {
|
|
quality = request.Verification.MinimumQualityMillis
|
|
if quality == 0 {
|
|
quality = 1000
|
|
}
|
|
result = capability.EvaluationSupported
|
|
}
|
|
evidence := verificationEvidence(request, matched)
|
|
writeJSON(w, capability.TaskCaseVerificationResponse{CaseID: request.CaseID, Result: result, QualityScoreMillis: quality, LatencyMicros: request.LatencyMicros, EvidenceArtifactRef: request.EvidenceArtifactRef, VerificationEvidenceRef: evidence, VerifierIdentity: verifierIdentity})
|
|
}
|
|
|
|
func verificationEvidence(request capability.TaskCaseVerificationRequest, matched bool) string {
|
|
payload := fmt.Sprintf("%s|%s|%s|%s|%s|%t", verifierIdentity, request.CaseID, request.EvidenceArtifactRef, request.Verification.Mode, request.Verification.FieldPath, matched)
|
|
digest := sha256.Sum256([]byte(payload))
|
|
return "verification:" + hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
func matches(request capability.TaskCaseVerificationRequest) bool {
|
|
if request.Verification.Mode == capability.TaskCaseVerificationExactText {
|
|
return strings.TrimSpace(request.ResponseText) == strings.TrimSpace(request.Verification.ExpectedText)
|
|
}
|
|
var document map[string]json.RawMessage
|
|
if json.Unmarshal([]byte(request.ResponseText), &document) != nil {
|
|
return false
|
|
}
|
|
value, ok := document[request.Verification.FieldPath]
|
|
return ok && strings.TrimSpace(string(value)) == strings.TrimSpace(request.Verification.ExpectedText)
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, value any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(value)
|
|
}
|