DEV-000283 Verify generic Work Graph candidate envelopes #261

Merged
m merged 1 commits from codex/dev-000248-self-development-runtime-status into main 2026-08-24 01:41:10 +00:00
7 changed files with 98 additions and 6 deletions
+13 -2
View File
@@ -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 {
+4 -2
View File
@@ -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)
}
}
+24
View File
@@ -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))
+12
View File
@@ -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)
}
}
+20
View File
@@ -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.
+2 -2
View File
@@ -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"]
}