DEV-000283 Verify generic Work Graph candidate envelopes #261
@@ -135,9 +135,20 @@ func (b bridge) executeGenericCognitiveWork(ctx context.Context, request work.Ag
|
||||
if response.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(response.AdvisoryText) == "" {
|
||||
return work.AgentExecutionResult{}, fmt.Errorf("generic cognitive work did not complete: %s", response.FinishReason)
|
||||
}
|
||||
candidateID := "candidate:" + request.Intent.DispatchID
|
||||
artifactRef := work.EvidenceRef("agent-output:" + request.Intent.DispatchID)
|
||||
verification, err := postJSON[work.GenericCandidateVerificationResponse](ctx, b.client, b.verifierURL+"/v1/work-graphs/candidates/verify", work.GenericCandidateVerificationRequest{
|
||||
GraphID: execution.GraphID, GraphRevision: execution.GraphRevision, NodeID: execution.NodeID, AuthorRunID: request.Intent.RunID, CandidateID: candidateID, Summary: response.AdvisoryText, EvidenceRefs: []work.EvidenceRef{artifactRef},
|
||||
})
|
||||
if err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
if !verification.Verified || verification.VerificationEvidenceRef == "" || verification.VerifierIdentity == "" {
|
||||
return work.AgentExecutionResult{State: work.AgentExecutionWaiting, Summary: "generic candidate envelope was not independently verified; work remains waiting", ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
|
||||
}
|
||||
candidate := work.GraphResultCallback{GraphID: execution.GraphID, Revision: execution.GraphRevision, Candidate: work.ResultCandidate{
|
||||
ID: "candidate:" + request.Intent.DispatchID, GraphID: execution.GraphID, GraphRevision: execution.GraphRevision, NodeID: execution.NodeID,
|
||||
AuthorRunID: request.Intent.RunID, ArtifactRefs: []work.EvidenceRef{work.EvidenceRef("agent-output:" + request.Intent.DispatchID)}, EvidenceSummary: "cognitive gateway output recorded as an unverified candidate",
|
||||
ID: candidateID, GraphID: execution.GraphID, GraphRevision: execution.GraphRevision, NodeID: execution.NodeID,
|
||||
AuthorRunID: request.Intent.RunID, ArtifactRefs: []work.EvidenceRef{artifactRef, work.EvidenceRef(verification.VerificationEvidenceRef)}, EvidenceSummary: "candidate attribution and evidence envelope independently verified; semantic content remains unverified",
|
||||
Summary: response.AdvisoryText,
|
||||
}}
|
||||
if _, err := postJSON[json.RawMessage](ctx, b.client, b.callbackURL+"/v1/work-graphs/result-callback", candidate); err != nil {
|
||||
|
||||
@@ -70,17 +70,19 @@ func TestBridgeRunsGenericCognitiveWorkAsUnverifiedGraphCandidate(t *testing.T)
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case "/v1/work-graphs/candidates/verify":
|
||||
_ = json.NewEncoder(w).Encode(work.GenericCandidateVerificationResponse{Verified: true, VerificationEvidenceRef: "verification:generic-1", VerifierIdentity: "deterministic-generic-envelope-verifier-v1", Summary: "envelope verified"})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
b := bridge{cognitiveURL: server.URL, callbackURL: server.URL, client: server.Client()}
|
||||
b := bridge{cognitiveURL: server.URL, callbackURL: server.URL, verifierURL: server.URL, client: server.Client()}
|
||||
result, err := b.execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{
|
||||
DispatchID: "dispatch-generic", RunID: "run-generic",
|
||||
TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 2, NodeID: "node-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", GenericMessage: "find the bounded solution", GenericPurpose: "investigate", GenericDataPolicy: string(capability.DataPolicyLocalOnly)},
|
||||
}})
|
||||
if err != nil || result.State != work.AgentExecutionCompleted || callback.Candidate.Summary != "candidate answer" || callback.Candidate.AuthorRunID != "run-generic" {
|
||||
if err != nil || result.State != work.AgentExecutionCompleted || callback.Candidate.Summary != "candidate answer" || callback.Candidate.AuthorRunID != "run-generic" || len(callback.Candidate.ArtifactRefs) != 2 {
|
||||
t.Fatalf("result=%+v callback=%+v err=%v", result, callback, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ import (
|
||||
"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()
|
||||
@@ -21,6 +23,7 @@ func main() {
|
||||
_, _ = 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
|
||||
@@ -30,6 +33,27 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestVerifyExactTextProducesIndependentEvidence(t *testing.T) {
|
||||
@@ -39,3 +41,13 @@ func TestVerifyJSONFieldEquals(t *testing.T) {
|
||||
t.Fatalf("unexpected JSON verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyGenericCandidateEnvelopeDoesNotClaimTruth(t *testing.T) {
|
||||
payload, _ := json.Marshal(work.GenericCandidateVerificationRequest{GraphID: "graph-1", GraphRevision: 2, NodeID: "node-1", AuthorRunID: "run-1", CandidateID: "candidate-1", Summary: "model proposal", EvidenceRefs: []work.EvidenceRef{"agent-output:1"}})
|
||||
recorder := httptest.NewRecorder()
|
||||
genericCandidateVerifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/work-graphs/candidates/verify", bytes.NewReader(payload)))
|
||||
var response work.GenericCandidateVerificationResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || !response.Verified || response.VerifierIdentity == "" || response.VerificationEvidenceRef == "" || !strings.Contains(response.Summary, "semantic content remains unverified") {
|
||||
t.Fatalf("unexpected generic verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,26 @@ type ResultCandidate struct {
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
// GenericCandidateVerificationRequest asks the independent deterministic
|
||||
// verifier to validate attribution and evidence-envelope integrity. It does
|
||||
// not assert that the candidate's semantic content is true.
|
||||
type GenericCandidateVerificationRequest struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
GraphRevision int64 `json:"graph_revision"`
|
||||
NodeID GraphNodeID `json:"node_id"`
|
||||
AuthorRunID AgentRunID `json:"author_run_id"`
|
||||
CandidateID string `json:"candidate_id"`
|
||||
Summary string `json:"summary"`
|
||||
EvidenceRefs []EvidenceRef `json:"evidence_refs"`
|
||||
}
|
||||
|
||||
type GenericCandidateVerificationResponse struct {
|
||||
Verified bool `json:"verified"`
|
||||
VerificationEvidenceRef string `json:"verification_evidence_ref"`
|
||||
VerifierIdentity string `json:"verifier_identity"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
// GraphResultCallback carries one agent's attributable candidate to Work.
|
||||
// Recording a candidate never makes the node successful; the node enters
|
||||
// VERIFYING until an independent synthesis/verifier accepts it.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000282-final-auto-deploy-evidence.json",
|
||||
"task_id": "DEV-000282",
|
||||
"task_file": "dev/tasks/DEV-000283-generic-candidate-verifier.json",
|
||||
"task_id": "DEV-000283",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000283",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "94baf98",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add an independent deterministic envelope verifier before a generic local Work Graph candidate is recorded.",
|
||||
"requirement_ids": ["independent candidate envelope verification", "model output is not truth", "graph success still requires synthesis"],
|
||||
"allowed_change_scope": {"modules": ["work"], "paths": ["dev/ACTIVE_TASK.json", "dev/tasks/DEV-000283-generic-candidate-verifier.json", "contracts/work/graph.go", "cmd/task-verifier/**", "cmd/agent-execution-bridge/**"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/identity/**", "modules/authority/**", "LLM-local/**", "deploy/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["contracts/work@v1", "task-verifier internal endpoint", "work graph result callback"], "may_change": ["contracts/work graph candidate verification payload"]},
|
||||
"semantic_owner": "work",
|
||||
"canonical_state": ["verification evidence is an integrity envelope only; candidate remains unverified semantically"],
|
||||
"state_transitions": ["generic model output -> deterministic envelope verification -> candidate recording -> VERIFYING"],
|
||||
"side_effects_and_privileges": ["local verifier call and work graph callback; no external mutation"],
|
||||
"persistence_and_migrations": ["reuse existing candidate evidence refs; no migration"],
|
||||
"failure_retry_restart": ["verifier unavailable returns retryable failure", "invalid envelope returns WAITING", "graph success remains gated by independent synthesis"],
|
||||
"assumptions": ["deterministic verification proves attribution/integrity only, not factual truth", "task-verifier is an independently running local service"],
|
||||
"acceptance_scenarios": ["valid generic candidate obtains verifier evidence", "invalid candidate is not recorded as completed", "model output remains unverified", "existing task evaluation path remains unchanged"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
Reference in New Issue
Block a user