Compare commits
206
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
931a0b0f0c | ||
|
|
e70be0103a | ||
|
|
2353f1f3bb | ||
|
|
355ba7db56 | ||
|
|
4839f139e8 | ||
|
|
ea90bf0fab | ||
|
|
7978b495d1 | ||
|
|
d73d4fc118 | ||
|
|
c81e2269e1 | ||
|
|
47327cc6d8 | ||
|
|
d0ede46352 | ||
|
|
e4ba1cb5fa | ||
|
|
90104d2553 | ||
|
|
ca0d002073 | ||
|
|
20db36f52e | ||
|
|
383d4c6e4f | ||
|
|
f3ddb45fe5 | ||
|
|
6061eb5c13 | ||
|
|
0e46a64702 | ||
|
|
0609561dfe | ||
|
|
1b88a35b82 | ||
|
|
be01b6b883 | ||
|
|
deaddbca12 | ||
|
|
b20611590a | ||
|
|
d4972afd11 | ||
|
|
60e8292c1d | ||
|
|
da251e1aa8 | ||
|
|
eac39a3fd4 | ||
|
|
1f8af98f31 | ||
|
|
ee16103e03 | ||
|
|
09d53adfd1 | ||
|
|
89694047b1 | ||
|
|
23e0525c08 | ||
|
|
45874f2186 | ||
|
|
ae5848d799 | ||
|
|
ec79f19a79 | ||
|
|
f63c4bb40c | ||
|
|
22f839df99 | ||
|
|
c41634a065 | ||
|
|
2609f31e59 | ||
|
|
7aa41b3be2 | ||
|
|
1f6ca363ee | ||
|
|
02471990ef | ||
|
|
40b56798e2 | ||
|
|
c53d7ded06 | ||
|
|
e080c34a77 | ||
|
|
306f53d484 | ||
|
|
c4411afaaf | ||
|
|
c508941a8a | ||
|
|
ee68e2207e | ||
|
|
3d9a5bccba | ||
|
|
d736500d98 | ||
|
|
33b51a4445 | ||
|
|
0aab10863c | ||
|
|
b17caa3ea9 | ||
|
|
66ed523248 | ||
|
|
c5b5055ff1 | ||
|
|
7476f43ffa | ||
|
|
be29ed5776 | ||
|
|
ab840878fe | ||
|
|
aebc932b97 | ||
|
|
05a246ea3f | ||
|
|
c847988630 | ||
|
|
62f0c529b3 | ||
|
|
54958d0902 | ||
|
|
8d3e7f7cac | ||
|
|
c9eb398857 | ||
|
|
284ea88553 | ||
|
|
f9b9765100 | ||
|
|
863a379048 | ||
|
|
a86d1994c3 | ||
|
|
699eac00eb | ||
|
|
dff6c91c8e | ||
|
|
7afe3ff48f | ||
|
|
6dec47c791 | ||
|
|
def0e4aff0 | ||
|
|
9a6ab6a9ca | ||
|
|
8f20326c07 | ||
|
|
a82592a130 | ||
|
|
feea909d69 | ||
|
|
b78a026ee1 | ||
|
|
cf35c0ae25 | ||
|
|
9f96393fb9 | ||
|
|
beee8deb34 | ||
|
|
faab913227 | ||
|
|
346a8c8467 | ||
|
|
288d18a4fb | ||
|
|
97ed2e90ee | ||
|
|
b4d166480e | ||
|
|
46b35161e9 | ||
|
|
bee238b61e | ||
|
|
ca9c4bede5 | ||
|
|
48d73c3a4b | ||
|
|
76c558ee1a | ||
|
|
e530058659 | ||
|
|
79feab8e41 | ||
|
|
03943f152d | ||
|
|
c8aa731eeb | ||
|
|
45435814ee | ||
|
|
c7170ee751 | ||
|
|
e9f338484a | ||
|
|
bb131147f8 | ||
|
|
22b84ead9c | ||
|
|
42c7df964b | ||
|
|
c542642bf0 | ||
|
|
fde14cf0b8 | ||
|
|
b52590c458 | ||
|
|
6854458e21 | ||
|
|
5658bed520 | ||
|
|
7252296a74 | ||
|
|
348856a9c2 | ||
|
|
7779dc37d1 | ||
|
|
c1db571a39 | ||
|
|
5f65f3d570 | ||
|
|
bb18cb4f1c | ||
|
|
3f7b1f6e9c | ||
|
|
413a432e8c | ||
|
|
ca5f96115b | ||
|
|
9fe2f37012 | ||
|
|
b7247c9abd | ||
|
|
be8561c7d8 | ||
|
|
b78083f0d9 | ||
|
|
5d9124d0df | ||
|
|
b7a4c0f83e | ||
|
|
50d4bdf174 | ||
|
|
5b92cd6a58 | ||
|
|
18d0911810 | ||
|
|
dcf8cbc855 | ||
|
|
3d30681279 | ||
|
|
41bc4b567b | ||
|
|
38f8f816d5 | ||
|
|
4d9d1b8023 | ||
|
|
c60a575fda | ||
|
|
a09a7e811f | ||
|
|
ecbb34627a | ||
|
|
cdbccb6238 | ||
|
|
6e28e02f98 | ||
|
|
5812974a80 | ||
|
|
0b6a249178 | ||
|
|
7ab4b02afa | ||
|
|
d3a50c7afd | ||
|
|
16a0e8c492 | ||
|
|
0d6326b45b | ||
|
|
6b3a6b2dfb | ||
|
|
30443027a6 | ||
|
|
416d7c3a82 | ||
|
|
4709ef4607 | ||
|
|
6f0e099632 | ||
|
|
775fa03ed4 | ||
|
|
2ed2302526 | ||
|
|
46538ba1e9 | ||
|
|
5960844fcb | ||
|
|
b60fc38333 | ||
|
|
dc1ee7ff3c | ||
|
|
a51bf57d7a | ||
|
|
d60fb8697a | ||
|
|
bf4b81e320 | ||
|
|
389dd0c23d | ||
|
|
064a650763 | ||
|
|
6ecb431ccd | ||
|
|
b29f8775e1 | ||
|
|
85ee05e291 | ||
|
|
35aa64546b | ||
|
|
6e0d1a2279 | ||
|
|
64642f4bae | ||
|
|
2f9e9bdc9f | ||
|
|
2c7a85779b | ||
|
|
b4c3027e6d | ||
|
|
503fe84074 | ||
|
|
3b551b2918 | ||
|
|
51427a246e | ||
|
|
9b5ce2a850 | ||
|
|
8f3920519b | ||
|
|
2277f8a699 | ||
|
|
205d8f6206 | ||
|
|
ae06d33ccc | ||
|
|
c7b57daaf8 | ||
|
|
8aa0123047 | ||
|
|
4a3d348e39 | ||
|
|
4086b75224 | ||
|
|
4f89c46545 | ||
|
|
5d5afc8888 | ||
|
|
79ce5a0b82 | ||
|
|
65a60d5f06 | ||
|
|
4739a22f7b | ||
|
|
c25a6346d0 | ||
|
|
5f71fa8dc4 | ||
|
|
bda71caece | ||
|
|
0656f88fd0 | ||
|
|
3cf44ada8a | ||
|
|
ca00377bec | ||
|
|
ada2da0c6c | ||
|
|
82df14bde9 | ||
|
|
e1186757bf | ||
|
|
1f002bd1a3 | ||
|
|
3a52c18bef | ||
|
|
fe0351edae | ||
|
|
077f16e50c | ||
|
|
3a9d3f0503 | ||
|
|
03c3484ee1 | ||
|
|
e47d363145 | ||
|
|
5703ab3ed5 | ||
|
|
d03e9d3307 | ||
|
|
283f5c044c | ||
|
|
ea5327799a | ||
|
|
dbbc54171d |
@@ -32,14 +32,25 @@ func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*
|
||||
// NewOpenAICompatibleLocalAdapterWithAPIKey creates an adapter for a gateway
|
||||
// whose bearer credential was resolved by the trusted runtime boundary.
|
||||
func NewOpenAICompatibleLocalAdapterWithAPIKey(raw, apiKey string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
|
||||
return NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(raw, apiKey, transport, 10*time.Second)
|
||||
}
|
||||
|
||||
// NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout creates a local gateway
|
||||
// adapter with an explicit invocation deadline. The request context remains
|
||||
// authoritative for per-call cancellation; this client deadline prevents a
|
||||
// provider from holding a worker indefinitely.
|
||||
func NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(raw, apiKey string, transport http.RoundTripper, timeout time.Duration) (*OpenAICompatibleLocalAdapter, error) {
|
||||
base, err := url.Parse(raw)
|
||||
if err != nil || base.Scheme != "http" || base.Host == "" || strings.TrimSuffix(base.Path, "/") != "/v1" || base.RawQuery != "" || base.Fragment != "" || base.User != nil {
|
||||
return nil, errors.New("invalid local compatible endpoint")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return nil, errors.New("local compatible timeout must be positive")
|
||||
}
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport
|
||||
}
|
||||
return &OpenAICompatibleLocalAdapter{base: base, client: &http.Client{Transport: transport, Timeout: 10 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, apiKey: strings.TrimSpace(apiKey)}, nil
|
||||
return &OpenAICompatibleLocalAdapter{base: base, client: &http.Client{Transport: transport, Timeout: timeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, apiKey: strings.TrimSpace(apiKey)}, nil
|
||||
}
|
||||
|
||||
type compatibleModelsResponse struct {
|
||||
@@ -85,11 +96,15 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover
|
||||
}
|
||||
|
||||
type compatibleChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []compatibleMessage `json:"messages"`
|
||||
Temperature int `json:"temperature"`
|
||||
Stream bool `json:"stream"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
Model string `json:"model"`
|
||||
Messages []compatibleMessage `json:"messages"`
|
||||
Temperature int `json:"temperature"`
|
||||
Stream bool `json:"stream"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
ChatTemplateKwargs compatibleTemplateKwargs `json:"chat_template_kwargs"`
|
||||
}
|
||||
type compatibleTemplateKwargs struct {
|
||||
EnableThinking bool `json:"enable_thinking"`
|
||||
}
|
||||
type compatibleMessage struct {
|
||||
Role string `json:"role"`
|
||||
@@ -110,7 +125,7 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
|
||||
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" || len(request.Context) != 0 {
|
||||
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
|
||||
}
|
||||
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4}
|
||||
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Provide non-canonical advisory text only. Never claim that an action occurred."}, {Role: "user", Content: request.Message}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
|
||||
var result compatibleChatResponse
|
||||
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,19 +2,48 @@ package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
type captureRoundTripper struct {
|
||||
authorization string
|
||||
body []byte
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterUsesConfiguredTimeout(t *testing.T) {
|
||||
adapter, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", &captureRoundTripper{}, 47*time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if adapter.client.Timeout != 47*time.Second {
|
||||
t.Fatalf("unexpected client timeout: %s", adapter.client.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterRejectsNonPositiveTimeout(t *testing.T) {
|
||||
if _, err := NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout("http://gateway/v1", "", nil, 0); err == nil {
|
||||
t.Fatal("expected non-positive timeout to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *captureRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
t.authorization = request.Header.Get("Authorization")
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"data":[]}`)), Header: make(http.Header), Request: request}, nil
|
||||
if request.Body != nil {
|
||||
t.body, _ = io.ReadAll(request.Body)
|
||||
}
|
||||
body := `{"data":[]}`
|
||||
if strings.HasSuffix(request.URL.Path, "/chat/completions") {
|
||||
body = `{"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"ok"}}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header), Request: request}, nil
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterUsesResolvedAPIKey(t *testing.T) {
|
||||
@@ -40,3 +69,26 @@ func TestOpenAICompatibleLocalAdapterDoesNotSendAuthorizationWithoutKey(t *testi
|
||||
t.Fatalf("unexpected authorization header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(t *testing.T) {
|
||||
transport := &captureRoundTripper{}
|
||||
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", transport)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = adapter.Advise(context.Background(), cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor", PrincipalID: "principal", ScopeID: "scope", Message: "help", Locale: "ru-RU", MaxOutputBytes: 256, ProviderBindingID: "binding-1", ProviderBindingRevision: 1, ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1", ProviderModelID: capability.ProviderModelID("medium-model")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var payload struct {
|
||||
ChatTemplateKwargs struct {
|
||||
EnableThinking bool `json:"enable_thinking"`
|
||||
} `json:"chat_template_kwargs"`
|
||||
}
|
||||
if err := json.Unmarshal(transport.body, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if payload.ChatTemplateKwargs.EnableThinking {
|
||||
t.Fatal("expected bounded advisory to disable template thinking")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +430,32 @@ func (r *CapabilityRepository) SaveCognitiveBinding(ctx context.Context, value c
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||
return r.loadCognitiveCandidates(ctx, capabilityID, owner, "")
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadCognitiveCandidatesForProfile(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, profileID capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) LoadCognitiveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
|
||||
var capabilityID capability.CapabilityID
|
||||
var owner identity.PrincipalID
|
||||
if err := r.pool.QueryRow(ctx, `SELECT capability_id,owner_principal_id FROM capability.cognitive_bindings WHERE binding_id=$1 AND revision=$2`, bindingID, revision).Scan(&capabilityID, &owner); err != nil {
|
||||
return capability.ResolvedCognitiveBinding{}, err
|
||||
}
|
||||
candidates, err := r.loadCognitiveCandidates(ctx, capabilityID, owner, "")
|
||||
if err != nil {
|
||||
return capability.ResolvedCognitiveBinding{}, err
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if candidate.Binding.ID == bindingID && candidate.Binding.Revision == revision {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return capability.ResolvedCognitiveBinding{}, pgx.ErrNoRows
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) loadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, profileID capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||
rows, err := r.pool.Query(ctx, `SELECT
|
||||
b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref,
|
||||
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),p.billing_mode,p.auth_mode,p.input_cost_micros,p.output_cost_micros,p.request_cost_micros,
|
||||
@@ -454,7 +480,7 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = r.loadCognitiveEvidence(ctx, &value); err != nil {
|
||||
if err = r.loadCognitiveEvidence(ctx, &value, profileID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, value)
|
||||
@@ -462,7 +488,7 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value *capability.ResolvedCognitiveBinding) error {
|
||||
func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value *capability.ResolvedCognitiveBinding, profileID capability.TaskEvaluationProfileID) error {
|
||||
var evaluation capability.EvaluatedCapability
|
||||
err := r.pool.QueryRow(ctx, `SELECT evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_evaluated_capabilities WHERE binding_id=$1 AND binding_revision=$2 ORDER BY observed_at_unix_millis DESC LIMIT 1`, value.Binding.ID, value.Binding.Revision).Scan(&evaluation.ID, &evaluation.BindingID, &evaluation.BindingRevision, &evaluation.CapabilityID, &evaluation.Result, &evaluation.QualityScoreMillis, &evaluation.CriticalFailureMillis, &evaluation.BenchmarkVersion, &evaluation.ArtifactID, &evaluation.ObservedAtUnixMillis)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
@@ -479,6 +505,24 @@ func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value
|
||||
if err == nil {
|
||||
value.Performance = &performance
|
||||
}
|
||||
var qualification capability.ModelQualificationObservation
|
||||
err = r.pool.QueryRow(ctx, `SELECT observation_id,provider_instance_id,provider_model_id,status,finish_reason,latency_millis,output_observed,quality_assessed,category,observed_at_unix_millis FROM capability.cognitive_model_qualification_observations WHERE provider_instance_id=$1 AND provider_model_id=$2 ORDER BY observed_at_unix_millis DESC LIMIT 1`, value.Provider.ID, value.Model.ProviderModelID).Scan(&qualification.ID, &qualification.ProviderInstanceID, &qualification.ProviderModelID, &qualification.Status, &qualification.FinishReason, &qualification.LatencyMillis, &qualification.OutputObserved, &qualification.QualityAssessed, &qualification.Category, &qualification.ObservedAtUnixMillis)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
value.Qualification = &qualification
|
||||
}
|
||||
if profileID != "" {
|
||||
var taskEvaluation capability.TaskEvaluation
|
||||
err = r.pool.QueryRow(ctx, `SELECT evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_task_evaluations WHERE task_profile_id=$1 AND binding_id=$2 AND binding_revision=$3 ORDER BY observed_at_unix_millis DESC LIMIT 1`, profileID, value.Binding.ID, value.Binding.Revision).Scan(&taskEvaluation.ID, &taskEvaluation.ProfileID, &taskEvaluation.BindingID, &taskEvaluation.BindingRevision, &taskEvaluation.CapabilityID, &taskEvaluation.Result, &taskEvaluation.QualityScoreMillis, &taskEvaluation.ReliabilityScoreMillis, &taskEvaluation.LatencyScoreMillis, &taskEvaluation.OverallScoreMillis, &taskEvaluation.CriticalFailureMillis, &taskEvaluation.BenchmarkVersion, &taskEvaluation.ArtifactID, &taskEvaluation.ObservedAtUnixMillis)
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
value.TaskEvaluation = &taskEvaluation
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -592,3 +636,13 @@ func (r *CapabilityRepository) SavePerformanceEvidence(ctx context.Context, valu
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveTaskEvaluation(ctx context.Context, value capability.TaskEvaluation) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_task_evaluations (evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.ProfileID, value.BindingID, value.BindingRevision, value.CapabilityID, value.Result, value.QualityScoreMillis, value.ReliabilityScoreMillis, value.LatencyScoreMillis, value.OverallScoreMillis, value.CriticalFailureMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *CapabilityRepository) SaveModelQualificationObservation(ctx context.Context, value capability.ModelQualificationObservation) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_model_qualification_observations (observation_id,provider_instance_id,provider_model_id,status,finish_reason,latency_millis,output_observed,quality_assessed,category,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT DO NOTHING`, value.ID, value.ProviderInstanceID, value.ProviderModelID, value.Status, value.FinishReason, value.LatencyMillis, value.OutputObserved, value.QualityAssessed, value.Category, value.ObservedAtUnixMillis)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,14 @@ import (
|
||||
|
||||
type EpistemicsRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
type TruthRuntimeStatus struct {
|
||||
BundleCount int64 `json:"bundle_count"`
|
||||
ClaimsByVerification map[string]int64 `json:"claims_by_verification"`
|
||||
BeliefsByState map[string]int64 `json:"beliefs_by_state"`
|
||||
SourcesByFreshness map[string]int64 `json:"sources_by_freshness"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
|
||||
func OpenEpistemics(ctx context.Context, config ContinuityConfig) (*EpistemicsRepository, error) {
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
@@ -25,6 +33,49 @@ func (r *EpistemicsRepository) Ping(ctx context.Context) error {
|
||||
return r.pool.Ping(ctx)
|
||||
}
|
||||
|
||||
// TruthRuntimeStatus is a read-only projection of durable truth bundles.
|
||||
// It does not promote claims, revise beliefs, or mutate epistemic state.
|
||||
func (r *EpistemicsRepository) TruthRuntimeStatus(ctx context.Context) (TruthRuntimeStatus, error) {
|
||||
status := TruthRuntimeStatus{
|
||||
ClaimsByVerification: map[string]int64{},
|
||||
BeliefsByState: map[string]int64{},
|
||||
SourcesByFreshness: map[string]int64{},
|
||||
ObservedAtUnixMillis: time.Now().UnixMilli(),
|
||||
}
|
||||
if err := r.pool.QueryRow(ctx, `SELECT count(*) FROM epistemics.truth_bundles`).Scan(&status.BundleCount); err != nil {
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
queries := []struct {
|
||||
query string
|
||||
destination map[string]int64
|
||||
}{
|
||||
{`SELECT claim_verification, count(*) FROM epistemics.truth_bundles GROUP BY claim_verification`, status.ClaimsByVerification},
|
||||
{`SELECT belief_state, count(*) FROM epistemics.truth_bundles GROUP BY belief_state`, status.BeliefsByState},
|
||||
{`SELECT snapshot_freshness, count(*) FROM epistemics.truth_bundles GROUP BY snapshot_freshness`, status.SourcesByFreshness},
|
||||
}
|
||||
for _, item := range queries {
|
||||
rows, err := r.pool.Query(ctx, item.query)
|
||||
if err != nil {
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var state string
|
||||
var count int64
|
||||
if err := rows.Scan(&state, &count); err != nil {
|
||||
rows.Close()
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
item.destination[state] = count
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return TruthRuntimeStatus{}, err
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (r *EpistemicsRepository) SaveSystemExpectation(ctx context.Context, value epistemics.SystemExpectation) error {
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.system_expectations (
|
||||
expectation_id, original_intent_id, source_utterance_id, expected_outcome, expected_effect,
|
||||
|
||||
@@ -2,10 +2,14 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -14,9 +18,11 @@ import (
|
||||
type WorkRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
var (
|
||||
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
|
||||
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
|
||||
ErrAgentCallbackRejected = errors.New("agent callback rejected")
|
||||
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
|
||||
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
|
||||
ErrAgentCallbackRejected = errors.New("agent callback rejected")
|
||||
ErrDevelopmentGateConflict = errors.New("development gate revision conflict")
|
||||
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
|
||||
)
|
||||
|
||||
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
|
||||
@@ -26,6 +32,54 @@ func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, er
|
||||
}
|
||||
return &WorkRepository{pool}, nil
|
||||
}
|
||||
|
||||
// WorkGraphRuntimeStatus is a read-only operational projection owned by Work.
|
||||
// It is not canonical graph state and cannot advance any lifecycle.
|
||||
type WorkGraphRuntimeStatus struct {
|
||||
GraphsByState map[string]int64 `json:"graphs_by_state"`
|
||||
NodesByState map[string]int64 `json:"nodes_by_state"`
|
||||
QueueByState map[string]int64 `json:"queue_by_state"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
|
||||
func (r *WorkRepository) RuntimeStatus(ctx context.Context) (WorkGraphRuntimeStatus, error) {
|
||||
status := WorkGraphRuntimeStatus{
|
||||
GraphsByState: map[string]int64{},
|
||||
NodesByState: map[string]int64{},
|
||||
QueueByState: map[string]int64{},
|
||||
ObservedAtUnixMillis: time.Now().UnixMilli(),
|
||||
}
|
||||
queries := []struct {
|
||||
query string
|
||||
destination map[string]int64
|
||||
}{
|
||||
{`SELECT state, count(*) FROM work.graphs GROUP BY state`, status.GraphsByState},
|
||||
{`SELECT state, count(*) FROM work.graph_nodes GROUP BY state`, status.NodesByState},
|
||||
{`SELECT state, count(*) FROM work.agent_dispatch_queue GROUP BY state`, status.QueueByState},
|
||||
}
|
||||
for _, item := range queries {
|
||||
rows, err := r.pool.Query(ctx, item.query)
|
||||
if err != nil {
|
||||
return WorkGraphRuntimeStatus{}, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var state string
|
||||
var count int64
|
||||
if err := rows.Scan(&state, &count); err != nil {
|
||||
rows.Close()
|
||||
return WorkGraphRuntimeStatus{}, err
|
||||
}
|
||||
item.destination[state] = count
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return WorkGraphRuntimeStatus{}, err
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) Close() { r.pool.Close() }
|
||||
func (r *WorkRepository) Ping(ctx context.Context) error { return r.pool.Ping(ctx) }
|
||||
func (r *WorkRepository) Create(ctx context.Context, target work.TaskTarget) error {
|
||||
@@ -49,33 +103,135 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LoadDevelopmentGate(ctx context.Context, id string) (work.DevelopmentGate, error) {
|
||||
var gate work.DevelopmentGate
|
||||
var evidence, producer, verifier []string
|
||||
err := r.pool.QueryRow(ctx, `SELECT gate_id,revision,updated_at_unix_millis,stage,evidence_refs,proposal_ref,task_packet_ref,worktree_ref,producer_run_refs,verifier_run_refs,verification_evidence,canary_evidence,promotion_ref,rollback_ref FROM work.development_gates WHERE gate_id=$1`, id).Scan(
|
||||
&gate.ID, &gate.Revision, &gate.UpdatedAtUnixMillis, &gate.Stage, &evidence, &gate.ProposalRef, &gate.TaskPacketRef, &gate.WorktreeRef, &producer, &verifier, &gate.VerificationEvidence, &gate.CanaryEvidence, &gate.PromotionRef, &gate.RollbackRef)
|
||||
if err != nil {
|
||||
return work.DevelopmentGate{}, fmt.Errorf("load development gate: %w", err)
|
||||
}
|
||||
gate.EvidenceRefs = append([]string(nil), evidence...)
|
||||
for _, run := range producer {
|
||||
gate.ProducerRunRefs = append(gate.ProducerRunRefs, work.AgentRunID(run))
|
||||
}
|
||||
for _, run := range verifier {
|
||||
gate.VerifierRunRefs = append(gate.VerifierRunRefs, work.AgentRunID(run))
|
||||
}
|
||||
return gate, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveDevelopmentGate(ctx context.Context, gate work.DevelopmentGate, expectedRevision int64) error {
|
||||
if gate.ID == "" || expectedRevision < 0 || gate.Revision < 1 || gate.UpdatedAtUnixMillis < 1 {
|
||||
return ErrDevelopmentGateConflict
|
||||
}
|
||||
producer := make([]string, 0, len(gate.ProducerRunRefs))
|
||||
for _, run := range gate.ProducerRunRefs {
|
||||
producer = append(producer, string(run))
|
||||
}
|
||||
verifier := make([]string, 0, len(gate.VerifierRunRefs))
|
||||
for _, run := range gate.VerifierRunRefs {
|
||||
verifier = append(verifier, string(run))
|
||||
}
|
||||
if expectedRevision == 0 {
|
||||
command, err := r.pool.Exec(ctx, `INSERT INTO work.development_gates(gate_id,revision,updated_at_unix_millis,stage,evidence_refs,proposal_ref,task_packet_ref,worktree_ref,producer_run_refs,verifier_run_refs,verification_evidence,canary_evidence,promotion_ref,rollback_ref) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(gate_id) DO NOTHING`, gate.ID, gate.Revision, gate.UpdatedAtUnixMillis, gate.Stage, gate.EvidenceRefs, gate.ProposalRef, gate.TaskPacketRef, gate.WorktreeRef, producer, verifier, gate.VerificationEvidence, gate.CanaryEvidence, gate.PromotionRef, gate.RollbackRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if command.RowsAffected() != 1 {
|
||||
return ErrDevelopmentGateConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
command, err := r.pool.Exec(ctx, `UPDATE work.development_gates SET revision=$1,updated_at_unix_millis=$2,stage=$3,evidence_refs=$4,proposal_ref=$5,task_packet_ref=$6,worktree_ref=$7,producer_run_refs=$8,verifier_run_refs=$9,verification_evidence=$10,canary_evidence=$11,promotion_ref=$12,rollback_ref=$13 WHERE gate_id=$14 AND revision=$15`, gate.Revision, gate.UpdatedAtUnixMillis, gate.Stage, gate.EvidenceRefs, gate.ProposalRef, gate.TaskPacketRef, gate.WorktreeRef, producer, verifier, gate.VerificationEvidence, gate.CanaryEvidence, gate.PromotionRef, gate.RollbackRef, gate.ID, expectedRevision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if command.RowsAffected() != 1 {
|
||||
return ErrDevelopmentGateConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveExtensionProposal(ctx context.Context, value work.ExtensionProposal) error {
|
||||
result, err := r.pool.Exec(ctx, `INSERT INTO work.extension_proposals(proposal_id,requested_by_principal_id,stage,evidence_refs,deduplicated_signal_keys,desired_outcome,smallest_change_layer,affected_owners,affected_contracts,risk_tier,evaluation_plan,alternatives,rollback_plan,assumptions,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(proposal_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.EvidenceRefs, value.DeduplicatedSignalKeys, value.DesiredOutcome, value.SmallestChangeLayer, value.AffectedOwners, value.AffectedContracts, value.RiskTier, value.EvaluationPlan, value.Alternatives, value.RollbackPlan, value.Assumptions, value.CreatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() != 1 {
|
||||
return ErrDevelopmentCandidateConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveDevelopmentScaffold(ctx context.Context, value work.DevelopmentScaffold) error {
|
||||
result, err := r.pool.Exec(ctx, `INSERT INTO work.development_scaffolds(scaffold_id,requested_by_principal_id,stage,proposal_ref,base_repo_revision,worktree_ref,task_intent,modules,allowed_paths,forbidden_paths,required_tests,artifact_lineage_refs,protected_main_access,production_access,secret_access,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT(scaffold_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.ProposalRef, value.BaseRepoRevision, value.WorktreeRef, value.TaskIntent, value.Modules, value.AllowedPaths, value.ForbiddenPaths, value.RequiredTests, value.ArtifactLineageRefs, value.ProtectedMainAccess, value.ProductionAccess, value.SecretAccess, value.CreatedAtUnixMillis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() != 1 {
|
||||
return ErrDevelopmentCandidateConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.AgentDispatchIntent) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis)
|
||||
profileJSON, casesJSON, err := marshalTaskPayload(intent.TaskEvaluationProfile, intent.TaskEvaluationCases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
executionJSON, err := json.Marshal(intent.TaskEvaluationExecution)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis, intent.SpecialistPurpose, intent.TaskEvaluationProfileID, intent.TaskEvaluationBindingID, intent.TaskEvaluationCaseIDs, profileJSON, casesJSON, executionJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save dispatch intent: %w", err)
|
||||
}
|
||||
var existing work.AgentDispatchIntent
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis)
|
||||
var profileJSONExisting, casesJSONExisting, executionJSONExisting []byte
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis, &existing.SpecialistPurpose, &existing.TaskEvaluationProfileID, &existing.TaskEvaluationBindingID, &existing.TaskEvaluationCaseIDs, &profileJSONExisting, &casesJSONExisting, &executionJSONExisting)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read dispatch intent: %w", err)
|
||||
}
|
||||
if existing != intent {
|
||||
if err := unmarshalTaskPayload(profileJSONExisting, casesJSONExisting, &existing.TaskEvaluationProfile, &existing.TaskEvaluationCases); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(executionJSONExisting) > 0 && string(executionJSONExisting) != "null" {
|
||||
if err := json.Unmarshal(executionJSONExisting, &existing.TaskEvaluationExecution); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !sameDispatchIntent(existing, intent) {
|
||||
return ErrDispatchIntentConflict
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func sameDispatchIntent(left, right work.AgentDispatchIntent) bool {
|
||||
return left.DispatchID == right.DispatchID && left.IdempotencyKey == right.IdempotencyKey && left.DelegationID == right.DelegationID && left.RunID == right.RunID && left.ImplementationID == right.ImplementationID && left.ImplementationRevision == right.ImplementationRevision && left.SourceRevision == right.SourceRevision && left.WorktreeID == right.WorktreeID && left.VerificationPlanRef == right.VerificationPlanRef && left.CreatedAtUnixMillis == right.CreatedAtUnixMillis && left.SpecialistPurpose == right.SpecialistPurpose && left.TaskEvaluationProfileID == right.TaskEvaluationProfileID && left.TaskEvaluationBindingID == right.TaskEvaluationBindingID && slices.Equal(left.TaskEvaluationCaseIDs, right.TaskEvaluationCaseIDs) && reflect.DeepEqual(left.TaskEvaluationProfile, right.TaskEvaluationProfile) && slices.Equal(left.TaskEvaluationCases, right.TaskEvaluationCases) && reflect.DeepEqual(left.TaskEvaluationExecution, right.TaskEvaluationExecution)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) LoadDispatchIntent(ctx context.Context, dispatchID string) (work.AgentDispatchIntent, error) {
|
||||
var intent work.AgentDispatchIntent
|
||||
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis)
|
||||
var profileJSON, casesJSON, executionJSON []byte
|
||||
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis, &intent.SpecialistPurpose, &intent.TaskEvaluationProfileID, &intent.TaskEvaluationBindingID, &intent.TaskEvaluationCaseIDs, &profileJSON, &casesJSON, &executionJSON)
|
||||
if err != nil {
|
||||
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
|
||||
}
|
||||
if err := unmarshalTaskPayload(profileJSON, casesJSON, &intent.TaskEvaluationProfile, &intent.TaskEvaluationCases); err != nil {
|
||||
return work.AgentDispatchIntent{}, err
|
||||
}
|
||||
if len(executionJSON) > 0 && string(executionJSON) != "null" {
|
||||
if err := json.Unmarshal(executionJSON, &intent.TaskEvaluationExecution); err != nil {
|
||||
return work.AgentDispatchIntent{}, err
|
||||
}
|
||||
}
|
||||
return intent, nil
|
||||
}
|
||||
|
||||
@@ -91,7 +247,7 @@ func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string,
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
var item work.AgentDispatchQueueItem
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
|
||||
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 AND attempts < retry_limit ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return work.AgentDispatchQueueItem{}, nil
|
||||
@@ -134,17 +290,36 @@ func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, worke
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) RetryDispatch(ctx context.Context, dispatchID, workerID string, nowUnixMillis int64, delay time.Duration) error {
|
||||
if delay < 0 {
|
||||
return fmt.Errorf("invalid retry delay")
|
||||
}
|
||||
availableAt := nowUnixMillis + delay.Milliseconds()
|
||||
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=CASE WHEN attempts >= retry_limit THEN 'FAILED' ELSE 'QUEUED' END,available_at_unix_millis=$1,claimed_by='',updated_at_unix_millis=$2 WHERE dispatch_id=$3 AND state='CLAIMED' AND claimed_by=$4`, availableAt, nowUnixMillis, dispatchID, workerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if command.RowsAffected() != 1 {
|
||||
return ErrAgentCallbackRejected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts) VALUES($1,$2,$3,$4,$5) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts); err != nil {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref,orchestration_bound,orchestration_goal_id,orchestration_goal_state,orchestration_search_round,orchestration_max_round,orchestration_cancel_requested,orchestration_verification_ready,orchestration_unresolved) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts, graph.ParentGraphID, graph.ParentGraphRevision, graph.TriggerEvidenceRef, graph.SearchInputBound, graph.SearchInput.GoalID, graph.SearchInput.GoalState, graph.SearchInput.SearchRound, graph.SearchInput.MaxRound, graph.SearchInput.CancelRequested, graph.SearchInput.VerificationReady, graph.SearchInput.Unresolved); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range graph.Nodes {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts); err != nil {
|
||||
profileJSON, casesJSON, err := marshalTaskPayload(node.TaskProfile, node.TaskCases)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids,task_evaluation_profile,task_evaluation_cases) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts, node.Purpose, node.TaskProfileID, node.BindingID, node.CaseIDs, profileJSON, casesJSON); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -158,17 +333,22 @@ func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph
|
||||
|
||||
func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revision int64) (work.WorkGraph, error) {
|
||||
var graph work.WorkGraph
|
||||
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts)
|
||||
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref,orchestration_bound,orchestration_goal_id,orchestration_goal_state,orchestration_search_round,orchestration_max_round,orchestration_cancel_requested,orchestration_verification_ready,orchestration_unresolved FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts, &graph.ParentGraphID, &graph.ParentGraphRevision, &graph.TriggerEvidenceRef, &graph.SearchInputBound, &graph.SearchInput.GoalID, &graph.SearchInput.GoalState, &graph.SearchInput.SearchRound, &graph.SearchInput.MaxRound, &graph.SearchInput.CancelRequested, &graph.SearchInput.VerificationReady, &graph.SearchInput.Unresolved)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
|
||||
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids,task_evaluation_profile,task_evaluation_cases FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
|
||||
if err != nil {
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
for rows.Next() {
|
||||
var node work.GraphNode
|
||||
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
|
||||
var profileJSON, casesJSON []byte
|
||||
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts, &node.Purpose, &node.TaskProfileID, &node.BindingID, &node.CaseIDs, &profileJSON, &casesJSON); err != nil {
|
||||
rows.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
if err := unmarshalTaskPayload(profileJSON, casesJSON, &node.TaskProfile, &node.TaskCases); err != nil {
|
||||
rows.Close()
|
||||
return work.WorkGraph{}, err
|
||||
}
|
||||
@@ -199,9 +379,48 @@ func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revi
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
|
||||
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state='QUEUED',claimed_by='',updated_at_unix_millis=$1 WHERE state='CLAIMED' AND updated_at_unix_millis <= $2`, nowUnixMillis, nowUnixMillis-staleAfterMillis)
|
||||
func (r *WorkRepository) SaveGraphSynthesis(ctx context.Context, conflict work.ConflictSet, candidates []work.ResultCandidate, synthesis work.GraphSynthesisCandidate) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
for _, candidate := range candidates {
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_result_candidates(result_candidate_id,graph_id,graph_revision,node_id,author_run_id,artifact_refs,evidence_summary,summary) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(result_candidate_id) DO NOTHING`, candidate.ID, candidate.GraphID, candidate.GraphRevision, candidate.NodeID, candidate.AuthorRunID, candidate.ArtifactRefs, candidate.EvidenceSummary, candidate.Summary); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_conflicts(conflict_id,graph_id,graph_revision,node_id,candidate_ids,resolved) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(conflict_id) DO NOTHING`, conflict.ID, conflict.GraphID, conflict.GraphRevision, conflict.NodeID, conflict.CandidateIDs, conflict.Resolved); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_synthesis_candidates(synthesis_id,graph_id,graph_revision,node_id,candidate_ids,selected_candidate_id,dissent_candidate_ids,verifier_run_id,verifier_identity,evidence_refs,summary,state) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT(synthesis_id) DO NOTHING`, synthesis.ID, synthesis.GraphID, synthesis.GraphRevision, synthesis.NodeID, synthesis.CandidateIDs, synthesis.SelectedCandidateID, synthesis.DissentCandidateIDs, synthesis.VerifierRunID, synthesis.VerifierIdentity, synthesis.EvidenceRefs, synthesis.Summary, synthesis.State); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (r *WorkRepository) SaveGraphResultCandidate(ctx context.Context, candidate work.ResultCandidate) error {
|
||||
if candidate.ID == "" || candidate.GraphID == "" || candidate.GraphRevision < 1 || candidate.NodeID == "" || candidate.AuthorRunID == "" || candidate.EvidenceSummary == "" || candidate.Summary == "" {
|
||||
return errors.New("invalid graph result candidate")
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `INSERT INTO work.graph_result_candidates(result_candidate_id,graph_id,graph_revision,node_id,author_run_id,artifact_refs,evidence_summary,summary) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(result_candidate_id) DO NOTHING`, candidate.ID, candidate.GraphID, candidate.GraphRevision, candidate.NodeID, candidate.AuthorRunID, candidate.ArtifactRefs, candidate.EvidenceSummary, candidate.Summary)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := tx.Exec(ctx, `UPDATE work.agent_leases AS l SET state='EXPIRED' FROM work.agent_dispatch_queue AS q JOIN work.agent_dispatch_intents AS i ON i.dispatch_id=q.dispatch_id WHERE q.state='CLAIMED' AND q.updated_at_unix_millis <= $1 AND l.run_id=i.run_id AND l.state='ACTIVE'`, nowUnixMillis-staleAfterMillis); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
command, err := tx.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=CASE WHEN attempts >= retry_limit THEN 'FAILED' ELSE 'QUEUED' END,available_at_unix_millis=$1,claimed_by='',updated_at_unix_millis=$2 WHERE state='CLAIMED' AND updated_at_unix_millis <= $3`, nowUnixMillis, nowUnixMillis, nowUnixMillis-staleAfterMillis)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return command.RowsAffected(), nil
|
||||
@@ -494,3 +713,32 @@ func latestRowErr(err error) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func marshalTaskPayload(profile *capability.TaskEvaluationProfile, cases []capability.TaskEvaluationCase) ([]byte, []byte, error) {
|
||||
var profileJSON []byte
|
||||
var err error
|
||||
if profile != nil {
|
||||
profileJSON, err = json.Marshal(profile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
casesJSON, err := json.Marshal(cases)
|
||||
return profileJSON, casesJSON, err
|
||||
}
|
||||
|
||||
func unmarshalTaskPayload(profileJSON, casesJSON []byte, profile **capability.TaskEvaluationProfile, cases *[]capability.TaskEvaluationCase) error {
|
||||
if len(profileJSON) > 0 && string(profileJSON) != "null" {
|
||||
var value capability.TaskEvaluationProfile
|
||||
if err := json.Unmarshal(profileJSON, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
*profile = &value
|
||||
}
|
||||
if len(casesJSON) > 0 && string(casesJSON) != "null" {
|
||||
if err := json.Unmarshal(casesJSON, cases); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,12 +6,16 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/application"
|
||||
"git.cin.su/m/Assistant/modules/work/ports"
|
||||
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
||||
)
|
||||
|
||||
type memoryStore struct{ writes int }
|
||||
@@ -20,7 +24,8 @@ func (s *memoryStore) SaveDispatchIntent(context.Context, work.AgentDispatchInte
|
||||
s.writes++
|
||||
return nil
|
||||
}
|
||||
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
|
||||
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
|
||||
func (s *memoryStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error { return nil }
|
||||
func (s *memoryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
|
||||
s.writes++
|
||||
return nil
|
||||
@@ -30,14 +35,24 @@ func main() {
|
||||
enabled := flag.Bool("enable-canary", false, "explicitly enable the manual canary")
|
||||
implementation := flag.String("implementation", "fake-worker", "allowlisted deterministic implementation")
|
||||
postgresMode := flag.Bool("postgres", false, "use the Work PostgreSQL repository instead of in-memory storage")
|
||||
runID := flag.String("run-id", "", "optional unique canary run suffix; generated when omitted")
|
||||
flag.Parse()
|
||||
|
||||
now := time.UnixMilli(1_700_000_000_000)
|
||||
intent := work.AgentDispatchIntent{DispatchID: "manual-dispatch-1", IdempotencyKey: "manual-idempotency-1", DelegationID: "manual-delegation-1", RunID: "manual-run-1", ImplementationID: *implementation, ImplementationRevision: 1, SourceRevision: "manual-source-1", WorktreeID: "manual-worktree-1", VerificationPlanRef: "manual-verification-1", CreatedAtUnixMillis: now.UnixMilli()}
|
||||
suffix := *runID
|
||||
if suffix == "" {
|
||||
suffix = fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
intent := work.AgentDispatchIntent{DispatchID: "manual-dispatch-" + suffix, IdempotencyKey: "manual-idempotency-" + suffix, DelegationID: work.DelegationID("manual-delegation-" + suffix), RunID: work.AgentRunID("manual-run-" + suffix), ImplementationID: *implementation, ImplementationRevision: 1, SourceRevision: "manual-source-" + suffix, WorktreeID: "manual-worktree-" + suffix, VerificationPlanRef: "manual-verification-" + suffix, CreatedAtUnixMillis: now.UnixMilli()}
|
||||
var store ports.ExecutionStore = &memoryStore{}
|
||||
var closeStore func()
|
||||
if *postgresMode {
|
||||
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")})
|
||||
password, err := resolveDatabasePassword()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
@@ -67,3 +82,27 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDatabasePassword() (string, error) {
|
||||
if _, ok := os.LookupEnv("WORK_POSTGRES_PASSWORD"); ok {
|
||||
return "", fmt.Errorf("WORK_POSTGRES_PASSWORD direct environment secret is forbidden")
|
||||
}
|
||||
path, ref := os.Getenv("WORK_POSTGRES_PASSWORD_FILE"), os.Getenv("WORK_POSTGRES_CREDENTIAL_REF")
|
||||
if path == "" || ref == "" {
|
||||
return "", fmt.Errorf("WORK_POSTGRES_PASSWORD_FILE and WORK_POSTGRES_CREDENTIAL_REF are required")
|
||||
}
|
||||
root := os.Getenv("SECRET_BACKEND_ROOT")
|
||||
if root == "" {
|
||||
root = "/run/secrets"
|
||||
}
|
||||
cleanRoot := filepath.Clean(root)
|
||||
cleanPath := filepath.Clean(path)
|
||||
if cleanPath != cleanRoot && !strings.HasPrefix(cleanPath, cleanRoot+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("database password file escapes protected secret root")
|
||||
}
|
||||
resolver, err := platformsecrets.NewFileResolver(cleanRoot)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerWorkWorker, Path: cleanPath})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveDatabasePasswordRejectsDirectEnvironmentSecret(t *testing.T) {
|
||||
t.Setenv("WORK_POSTGRES_PASSWORD", "literal-secret")
|
||||
if _, err := resolveDatabasePassword(); err == nil {
|
||||
t.Fatal("direct database password environment variable was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveDatabasePasswordRejectsPathOutsideProtectedRoot(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("SECRET_BACKEND_ROOT", root)
|
||||
t.Setenv("WORK_POSTGRES_PASSWORD_FILE", filepath.Join(root, "..", "outside"))
|
||||
t.Setenv("WORK_POSTGRES_CREDENTIAL_REF", "credential://domovoy/work/db-password/v1")
|
||||
if _, err := resolveDatabasePassword(); err == nil {
|
||||
t.Fatal("database password path outside protected root was accepted")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
// localExecutor provides the bounded local execution capability. It never
|
||||
// claims that an unsupported or external action happened. The work owner
|
||||
// records the WAITING callback and can retry after a suitable executor is
|
||||
// configured or a human chooses the next step.
|
||||
type localExecutor struct {
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func newLocalExecutor() localExecutor {
|
||||
return localExecutor{now: time.Now}
|
||||
}
|
||||
|
||||
func (e localExecutor) Execute(_ context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
|
||||
now := time.Now()
|
||||
if e.now != nil {
|
||||
now = e.now()
|
||||
}
|
||||
return work.AgentExecutionResult{
|
||||
State: work.AgentExecutionWaiting,
|
||||
Summary: "Локальный исполнитель не поддерживает эту операцию; durable работа сохранена и ожидает подходящего исполнителя.",
|
||||
ObservedAtUnixMillis: now.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestLocalExecutorNeverClaimsUnsupportedExecution(t *testing.T) {
|
||||
when := time.UnixMilli(1_700_000_000_000)
|
||||
executor := localExecutor{now: func() time.Time { return when }}
|
||||
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.State != work.AgentExecutionWaiting || result.ObservedAtUnixMillis != when.UnixMilli() {
|
||||
t.Fatalf("unexpected fallback result: %+v", result)
|
||||
}
|
||||
if result.OutcomeRef != "" || result.Summary == "" {
|
||||
t.Fatalf("local executor must not claim an outcome: %+v", result)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/agentbridge"
|
||||
@@ -39,22 +41,30 @@ func main() {
|
||||
}
|
||||
defer repository.Close()
|
||||
var executor ports.AgentExecutor
|
||||
if bridgeURL := os.Getenv("AGENT_EXECUTION_BRIDGE_URL"); bridgeURL != "" {
|
||||
localFallback := false
|
||||
if bridgeURL, bridgeConfigured := loadProtectedBridgeURL(); bridgeConfigured {
|
||||
executor, err = agentbridge.NewHTTPExecutor(bridgeURL, &http.Client{Timeout: *leaseTTL})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "agent execution bridge is not configured; dispatch claims are disabled; recovery-only mode")
|
||||
// The local executor keeps supported task-qualification work alive, but
|
||||
// it is not an external Agent Execution Bridge and must never be reported
|
||||
// as one. Unsupported work remains durably WAITING.
|
||||
localFallback = true
|
||||
executor = newTaskEvaluationExecutor(envOr("COGNITIVE_WORKER_URL", "http://cognitive-worker:8083"), envOr("WORK_GRAPH_RUNTIME_URL", "http://work-graph-runtime:8088"), envOr("TASK_VERIFIER_URL", "http://task-verifier:8091"), &http.Client{Timeout: *leaseTTL})
|
||||
fmt.Fprintln(os.Stderr, "protected agent execution bridge URL is not configured; task evaluations use the local durable evaluator, other work remains in fallback")
|
||||
}
|
||||
runtimeStatus := newAgentWorkerRuntimeStatus()
|
||||
go func() {
|
||||
if serveErr := http.ListenAndServe(*statusAddr, workerStatusHandler(executor != nil)); serveErr != nil {
|
||||
if serveErr := http.ListenAndServe(*statusAddr, workerStatusHandler(executor != nil && !localFallback, localFallback, runtimeStatus)); serveErr != nil {
|
||||
fmt.Fprintln(os.Stderr, "agent worker status server:", serveErr)
|
||||
}
|
||||
}()
|
||||
ctx := context.Background()
|
||||
for {
|
||||
runtimeStatus.cycleStarted(time.Now())
|
||||
var result application.WorkerCycleResult
|
||||
var cycleErr error
|
||||
if executor == nil {
|
||||
@@ -63,11 +73,19 @@ func main() {
|
||||
result, cycleErr = application.ExecuteWorkerCycleWithExecutor(ctx, repository, executor, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
|
||||
}
|
||||
if cycleErr != nil {
|
||||
runtimeStatus.cycleFailed(time.Now(), cycleErr)
|
||||
fmt.Fprintln(os.Stderr, cycleErr)
|
||||
if *once {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
cycleResult := "idle"
|
||||
if result.Completed {
|
||||
cycleResult = "completed"
|
||||
} else if result.RecoveredClaims > 0 {
|
||||
cycleResult = "recovered"
|
||||
}
|
||||
runtimeStatus.cycleFinished(time.Now(), result.RecoveredClaims, result.DispatchID, cycleResult)
|
||||
if cycleErr == nil && (result.Completed || result.RecoveredClaims > 0) {
|
||||
_ = json.NewEncoder(os.Stdout).Encode(result)
|
||||
}
|
||||
@@ -81,6 +99,34 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadProtectedBridgeURL() (string, bool) {
|
||||
return loadProtectedBridgeURLFrom("/run/secrets", "/run/secrets/agent_execution_bridge_url")
|
||||
}
|
||||
|
||||
func loadProtectedBridgeURLFrom(root, path string) (string, bool) {
|
||||
clean := filepath.Clean(path)
|
||||
cleanRoot := filepath.Clean(root)
|
||||
if clean != cleanRoot && !strings.HasPrefix(clean, cleanRoot+string(os.PathSeparator)) {
|
||||
return "", false
|
||||
}
|
||||
payload, err := os.ReadFile(clean)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
value := strings.TrimSpace(string(payload))
|
||||
if value == "" || strings.ContainsAny(value, "\r\n") {
|
||||
return "", false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
|
||||
if _, ok := os.LookupEnv(name); ok {
|
||||
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
|
||||
|
||||
+100
-13
@@ -3,28 +3,104 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type workerStatusResponse struct {
|
||||
Runtime string `json:"runtime"`
|
||||
ExecutorConfigured bool `json:"executor_configured"`
|
||||
Mode string `json:"mode"`
|
||||
Ready bool `json:"ready"`
|
||||
Reason string `json:"reason"`
|
||||
Runtime string `json:"runtime"`
|
||||
ExecutorConfigured bool `json:"executor_configured"`
|
||||
Mode string `json:"mode"`
|
||||
Ready bool `json:"ready"`
|
||||
Reason string `json:"reason"`
|
||||
LocalFallback bool `json:"local_fallback"`
|
||||
LocalExecutor bool `json:"local_executor"`
|
||||
Capabilities []string `json:"capabilities,omitempty"`
|
||||
StartedAt time.Time `json:"started_at,omitempty"`
|
||||
LastCycleAt time.Time `json:"last_cycle_at,omitempty"`
|
||||
LastCompletedAt time.Time `json:"last_completed_at,omitempty"`
|
||||
LastRecoveredClaims int64 `json:"last_recovered_claims"`
|
||||
TotalRecoveredClaims int64 `json:"total_recovered_claims"`
|
||||
LastDispatchID string `json:"last_dispatch_id,omitempty"`
|
||||
LastResult string `json:"last_result,omitempty"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
func workerStatusHandler(executorConfigured bool) http.Handler {
|
||||
status := workerStatusResponse{
|
||||
type agentWorkerRuntimeStatus struct {
|
||||
mu sync.RWMutex
|
||||
startedAt time.Time
|
||||
lastCycleAt time.Time
|
||||
lastCompletedAt time.Time
|
||||
lastRecoveredClaims int64
|
||||
totalRecoveredClaims int64
|
||||
lastDispatchID string
|
||||
lastResult string
|
||||
lastError string
|
||||
}
|
||||
|
||||
func newAgentWorkerRuntimeStatus() *agentWorkerRuntimeStatus {
|
||||
return &agentWorkerRuntimeStatus{startedAt: time.Now().UTC()}
|
||||
}
|
||||
|
||||
func (s *agentWorkerRuntimeStatus) cycleStarted(at time.Time) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastCycleAt = at.UTC()
|
||||
s.lastResult = "running"
|
||||
s.lastError = ""
|
||||
}
|
||||
|
||||
func (s *agentWorkerRuntimeStatus) cycleFinished(at time.Time, recoveredClaims int64, dispatchID string, result string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastCompletedAt = at.UTC()
|
||||
s.lastRecoveredClaims = recoveredClaims
|
||||
s.totalRecoveredClaims += recoveredClaims
|
||||
s.lastDispatchID = dispatchID
|
||||
s.lastResult = result
|
||||
s.lastError = ""
|
||||
}
|
||||
|
||||
func (s *agentWorkerRuntimeStatus) cycleFailed(at time.Time, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastCompletedAt = at.UTC()
|
||||
s.lastResult = "failed"
|
||||
s.lastError = err.Error()
|
||||
}
|
||||
|
||||
func workerStatusHandler(executorConfigured bool, localFallback ...interface{}) http.Handler {
|
||||
local := false
|
||||
var runtime *agentWorkerRuntimeStatus
|
||||
for _, argument := range localFallback {
|
||||
switch value := argument.(type) {
|
||||
case bool:
|
||||
local = value
|
||||
case *agentWorkerRuntimeStatus:
|
||||
runtime = value
|
||||
}
|
||||
}
|
||||
if runtime == nil {
|
||||
runtime = newAgentWorkerRuntimeStatus()
|
||||
}
|
||||
baseStatus := workerStatusResponse{
|
||||
Runtime: "agent-worker",
|
||||
ExecutorConfigured: executorConfigured,
|
||||
Mode: "recovery-only",
|
||||
Ready: false,
|
||||
Reason: "AGENT_EXECUTION_BRIDGE_URL is not configured",
|
||||
Reason: "protected agent execution bridge URL is not configured",
|
||||
LocalFallback: local,
|
||||
LocalExecutor: local,
|
||||
}
|
||||
if local {
|
||||
baseStatus.Mode = "local-executor"
|
||||
baseStatus.Reason = "bounded local task-evaluation executor is active; unsupported work remains durably waiting"
|
||||
baseStatus.Capabilities = []string{"task-evaluation"}
|
||||
}
|
||||
if executorConfigured {
|
||||
status.Mode = "execution"
|
||||
status.Ready = true
|
||||
status.Reason = "typed agent execution bridge configured"
|
||||
baseStatus.Mode = "execution"
|
||||
baseStatus.Ready = true
|
||||
baseStatus.Reason = "typed agent execution bridge configured"
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || (r.URL.Path != "/healthz" && r.URL.Path != "/readyz" && r.URL.Path != "/status") {
|
||||
@@ -32,9 +108,20 @@ func workerStatusHandler(executorConfigured bool) http.Handler {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/readyz" && !status.Ready {
|
||||
if r.URL.Path == "/readyz" && !baseStatus.Ready {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(status)
|
||||
response := baseStatus
|
||||
runtime.mu.RLock()
|
||||
response.StartedAt = runtime.startedAt
|
||||
response.LastCycleAt = runtime.lastCycleAt
|
||||
response.LastCompletedAt = runtime.lastCompletedAt
|
||||
response.LastRecoveredClaims = runtime.lastRecoveredClaims
|
||||
response.TotalRecoveredClaims = runtime.totalRecoveredClaims
|
||||
response.LastDispatchID = runtime.lastDispatchID
|
||||
response.LastResult = runtime.lastResult
|
||||
response.LastError = runtime.lastError
|
||||
runtime.mu.RUnlock()
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,9 +4,29 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadProtectedBridgeURL(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, "agent_execution_bridge_url")
|
||||
if err := os.WriteFile(path, []byte("http://bridge.local\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value, configured := loadProtectedBridgeURLFrom(root, path); !configured || value != "http://bridge.local" {
|
||||
t.Fatalf("protected bridge was not loaded: %q %v", value, configured)
|
||||
}
|
||||
if value, configured := loadProtectedBridgeURLFrom(root, filepath.Join(root, "missing")); configured || value != "" {
|
||||
t.Fatalf("missing bridge was accepted: %q %v", value, configured)
|
||||
}
|
||||
if value, configured := loadProtectedBridgeURLFrom(root, "/tmp/not-protected"); configured || value != "" {
|
||||
t.Fatalf("path outside protected root was accepted: %q %v", value, configured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerStatusReportsRecoveryOnlyWithoutExecutor(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workerStatusHandler(false).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
|
||||
@@ -19,6 +39,33 @@ func TestWorkerStatusReportsRecoveryOnlyWithoutExecutor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerStatusReportsLocalExecutor(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
|
||||
var value workerStatusResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Code != http.StatusOK || value.Ready || value.Mode != "local-executor" || !value.LocalFallback || !value.LocalExecutor {
|
||||
t.Fatalf("unexpected status: code=%d value=%+v", response.Code, value)
|
||||
}
|
||||
if len(value.Capabilities) != 1 || value.Capabilities[0] != "task-evaluation" {
|
||||
t.Fatalf("unexpected local capabilities: %+v", value.Capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerStatusDoesNotCallLocalFallbackAnExternalExecutor(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
|
||||
var value workerStatusResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value.ExecutorConfigured || value.Ready || !value.LocalFallback || !value.LocalExecutor || value.Mode != "local-executor" {
|
||||
t.Fatalf("local fallback was presented as external execution: %+v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerReadyzRequiresExecutor(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workerStatusHandler(false).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||||
@@ -31,3 +78,32 @@ func TestWorkerReadyzRequiresExecutor(t *testing.T) {
|
||||
t.Fatalf("configured readyz status=%d", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerStatusReportsRecoveryCycle(t *testing.T) {
|
||||
runtime := newAgentWorkerRuntimeStatus()
|
||||
runtime.cycleStarted(time.Date(2026, 8, 23, 10, 0, 0, 0, time.UTC))
|
||||
runtime.cycleFinished(time.Date(2026, 8, 23, 10, 0, 1, 0, time.UTC), 3, "dispatch-1", "recovered")
|
||||
response := httptest.NewRecorder()
|
||||
workerStatusHandler(false, true, runtime).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
|
||||
var value workerStatusResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value.LastRecoveredClaims != 3 || value.TotalRecoveredClaims != 3 || value.LastDispatchID != "dispatch-1" || value.LastResult != "recovered" {
|
||||
t.Fatalf("recovery evidence missing: %+v", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerStatusReportsCycleError(t *testing.T) {
|
||||
runtime := newAgentWorkerRuntimeStatus()
|
||||
runtime.cycleFailed(time.Date(2026, 8, 23, 10, 0, 1, 0, time.UTC), os.ErrClosed)
|
||||
response := httptest.NewRecorder()
|
||||
workerStatusHandler(false, runtime).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
|
||||
var value workerStatusResponse
|
||||
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if value.LastResult != "failed" || value.LastError == "" {
|
||||
t.Fatalf("cycle error missing: %+v", value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
|
||||
)
|
||||
|
||||
type taskEvaluationExecutor struct {
|
||||
cognitiveURL string
|
||||
callbackURL string
|
||||
verifierURL string
|
||||
client *http.Client
|
||||
fallback localExecutor
|
||||
}
|
||||
|
||||
func newTaskEvaluationExecutor(cognitiveURL, callbackURL, verifierURL string, client *http.Client) taskEvaluationExecutor {
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return taskEvaluationExecutor{cognitiveURL: strings.TrimRight(cognitiveURL, "/"), callbackURL: strings.TrimRight(callbackURL, "/"), verifierURL: strings.TrimRight(verifierURL, "/"), client: client, fallback: newLocalExecutor()}
|
||||
}
|
||||
|
||||
func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
|
||||
intent := request.Intent
|
||||
if intent.TaskEvaluationExecution == nil || intent.TaskEvaluationProfile == nil || len(intent.TaskEvaluationCases) == 0 {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
execution := intent.TaskEvaluationExecution
|
||||
if execution.GraphID == "" || execution.NodeID == "" || execution.BindingRevision < 1 || execution.ActorID == "" || execution.PrincipalID == "" || execution.ScopeID == "" {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
if e.cognitiveURL == "" || e.callbackURL == "" || e.verifierURL == "" {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
payload := capability.TaskEvaluationExecutionRequest{Profile: *intent.TaskEvaluationProfile, CapabilityID: intent.TaskEvaluationProfile.CapabilityID, BindingID: intent.TaskEvaluationBindingID, BindingRevision: execution.BindingRevision, ActorID: execution.ActorID, PrincipalID: execution.PrincipalID, ScopeID: execution.ScopeID, Cases: intent.TaskEvaluationCases, MaxOutputBytes: 4096}
|
||||
response, err := postJSON[capability.TaskEvaluationExecutionResponse](ctx, e.client, e.cognitiveURL+"/v1/task-evaluations/execute", payload)
|
||||
if err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
if len(response.Responses) == 0 || response.ObservedAtUnixMillis < 1 {
|
||||
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation returned no observations")
|
||||
}
|
||||
if len(intent.TaskEvaluationCases) == 0 {
|
||||
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation cases are missing")
|
||||
}
|
||||
caseByID := make(map[string]capability.TaskEvaluationCase, len(intent.TaskEvaluationCases))
|
||||
for _, item := range intent.TaskEvaluationCases {
|
||||
caseByID[item.CaseID] = item
|
||||
}
|
||||
observations := make([]capability.TaskCaseObservation, 0, len(response.Responses))
|
||||
for _, item := range response.Responses {
|
||||
caseValue, ok := caseByID[item.CaseID]
|
||||
if !ok || caseValue.Verification.Mode == "" {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
verified, verifyErr := postJSON[capability.TaskCaseVerificationResponse](ctx, e.client, e.verifierURL+"/v1/task-evaluations/verify", capability.TaskCaseVerificationRequest{CaseID: item.CaseID, ResponseText: item.ResponseText, LatencyMicros: item.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef, Verification: caseValue.Verification})
|
||||
if verifyErr != nil {
|
||||
return e.fallback.Execute(ctx, request)
|
||||
}
|
||||
observations = append(observations, capability.TaskCaseObservation{CaseID: verified.CaseID, Result: verified.Result, QualityScoreMillis: verified.QualityScoreMillis, LatencyMicros: verified.LatencyMicros, CriticalFailure: verified.CriticalFailure, EvidenceArtifactRef: verified.EvidenceArtifactRef, VerificationEvidenceRef: verified.VerificationEvidenceRef, VerifierIdentity: verified.VerifierIdentity})
|
||||
}
|
||||
evaluation, err := capabilitydomain.BuildTaskEvaluation(*intent.TaskEvaluationProfile, capability.TaskEvaluationID("evaluation:"+intent.DispatchID), response.BindingID, response.BindingRevision, intent.TaskEvaluationProfile.CapabilityID, observations, artifact.ArtifactID(intent.TaskEvaluationCases[0].EvidenceArtifactRef), response.ObservedAtUnixMillis)
|
||||
if err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
callback := work.TaskEvaluationEvidenceCallback{GraphID: execution.GraphID, Revision: execution.GraphRevision, NodeID: execution.NodeID, Evaluation: evaluation, Succeeded: true}
|
||||
if _, err := postJSON[json.RawMessage](ctx, e.client, e.callbackURL+"/v1/work-graphs/task-evaluations/callback", callback); err != nil {
|
||||
return work.AgentExecutionResult{}, err
|
||||
}
|
||||
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "task evaluation executed, aggregated and accepted by Work Graph callback", OutcomeRef: string(evaluation.ID), ArtifactRefs: []string{string(evaluation.ArtifactID)}, ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
|
||||
}
|
||||
|
||||
func postJSON[T any](ctx context.Context, client *http.Client, url string, payload any) (T, error) {
|
||||
var result T
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return result, fmt.Errorf("POST %s: status %s", url, resp.Status)
|
||||
}
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
return result, nil
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestTaskEvaluationExecutorInvokesAggregatesAndCallbacks(t *testing.T) {
|
||||
profile := capability.TaskEvaluationProfile{ID: "profile-1", Revision: 1, CapabilityID: "capability-1", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-1"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||
verification := capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/v1/task-evaluations/execute" {
|
||||
_ = json.NewEncoder(w).Encode(capability.TaskEvaluationExecutionResponse{ProfileID: profile.ID, BindingID: "binding-1", BindingRevision: 3, Responses: []capability.TaskCaseResponse{{CaseID: "case-1", ResponseText: "answer", LatencyMicros: 2, EvidenceArtifactRef: "artifact-1"}}, ObservedAtUnixMillis: 10})
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v1/task-evaluations/verify" {
|
||||
_ = json.NewEncoder(w).Encode(capability.TaskCaseVerificationResponse{CaseID: "case-1", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 2, EvidenceArtifactRef: "artifact-1", VerificationEvidenceRef: "verify-1", VerifierIdentity: "verifier-1"})
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v1/work-graphs/task-evaluations/callback" {
|
||||
var callback work.TaskEvaluationEvidenceCallback
|
||||
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil || callback.GraphID != "graph-1" || callback.NodeID != "node-1" || callback.Evaluation.Result != capability.EvaluationSupported {
|
||||
http.Error(w, "invalid callback", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer server.Close()
|
||||
executor := newTaskEvaluationExecutor(server.URL, server.URL, server.URL, server.Client())
|
||||
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1", TaskEvaluationBindingID: "binding-1", TaskEvaluationProfile: &profile, TaskEvaluationCases: []capability.TaskEvaluationCase{{CaseID: "case-1", Prompt: "answer", EvidenceArtifactRef: "artifact-1", Verification: verification}}, TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", BindingRevision: 3, ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1"}}})
|
||||
if err != nil || result.State != work.AgentExecutionCompleted || result.OutcomeRef == "" {
|
||||
t.Fatalf("result=%+v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
@@ -98,8 +98,9 @@ func main() {
|
||||
manifestPath := flag.String("manifest", "", "benchmark manifest JSON")
|
||||
outputPath := flag.String("output", "", "output artifact JSON")
|
||||
timeout := flag.Duration("case-timeout", 30*time.Second, "deadline for each case")
|
||||
maxOutputBytes := flag.Int64("max-output-bytes", 4096, "maximum advisory output bytes per case")
|
||||
flag.Parse()
|
||||
if *manifestPath == "" || *outputPath == "" || *timeout <= 0 {
|
||||
if *manifestPath == "" || *outputPath == "" || *timeout <= 0 || *maxOutputBytes < 1 {
|
||||
fatal(errors.New("manifest, output and positive case-timeout are required"))
|
||||
}
|
||||
input, err := os.ReadFile(*manifestPath)
|
||||
@@ -110,7 +111,7 @@ func main() {
|
||||
if err := json.Unmarshal(input, &plan); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
artifact, err := run(context.Background(), plan, *timeout, time.Now().UTC())
|
||||
artifact, err := run(context.Background(), plan, *timeout, *maxOutputBytes, time.Now().UTC())
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
@@ -125,7 +126,7 @@ func main() {
|
||||
fmt.Printf("artifact=%s models=%d output=%s\n", artifact.ArtifactID, len(artifact.Results), *outputPath)
|
||||
}
|
||||
|
||||
func run(ctx context.Context, plan manifest, timeout time.Duration, observed time.Time) (benchmarkArtifact, error) {
|
||||
func run(ctx context.Context, plan manifest, timeout time.Duration, maxOutputBytes int64, observed time.Time) (benchmarkArtifact, error) {
|
||||
if plan.ArtifactID == "" || len(plan.Providers) == 0 {
|
||||
return benchmarkArtifact{}, errors.New("invalid benchmark manifest")
|
||||
}
|
||||
@@ -134,7 +135,7 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
|
||||
artifact.Corpus = append(artifact.Corpus, corpusRecord{ID: item.id, Prompt: item.prompt, PromptSHA256: digest(item.prompt), Critical: item.critical})
|
||||
}
|
||||
for _, provider := range plan.Providers {
|
||||
adapter, err := newProvider(provider)
|
||||
adapter, err := newProvider(provider, timeout)
|
||||
if err != nil {
|
||||
return benchmarkArtifact{}, err
|
||||
}
|
||||
@@ -142,7 +143,7 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
|
||||
return provider.Candidates[i].ProviderModelID < provider.Candidates[j].ProviderModelID
|
||||
})
|
||||
for _, candidate := range provider.Candidates {
|
||||
result, err := runModel(ctx, adapter, provider.ProviderInstanceID, candidate, timeout)
|
||||
result, err := runModel(ctx, adapter, provider.ProviderInstanceID, candidate, timeout, maxOutputBytes)
|
||||
if err != nil {
|
||||
return benchmarkArtifact{}, err
|
||||
}
|
||||
@@ -158,10 +159,10 @@ func run(ctx context.Context, plan manifest, timeout time.Duration, observed tim
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
|
||||
func newProvider(candidate providerCandidate, timeout time.Duration) (models.LocalProvider, error) {
|
||||
switch candidate.ProviderKind {
|
||||
case "openai-compatible-chat-v1":
|
||||
return models.NewOpenAICompatibleLocalAdapter(candidate.Endpoint, nil)
|
||||
return models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(candidate.Endpoint, "", nil, timeout)
|
||||
case "ollama-v1":
|
||||
return models.NewOllamaAdapter(candidate.Endpoint, nil)
|
||||
default:
|
||||
@@ -169,7 +170,7 @@ func newProvider(candidate providerCandidate) (models.LocalProvider, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func runModel(ctx context.Context, adapter models.LocalProvider, providerID string, candidate modelCandidate, timeout time.Duration) (modelResult, error) {
|
||||
func runModel(ctx context.Context, adapter models.LocalProvider, providerID string, candidate modelCandidate, timeout time.Duration, maxOutputBytes int64) (modelResult, error) {
|
||||
if providerID == "" || candidate.ProviderModelID == "" || candidate.ModelDescriptorID == "" || candidate.ModelRevision < 1 || candidate.BindingID == "" || candidate.BindingRevision < 1 {
|
||||
return modelResult{}, errors.New("invalid benchmark candidate")
|
||||
}
|
||||
@@ -178,7 +179,7 @@ func runModel(ctx context.Context, adapter models.LocalProvider, providerID stri
|
||||
for index, item := range corpus {
|
||||
caseCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
started := time.Now()
|
||||
response, err := adapter.Advise(caseCtx, cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("bench-%s-%d", digest(candidate.BindingID)[:12], index)), ActorID: "benchmark-actor", PrincipalID: "domovoy-creator-principal", ScopeID: "benchmark-public-synthetic", Message: item.prompt, Locale: "ru-RU", MaxOutputBytes: 4096, ProviderBindingID: capability.CapabilityBindingID(candidate.BindingID), ProviderBindingRevision: candidate.BindingRevision, ProviderInstanceID: capability.ProviderInstanceID(providerID), ModelDescriptorID: capability.ModelDescriptorID(candidate.ModelDescriptorID), ProviderModelID: capability.ProviderModelID(candidate.ProviderModelID)})
|
||||
response, err := adapter.Advise(caseCtx, cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("bench-%s-%d", digest(candidate.BindingID)[:12], index)), ActorID: "benchmark-actor", PrincipalID: "domovoy-creator-principal", ScopeID: "benchmark-public-synthetic", Message: item.prompt, Locale: "ru-RU", MaxOutputBytes: int(maxOutputBytes), ProviderBindingID: capability.CapabilityBindingID(candidate.BindingID), ProviderBindingRevision: candidate.BindingRevision, ProviderInstanceID: capability.ProviderInstanceID(providerID), ModelDescriptorID: capability.ModelDescriptorID(candidate.ModelDescriptorID), ProviderModelID: capability.ProviderModelID(candidate.ProviderModelID)})
|
||||
cancel()
|
||||
caseResult := caseResult{CaseID: item.id, LatencyMillis: time.Since(started).Milliseconds()}
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -17,6 +18,10 @@ type discoveryService struct {
|
||||
}
|
||||
|
||||
func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/v1/provider-discovery/status" {
|
||||
s.statusHandler(w, r)
|
||||
return
|
||||
}
|
||||
registration, ok := s.providers[r.PathValue("provider")]
|
||||
if !ok || registration.Discovery == nil {
|
||||
http.Error(w, "provider discovery not found", http.StatusNotFound)
|
||||
@@ -35,3 +40,42 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
type discoveryStatus struct {
|
||||
Provider string `json:"provider"`
|
||||
ProviderKind capability.ProviderKind `json:"provider_kind"`
|
||||
Health capability.HealthState `json:"health"`
|
||||
FailureClass capability.CognitiveFailureType `json:"failure_class,omitempty"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
Models []discoveredModelStatus `json:"models"`
|
||||
}
|
||||
|
||||
type discoveredModelStatus struct {
|
||||
ProviderModelID capability.ProviderModelID `json:"provider_model_id"`
|
||||
Fingerprint capability.ModelFingerprint `json:"fingerprint,omitempty"`
|
||||
Capabilities []capability.ModelCapability `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
func (s discoveryService) statusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
providers := make([]string, 0, len(s.providers))
|
||||
for name := range s.providers {
|
||||
providers = append(providers, name)
|
||||
}
|
||||
sort.Strings(providers)
|
||||
result := make([]discoveryStatus, 0, len(providers))
|
||||
for _, name := range providers {
|
||||
registration := s.providers[name]
|
||||
status := discoveryStatus{Provider: name, ProviderKind: registration.Kind, Health: capability.HealthUnknown, ObservedAtUnixMillis: s.now().UnixMilli(), Models: []discoveredModelStatus{}}
|
||||
if registration.Discovery != nil {
|
||||
models, probe := registration.Discovery.Discover(r.Context())
|
||||
status.Health = probe.Health
|
||||
status.FailureClass = capability.CognitiveFailureType(probe.Failure)
|
||||
for _, model := range models {
|
||||
status.Models = append(status.Models, discoveredModelStatus{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, Capabilities: model.Capabilities})
|
||||
}
|
||||
}
|
||||
result = append(result, status)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type discoveryTestProvider struct{}
|
||||
|
||||
func (discoveryTestProvider) Probe(context.Context) models.ProviderProbe {
|
||||
return models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
func (discoveryTestProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
|
||||
return []models.DiscoveredModel{{ProviderModelID: "local-model", Fingerprint: "fingerprint-1", Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}}}, models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
func (discoveryTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
|
||||
return cognitive.AdvisoryResponse{}, nil
|
||||
}
|
||||
|
||||
func TestDiscoveryStatusIsReadOnlyAndDoesNotExposeCredentialMetadata(t *testing.T) {
|
||||
service := discoveryService{
|
||||
providers: map[string]providerRegistration{"cpu": {ProviderInstanceID: "provider-1", Kind: "openai-compatible-chat-v1", CredentialRef: "credential://must-not-be-returned", Discovery: discoveryTestProvider{}}},
|
||||
owner: identity.PrincipalID("owner-1"),
|
||||
now: func() time.Time { return time.UnixMilli(1234) },
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
service.handler(response, httptest.NewRequest(http.MethodGet, "/v1/provider-discovery/status", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d", response.Code)
|
||||
}
|
||||
body := response.Body.String()
|
||||
var result []discoveryStatus
|
||||
if err := json.NewDecoder(strings.NewReader(body)).Decode(&result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result) != 1 || result[0].Provider != "cpu" || result[0].Health != capability.HealthAvailable || len(result[0].Models) != 1 || result[0].ObservedAtUnixMillis != 1234 {
|
||||
t.Fatalf("unexpected discovery status: %+v", result)
|
||||
}
|
||||
if strings.Contains(body, "must-not-be-returned") || strings.Contains(body, "credential://") {
|
||||
t.Fatalf("credential metadata leaked in discovery status: %s", body)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ type fleetEndpoint struct {
|
||||
ModelID string
|
||||
Tier capability.FleetTier
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type fleetStatusResponse struct {
|
||||
@@ -37,7 +39,14 @@ func fleetReadinessHandler(client *http.Client, preferred capability.FleetTier,
|
||||
}
|
||||
request, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint.URL, nil)
|
||||
if err == nil {
|
||||
response, requestErr := client.Do(request)
|
||||
for name, value := range endpoint.Headers {
|
||||
request.Header.Set(name, value)
|
||||
}
|
||||
probeClient := endpoint.Client
|
||||
if probeClient == nil {
|
||||
probeClient = client
|
||||
}
|
||||
response, requestErr := probeClient.Do(request)
|
||||
if requestErr == nil {
|
||||
response.Body.Close()
|
||||
if response.StatusCode >= 200 && response.StatusCode < 300 {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func TestFleetReadinessSelectsLifeboatWhenPrimaryTiersUnavailable(t *testing.T) {
|
||||
func TestFleetReadinessSelectsBestAvailablePrimaryTierBeforeLifeboat(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }))
|
||||
defer server.Close()
|
||||
handler := fleetReadinessHandler(server.Client(), capability.FleetTierHeavy, []fleetEndpoint{
|
||||
@@ -21,7 +21,26 @@ func TestFleetReadinessSelectsLifeboatWhenPrimaryTiersUnavailable(t *testing.T)
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil)
|
||||
handler.ServeHTTP(recorder, request)
|
||||
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"LIFEBOAT"`) || !strings.Contains(recorder.Body.String(), `"tier":"MEDIUM"`) {
|
||||
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"selected_tier":"FAST"`) || !strings.Contains(recorder.Body.String(), `"tier":"MEDIUM"`) {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetReadinessSendsConfiguredProviderHeaders(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer fleet-key" {
|
||||
t.Fatalf("authorization=%q", r.Header.Get("Authorization"))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
handler := fleetReadinessHandler(server.Client(), capability.FleetTierFast, []fleetEndpoint{{
|
||||
ProviderID: "cpu", ModelID: "cpu-model", Tier: capability.FleetTierFast, URL: server.URL,
|
||||
Headers: map[string]string{"Authorization": "Bearer fleet-key"},
|
||||
}})
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/v1/fleet/readiness", nil))
|
||||
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"ready":true`) {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ type bindingResolver interface {
|
||||
RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error)
|
||||
}
|
||||
|
||||
type candidateBindingResolver interface {
|
||||
ResolveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error)
|
||||
}
|
||||
|
||||
type admissionClient interface {
|
||||
Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error)
|
||||
Release(context.Context, capability.AdmissionLease) error
|
||||
@@ -294,12 +298,21 @@ func fallbackEligible(reason cognitive.FinishReason) bool {
|
||||
}
|
||||
|
||||
func (g cognitiveGateway) provider(selected capability.ResolvedCognitiveBinding) advisoryProvider {
|
||||
var byStableID advisoryProvider
|
||||
for _, registration := range g.providers[selected.Provider.Kind] {
|
||||
if registration.EndpointRef == selected.Provider.EndpointRef && registration.CredentialRef == selected.Provider.CredentialRef {
|
||||
return registration.Provider
|
||||
}
|
||||
// Capability revisions can legitimately retain an older credential
|
||||
// reference while the protected runtime has rotated or reloaded the
|
||||
// current credential. ProviderInstanceID is the stable owner identity;
|
||||
// use it after exact metadata matching so stale non-secret metadata does
|
||||
// not force an unnecessary builtin fallback.
|
||||
if byStableID == nil && registration.ProviderInstanceID == selected.Provider.ID && registration.Kind == selected.Provider.Kind {
|
||||
byStableID = registration.Provider
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return byStableID
|
||||
}
|
||||
|
||||
func writeGatewayResponse(w http.ResponseWriter, response cognitive.GatewayAdvisoryResponse) {
|
||||
@@ -341,6 +354,26 @@ func (r *httpBindingResolver) Resolve(ctx context.Context, value capability.Cogn
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *httpBindingResolver) ResolveCandidate(ctx context.Context, bindingID capability.CapabilityBindingID, revision int64) (capability.ResolvedCognitiveBinding, error) {
|
||||
var result capability.ResolvedCognitiveBinding
|
||||
target := *r.base
|
||||
target.Path = "/v1/cognitive-bindings/" + url.PathEscape(string(bindingID)) + "/revisions/" + fmt.Sprint(revision) + "/snapshot"
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, target.String(), nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
response, err := r.client.Do(request)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return result, fmt.Errorf("candidate status %d", response.StatusCode)
|
||||
}
|
||||
err = json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(&result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *httpBindingResolver) Observe(ctx context.Context, value capability.CognitiveUsageObservation) error {
|
||||
return r.post(ctx, "/v1/cognitive-usage-observations", value, nil)
|
||||
}
|
||||
|
||||
@@ -88,6 +88,19 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderUsesStableInstanceWhenCredentialMetadataIsStale(t *testing.T) {
|
||||
provider := &stubAdvisoryProvider{}
|
||||
gateway := cognitiveGateway{providers: map[capability.ProviderKind][]providerRegistration{
|
||||
"provider-kind-1": {{ProviderInstanceID: "provider-1", Kind: "provider-kind-1", EndpointRef: "current-endpoint", CredentialRef: "current-credential", Provider: provider}},
|
||||
}}
|
||||
selected := gatewaySelection()
|
||||
selected.Provider.EndpointRef = "old-endpoint"
|
||||
selected.Provider.CredentialRef = ""
|
||||
if got := gateway.provider(selected); got != provider {
|
||||
t.Fatalf("stable provider identity was not used for stale metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanaryFlagIsForwardedToCapabilityResolver(t *testing.T) {
|
||||
registry := ®istryStub{err: errors.New("stop after resolve")}
|
||||
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{}, now: time.Now, allowCanary: true}
|
||||
|
||||
+151
-26
@@ -17,6 +17,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -45,7 +46,7 @@ func main() {
|
||||
discoveries := map[string]providerRegistration{}
|
||||
var builtinLocal *providerRegistration
|
||||
externalRecovery := []providerRegistration{}
|
||||
cpuAPIKey, err := loadLocalCPUProviderAPIKey()
|
||||
cpuAPIKey, err := loadProviderAPIKey("LOCAL_CPU_PROVIDER_API_KEY", "LOCAL_CPU_PROVIDER_API_KEY_FILE", "LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -62,6 +63,28 @@ func main() {
|
||||
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", CredentialRef: cpuCredentialRef, AuthMode: cpuAuthMode, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: cpuAdapter, Discovery: cpuAdapter}
|
||||
providers[cpu.Kind] = append(providers[cpu.Kind], cpu)
|
||||
discoveries["cpu"] = cpu
|
||||
mediumURL := strings.TrimSpace(os.Getenv("LOCAL_MEDIUM_PROVIDER_URL"))
|
||||
mediumAPIKey := ""
|
||||
if mediumURL != "" {
|
||||
mediumAPIKey, err = loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mediumTimeout := durationEnv("LOCAL_MEDIUM_PROVIDER_TIMEOUT", 60*time.Second)
|
||||
mediumAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKeyAndTimeout(strings.TrimRight(mediumURL, "/"), mediumAPIKey, nil, mediumTimeout)
|
||||
if adapterErr != nil {
|
||||
log.Fatal(adapterErr)
|
||||
}
|
||||
mediumAuthMode := capability.AuthNone
|
||||
mediumCredentialRef := capability.CredentialRef("")
|
||||
if mediumAPIKey != "" {
|
||||
mediumAuthMode = capability.AuthKey
|
||||
mediumCredentialRef = capability.CredentialRef(envOr("LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1"))
|
||||
}
|
||||
medium := providerRegistration{ProviderInstanceID: "local-medium-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-medium-primary-v1", CredentialRef: mediumCredentialRef, AuthMode: mediumAuthMode, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: mediumAdapter, Discovery: mediumAdapter}
|
||||
providers[medium.Kind] = append(providers[medium.Kind], medium)
|
||||
discoveries["medium"] = medium
|
||||
}
|
||||
if lifeboat, enabled, lifeboatErr := newLifeboatProvider(os.Getenv("LOCAL_LIFEBOAT_PROVIDER_URL")); lifeboatErr != nil {
|
||||
log.Fatal(lifeboatErr)
|
||||
} else if enabled {
|
||||
@@ -70,14 +93,20 @@ func main() {
|
||||
discoveries["lifeboat"] = lifeboat
|
||||
builtinLocal = &lifeboat
|
||||
}
|
||||
gpuAdapter, err := models.NewOllamaAdapter(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
gpuURL, gpuKey, gpuConfigured, gpuErr := loadProtectedGPUConfiguration()
|
||||
if gpuErr != nil {
|
||||
log.Printf("optional GPU provider disabled: %v", gpuErr)
|
||||
} else if gpuConfigured {
|
||||
gpuAdapter, adapterErr := models.NewOpenAICompatibleLocalAdapterWithAPIKey(gpuURL, gpuKey, nil)
|
||||
if adapterErr != nil {
|
||||
log.Printf("optional GPU provider disabled: %v", adapterErr)
|
||||
} else {
|
||||
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "openai-compatible-chat-v1", EndpointRef: "local-gpu-opportunistic-v1", CredentialRef: "credential://domovoy/gpu/provider-api-key/v1", AuthMode: capability.AuthKey, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter}
|
||||
providers[gpu.Kind] = append(providers[gpu.Kind], gpu)
|
||||
discoveries["gpu"] = gpu
|
||||
externalRecovery = append(externalRecovery, gpu)
|
||||
}
|
||||
}
|
||||
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter}
|
||||
providers[gpu.Kind] = append(providers[gpu.Kind], gpu)
|
||||
discoveries["gpu"] = gpu
|
||||
externalRecovery = append(externalRecovery, gpu)
|
||||
if openAIProvider != nil {
|
||||
kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1"))
|
||||
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
|
||||
@@ -85,27 +114,63 @@ func main() {
|
||||
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096, admission: newHTTPAdmissionClient(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 5 * time.Second})}
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
readinessClient := &http.Client{Timeout: 2 * time.Second}
|
||||
cpuReadinessClient := &http.Client{Timeout: 10 * time.Second}
|
||||
mediumReadinessClient := &http.Client{Timeout: 10 * time.Second}
|
||||
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: []health.Probe{
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
|
||||
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
|
||||
health.HTTPProbe(readinessClient, lifeboatURL),
|
||||
health.OptionalHTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
|
||||
}})
|
||||
fleetClient := &http.Client{Timeout: 2 * time.Second}
|
||||
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierHeavy)))
|
||||
mux.Handle("/v1/fleet/readiness", fleetReadinessHandler(fleetClient, preferredFleet, []fleetEndpoint{
|
||||
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"},
|
||||
{ProviderID: "local-medium-unconfigured", ModelID: "medium-unconfigured", Tier: capability.FleetTierMedium},
|
||||
{ProviderID: "local-gpu-opportunistic", ModelID: "gpu-opportunistic", Tier: capability.FleetTierHeavy, URL: strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/") + "/api/tags"},
|
||||
preferredFleet := capability.FleetTier(envOr("COGNITIVE_PREFERRED_FLEET_TIER", string(capability.FleetTierFast)))
|
||||
cpuFleetHeaders := map[string]string{}
|
||||
if cpuAPIKey != "" {
|
||||
cpuFleetHeaders["Authorization"] = "Bearer " + cpuAPIKey
|
||||
}
|
||||
cpuEndpoint := strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/") + "/models"
|
||||
cpuHealthProbe := health.OptionalHTTPProbeWithHeaders(cpuReadinessClient, cpuEndpoint, cpuFleetHeaders)
|
||||
cpuHealthProbe.Timeout = 10 * time.Second
|
||||
mediumEndpoint := ""
|
||||
mediumHeaders := map[string]string{}
|
||||
mediumHealthProbe := health.Probe{}
|
||||
if mediumURL != "" {
|
||||
mediumEndpoint = strings.TrimRight(mediumURL, "/") + "/models"
|
||||
if mediumAPIKey != "" {
|
||||
mediumHeaders["Authorization"] = "Bearer " + mediumAPIKey
|
||||
}
|
||||
mediumHealthProbe = health.OptionalHTTPProbeWithHeaders(mediumReadinessClient, mediumEndpoint, mediumHeaders)
|
||||
mediumHealthProbe.Timeout = 10 * time.Second
|
||||
}
|
||||
readinessProbes := []health.Probe{
|
||||
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
|
||||
cpuHealthProbe,
|
||||
}
|
||||
if mediumURL != "" {
|
||||
readinessProbes = append(readinessProbes, mediumHealthProbe)
|
||||
}
|
||||
readinessProbes = append(readinessProbes, health.HTTPProbe(readinessClient, lifeboatURL))
|
||||
if gpuConfigured {
|
||||
gpuHeaders := map[string]string{"Authorization": "Bearer " + gpuKey}
|
||||
gpuProbe := health.OptionalHTTPProbeWithHeaders(readinessClient, strings.TrimRight(gpuURL, "/")+"/models", gpuHeaders)
|
||||
gpuProbe.Timeout = 10 * time.Second
|
||||
readinessProbes = append(readinessProbes, gpuProbe)
|
||||
}
|
||||
|
||||
fleetEndpoints := []fleetEndpoint{
|
||||
{ProviderID: "local-cpu-primary", ModelID: "local-cpu-model", Tier: capability.FleetTierFast, URL: cpuEndpoint, Headers: cpuFleetHeaders, Client: &http.Client{Timeout: 10 * time.Second}},
|
||||
{ProviderID: "local-medium-primary", ModelID: "local-medium-model", Tier: capability.FleetTierMedium, URL: mediumEndpoint, Headers: mediumHeaders, Client: mediumReadinessClient},
|
||||
{ProviderID: "cognitive-lifeboat", ModelID: "qwen3-8b-lifeboat", Tier: capability.FleetTierLifeboat, URL: lifeboatURL},
|
||||
}))
|
||||
}
|
||||
if gpuConfigured {
|
||||
fleetEndpoints = append(fleetEndpoints, fleetEndpoint{ProviderID: "local-gpu-opportunistic", ModelID: "gpu-opportunistic", Tier: capability.FleetTierHeavy, URL: strings.TrimRight(gpuURL, "/") + "/models", Headers: map[string]string{"Authorization": "Bearer " + gpuKey}})
|
||||
}
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: readinessProbes})
|
||||
mux.Handle("/v1/fleet/readiness", fleetReadinessHandler(fleetClient, preferredFleet, fleetEndpoints))
|
||||
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
|
||||
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
|
||||
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
|
||||
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
|
||||
mux.HandleFunc("POST /v1/task-evaluations/execute", gateway.taskEvaluationHandler)
|
||||
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
|
||||
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
|
||||
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(discoveries, string(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), httpQualificationObserver{resolver: resolver}))
|
||||
mux.HandleFunc("GET /v1/provider-discovery/status", discovery.statusHandler)
|
||||
refreshProviderDiscoveries(resolver, discovery)
|
||||
log.Printf("cognitive-worker listening on %s", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
@@ -113,6 +178,19 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func durationEnv(name string, fallback time.Duration) time.Duration {
|
||||
value := strings.TrimSpace(os.Getenv(name))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := time.ParseDuration(value)
|
||||
if err != nil || parsed <= 0 {
|
||||
log.Printf("invalid %s=%q; using %s", name, value, fallback)
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
|
||||
for name := range service.providers {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
@@ -157,17 +235,20 @@ func refreshProviderDiscoveries(resolver bindingResolver, service discoveryServi
|
||||
}
|
||||
}
|
||||
|
||||
func loadLocalCPUProviderAPIKey() (string, error) {
|
||||
if _, set := os.LookupEnv("LOCAL_CPU_PROVIDER_API_KEY"); set {
|
||||
return "", fmt.Errorf("LOCAL_CPU_PROVIDER_API_KEY direct environment secret is forbidden; use LOCAL_CPU_PROVIDER_API_KEY_FILE")
|
||||
func loadProviderAPIKey(directEnvName, fileEnvName, refEnvName, defaultRef string) (string, error) {
|
||||
if _, set := os.LookupEnv(directEnvName); set {
|
||||
return "", fmt.Errorf("%s direct environment secret is forbidden; use %s", directEnvName, fileEnvName)
|
||||
}
|
||||
fileName := strings.TrimSpace(os.Getenv("LOCAL_CPU_PROVIDER_API_KEY_FILE"))
|
||||
fileName := strings.TrimSpace(os.Getenv(fileEnvName))
|
||||
if fileName == "" {
|
||||
return "", nil
|
||||
}
|
||||
ref := strings.TrimSpace(os.Getenv("LOCAL_CPU_PROVIDER_CREDENTIAL_REF"))
|
||||
ref := strings.TrimSpace(os.Getenv(refEnvName))
|
||||
if ref == "" {
|
||||
return "", fmt.Errorf("LOCAL_CPU_PROVIDER_CREDENTIAL_REF is required when LOCAL_CPU_PROVIDER_API_KEY_FILE is configured")
|
||||
ref = defaultRef
|
||||
}
|
||||
if ref == "" {
|
||||
return "", fmt.Errorf("%s is required when %s is configured", refEnvName, fileEnvName)
|
||||
}
|
||||
resolver, err := platformsecrets.NewFileResolver(envOr("SECRET_BACKEND_ROOT", "/run/secrets"))
|
||||
if err != nil {
|
||||
@@ -176,6 +257,50 @@ func loadLocalCPUProviderAPIKey() (string, error) {
|
||||
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeProviderAPIKey, Consumer: secretcontracts.RuntimeConsumerCognitive, Path: fileName})
|
||||
}
|
||||
|
||||
func loadLocalCPUProviderAPIKey() (string, error) {
|
||||
return loadProviderAPIKey("LOCAL_CPU_PROVIDER_API_KEY", "LOCAL_CPU_PROVIDER_API_KEY_FILE", "LOCAL_CPU_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-cpu/provider-api-key/v1")
|
||||
}
|
||||
|
||||
const (
|
||||
gpuProviderURLFile = "/run/secrets/llm_gpu_gateway_url"
|
||||
gpuProviderKeyFile = "/run/secrets/llm_gpu_gateway_api_key"
|
||||
)
|
||||
|
||||
func loadProtectedGPUConfiguration() (string, string, bool, error) {
|
||||
urlValue, err := readProtectedRuntimeValue(gpuProviderURLFile)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", "", false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
key, err := readProtectedRuntimeValue(gpuProviderKeyFile)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", "", false, fmt.Errorf("GPU provider key is not configured")
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", false, err
|
||||
}
|
||||
return urlValue, key, true, nil
|
||||
}
|
||||
|
||||
func readProtectedRuntimeValue(path string) (string, error) {
|
||||
const root = "/run/secrets"
|
||||
clean := filepath.Clean(path)
|
||||
if clean != root && !strings.HasPrefix(clean, root+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("protected runtime path outside secret root")
|
||||
}
|
||||
payload, err := os.ReadFile(clean)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
value := strings.TrimSpace(string(payload))
|
||||
if value == "" || strings.ContainsAny(value, "\r\n") {
|
||||
return "", fmt.Errorf("protected runtime value is empty or multiline")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func deterministicAdvisory(w http.ResponseWriter, r *http.Request) {
|
||||
request, ok := decodeAdvisoryRequest(w, r)
|
||||
if !ok {
|
||||
|
||||
@@ -43,3 +43,34 @@ func TestLoadLocalCPUProviderAPIKeyRejectsDirectEnvironmentSecret(t *testing.T)
|
||||
t.Fatal("expected direct environment secret to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProviderAPIKeySupportsMediumCredentialRef(t *testing.T) {
|
||||
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY", "")
|
||||
_ = os.Unsetenv("LOCAL_MEDIUM_PROVIDER_API_KEY")
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, "medium_provider_api_key")
|
||||
if err := os.WriteFile(path, []byte("medium-key\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", path)
|
||||
t.Setenv("LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
|
||||
t.Setenv("SECRET_BACKEND_ROOT", root)
|
||||
key, err := loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1")
|
||||
if err != nil || key != "medium-key" {
|
||||
t.Fatalf("unexpected medium key resolution result: err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadProviderAPIKeyRejectsMediumDirectEnvironmentSecret(t *testing.T) {
|
||||
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY", "must-not-be-used")
|
||||
t.Setenv("LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "")
|
||||
if _, err := loadProviderAPIKey("LOCAL_MEDIUM_PROVIDER_API_KEY", "LOCAL_MEDIUM_PROVIDER_API_KEY_FILE", "LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF", "credential://domovoy/local-medium/provider-api-key/v1"); err == nil {
|
||||
t.Fatal("expected direct medium environment secret to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadProtectedRuntimeValueRejectsPathOutsideSecretRoot(t *testing.T) {
|
||||
if _, err := readProtectedRuntimeValue("/tmp/not-a-domovoy-secret"); err == nil {
|
||||
t.Fatal("expected protected path rejection")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type qualificationObserver interface {
|
||||
ObserveQualification(context.Context, capability.ModelQualificationObservation) error
|
||||
}
|
||||
|
||||
type httpQualificationObserver struct{ resolver *httpBindingResolver }
|
||||
|
||||
func (o httpQualificationObserver) ObserveQualification(ctx context.Context, value capability.ModelQualificationObservation) error {
|
||||
if o.resolver == nil {
|
||||
return fmt.Errorf("qualification observer resolver is nil")
|
||||
}
|
||||
return o.resolver.post(ctx, "/v1/cognitive-registry/model-qualification-observations", value, nil)
|
||||
}
|
||||
|
||||
type modelQualificationRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
MaxOutputBytes int `json:"max_output_bytes"`
|
||||
ModelIDs []capability.ProviderModelID `json:"model_ids,omitempty"`
|
||||
}
|
||||
|
||||
type modelQualificationResult struct {
|
||||
ProviderModelID capability.ProviderModelID `json:"provider_model_id"`
|
||||
Status string `json:"status"`
|
||||
FinishReason cognitive.FinishReason `json:"finish_reason,omitempty"`
|
||||
LatencyMillis int64 `json:"latency_millis"`
|
||||
OutputObserved bool `json:"output_observed"`
|
||||
QualityAssessed bool `json:"quality_assessed"`
|
||||
Category capability.ModelRatingCategory `json:"category"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type modelQualificationResponse struct {
|
||||
Provider capability.ProviderInstanceID `json:"provider"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
Results []modelQualificationResult `json:"results"`
|
||||
}
|
||||
|
||||
func modelQualificationHandler(providers map[string]providerRegistration, owner string, observers ...qualificationObserver) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("provider")
|
||||
registration, ok := providers[name]
|
||||
if !ok || registration.Discovery == nil || registration.Provider == nil {
|
||||
http.Error(w, "provider qualification not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
var request modelQualificationRequest
|
||||
if !decodeJSON(w, r, 16<<10, &request) || request.Prompt == "" {
|
||||
return
|
||||
}
|
||||
if request.MaxOutputBytes < 1 {
|
||||
request.MaxOutputBytes = 512
|
||||
}
|
||||
if request.MaxOutputBytes > 4096 {
|
||||
http.Error(w, "max_output_bytes exceeds qualification bound", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
modelsFound, probe := registration.Discovery.Discover(r.Context())
|
||||
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
|
||||
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if len(request.ModelIDs) > 0 {
|
||||
modelsFound = slices.DeleteFunc(modelsFound, func(model models.DiscoveredModel) bool {
|
||||
return !slices.Contains(request.ModelIDs, model.ProviderModelID)
|
||||
})
|
||||
}
|
||||
if len(modelsFound) == 0 {
|
||||
http.Error(w, "requested model not discovered", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if len(modelsFound) > 16 {
|
||||
modelsFound = modelsFound[:16]
|
||||
}
|
||||
results := qualifyModels(r.Context(), registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
|
||||
if len(observers) > 0 && observers[0] != nil {
|
||||
if err := persistQualificationResults(r.Context(), observers[0], registration, results); err != nil {
|
||||
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = jsonEncode(w, modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results})
|
||||
})
|
||||
}
|
||||
|
||||
func qualifyModels(ctx context.Context, registration providerRegistration, owner, prompt string, maxOutputBytes int, discovered []models.DiscoveredModel) []modelQualificationResult {
|
||||
results := make([]modelQualificationResult, len(discovered))
|
||||
var wg sync.WaitGroup
|
||||
for index, model := range discovered {
|
||||
wg.Add(1)
|
||||
go func(index int, model models.DiscoveredModel) {
|
||||
defer wg.Done()
|
||||
started := time.Now()
|
||||
request := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("qualification:%s:%d", model.ProviderModelID, started.UnixNano())), ActorID: identity.ActorID("qualification-runtime"), PrincipalID: identity.PrincipalID(owner), ScopeID: authority.ScopeID("qualification-scope"), Message: prompt, Locale: "en-US", MaxOutputBytes: maxOutputBytes, ProviderBindingID: "qualification-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: registration.ProviderInstanceID, ModelDescriptorID: capability.ModelDescriptorID("qualification-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID}
|
||||
response, err := registration.Provider.Advise(ctx, request)
|
||||
result := modelQualificationResult{ProviderModelID: model.ProviderModelID, LatencyMillis: time.Since(started).Milliseconds(), QualityAssessed: false, Category: capability.ModelRatingUnrated}
|
||||
if err != nil {
|
||||
result.Status = "UNAVAILABLE"
|
||||
result.FinishReason = cognitive.FinishProviderUnavailable
|
||||
result.Error = err.Error()
|
||||
results[index] = result
|
||||
return
|
||||
}
|
||||
result.FinishReason = response.FinishReason
|
||||
result.OutputObserved = response.AdvisoryText != ""
|
||||
if response.FinishReason == cognitive.FinishCompleted && result.OutputObserved {
|
||||
result.Status = "REACHABLE"
|
||||
} else {
|
||||
result.Status = "RESPONDED_WITHOUT_QUALIFICATION"
|
||||
}
|
||||
results[index] = result
|
||||
}(index, model)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
func persistQualificationResults(ctx context.Context, observer qualificationObserver, registration providerRegistration, results []modelQualificationResult) error {
|
||||
for _, result := range results {
|
||||
observationID, err := ids.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status := capability.ModelQualificationRespondedWithoutRating
|
||||
switch result.Status {
|
||||
case "REACHABLE":
|
||||
status = capability.ModelQualificationReachable
|
||||
case "UNAVAILABLE":
|
||||
status = capability.ModelQualificationUnavailable
|
||||
}
|
||||
observation := capability.ModelQualificationObservation{
|
||||
ID: capability.ModelQualificationObservationID(observationID), ProviderInstanceID: registration.ProviderInstanceID, ProviderModelID: result.ProviderModelID,
|
||||
Status: status, FinishReason: string(result.FinishReason), LatencyMillis: result.LatencyMillis,
|
||||
OutputObserved: result.OutputObserved, QualityAssessed: false, Category: capability.ModelRatingUnrated,
|
||||
ObservedAtUnixMillis: time.Now().UnixMilli(),
|
||||
}
|
||||
if err := observation.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := observer.ObserveQualification(ctx, observation); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func jsonEncode(w http.ResponseWriter, value any) error {
|
||||
return json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
type qualificationProvider struct{}
|
||||
|
||||
func (qualificationProvider) Probe(context.Context) models.ProviderProbe {
|
||||
return models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
func (qualificationProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
|
||||
return []models.DiscoveredModel{{ProviderModelID: "model-a"}, {ProviderModelID: "model-b"}}, models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
func (qualificationProvider) Advise(_ context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
|
||||
if err := models.ValidateAdvisoryRequest(request); err != nil {
|
||||
return cognitive.AdvisoryResponse{}, err
|
||||
}
|
||||
return cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: "reachable", FinishReason: cognitive.FinishCompleted}, nil
|
||||
}
|
||||
|
||||
func TestModelQualificationUsesDiscoveredModelsAndLeavesQualityUnrated(t *testing.T) {
|
||||
provider := qualificationProvider{}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"gpu": {ProviderInstanceID: "gpu", Provider: provider, Discovery: provider}}, "owner"))
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
request := httptest.NewRequest(http.MethodPost, server.URL+"/v1/provider-discovery/gpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":128}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
mux.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("qualification status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
if !strings.Contains(response.Body.String(), `"status":"REACHABLE"`) || !strings.Contains(response.Body.String(), `"category":"UNRATED"`) || strings.Contains(response.Body.String(), `"quality_assessed":true`) {
|
||||
t.Fatalf("qualification overclaimed quality: %s", response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelQualificationRejectsUnboundedOutput(t *testing.T) {
|
||||
provider := qualificationProvider{}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"cpu": {ProviderInstanceID: "cpu", Provider: provider, Discovery: provider}}, "owner"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/provider-discovery/cpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":8192}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
mux.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusBadRequest {
|
||||
t.Fatalf("unbounded qualification status=%d", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
type qualificationObserverStub struct {
|
||||
observations []capability.ModelQualificationObservation
|
||||
}
|
||||
|
||||
func (s *qualificationObserverStub) ObserveQualification(_ context.Context, value capability.ModelQualificationObservation) error {
|
||||
s.observations = append(s.observations, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestModelQualificationPersistsReachabilityWithoutQuality(t *testing.T) {
|
||||
provider := qualificationProvider{}
|
||||
observer := &qualificationObserverStub{}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"cpu": {ProviderInstanceID: "cpu", Provider: provider, Discovery: provider}}, "owner", observer))
|
||||
request := httptest.NewRequest(http.MethodPost, "/v1/provider-discovery/cpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":128,"model_ids":["model-a"]}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
mux.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || len(observer.observations) != 1 {
|
||||
t.Fatalf("qualification persistence status=%d observations=%d body=%s", response.Code, len(observer.observations), response.Body.String())
|
||||
}
|
||||
if err := observer.observations[0].Validate(); err != nil || observer.observations[0].Status != capability.ModelQualificationReachable {
|
||||
t.Fatalf("invalid qualification observation: %+v err=%v", observer.observations[0], err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request capability.TaskEvaluationExecutionRequest
|
||||
if !decodeJSON(w, r, 256<<10, &request) {
|
||||
return
|
||||
}
|
||||
if err := request.Profile.Validate(); err != nil || request.CapabilityID == "" || request.CapabilityID != request.Profile.CapabilityID || request.BindingID == "" || request.BindingRevision < 1 || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || request.MaxOutputBytes < 1 || len(request.Cases) != len(request.Profile.RequiredCaseIDs) {
|
||||
http.Error(w, "invalid task evaluation execution request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
resolver, ok := g.resolver.(candidateBindingResolver)
|
||||
if !ok {
|
||||
http.Error(w, "exact candidate resolver unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
selected, err := resolver.ResolveCandidate(r.Context(), request.BindingID, request.BindingRevision)
|
||||
if err != nil || selected.Binding.ID != request.BindingID || selected.Binding.Revision != request.BindingRevision || selected.Binding.CapabilityID != request.CapabilityID || selected.Binding.OwnerPrincipalID != request.PrincipalID {
|
||||
http.Error(w, "cognitive binding snapshot unavailable", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged {
|
||||
http.Error(w, "external egress acknowledgement required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
provider := g.provider(selected)
|
||||
if provider == nil {
|
||||
http.Error(w, "cognitive provider unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
caseSet := make(map[string]struct{}, len(request.Profile.RequiredCaseIDs))
|
||||
for _, id := range request.Profile.RequiredCaseIDs {
|
||||
caseSet[id] = struct{}{}
|
||||
}
|
||||
observations := make([]capability.TaskCaseObservation, len(request.Cases))
|
||||
responses := make([]capability.TaskCaseResponse, len(request.Cases))
|
||||
var wg sync.WaitGroup
|
||||
seenCases := make(map[string]struct{}, len(request.Cases))
|
||||
for i, item := range request.Cases {
|
||||
if item.CaseID == "" || item.Prompt == "" || item.EvidenceArtifactRef == "" {
|
||||
http.Error(w, "invalid task evaluation case", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if _, exists := caseSet[item.CaseID]; !exists {
|
||||
http.Error(w, "task evaluation case is not in profile", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if _, exists := seenCases[item.CaseID]; exists {
|
||||
http.Error(w, "duplicate task evaluation case", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
seenCases[item.CaseID] = struct{}{}
|
||||
wg.Add(1)
|
||||
go func(index int, item capability.TaskEvaluationCase) {
|
||||
defer wg.Done()
|
||||
started := time.Now()
|
||||
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
|
||||
response, invokeErr := g.invokeAdmitted(r.Context(), string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
|
||||
observation := capability.TaskCaseObservation{CaseID: item.CaseID, LatencyMicros: time.Since(started).Microseconds(), EvidenceArtifactRef: item.EvidenceArtifactRef}
|
||||
responses[index] = capability.TaskCaseResponse{CaseID: item.CaseID, ResponseText: response.AdvisoryText, LatencyMicros: observation.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef}
|
||||
if invokeErr == nil && response.FinishReason == cognitive.FinishCompleted && response.AdvisoryText != "" {
|
||||
// Invocation success proves reachability only. Without an
|
||||
// independent verifier and answer evidence, semantic support and
|
||||
// quality must remain UNKNOWN rather than becoming false evidence.
|
||||
observation.Result = capability.EvaluationUnknown
|
||||
observation.QualityScoreMillis = 0
|
||||
} else {
|
||||
observation.Result = capability.EvaluationUnknown
|
||||
observation.CriticalFailure = invokeErr != nil
|
||||
}
|
||||
observations[index] = observation
|
||||
}(i, item)
|
||||
}
|
||||
if len(seenCases) != len(caseSet) {
|
||||
http.Error(w, "task evaluation case set is incomplete", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
wg.Wait()
|
||||
writeTaskEvaluationJSON(w, capability.TaskEvaluationExecutionResponse{ProfileID: request.Profile.ID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, Observations: observations, Responses: responses, ObservedAtUnixMillis: time.Now().UnixMilli()})
|
||||
}
|
||||
|
||||
func writeTaskEvaluationJSON(w http.ResponseWriter, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type candidateRegistryStub struct {
|
||||
registryStub
|
||||
selected capability.ResolvedCognitiveBinding
|
||||
}
|
||||
|
||||
func (s *candidateRegistryStub) ResolveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error) {
|
||||
return s.selected, nil
|
||||
}
|
||||
|
||||
func TestTaskEvaluationHandlerInvokesEveryBoundedCase(t *testing.T) {
|
||||
selected := gatewaySelection()
|
||||
registry := &candidateRegistryStub{selected: selected}
|
||||
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, FinishReason: cognitive.FinishCompleted, AdvisoryText: "observed response"}}
|
||||
gateway := cognitiveGateway{
|
||||
resolver: registry,
|
||||
providers: map[capability.ProviderKind][]providerRegistration{
|
||||
selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, EndpointRef: selected.Provider.EndpointRef, CredentialRef: selected.Provider.CredentialRef, Provider: provider}},
|
||||
},
|
||||
now: func() time.Time { return time.UnixMilli(1000) },
|
||||
}
|
||||
profile := capability.TaskEvaluationProfile{ID: "profile-general", Revision: 1, CapabilityID: selected.Binding.CapabilityID, Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-a", "case-b"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||
request := capability.TaskEvaluationExecutionRequest{Profile: profile, CapabilityID: selected.Binding.CapabilityID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, ActorID: identity.ActorID("actor-1"), PrincipalID: selected.Binding.OwnerPrincipalID, ScopeID: authority.ScopeID("scope-1"), MaxOutputBytes: 512, EgressAcknowledged: true, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "one", EvidenceArtifactRef: "artifact:a"}, {CaseID: "case-b", Prompt: "two", EvidenceArtifactRef: "artifact:b"}}}
|
||||
payload, _ := json.Marshal(request)
|
||||
recorder := httptest.NewRecorder()
|
||||
gateway.taskEvaluationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/execute", bytes.NewReader(payload)))
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
var response capability.TaskEvaluationExecutionResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(response.Observations) != 2 || response.Observations[0].Result != capability.EvaluationUnknown || response.Observations[1].Result != capability.EvaluationUnknown {
|
||||
t.Fatalf("unexpected observations: %+v", response.Observations)
|
||||
}
|
||||
for _, observation := range response.Observations {
|
||||
if observation.QualityScoreMillis != 0 || observation.CriticalFailure {
|
||||
t.Fatalf("reachability-only evidence must not carry quality or failure claims: %+v", observation)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
workdomain "git.cin.su/m/Assistant/modules/work/domain"
|
||||
)
|
||||
|
||||
var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one")
|
||||
|
||||
func prepareDevelopmentGate(value work.DevelopmentGate, now time.Time) (work.DevelopmentGate, error) {
|
||||
if value.Stage != work.DevelopmentEvidenceCollected || value.Revision != 1 {
|
||||
return work.DevelopmentGate{}, errDevelopmentGateCreate
|
||||
}
|
||||
value.UpdatedAtUnixMillis = now.UnixMilli()
|
||||
if err := workdomain.ValidateDevelopmentGate(value); err != nil {
|
||||
return work.DevelopmentGate{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func advanceDevelopmentGate(value work.DevelopmentGate, next work.DevelopmentStage, now time.Time) (work.DevelopmentGate, error) {
|
||||
advanced, err := workdomain.AdvanceDevelopmentGate(value, next)
|
||||
if err != nil {
|
||||
return work.DevelopmentGate{}, err
|
||||
}
|
||||
advanced.Revision = value.Revision + 1
|
||||
advanced.UpdatedAtUnixMillis = now.UnixMilli()
|
||||
return advanced, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
func TestDevelopmentGateControllerRequiresSequentialVerifiedEvidence(t *testing.T) {
|
||||
now := time.UnixMilli(10)
|
||||
gate, err := prepareDevelopmentGate(work.DevelopmentGate{ID: "gate-1", Revision: 1, Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"evidence-1"}}, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.ProposalRef = "proposal-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentProposalCreated, now)
|
||||
if err != nil || gate.Revision != 2 {
|
||||
t.Fatalf("proposal transition failed: %#v %v", gate, err)
|
||||
}
|
||||
gate.TaskPacketRef = "task-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentTaskAdmitted, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.WorktreeRef = "isolated-worktree"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentIsolated, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gate.ProducerRunRefs = []work.AgentRunID{"producer"}
|
||||
gate.VerifierRunRefs = []work.AgentRunID{"verifier"}
|
||||
gate.VerificationEvidence = "verification-1"
|
||||
gate, err = advanceDevelopmentGate(gate, work.DevelopmentVerified, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := advanceDevelopmentGate(gate, work.DevelopmentPromoted, now); err == nil {
|
||||
t.Fatal("promotion bypassed canary")
|
||||
}
|
||||
}
|
||||
+182
-1
@@ -116,6 +116,10 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
researchClient, err := newResearchRuntimeClient(envOr("RESEARCH_RUNTIME_URL", "http://research-runtime:8087"), &http.Client{Timeout: 12 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
@@ -178,6 +182,135 @@ func main() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(session)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/research/web", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var request epistemics.WebResearchRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid web research request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result, err := researchClient.EvaluateWeb(r.Context(), request)
|
||||
if err != nil {
|
||||
http.Error(w, "web research unavailable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/proposals", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Evidence []work.ExtensionEvidence `json:"evidence"`
|
||||
Proposal work.ExtensionProposalRequest `json:"proposal"`
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid development proposal", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
proposal, err := workdomain.BuildExtensionProposal(request.Evidence, request.Proposal)
|
||||
if err != nil {
|
||||
http.Error(w, "development proposal rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
proposal.RequestedByPrincipalID = string(session.PrincipalID)
|
||||
if err := workRepository.SaveExtensionProposal(r.Context(), proposal); err != nil {
|
||||
http.Error(w, "development proposal conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(proposal)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/scaffolds", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request work.DevelopmentScaffoldRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
http.Error(w, "invalid development scaffold", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
scaffold, err := workdomain.BuildDevelopmentScaffold(request)
|
||||
if err != nil {
|
||||
http.Error(w, "development scaffold rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
scaffold.RequestedByPrincipalID = string(session.PrincipalID)
|
||||
if err := workRepository.SaveDevelopmentScaffold(r.Context(), scaffold); err != nil {
|
||||
http.Error(w, "development scaffold conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(scaffold)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/gates", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var gate work.DevelopmentGate
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&gate); err != nil {
|
||||
http.Error(w, "invalid development gate", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gate, err := prepareDevelopmentGate(gate, time.Now())
|
||||
if err != nil {
|
||||
http.Error(w, "development gate rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, 0); err != nil {
|
||||
http.Error(w, "development gate conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(gate)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/development/gates/{id}/advance", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateCreator(w, r); !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
ExpectedRevision int64 `json:"expected_revision"`
|
||||
NextStage work.DevelopmentStage `json:"next_stage"`
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.NextStage == "" {
|
||||
http.Error(w, "invalid development gate transition", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
gate, err := workRepository.LoadDevelopmentGate(r.Context(), r.PathValue("id"))
|
||||
if err != nil || gate.Revision != request.ExpectedRevision {
|
||||
http.Error(w, "development gate revision conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
gate, err = advanceDevelopmentGate(gate, request.NextStage, time.Now())
|
||||
if err != nil {
|
||||
http.Error(w, "development gate transition rejected", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, request.ExpectedRevision); err != nil {
|
||||
http.Error(w, "development gate conflict", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(gate)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
@@ -834,6 +967,20 @@ func main() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(binding)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/cognitive-bindings/{id}/revisions/{revision}/snapshot", func(w http.ResponseWriter, r *http.Request) {
|
||||
revision, err := strconv.ParseInt(r.PathValue("revision"), 10, 64)
|
||||
if err != nil || revision < 1 {
|
||||
http.Error(w, "invalid cognitive binding revision", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
snapshot, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), capability.CapabilityBindingID(r.PathValue("id")), revision)
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive binding snapshot not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(snapshot)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/cognitive-bindings/resolve", func(w http.ResponseWriter, r *http.Request) {
|
||||
var requirement capability.CognitiveRequirement
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, 16*1024+1))
|
||||
@@ -842,7 +989,13 @@ func main() {
|
||||
http.Error(w, "invalid cognitive requirement", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
|
||||
var candidates []capability.ResolvedCognitiveBinding
|
||||
var err error
|
||||
if requirement.TaskProfileID != "" {
|
||||
candidates, err = capabilityRepository.LoadCognitiveCandidatesForProfile(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID, requirement.TaskProfileID)
|
||||
} else {
|
||||
candidates, err = capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
@@ -891,6 +1044,34 @@ func main() {
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/cognitive-registry/task-evaluations", func(w http.ResponseWriter, r *http.Request) {
|
||||
var value capability.TaskEvaluation
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&value); err != nil || capabilitydomain.ValidateTaskEvaluation(value) != nil {
|
||||
http.Error(w, "invalid task evaluation", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := capabilityRepository.SaveTaskEvaluation(r.Context(), value); err != nil {
|
||||
http.Error(w, "save task evaluation", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/cognitive-registry/model-qualification-observations", func(w http.ResponseWriter, r *http.Request) {
|
||||
var value capability.ModelQualificationObservation
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&value); err != nil || value.Validate() != nil {
|
||||
http.Error(w, "invalid model qualification observation", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := capabilityRepository.SaveModelQualificationObservation(r.Context(), value); err != nil {
|
||||
http.Error(w, "save model qualification observation", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
type researchRuntimeClient struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newResearchRuntimeClient(rawURL string, client *http.Client) (researchRuntimeClient, error) {
|
||||
base := strings.TrimRight(strings.TrimSpace(rawURL), "/")
|
||||
if base == "" {
|
||||
return researchRuntimeClient{}, fmt.Errorf("research runtime URL is empty")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return researchRuntimeClient{baseURL: base, client: client}, nil
|
||||
}
|
||||
|
||||
func (c researchRuntimeClient) EvaluateWeb(ctx context.Context, request epistemics.WebResearchRequest) (epistemics.ResearchResult, error) {
|
||||
payload, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/research/web/evaluate", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
response, err := c.client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return epistemics.ResearchResult{}, fmt.Errorf("research runtime status %d", response.StatusCode)
|
||||
}
|
||||
var result epistemics.ResearchResult
|
||||
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
||||
return epistemics.ResearchResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/epistemics"
|
||||
)
|
||||
|
||||
func TestResearchRuntimeClientForwardsBoundedWebRequest(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/v1/research/web/evaluate" || r.Header.Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var request epistemics.WebResearchRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.URL != "https://assistant.cin.su/healthz" {
|
||||
t.Fatalf("unexpected request body: %#v %v", request, err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(epistemics.ResearchResult{PlanID: request.Plan.ID, Findings: []epistemics.ResearchFinding{{PropositionKey: request.PropositionKey, State: epistemics.FindingSupported}}})
|
||||
}))
|
||||
defer server.Close()
|
||||
client, err := newResearchRuntimeClient(server.URL, server.Client())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.EvaluateWeb(context.Background(), epistemics.WebResearchRequest{Plan: epistemics.ResearchPlan{ID: "plan-1"}, PropositionKey: "health", URL: "https://assistant.cin.su/healthz"})
|
||||
if err != nil || result.PlanID != "plan-1" || len(result.Findings) != 1 || result.Findings[0].State != epistemics.FindingSupported {
|
||||
t.Fatalf("unexpected result: %#v %v", result, err)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
|
||||
if r.URL.Path == "/" || r.URL.Path == "/login" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
|
||||
g.serveEmbeddedHTML(w, "chat.html")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -354,6 +354,18 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginRouteServesChatShell(t *testing.T) {
|
||||
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/login", nil))
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "id=\"username\"") || !strings.Contains(response.Body.String(), "id=\"password\"") {
|
||||
t.Fatalf("login response = %d", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvisoryModeRejectsInvalidConfiguration(t *testing.T) {
|
||||
_, err := newHandlerWithAdvisoryMode("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, "browser-selected")
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid advisory binding mode") {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -29,9 +31,21 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
defer repository.Close()
|
||||
status := newWorkerStatus(*workerID, *once)
|
||||
listenAddr := os.Getenv("REFLECTION_LISTEN_ADDR")
|
||||
if listenAddr == "" {
|
||||
listenAddr = ":8092"
|
||||
}
|
||||
go func() {
|
||||
if err := http.ListenAndServe(listenAddr, status.handler()); err != nil {
|
||||
log.Printf("reflection status server stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
now := time.Now().UnixMilli()
|
||||
status.cycleStarted(time.Now())
|
||||
if _, err := repository.RecoverReflectionClaims(ctx, now); err != nil {
|
||||
status.cycleFailed(time.Now(), err)
|
||||
panic(err)
|
||||
}
|
||||
candidate, claimErr := repository.ClaimReflectionCandidate(ctx, *workerID, now, lease.Milliseconds())
|
||||
@@ -39,16 +53,19 @@ func main() {
|
||||
// This worker only closes the durable evaluation loop. Any cognitive
|
||||
// proposal remains a candidate and must be admitted by a separate governed flow.
|
||||
if err := repository.ResolveReflectionCandidate(ctx, candidate.ID, *workerID, time.Now().UnixMilli()); err != nil {
|
||||
status.cycleFailed(time.Now(), err)
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("resolved reflection candidate %s\n", candidate.ID)
|
||||
} else if *once {
|
||||
fmt.Println("no reflection candidate")
|
||||
}
|
||||
status.cycleFinished(time.Now(), map[bool]string{true: "candidate-resolved", false: "idle"}[claimErr == nil])
|
||||
if *once {
|
||||
return
|
||||
}
|
||||
if err := repository.WaitForReflectionCandidate(ctx, *pollInterval); err != nil {
|
||||
status.cycleFailed(time.Now(), err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/runtime/health"
|
||||
)
|
||||
|
||||
type workerStatus struct {
|
||||
mu sync.RWMutex
|
||||
workerID string
|
||||
once bool
|
||||
startedAt time.Time
|
||||
lastCycleAt time.Time
|
||||
lastCompleted time.Time
|
||||
lastResult string
|
||||
lastError string
|
||||
}
|
||||
|
||||
func newWorkerStatus(workerID string, once bool) *workerStatus {
|
||||
return &workerStatus{workerID: workerID, once: once, startedAt: time.Now().UTC()}
|
||||
}
|
||||
|
||||
func (s *workerStatus) cycleStarted(at time.Time) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastCycleAt = at.UTC()
|
||||
s.lastResult = "running"
|
||||
s.lastError = ""
|
||||
}
|
||||
|
||||
func (s *workerStatus) cycleFinished(at time.Time, result string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastCompleted = at.UTC()
|
||||
s.lastResult = result
|
||||
s.lastError = ""
|
||||
}
|
||||
|
||||
func (s *workerStatus) cycleFailed(at time.Time, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastCompleted = at.UTC()
|
||||
s.lastResult = "failed"
|
||||
s.lastError = err.Error()
|
||||
}
|
||||
|
||||
func (s *workerStatus) handler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "reflection-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
mux.HandleFunc("/status", func(w http.ResponseWriter, _ *http.Request) {
|
||||
s.mu.RLock()
|
||||
response := struct {
|
||||
Runtime string `json:"runtime"`
|
||||
WorkerID string `json:"worker_id"`
|
||||
Mode string `json:"mode"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
LastCycleAt time.Time `json:"last_cycle_at,omitempty"`
|
||||
LastCompleted time.Time `json:"last_completed_at,omitempty"`
|
||||
LastResult string `json:"last_result"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}{Runtime: "reflection-worker", WorkerID: s.workerID, Mode: "durable-reflection-loop", StartedAt: s.startedAt, LastCycleAt: s.lastCycleAt, LastCompleted: s.lastCompleted, LastResult: s.lastResult, LastError: s.lastError}
|
||||
if s.once {
|
||||
response.Mode = "durable-reflection-once"
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
})
|
||||
return mux
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWorkerStatusReportsCycle(t *testing.T) {
|
||||
status := newWorkerStatus("test-worker", false)
|
||||
status.cycleStarted(time.Date(2026, 8, 23, 10, 0, 0, 0, time.UTC))
|
||||
status.cycleFinished(time.Date(2026, 8, 23, 10, 0, 1, 0, time.UTC), "idle")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
status.handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/status", nil))
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d, want 200", recorder.Code)
|
||||
}
|
||||
body := recorder.Body.String()
|
||||
for _, expected := range []string{"\"runtime\":\"reflection-worker\"", "\"worker_id\":\"test-worker\"", "\"last_result\":\"idle\""} {
|
||||
if !strings.Contains(body, expected) {
|
||||
t.Fatalf("status body missing %q: %s", expected, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerHealthIsAvailable(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
newWorkerStatus("test-worker", true).handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("health status code = %d, want 200", recorder.Code)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "research-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "research-runtime", Probes: []health.Probe{{Name: "epistemics-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
|
||||
mux.Handle("/v1/research/status", truthRuntimeStatusHandler(repository))
|
||||
mux.HandleFunc("POST /v1/research/evaluate", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request epistemics.ResearchEvaluationRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 2<<20)).Decode(&request); err != nil {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
)
|
||||
|
||||
type truthRuntimeStatusReader interface {
|
||||
TruthRuntimeStatus(context.Context) (postgres.TruthRuntimeStatus, error)
|
||||
}
|
||||
|
||||
type truthRuntimeStatusResponse struct {
|
||||
Runtime string `json:"runtime"`
|
||||
Status string `json:"status"`
|
||||
BundleCount int64 `json:"bundle_count"`
|
||||
ClaimsByVerification map[string]int64 `json:"claims_by_verification"`
|
||||
BeliefsByState map[string]int64 `json:"beliefs_by_state"`
|
||||
SourcesByFreshness map[string]int64 `json:"sources_by_freshness"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
|
||||
func truthRuntimeStatusHandler(reader truthRuntimeStatusReader) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/research/status" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
status, err := reader.TruthRuntimeStatus(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "research status unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(truthRuntimeStatusResponse{
|
||||
Runtime: "research-runtime",
|
||||
Status: "ready",
|
||||
BundleCount: status.BundleCount,
|
||||
ClaimsByVerification: status.ClaimsByVerification,
|
||||
BeliefsByState: status.BeliefsByState,
|
||||
SourcesByFreshness: status.SourcesByFreshness,
|
||||
ObservedAtUnixMillis: status.ObservedAtUnixMillis,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
)
|
||||
|
||||
type fakeTruthRuntimeStatusReader struct {
|
||||
status postgres.TruthRuntimeStatus
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeTruthRuntimeStatusReader) TruthRuntimeStatus(context.Context) (postgres.TruthRuntimeStatus, error) {
|
||||
return f.status, f.err
|
||||
}
|
||||
|
||||
func TestTruthRuntimeStatusReportsEvidenceCounts(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
truthRuntimeStatusHandler(fakeTruthRuntimeStatusReader{status: postgres.TruthRuntimeStatus{
|
||||
BundleCount: 4,
|
||||
ClaimsByVerification: map[string]int64{"UNVERIFIED": 3, "VERIFIED": 1},
|
||||
BeliefsByState: map[string]int64{"PROVISIONAL": 4},
|
||||
SourcesByFreshness: map[string]int64{"FRESH": 4},
|
||||
ObservedAtUnixMillis: 1700000000000,
|
||||
}}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/research/status", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d", response.Code)
|
||||
}
|
||||
for _, expected := range []string{"\"status\":\"ready\"", "\"bundle_count\":4", "\"VERIFIED\":1", "\"PROVISIONAL\":4", "1700000000000"} {
|
||||
if !strings.Contains(response.Body.String(), expected) {
|
||||
t.Fatalf("response missing %q: %s", expected, response.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruthRuntimeStatusFailsClosedOnDatabaseError(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
truthRuntimeStatusHandler(fakeTruthRuntimeStatusReader{err: errors.New("database unavailable")}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/research/status", nil))
|
||||
if response.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status code = %d", response.Code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
const verifierIdentity = "deterministic-task-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)
|
||||
addr := ":8091"
|
||||
if value := strings.TrimSpace(os.Getenv("LISTEN_ADDR")); value != "" {
|
||||
addr = value
|
||||
}
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
func TestVerifyExactTextProducesIndependentEvidence(t *testing.T) {
|
||||
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "case-1", ResponseText: " answer ", EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}})
|
||||
recorder := httptest.NewRecorder()
|
||||
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
|
||||
var response capability.TaskCaseVerificationResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationSupported || response.QualityScoreMillis != 900 || response.VerifierIdentity == "" || response.VerificationEvidenceRef == "" {
|
||||
t.Fatalf("unexpected verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyMismatchIsUnsupportedWithEvidence(t *testing.T) {
|
||||
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "case-1", ResponseText: "wrong", EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}})
|
||||
recorder := httptest.NewRecorder()
|
||||
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
|
||||
var response capability.TaskCaseVerificationResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationUnsupported || response.QualityScoreMillis != 0 || response.VerificationEvidenceRef == "" {
|
||||
t.Fatalf("unexpected mismatch response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyJSONFieldEquals(t *testing.T) {
|
||||
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "json-1", ResponseText: `{"answer":42}`, EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationJSONFieldEquals, FieldPath: "answer", ExpectedText: "42", MinimumQualityMillis: 850}})
|
||||
recorder := httptest.NewRecorder()
|
||||
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
|
||||
var response capability.TaskCaseVerificationResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationSupported || response.QualityScoreMillis != 850 {
|
||||
t.Fatalf("unexpected JSON verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
"git.cin.su/m/Assistant/modules/work/application"
|
||||
"git.cin.su/m/Assistant/platform/secrets"
|
||||
"git.cin.su/m/Assistant/runtime/health"
|
||||
@@ -26,9 +28,14 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer repository.Close()
|
||||
evidenceSink, err := newCoreTaskEvaluationSink(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), &http.Client{Timeout: 12 * time.Second})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", health.Handler{Runtime: "work-graph-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
|
||||
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "work-graph-runtime", Probes: []health.Probe{{Name: "work-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}})
|
||||
mux.Handle("/v1/work-graphs/status", workGraphStatusHandler(repository))
|
||||
mux.HandleFunc("POST /v1/work-graphs/schedule", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.GraphScheduleRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
|
||||
@@ -42,19 +49,97 @@ func main() {
|
||||
}
|
||||
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/schedule", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.TaskEvaluationScheduleRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid task evaluation graph request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
graph, ready, err := application.AdmitAndScheduleTaskEvaluation(r.Context(), repository, request)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/orchestration/next", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request work.SolutionSearchInput
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid orchestration input", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
decision, err := application.DecideSolutionSearch(request)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid orchestration input", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, decision)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/orchestration/wave", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request work.SolutionSearchWaveRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid orchestration wave request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
result, err := application.OrchestrateSolutionSearch(r.Context(), repository, request)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid orchestration wave request", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, result)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/result-callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request work.GraphResultCallback
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid graph result callback", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
graph, err := application.RecordGraphResultCandidate(r.Context(), repository, repository, request)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid graph result callback", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, graph)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.GraphCompleteRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid graph completion request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
graph, err := application.CompleteGraphNode(r.Context(), repository, request.GraphID, request.Revision, request.NodeID, request.Succeeded)
|
||||
result, err := application.CompleteGraphNodeWithReplan(r.Context(), repository, request)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, result)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request work.TaskEvaluationEvidenceCallback
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid task evaluation callback", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
graph, err := application.CompleteTaskEvaluationNode(r.Context(), repository, evidenceSink, request)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, graph)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/work-graphs/synthesis", func(w http.ResponseWriter, r *http.Request) {
|
||||
var request application.GraphSynthesisRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
|
||||
http.Error(w, "invalid graph synthesis request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
conflict, synthesis, graph, err := application.SynthesizeAndCompleteGraph(r.Context(), repository, repository, request)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"conflict": conflict, "synthesis": synthesis, "graph": graph})
|
||||
})
|
||||
addr := os.Getenv("LISTEN_ADDR")
|
||||
if addr == "" {
|
||||
addr = ":8088"
|
||||
@@ -63,6 +148,13 @@ func main() {
|
||||
log.Fatal(http.ListenAndServe(addr, mux))
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, value any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(value)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
)
|
||||
|
||||
type workGraphStatusReader interface {
|
||||
RuntimeStatus(context.Context) (postgres.WorkGraphRuntimeStatus, error)
|
||||
}
|
||||
|
||||
type workGraphStatusResponse struct {
|
||||
Runtime string `json:"runtime"`
|
||||
Status string `json:"status"`
|
||||
GraphsByState map[string]int64 `json:"graphs_by_state"`
|
||||
NodesByState map[string]int64 `json:"nodes_by_state"`
|
||||
QueueByState map[string]int64 `json:"queue_by_state"`
|
||||
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
|
||||
}
|
||||
|
||||
func workGraphStatusHandler(reader workGraphStatusReader) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/work-graphs/status" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
status, err := reader.RuntimeStatus(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "work graph status unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(workGraphStatusResponse{
|
||||
Runtime: "work-graph-runtime",
|
||||
Status: "ready",
|
||||
GraphsByState: status.GraphsByState,
|
||||
NodesByState: status.NodesByState,
|
||||
QueueByState: status.QueueByState,
|
||||
ObservedAtUnixMillis: status.ObservedAtUnixMillis,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/postgres"
|
||||
)
|
||||
|
||||
type fakeWorkGraphStatusReader struct {
|
||||
status postgres.WorkGraphRuntimeStatus
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeWorkGraphStatusReader) RuntimeStatus(context.Context) (postgres.WorkGraphRuntimeStatus, error) {
|
||||
return f.status, f.err
|
||||
}
|
||||
|
||||
func TestWorkGraphStatusReportsDurableCounts(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workGraphStatusHandler(fakeWorkGraphStatusReader{status: postgres.WorkGraphRuntimeStatus{
|
||||
GraphsByState: map[string]int64{"RUNNING": 2},
|
||||
NodesByState: map[string]int64{"LEASED": 3, "SUCCEEDED": 1},
|
||||
QueueByState: map[string]int64{"QUEUED": 3},
|
||||
ObservedAtUnixMillis: 1700000000000,
|
||||
}}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/work-graphs/status", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d", response.Code)
|
||||
}
|
||||
body := response.Body.String()
|
||||
for _, expected := range []string{"\"status\":\"ready\"", "\"RUNNING\":2", "\"LEASED\":3", "\"QUEUED\":3", "1700000000000"} {
|
||||
if !contains(body, expected) {
|
||||
t.Fatalf("response missing %q: %s", expected, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkGraphStatusFailsClosedOnDatabaseError(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
workGraphStatusHandler(fakeWorkGraphStatusReader{err: errors.New("database unavailable")}).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/work-graphs/status", nil))
|
||||
if response.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status code = %d", response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(value, expected string) bool {
|
||||
for i := 0; i+len(expected) <= len(value); i++ {
|
||||
if value[i:i+len(expected)] == expected {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
type coreTaskEvaluationSink struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newCoreTaskEvaluationSink(baseURL string, client *http.Client) (*coreTaskEvaluationSink, error) {
|
||||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
if baseURL == "" {
|
||||
return nil, fmt.Errorf("core runtime URL is required")
|
||||
}
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
return &coreTaskEvaluationSink{baseURL: baseURL, client: client}, nil
|
||||
}
|
||||
|
||||
func (s *coreTaskEvaluationSink) SaveTaskEvaluation(ctx context.Context, value capability.TaskEvaluation) error {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, s.baseURL+"/v1/cognitive-registry/task-evaluations", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response, err := s.client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return fmt.Errorf("save task evaluation: status %s", response.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -26,6 +26,7 @@ type PerformanceEvidenceID ids.ID
|
||||
type BillingMode string
|
||||
type AuthMode string
|
||||
type SelectionObjective string
|
||||
type ModelRatingCategory string
|
||||
|
||||
const (
|
||||
QualificationDiscovered QualificationState = "DISCOVERED"
|
||||
@@ -73,6 +74,11 @@ const (
|
||||
SelectionQuality SelectionObjective = "QUALITY"
|
||||
SelectionSpeed SelectionObjective = "SPEED"
|
||||
SelectionEconomy SelectionObjective = "ECONOMY"
|
||||
|
||||
ModelRatingUnrated ModelRatingCategory = "UNRATED"
|
||||
ModelRatingFast ModelRatingCategory = "FAST"
|
||||
ModelRatingBalanced ModelRatingCategory = "BALANCED"
|
||||
ModelRatingQuality ModelRatingCategory = "QUALITY"
|
||||
)
|
||||
|
||||
// ProviderInstance contains only opaque references to endpoint and credentials.
|
||||
@@ -154,16 +160,33 @@ type CognitiveRequirement struct {
|
||||
ExcludedBindingIDs []CapabilityBindingID
|
||||
Objective SelectionObjective
|
||||
MaxCostMicros *int64
|
||||
TaskProfileID TaskEvaluationProfileID
|
||||
}
|
||||
|
||||
// ResolvedCognitiveBinding is a provider-neutral invocation snapshot selected
|
||||
// by Capability. It contains references and declared metadata, never secrets.
|
||||
type ResolvedCognitiveBinding struct {
|
||||
Binding CognitiveBinding
|
||||
Provider ProviderInstance
|
||||
Model ModelDescriptor
|
||||
Evaluation *EvaluatedCapability
|
||||
Performance *PerformanceEvidence
|
||||
Binding CognitiveBinding
|
||||
Provider ProviderInstance
|
||||
Model ModelDescriptor
|
||||
Evaluation *EvaluatedCapability
|
||||
Performance *PerformanceEvidence
|
||||
TaskEvaluation *TaskEvaluation
|
||||
Qualification *ModelQualificationObservation
|
||||
Rating *ModelRating
|
||||
}
|
||||
|
||||
// ModelRating is a derived, provider-neutral explanation of why an eligible
|
||||
// model is routed. It never grants qualification or activation authority.
|
||||
type ModelRating struct {
|
||||
TaskProfileID TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
|
||||
TaskScoreMillis int64 `json:"task_score_millis,omitempty"`
|
||||
Category ModelRatingCategory `json:"category"`
|
||||
OverallScoreMillis int64 `json:"overall_score_millis"`
|
||||
QualityScoreMillis int64 `json:"quality_score_millis"`
|
||||
ReliabilityScoreMillis int64 `json:"reliability_score_millis"`
|
||||
LatencyScoreMillis int64 `json:"latency_score_millis"`
|
||||
EvidenceScoreMillis int64 `json:"evidence_score_millis"`
|
||||
}
|
||||
|
||||
type CognitiveResultClass string
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package capability
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type ModelQualificationObservationID ids.ID
|
||||
type ModelQualificationStatus string
|
||||
|
||||
const (
|
||||
ModelQualificationReachable ModelQualificationStatus = "REACHABLE"
|
||||
ModelQualificationRespondedWithoutRating ModelQualificationStatus = "RESPONDED_WITHOUT_QUALIFICATION"
|
||||
ModelQualificationUnavailable ModelQualificationStatus = "UNAVAILABLE"
|
||||
)
|
||||
|
||||
// ModelQualificationObservation is operational evidence only. It records
|
||||
// reachability and latency, never quality, activation, routing priority or
|
||||
// provider credentials.
|
||||
type ModelQualificationObservation struct {
|
||||
ID ModelQualificationObservationID
|
||||
ProviderInstanceID ProviderInstanceID
|
||||
ProviderModelID ProviderModelID
|
||||
Status ModelQualificationStatus
|
||||
FinishReason string
|
||||
LatencyMillis int64
|
||||
OutputObserved bool
|
||||
QualityAssessed bool
|
||||
Category ModelRatingCategory
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
|
||||
func (o ModelQualificationObservation) Validate() error {
|
||||
if o.ID == "" || o.ProviderInstanceID == "" || o.ProviderModelID == "" || o.FinishReason == "" || o.LatencyMillis < 0 || o.ObservedAtUnixMillis < 1 {
|
||||
return errors.New("model qualification observation identity and timing are incomplete")
|
||||
}
|
||||
switch o.Status {
|
||||
case ModelQualificationReachable, ModelQualificationRespondedWithoutRating, ModelQualificationUnavailable:
|
||||
default:
|
||||
return errors.New("model qualification observation status is unsupported")
|
||||
}
|
||||
if o.Category != ModelRatingUnrated || o.QualityAssessed {
|
||||
return errors.New("model qualification observation cannot assert quality")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package capability
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/artifact"
|
||||
"git.cin.su/m/Assistant/platform/ids"
|
||||
)
|
||||
|
||||
type TaskEvaluationProfileID ids.ID
|
||||
type TaskEvaluationID ids.ID
|
||||
type TaskEvaluationProfileStage string
|
||||
|
||||
const (
|
||||
TaskEvaluationGeneral TaskEvaluationProfileStage = "GENERAL"
|
||||
TaskEvaluationAdditional TaskEvaluationProfileStage = "ADDITIONAL"
|
||||
)
|
||||
|
||||
// TaskEvaluationProfile is the extensible, provider-neutral description of a
|
||||
// task-specific qualification run. Case payloads live in Artifact; the
|
||||
// profile stores only stable case references and scoring policy.
|
||||
type TaskEvaluationProfile struct {
|
||||
ID TaskEvaluationProfileID
|
||||
Revision int64
|
||||
CapabilityID CapabilityID
|
||||
Stage TaskEvaluationProfileStage
|
||||
ParentProfileID TaskEvaluationProfileID
|
||||
TriggerEvidenceRef string
|
||||
BenchmarkVersion string
|
||||
RequiredCaseIDs []string
|
||||
QualityWeightMillis int64
|
||||
ReliabilityWeightMillis int64
|
||||
LatencyWeightMillis int64
|
||||
}
|
||||
|
||||
func (p TaskEvaluationProfile) Validate() error {
|
||||
if p.ID == "" || p.Revision < 1 || p.CapabilityID == "" || (p.Stage != TaskEvaluationGeneral && p.Stage != TaskEvaluationAdditional) || p.BenchmarkVersion == "" || len(p.RequiredCaseIDs) == 0 {
|
||||
return errors.New("task evaluation profile identity and cases are incomplete")
|
||||
}
|
||||
if p.Stage == TaskEvaluationGeneral && (p.ParentProfileID != "" || p.TriggerEvidenceRef != "") {
|
||||
return errors.New("general task evaluation profile cannot have an expansion parent")
|
||||
}
|
||||
if p.Stage == TaskEvaluationAdditional && (p.ParentProfileID == "" || p.TriggerEvidenceRef == "") {
|
||||
return errors.New("additional task evaluation profile requires parent and trigger evidence")
|
||||
}
|
||||
if p.QualityWeightMillis < 0 || p.ReliabilityWeightMillis < 0 || p.LatencyWeightMillis < 0 || p.QualityWeightMillis+p.ReliabilityWeightMillis+p.LatencyWeightMillis != 1000 {
|
||||
return errors.New("task evaluation profile weights must be non-negative and sum to 1000")
|
||||
}
|
||||
seen := make(map[string]struct{}, len(p.RequiredCaseIDs))
|
||||
for _, caseID := range p.RequiredCaseIDs {
|
||||
if caseID == "" {
|
||||
return errors.New("task evaluation profile contains an empty case")
|
||||
}
|
||||
if _, ok := seen[caseID]; ok {
|
||||
return errors.New("task evaluation profile contains duplicate cases")
|
||||
}
|
||||
seen[caseID] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaskEvaluationExpansionRequest is emitted after a task remains unresolved
|
||||
// with the current best eligible candidate. It creates only a bounded new
|
||||
// profile; it never silently rewrites the general benchmark.
|
||||
type TaskEvaluationExpansionRequest struct {
|
||||
ParentProfileID TaskEvaluationProfileID
|
||||
CapabilityID CapabilityID
|
||||
TriggerEvidenceRef string
|
||||
NewCaseIDs []string
|
||||
BenchmarkVersion string
|
||||
}
|
||||
|
||||
type TaskCaseVerificationMode string
|
||||
|
||||
const (
|
||||
TaskCaseVerificationExactText TaskCaseVerificationMode = "EXACT_TEXT"
|
||||
TaskCaseVerificationJSONFieldEquals TaskCaseVerificationMode = "JSON_FIELD_EQUALS"
|
||||
)
|
||||
|
||||
// TaskCaseVerification is a bounded, task-owned rubric. The expected text is
|
||||
// test data, not model output, and is never sent to the cognitive provider.
|
||||
type TaskCaseVerification struct {
|
||||
Mode TaskCaseVerificationMode
|
||||
FieldPath string
|
||||
ExpectedText string
|
||||
MinimumQualityMillis int64
|
||||
}
|
||||
|
||||
// TaskCaseObservation is produced by the bounded evaluator and verifier. A
|
||||
// score without a case result and evidence reference is not accepted as
|
||||
// qualification evidence.
|
||||
type TaskCaseObservation struct {
|
||||
CaseID string
|
||||
Result EvaluationResult
|
||||
QualityScoreMillis int64
|
||||
LatencyMicros int64
|
||||
CriticalFailure bool
|
||||
EvidenceArtifactRef string
|
||||
// VerificationEvidenceRef and VerifierIdentity are required whenever an
|
||||
// observation claims semantic support or a non-zero quality score.
|
||||
VerificationEvidenceRef string
|
||||
VerifierIdentity string
|
||||
}
|
||||
|
||||
// TaskEvaluation is durable evidence for one model/binding revision under one
|
||||
// task profile. It is not activation authority.
|
||||
type TaskEvaluation struct {
|
||||
ID TaskEvaluationID
|
||||
ProfileID TaskEvaluationProfileID
|
||||
BindingID CapabilityBindingID
|
||||
BindingRevision int64
|
||||
CapabilityID CapabilityID
|
||||
Result EvaluationResult
|
||||
QualityScoreMillis int64
|
||||
ReliabilityScoreMillis int64
|
||||
LatencyScoreMillis int64
|
||||
OverallScoreMillis int64
|
||||
CriticalFailureMillis int64
|
||||
BenchmarkVersion string
|
||||
ArtifactID artifact.ArtifactID
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package capability
|
||||
|
||||
import (
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
// TaskEvaluationCase carries one bounded prompt selected by the evaluator.
|
||||
// The prompt is an input artifact, not a model-produced fact.
|
||||
type TaskEvaluationCase struct {
|
||||
CaseID string
|
||||
Prompt string
|
||||
EvidenceArtifactRef string
|
||||
Verification TaskCaseVerification
|
||||
}
|
||||
|
||||
type TaskCaseResponse struct {
|
||||
CaseID string
|
||||
ResponseText string
|
||||
LatencyMicros int64
|
||||
EvidenceArtifactRef string
|
||||
}
|
||||
|
||||
type TaskCaseVerificationRequest struct {
|
||||
CaseID string
|
||||
ResponseText string
|
||||
LatencyMicros int64
|
||||
EvidenceArtifactRef string
|
||||
Verification TaskCaseVerification
|
||||
}
|
||||
|
||||
type TaskCaseVerificationResponse struct {
|
||||
CaseID string
|
||||
Result EvaluationResult
|
||||
QualityScoreMillis int64
|
||||
LatencyMicros int64
|
||||
CriticalFailure bool
|
||||
EvidenceArtifactRef string
|
||||
VerificationEvidenceRef string
|
||||
VerifierIdentity string
|
||||
}
|
||||
|
||||
type TaskEvaluationExecutionRequest struct {
|
||||
Profile TaskEvaluationProfile
|
||||
CapabilityID CapabilityID
|
||||
BindingID CapabilityBindingID
|
||||
BindingRevision int64
|
||||
ActorID identity.ActorID
|
||||
PrincipalID identity.PrincipalID
|
||||
ScopeID authority.ScopeID
|
||||
Cases []TaskEvaluationCase
|
||||
MaxOutputBytes int64
|
||||
EgressAcknowledged bool
|
||||
}
|
||||
|
||||
type TaskEvaluationExecutionResponse struct {
|
||||
ProfileID TaskEvaluationProfileID
|
||||
BindingID CapabilityBindingID
|
||||
BindingRevision int64
|
||||
Observations []TaskCaseObservation
|
||||
Responses []TaskCaseResponse
|
||||
ObservedAtUnixMillis int64
|
||||
}
|
||||
+114
-33
@@ -1,11 +1,17 @@
|
||||
package work
|
||||
|
||||
import "git.cin.su/m/Assistant/contracts/capability"
|
||||
|
||||
type GraphNodeID string
|
||||
|
||||
type GraphState string
|
||||
type GraphNodeState string
|
||||
type GraphNodePurpose string
|
||||
|
||||
const (
|
||||
GraphNodePurposeTaskEvaluation GraphNodePurpose = "TASK_EVALUATION_SPECIALIST"
|
||||
GraphNodePurposeGeneric GraphNodePurpose = "GENERIC"
|
||||
|
||||
GraphDraft GraphState = "DRAFT"
|
||||
GraphAdmitted GraphState = "ADMITTED"
|
||||
GraphRunning GraphState = "RUNNING"
|
||||
@@ -15,14 +21,15 @@ const (
|
||||
GraphFailed GraphState = "FAILED"
|
||||
GraphUnknown GraphState = "UNKNOWN"
|
||||
|
||||
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
|
||||
GraphNodeReady GraphNodeState = "READY"
|
||||
GraphNodeLeased GraphNodeState = "LEASED"
|
||||
GraphNodeRunning GraphNodeState = "RUNNING"
|
||||
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
|
||||
GraphNodeFailed GraphNodeState = "FAILED"
|
||||
GraphNodeCancel GraphNodeState = "CANCELLED"
|
||||
GraphNodeUnknown GraphNodeState = "UNKNOWN"
|
||||
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
|
||||
GraphNodeReady GraphNodeState = "READY"
|
||||
GraphNodeLeased GraphNodeState = "LEASED"
|
||||
GraphNodeRunning GraphNodeState = "RUNNING"
|
||||
GraphNodeVerifying GraphNodeState = "VERIFYING"
|
||||
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
|
||||
GraphNodeFailed GraphNodeState = "FAILED"
|
||||
GraphNodeCancel GraphNodeState = "CANCELLED"
|
||||
GraphNodeUnknown GraphNodeState = "UNKNOWN"
|
||||
)
|
||||
|
||||
type GraphBudget struct {
|
||||
@@ -31,11 +38,17 @@ type GraphBudget struct {
|
||||
}
|
||||
|
||||
type GraphNode struct {
|
||||
ID GraphNodeID `json:"id"`
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
State GraphNodeState `json:"state"`
|
||||
Attempts int `json:"attempts"`
|
||||
ID GraphNodeID `json:"id"`
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
State GraphNodeState `json:"state"`
|
||||
Attempts int `json:"attempts"`
|
||||
Purpose GraphNodePurpose `json:"purpose,omitempty"`
|
||||
TaskProfileID capability.TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
|
||||
BindingID capability.CapabilityBindingID `json:"binding_id,omitempty"`
|
||||
CaseIDs []string `json:"case_ids,omitempty"`
|
||||
TaskProfile *capability.TaskEvaluationProfile `json:"task_profile,omitempty"`
|
||||
TaskCases []capability.TaskEvaluationCase `json:"task_cases,omitempty"`
|
||||
}
|
||||
|
||||
type GraphEdge struct {
|
||||
@@ -44,30 +57,98 @@ type GraphEdge struct {
|
||||
}
|
||||
|
||||
type WorkGraph struct {
|
||||
ID string `json:"id"`
|
||||
Revision int64 `json:"revision"`
|
||||
State GraphState `json:"state"`
|
||||
Nodes []GraphNode `json:"nodes"`
|
||||
Edges []GraphEdge `json:"edges"`
|
||||
Budget GraphBudget `json:"budget"`
|
||||
ID string `json:"id"`
|
||||
Revision int64 `json:"revision"`
|
||||
State GraphState `json:"state"`
|
||||
Nodes []GraphNode `json:"nodes"`
|
||||
Edges []GraphEdge `json:"edges"`
|
||||
Budget GraphBudget `json:"budget"`
|
||||
ParentGraphID string `json:"parent_graph_id,omitempty"`
|
||||
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
|
||||
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
|
||||
SearchInputBound bool `json:"search_input_bound,omitempty"`
|
||||
SearchInput SolutionSearchInput `json:"search_input,omitempty"`
|
||||
}
|
||||
|
||||
// TaskEvaluationGraphRequest describes a durable fan-out. The graph contains
|
||||
// one specialist node per binding; each node receives the profile's bounded
|
||||
// cases and reports evidence back to Capability for fan-in aggregation.
|
||||
type TaskEvaluationGraphRequest struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
Profile capability.TaskEvaluationProfile `json:"profile"`
|
||||
BindingIDs []capability.CapabilityBindingID `json:"binding_ids"`
|
||||
DispatchIDs []string `json:"dispatch_ids"`
|
||||
IdempotencyPrefix string `json:"idempotency_prefix"`
|
||||
Budget GraphBudget `json:"budget"`
|
||||
ParentGraphID string `json:"parent_graph_id,omitempty"`
|
||||
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
|
||||
ParentGraphState GraphState `json:"parent_graph_state,omitempty"`
|
||||
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
|
||||
Cases []capability.TaskEvaluationCase `json:"cases,omitempty"`
|
||||
BindingRevision int64 `json:"binding_revision,omitempty"`
|
||||
ActorID string `json:"actor_id,omitempty"`
|
||||
PrincipalID string `json:"principal_id,omitempty"`
|
||||
ScopeID string `json:"scope_id,omitempty"`
|
||||
}
|
||||
|
||||
// TaskEvaluationEvidenceCallback is emitted by a specialist after it has
|
||||
// completed the bounded case set. Capability remains the evidence owner;
|
||||
// Work only advances its matching graph node after the evidence is accepted.
|
||||
type TaskEvaluationEvidenceCallback struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
NodeID GraphNodeID `json:"node_id"`
|
||||
Evaluation capability.TaskEvaluation `json:"evaluation"`
|
||||
Succeeded bool `json:"succeeded"`
|
||||
}
|
||||
|
||||
type ResultCandidate struct {
|
||||
ID string
|
||||
GraphID string
|
||||
GraphRevision int64
|
||||
NodeID GraphNodeID
|
||||
AuthorRunID AgentRunID
|
||||
ArtifactRefs []EvidenceRef
|
||||
EvidenceSummary string
|
||||
Summary string
|
||||
ID string `json:"id"`
|
||||
GraphID string `json:"graph_id"`
|
||||
GraphRevision int64 `json:"graph_revision"`
|
||||
NodeID GraphNodeID `json:"node_id"`
|
||||
AuthorRunID AgentRunID `json:"author_run_id"`
|
||||
ArtifactRefs []EvidenceRef `json:"artifact_refs"`
|
||||
EvidenceSummary string `json:"evidence_summary"`
|
||||
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.
|
||||
type GraphResultCallback struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
Revision int64 `json:"revision"`
|
||||
Candidate ResultCandidate `json:"candidate"`
|
||||
}
|
||||
|
||||
type ConflictSet struct {
|
||||
ID string
|
||||
GraphID string
|
||||
GraphRevision int64
|
||||
NodeID GraphNodeID
|
||||
CandidateIDs []string
|
||||
Resolved bool
|
||||
ID string `json:"id"`
|
||||
GraphID string `json:"graph_id"`
|
||||
GraphRevision int64 `json:"graph_revision"`
|
||||
NodeID GraphNodeID `json:"node_id"`
|
||||
CandidateIDs []string `json:"candidate_ids"`
|
||||
Resolved bool `json:"resolved"`
|
||||
}
|
||||
|
||||
type GraphSynthesisState string
|
||||
|
||||
const GraphSynthesisVerified GraphSynthesisState = "VERIFIED"
|
||||
|
||||
// GraphSynthesisCandidate retains every competing result while recording the
|
||||
// independently verified selection. It is a candidate, not an external action
|
||||
// or authority grant.
|
||||
type GraphSynthesisCandidate struct {
|
||||
ID string `json:"id"`
|
||||
GraphID string `json:"graph_id"`
|
||||
GraphRevision int64 `json:"graph_revision"`
|
||||
NodeID GraphNodeID `json:"node_id"`
|
||||
CandidateIDs []string `json:"candidate_ids"`
|
||||
SelectedCandidateID string `json:"selected_candidate_id"`
|
||||
DissentCandidateIDs []string `json:"dissent_candidate_ids"`
|
||||
VerifierRunID AgentRunID `json:"verifier_run_id"`
|
||||
VerifierIdentity string `json:"verifier_identity"`
|
||||
EvidenceRefs []EvidenceRef `json:"evidence_refs"`
|
||||
Summary string `json:"summary"`
|
||||
State GraphSynthesisState `json:"state"`
|
||||
}
|
||||
|
||||
+34
-11
@@ -1,6 +1,12 @@
|
||||
package work
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
type AgentLeaseID string
|
||||
type AgentFencingToken uint64
|
||||
@@ -50,14 +56,31 @@ type IndependentVerification struct {
|
||||
}
|
||||
|
||||
type AgentDispatchIntent struct {
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
DelegationID DelegationID `json:"delegation_id"`
|
||||
RunID AgentRunID `json:"run_id"`
|
||||
ImplementationID string `json:"implementation_id"`
|
||||
ImplementationRevision int64 `json:"implementation_revision"`
|
||||
SourceRevision string `json:"source_revision"`
|
||||
WorktreeID string `json:"worktree_id"`
|
||||
VerificationPlanRef string `json:"verification_plan_ref"`
|
||||
CreatedAtUnixMillis int64 `json:"created_at_unix_millis"`
|
||||
DispatchID string `json:"dispatch_id"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
DelegationID DelegationID `json:"delegation_id"`
|
||||
RunID AgentRunID `json:"run_id"`
|
||||
ImplementationID string `json:"implementation_id"`
|
||||
ImplementationRevision int64 `json:"implementation_revision"`
|
||||
SourceRevision string `json:"source_revision"`
|
||||
WorktreeID string `json:"worktree_id"`
|
||||
VerificationPlanRef string `json:"verification_plan_ref"`
|
||||
CreatedAtUnixMillis int64 `json:"created_at_unix_millis"`
|
||||
SpecialistPurpose string `json:"specialist_purpose,omitempty"`
|
||||
TaskEvaluationProfileID capability.TaskEvaluationProfileID `json:"task_evaluation_profile_id,omitempty"`
|
||||
TaskEvaluationBindingID capability.CapabilityBindingID `json:"task_evaluation_binding_id,omitempty"`
|
||||
TaskEvaluationCaseIDs []string `json:"task_evaluation_case_ids,omitempty"`
|
||||
TaskEvaluationProfile *capability.TaskEvaluationProfile `json:"task_evaluation_profile,omitempty"`
|
||||
TaskEvaluationCases []capability.TaskEvaluationCase `json:"task_evaluation_cases,omitempty"`
|
||||
TaskEvaluationExecution *TaskEvaluationDispatchContext `json:"task_evaluation_execution,omitempty"`
|
||||
}
|
||||
|
||||
type TaskEvaluationDispatchContext struct {
|
||||
GraphID string `json:"graph_id"`
|
||||
GraphRevision int64 `json:"graph_revision"`
|
||||
NodeID GraphNodeID `json:"node_id"`
|
||||
BindingRevision int64 `json:"binding_revision"`
|
||||
ActorID identity.ActorID `json:"actor_id"`
|
||||
PrincipalID identity.PrincipalID `json:"principal_id"`
|
||||
ScopeID authority.ScopeID `json:"scope_id"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package work
|
||||
|
||||
type OrchestrationState string
|
||||
type SearchOptionKind string
|
||||
|
||||
const (
|
||||
OrchestrationSearching OrchestrationState = "SEARCHING"
|
||||
OrchestrationExecuting OrchestrationState = "EXECUTING"
|
||||
OrchestrationVerifying OrchestrationState = "VERIFYING"
|
||||
OrchestrationWaiting OrchestrationState = "WAITING"
|
||||
OrchestrationCompleted OrchestrationState = "COMPLETED"
|
||||
OrchestrationCancelled OrchestrationState = "CANCELLED"
|
||||
|
||||
SearchOptionAgents SearchOptionKind = "AGENT_TEAM"
|
||||
SearchOptionCapability SearchOptionKind = "CAPABILITY_DISCOVERY"
|
||||
SearchOptionResearch SearchOptionKind = "TRUTH_RESEARCH"
|
||||
SearchOptionReflection SearchOptionKind = "REFLECTION_MEMORY"
|
||||
SearchOptionHuman SearchOptionKind = "HUMAN_ATTENTION"
|
||||
)
|
||||
|
||||
// SolutionSearchInput is the durable orchestration snapshot. The scheduler
|
||||
// stores the graph/goal separately; this value only decides the next bounded
|
||||
// search wave and never grants authority by itself.
|
||||
type SolutionSearchInput struct {
|
||||
GoalID GoalID
|
||||
GoalState GoalState
|
||||
SearchRound int
|
||||
MaxRound int
|
||||
CancelRequested bool
|
||||
ActiveOptionCount int
|
||||
VerificationReady bool
|
||||
Unresolved bool
|
||||
}
|
||||
|
||||
type SolutionSearchDecision struct {
|
||||
GoalID GoalID
|
||||
State OrchestrationState
|
||||
ContinueSearching bool
|
||||
NextSearchRound int
|
||||
Options []SearchOptionKind
|
||||
Reason string
|
||||
}
|
||||
|
||||
type SolutionSearchWaveRequest struct {
|
||||
Input SolutionSearchInput `json:"input"`
|
||||
GraphID string `json:"graph_id"`
|
||||
IdempotencyPrefix string `json:"idempotency_prefix"`
|
||||
Dispatches []SolutionSearchDispatch `json:"dispatches"`
|
||||
Budget GraphBudget `json:"budget"`
|
||||
ParentGraphID string `json:"parent_graph_id,omitempty"`
|
||||
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
|
||||
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
|
||||
NowUnixMillis int64 `json:"now_unix_millis"`
|
||||
}
|
||||
|
||||
type SolutionSearchDispatch struct {
|
||||
Option SearchOptionKind `json:"option"`
|
||||
Intent AgentDispatchIntent `json:"intent"`
|
||||
}
|
||||
|
||||
type SolutionSearchWaveResult struct {
|
||||
Decision SolutionSearchDecision `json:"decision"`
|
||||
Graph WorkGraph `json:"graph"`
|
||||
ScheduledNodeIDs []GraphNodeID `json:"scheduled_node_ids,omitempty"`
|
||||
GraphMaterialized bool `json:"graph_materialized"`
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package work
|
||||
|
||||
import "time"
|
||||
|
||||
type DevelopmentStage string
|
||||
|
||||
const (
|
||||
@@ -15,6 +17,8 @@ const (
|
||||
|
||||
type DevelopmentGate struct {
|
||||
ID string
|
||||
Revision int64
|
||||
UpdatedAtUnixMillis int64
|
||||
Stage DevelopmentStage
|
||||
EvidenceRefs []string
|
||||
ProposalRef string
|
||||
@@ -27,3 +31,103 @@ type DevelopmentGate struct {
|
||||
PromotionRef string
|
||||
RollbackRef string
|
||||
}
|
||||
|
||||
// ExtensionProposalStage is deliberately proposal-only in this contract. A
|
||||
// proposal does not authorize source changes, provider calls, deployment or
|
||||
// promotion; those stages require the existing governed development gate.
|
||||
type ExtensionProposalStage string
|
||||
|
||||
const ExtensionProposalProposed ExtensionProposalStage = "PROPOSED"
|
||||
|
||||
// ExtensionEvidence is an immutable reference to an observation that points
|
||||
// at a repeated or corroborated improvement signal. The payload is owned by
|
||||
// its source module; Work stores only the typed reference and stable signal
|
||||
// key.
|
||||
type ExtensionEvidence struct {
|
||||
Ref string
|
||||
SignalKey string
|
||||
}
|
||||
|
||||
// ExtensionProposalRequest contains the operator/governance information
|
||||
// needed to decide whether an evidence pattern is worth reviewing. Empty
|
||||
// fields are rejected by the proposal generator rather than inferred.
|
||||
type ExtensionProposalRequest struct {
|
||||
ID string
|
||||
DesiredOutcome string
|
||||
SmallestChangeLayer string
|
||||
AffectedOwners []string
|
||||
AffectedContracts []string
|
||||
RiskTier string
|
||||
EvaluationPlan []string
|
||||
Alternatives []string
|
||||
RollbackPlan string
|
||||
Assumptions []string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// ExtensionProposal is a bounded, evidence-bearing candidate for governed
|
||||
// development. It intentionally contains no executable command, credential,
|
||||
// authority grant, implementation body or promotion decision.
|
||||
type ExtensionProposal struct {
|
||||
ID string
|
||||
RequestedByPrincipalID string
|
||||
Stage ExtensionProposalStage
|
||||
EvidenceRefs []string
|
||||
DeduplicatedSignalKeys []string
|
||||
DesiredOutcome string
|
||||
SmallestChangeLayer string
|
||||
AffectedOwners []string
|
||||
AffectedContracts []string
|
||||
RiskTier string
|
||||
EvaluationPlan []string
|
||||
Alternatives []string
|
||||
RollbackPlan string
|
||||
Assumptions []string
|
||||
CreatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
type DevelopmentScaffoldStage string
|
||||
|
||||
const DevelopmentScaffoldSandboxed DevelopmentScaffoldStage = "SANDBOXED"
|
||||
|
||||
// DevelopmentScaffoldRequest describes the boundary a later control plane
|
||||
// would use to create an isolated worktree. This value object itself performs
|
||||
// no checkout or filesystem operation.
|
||||
type DevelopmentScaffoldRequest struct {
|
||||
ID string
|
||||
ProposalRef string
|
||||
BaseRepoRevision string
|
||||
WorktreeRef string
|
||||
TaskIntent string
|
||||
Modules []string
|
||||
AllowedPaths []string
|
||||
ForbiddenPaths []string
|
||||
ProtectedPaths []string
|
||||
RequiredTests []string
|
||||
ArtifactLineageRefs []string
|
||||
ProtectedMainAccess string
|
||||
ProductionAccess string
|
||||
SecretAccess string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// DevelopmentScaffold is a provenance-linked task-packet candidate. It has
|
||||
// no executable instruction, credential, checkout handle or authority grant.
|
||||
type DevelopmentScaffold struct {
|
||||
ID string
|
||||
RequestedByPrincipalID string
|
||||
Stage DevelopmentScaffoldStage
|
||||
ProposalRef string
|
||||
BaseRepoRevision string
|
||||
WorktreeRef string
|
||||
TaskIntent string
|
||||
Modules []string
|
||||
AllowedPaths []string
|
||||
ForbiddenPaths []string
|
||||
RequiredTests []string
|
||||
ArtifactLineageRefs []string
|
||||
ProtectedMainAccess string
|
||||
ProductionAccess string
|
||||
SecretAccess string
|
||||
CreatedAtUnixMillis int64
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.graph_result_candidates (
|
||||
result_candidate_id TEXT PRIMARY KEY,
|
||||
graph_id TEXT NOT NULL,
|
||||
graph_revision BIGINT NOT NULL,
|
||||
node_id TEXT NOT NULL,
|
||||
author_run_id TEXT NOT NULL,
|
||||
artifact_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
evidence_summary TEXT NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (result_candidate_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0),
|
||||
CHECK (node_id <> ''), CHECK (author_run_id <> ''), CHECK (evidence_summary <> ''), CHECK (summary <> ''),
|
||||
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.graph_conflicts (
|
||||
conflict_id TEXT PRIMARY KEY,
|
||||
graph_id TEXT NOT NULL,
|
||||
graph_revision BIGINT NOT NULL,
|
||||
node_id TEXT NOT NULL,
|
||||
candidate_ids TEXT[] NOT NULL,
|
||||
resolved BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (conflict_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0), CHECK (node_id <> ''),
|
||||
CHECK (cardinality(candidate_ids) >= 2),
|
||||
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.graph_synthesis_candidates (
|
||||
synthesis_id TEXT PRIMARY KEY,
|
||||
graph_id TEXT NOT NULL,
|
||||
graph_revision BIGINT NOT NULL,
|
||||
node_id TEXT NOT NULL,
|
||||
candidate_ids TEXT[] NOT NULL,
|
||||
selected_candidate_id TEXT NOT NULL,
|
||||
dissent_candidate_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
verifier_run_id TEXT NOT NULL,
|
||||
verifier_identity TEXT NOT NULL,
|
||||
evidence_refs TEXT[] NOT NULL,
|
||||
summary TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (synthesis_id <> ''), CHECK (graph_id <> ''), CHECK (graph_revision > 0), CHECK (node_id <> ''),
|
||||
CHECK (cardinality(candidate_ids) >= 2), CHECK (selected_candidate_id <> ''), CHECK (verifier_run_id <> ''),
|
||||
CHECK (verifier_identity <> ''), CHECK (cardinality(evidence_refs) >= 1), CHECK (summary <> ''), CHECK (state = 'VERIFIED'),
|
||||
FOREIGN KEY (graph_id, graph_revision) REFERENCES work.graphs(graph_id, revision)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS graph_result_candidates_node_idx ON work.graph_result_candidates(graph_id, graph_revision, node_id);
|
||||
CREATE INDEX IF NOT EXISTS graph_conflicts_node_idx ON work.graph_conflicts(graph_id, graph_revision, node_id);
|
||||
CREATE INDEX IF NOT EXISTS graph_synthesis_node_idx ON work.graph_synthesis_candidates(graph_id, graph_revision, node_id);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS work.development_gates (
|
||||
gate_id TEXT PRIMARY KEY,
|
||||
revision BIGINT NOT NULL CHECK (revision > 0),
|
||||
updated_at_unix_millis BIGINT NOT NULL CHECK (updated_at_unix_millis > 0),
|
||||
stage TEXT NOT NULL,
|
||||
evidence_refs TEXT[] NOT NULL,
|
||||
proposal_ref TEXT NOT NULL,
|
||||
task_packet_ref TEXT NOT NULL,
|
||||
worktree_ref TEXT NOT NULL,
|
||||
producer_run_refs TEXT[] NOT NULL,
|
||||
verifier_run_refs TEXT[] NOT NULL,
|
||||
verification_evidence TEXT NOT NULL,
|
||||
canary_evidence TEXT NOT NULL,
|
||||
promotion_ref TEXT NOT NULL,
|
||||
rollback_ref TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE work.development_gates OWNER TO work_role;
|
||||
GRANT SELECT, INSERT, UPDATE ON work.development_gates TO work_role;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,35 @@
|
||||
BEGIN;
|
||||
SET ROLE capability_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.cognitive_task_evaluations (
|
||||
evaluation_id TEXT PRIMARY KEY,
|
||||
task_profile_id TEXT NOT NULL,
|
||||
binding_id TEXT NOT NULL,
|
||||
binding_revision BIGINT NOT NULL,
|
||||
capability_id TEXT NOT NULL,
|
||||
result TEXT NOT NULL,
|
||||
quality_score_millis BIGINT NOT NULL,
|
||||
reliability_score_millis BIGINT NOT NULL,
|
||||
latency_score_millis BIGINT NOT NULL,
|
||||
overall_score_millis BIGINT NOT NULL,
|
||||
critical_failure_millis BIGINT NOT NULL,
|
||||
benchmark_version TEXT NOT NULL,
|
||||
artifact_id TEXT NOT NULL,
|
||||
observed_at_unix_millis BIGINT NOT NULL,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (binding_revision > 0),
|
||||
CHECK (result IN ('UNKNOWN','SUPPORTED','UNSUPPORTED')),
|
||||
CHECK (quality_score_millis BETWEEN 0 AND 1000),
|
||||
CHECK (reliability_score_millis BETWEEN 0 AND 1000),
|
||||
CHECK (latency_score_millis BETWEEN 0 AND 1000),
|
||||
CHECK (overall_score_millis BETWEEN 0 AND 1000),
|
||||
CHECK (critical_failure_millis BETWEEN 0 AND 1000),
|
||||
CHECK (observed_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS cognitive_task_evaluations_lookup
|
||||
ON capability.cognitive_task_evaluations(task_profile_id, binding_id, binding_revision, observed_at_unix_millis DESC);
|
||||
|
||||
GRANT SELECT, INSERT ON capability.cognitive_task_evaluations TO capability_role;
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,23 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_intents
|
||||
ADD COLUMN IF NOT EXISTS specialist_purpose TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_profile_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_binding_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_case_ids TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
ALTER TABLE work.graphs
|
||||
ADD COLUMN IF NOT EXISTS parent_graph_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS parent_graph_revision BIGINT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS trigger_evidence_ref TEXT NOT NULL DEFAULT '';
|
||||
|
||||
ALTER TABLE work.graph_nodes
|
||||
ADD COLUMN IF NOT EXISTS purpose TEXT NOT NULL DEFAULT 'GENERIC',
|
||||
ADD COLUMN IF NOT EXISTS task_profile_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS binding_id TEXT NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS case_ids TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,16 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_intents
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_profile JSONB,
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_cases JSONB NOT NULL DEFAULT '[]',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_execution JSONB;
|
||||
|
||||
ALTER TABLE work.graph_nodes
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_profile JSONB,
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_cases JSONB NOT NULL DEFAULT '[]',
|
||||
ADD COLUMN IF NOT EXISTS task_evaluation_execution JSONB;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,15 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_queue
|
||||
ADD COLUMN IF NOT EXISTS retry_limit INTEGER NOT NULL DEFAULT 3;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_queue
|
||||
DROP CONSTRAINT IF EXISTS agent_dispatch_queue_retry_limit_check;
|
||||
|
||||
ALTER TABLE work.agent_dispatch_queue
|
||||
ADD CONSTRAINT agent_dispatch_queue_retry_limit_check CHECK (retry_limit > 0);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,45 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.extension_proposals (
|
||||
proposal_id TEXT PRIMARY KEY,
|
||||
requested_by_principal_id TEXT NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
deduplicated_signal_keys TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
desired_outcome TEXT NOT NULL,
|
||||
smallest_change_layer TEXT NOT NULL,
|
||||
affected_owners TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
affected_contracts TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
risk_tier TEXT NOT NULL,
|
||||
evaluation_plan TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
alternatives TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
rollback_plan TEXT NOT NULL,
|
||||
assumptions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (proposal_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'PROPOSED'), CHECK (desired_outcome <> ''), CHECK (smallest_change_layer <> ''), CHECK (risk_tier <> ''), CHECK (rollback_plan <> ''), CHECK (created_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work.development_scaffolds (
|
||||
scaffold_id TEXT PRIMARY KEY,
|
||||
requested_by_principal_id TEXT NOT NULL,
|
||||
stage TEXT NOT NULL,
|
||||
proposal_ref TEXT NOT NULL,
|
||||
base_repo_revision TEXT NOT NULL,
|
||||
worktree_ref TEXT NOT NULL,
|
||||
task_intent TEXT NOT NULL,
|
||||
modules TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
allowed_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
forbidden_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
required_tests TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
artifact_lineage_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
protected_main_access TEXT NOT NULL,
|
||||
production_access TEXT NOT NULL,
|
||||
secret_access TEXT NOT NULL,
|
||||
created_at_unix_millis BIGINT NOT NULL,
|
||||
CHECK (scaffold_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'SANDBOXED'), CHECK (proposal_ref <> ''), CHECK (base_repo_revision <> ''), CHECK (worktree_ref <> ''), CHECK (task_intent <> ''), CHECK (protected_main_access = 'DENIED'), CHECK (production_access = 'DENIED'), CHECK (secret_access = 'DENIED'), CHECK (created_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,21 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE work_role;
|
||||
|
||||
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_bound BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_goal_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_goal_state TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_search_round INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_max_round INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_cancel_requested BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_verification_ready BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_unresolved BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE work.graphs DROP CONSTRAINT IF EXISTS work_graphs_orchestration_snapshot_check;
|
||||
ALTER TABLE work.graphs ADD CONSTRAINT work_graphs_orchestration_snapshot_check CHECK (
|
||||
(NOT orchestration_bound AND orchestration_goal_id = '' AND orchestration_search_round = 0 AND orchestration_max_round = 0)
|
||||
OR (orchestration_bound AND orchestration_goal_id <> '' AND orchestration_search_round >= 1 AND orchestration_max_round >= 1)
|
||||
);
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -0,0 +1,24 @@
|
||||
BEGIN;
|
||||
|
||||
SET ROLE capability_role;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS capability.cognitive_model_qualification_observations (
|
||||
observation_id TEXT PRIMARY KEY,
|
||||
provider_instance_id TEXT NOT NULL,
|
||||
provider_model_id TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('REACHABLE','RESPONDED_WITHOUT_QUALIFICATION','UNAVAILABLE')),
|
||||
finish_reason TEXT NOT NULL,
|
||||
latency_millis BIGINT NOT NULL CHECK (latency_millis >= 0),
|
||||
output_observed BOOLEAN NOT NULL,
|
||||
quality_assessed BOOLEAN NOT NULL DEFAULT FALSE CHECK (quality_assessed = FALSE),
|
||||
category TEXT NOT NULL CHECK (category = 'UNRATED'),
|
||||
observed_at_unix_millis BIGINT NOT NULL CHECK (observed_at_unix_millis > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS cognitive_model_qualification_lookup
|
||||
ON capability.cognitive_model_qualification_observations(provider_instance_id, provider_model_id, observed_at_unix_millis DESC);
|
||||
|
||||
GRANT SELECT, INSERT ON capability.cognitive_model_qualification_observations TO capability_role;
|
||||
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -99,8 +99,12 @@ services:
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
|
||||
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
|
||||
agent-worker:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
@@ -114,10 +118,24 @@ services:
|
||||
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
|
||||
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
depends_on: { postgres: { condition: service_healthy } }
|
||||
TASK_VERIFIER_URL: http://task-verifier:8091
|
||||
depends_on:
|
||||
postgres: { condition: service_healthy }
|
||||
task-verifier: { condition: service_healthy }
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
|
||||
task-verifier:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: task-verifier, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: :8091
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8091/healthz >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
work-graph-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: work-graph-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
@@ -211,11 +229,18 @@ services:
|
||||
LISTEN_ADDR: ":8083"
|
||||
CORE_RUNTIME_URL: http://core-runtime:8080
|
||||
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
|
||||
LOCAL_CPU_PROVIDER_API_KEY_FILE: ${LOCAL_CPU_PROVIDER_API_KEY_FILE:-}
|
||||
LOCAL_CPU_PROVIDER_CREDENTIAL_REF: ${LOCAL_CPU_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-cpu/provider-api-key/v1}
|
||||
LOCAL_MEDIUM_PROVIDER_URL: ${LOCAL_MEDIUM_PROVIDER_URL:-http://llm-local-api:8080/v1}
|
||||
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-}
|
||||
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
|
||||
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
|
||||
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
|
||||
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
|
||||
COGNITIVE_ALLOW_CANARY: "true"
|
||||
depends_on: [core-runtime, cognitive-lifeboat]
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
networks: [default, llm-local]
|
||||
|
||||
cognitive-lifeboat:
|
||||
image: ghcr.io/ggml-org/llama.cpp:server
|
||||
@@ -282,3 +307,8 @@ volumes:
|
||||
external: true
|
||||
assistant_research_sources:
|
||||
name: assistant_research_sources
|
||||
|
||||
networks:
|
||||
llm-local:
|
||||
external: true
|
||||
name: llm-local_default
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000152-cognitive-provider-secret-reference.json",
|
||||
"task_id": "DEV-000152",
|
||||
"task_file": "dev/tasks/DEV-000247-research-runtime-status.json",
|
||||
"task_id": "DEV-000247",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000153",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0010",
|
||||
"base_repo_revision": "dbbc54171d9c8a9edaa93e5ff5870d3ec434d799",
|
||||
"risk_tier": "E",
|
||||
"intent": "Wire the production cognitive-worker to the existing protected secret volume for optional local CPU gateway authentication without placing credential material in compose environment values.",
|
||||
"requirement_ids": ["AUDIT-WP-003", "AUDIT-WP-201", "CONSTITUTION-1.3.28"],
|
||||
"allowed_change_scope": {"modules": ["cognitive"], "paths": ["deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000153-fleet-secret-wiring.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/devctl/**", ".gitea/**", "database/**", "contracts/**", "modules/**", "adapters/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["assistant_core_secrets read-only volume", "optional LOCAL_CPU_PROVIDER_API_KEY_FILE reference"], "may_change": []},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": ["provider credential material remains outside repository and compose environment"],
|
||||
"state_transitions": ["when operators provision the referenced file, cognitive-worker resolves it and authenticates the local CPU gateway; when absent, existing recovery behavior remains available"],
|
||||
"side_effects_and_privileges": ["read-only mount only; no secret value is added to compose"],
|
||||
"persistence_and_migrations": ["no database migration"],
|
||||
"failure_retry_restart": ["empty optional file reference preserves current unauthenticated startup", "configured missing credential file fails closed at worker startup"],
|
||||
"assumptions": ["the gateway credential is provisioned by production operators into assistant_core_secrets; this task does not copy or reveal it"],
|
||||
"acceptance_scenarios": ["compose renders with empty optional key path", "cognitive-worker receives read-only /run/secrets mount", "no literal provider credential occurs in repository or compose", "CI and production deployment succeed"],
|
||||
"required_tests": ["go test", "go vet", "docker compose config", "architecture preflight", "check-diff", "git diff --check", "independent", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_ARCHITECTURE_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000154",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0010",
|
||||
"base_repo_revision": "283f5c044c3bf6739c8ed859412cd736754e3177",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record bounded production evidence that the authenticated local CPU provider is available and invokable through the cognitive boundary.",
|
||||
"requirement_ids": ["AUDIT-WP-003", "AUDIT-WP-201", "WP-601"],
|
||||
"allowed_change_scope": {"modules": ["capability", "cognitive"], "paths": ["docs/evidence/wp-601-fast-gateway-live-smoke-2026-08-22-v1.json", "docs/evidence/WP-601-FAST-GATEWAY-LIVE-SMOKE-2026-08-22.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000154-fast-live-smoke-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/devctl/**", ".gitea/**", "database/**", "deploy/**", "contracts/**", "modules/**", "adapters/**"]},
|
||||
"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": ["production cognitive discovery and bounded advisory response evidence"], "may_change": []},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": ["none; append-only evidence artifact only"],
|
||||
"state_transitions": ["none; evidence does not activate or promote a binding"],
|
||||
"side_effects_and_privileges": ["four bounded local-only synthetic advisory requests"],
|
||||
"persistence_and_migrations": ["no database migration"],
|
||||
"failure_retry_restart": ["not applicable to evidence artifact"],
|
||||
"assumptions": ["provider key remains in protected production volume and is not copied to repository or logs"],
|
||||
"acceptance_scenarios": ["evidence records available API_KEY provider", "four requests complete with expected sentinels", "limitations remain explicit", "no secret material is present"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "architecture preflight", "check-diff", "git diff --check", "independent", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_ARCHITECTURE_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000155",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0010",
|
||||
"base_repo_revision": "5703ab3ed5e22a5424a2db7f19886c0305e54280",
|
||||
"risk_tier": "E",
|
||||
"intent": "Synchronize the project status projection with current live audit evidence, explicitly distinguishing completed runtime foundations from external blockers.",
|
||||
"requirement_ids": ["AUDIT-TRUTHFUL-STATUS", "WP-003", "WP-301", "WP-800"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000155-current-audit-status.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/devctl/**", ".gitea/**", "database/**", "deploy/**", "contracts/**", "modules/**", "adapters/**"]},
|
||||
"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": ["live audit observations and committed evidence artifacts"], "may_change": []},
|
||||
"semantic_owner": "none; documentation projection",
|
||||
"canonical_state": ["none"],
|
||||
"state_transitions": ["none"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["no database migration"],
|
||||
"failure_retry_restart": ["not applicable"],
|
||||
"assumptions": ["status projection remains subordinate to runtime evidence and does not declare blocked items complete"],
|
||||
"acceptance_scenarios": ["current revision and FAST evidence are referenced", "Medium/Heavy and bridge gaps are explicit", "UI acceptance remains pending without session", "no secret value appears"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "architecture preflight", "check-diff", "git diff --check", "independent", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_ARCHITECTURE_GAP"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000156",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "03c3484ee1488ebc3b2349ab6ba5c8005c895110",
|
||||
"risk_tier": "E",
|
||||
"intent": "Capture fresh production evidence for model fleet readiness and Agent Execution capability without changing runtime behavior.",
|
||||
"requirement_ids": ["WP-200", "WP-301", "AUDIT-TRUTHFUL-STATUS"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000156-fleet-execution-status-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["fleet readiness response", "agent-worker status response"], "may_change": []},
|
||||
"semantic_owner": "none; evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["evidence only; no activation, promotion or configuration change"],
|
||||
"side_effects_and_privileges": ["bounded read-only production status probes"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifact only"],
|
||||
"failure_retry_restart": ["probe failures are recorded as observed facts; no retry-dependent claim"],
|
||||
"assumptions": ["all probes are internal read-only GET requests", "no secret values are collected"],
|
||||
"acceptance_scenarios": ["FAST readiness auth mismatch is distinguished from successful bounded gateway smoke", "MEDIUM remains NOT_CONFIGURED", "HEAVY timeout is recorded", "LIFEBOAT is the selected fallback", "Agent Execution is recovery-only", "no secret value appears"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "git diff --check", "independent", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000157",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0010",
|
||||
"base_repo_revision": "077f16e50ca22e14dbf00214005f0fe98a1c8308",
|
||||
"risk_tier": "E",
|
||||
"intent": "Capture fresh production PostgreSQL backup checksum and isolated restore verification evidence without changing the production database.",
|
||||
"requirement_ids": ["WP-002", "AUDIT-RECOVERY-TRUTH"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000157-postgres-restore-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["backup artifact format", "restore verification output"], "may_change": []},
|
||||
"semantic_owner": "none; evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["evidence only; no production database mutation"],
|
||||
"side_effects_and_privileges": ["isolated temporary restore container created and removed by the existing verifier"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifact only"],
|
||||
"failure_retry_restart": ["checksum or restore failure is recorded as failure; no blind retry claim"],
|
||||
"assumptions": ["the standard verifier uses tmpfs and cleans up its temporary container", "backup contents and credentials are not copied into repository evidence"],
|
||||
"acceptance_scenarios": ["latest backup directory is selected", "checksums pass", "isolated restore reports expected schema/table counts", "production database remains untouched"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "git diff --check", "independent", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000158",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0010",
|
||||
"base_repo_revision": "3a52c18bef7e29a0369ad1edf05adeed1be5935e",
|
||||
"risk_tier": "E",
|
||||
"intent": "Synchronize the project status projection with the latest production revision and committed fleet/recovery evidence.",
|
||||
"requirement_ids": ["AUDIT-TRUTHFUL-STATUS", "WP-002", "WP-200", "WP-301"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000158-status-evidence-consolidation.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["production revision", "committed fleet evidence", "committed restore evidence"], "may_change": []},
|
||||
"semantic_owner": "none; documentation projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["evidence projection only"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["no database migration"],
|
||||
"failure_retry_restart": ["not applicable"],
|
||||
"assumptions": ["status remains subordinate to live evidence and does not declare unavailable capabilities complete"],
|
||||
"acceptance_scenarios": ["latest production revision is shown", "fleet and restore evidence are linked", "remaining gaps remain explicit", "no secret value appears"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "git diff --check", "independent", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000159",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "e1186757bfcf9a349ce0ced603f4a4ebdad636fe",
|
||||
"risk_tier": "E",
|
||||
"intent": "Expose the already-supported optional Agent Execution bridge URL to the production agent-worker without enabling it when unset.",
|
||||
"requirement_ids": ["WP-301", "Agent Execution runtime and recovery"],
|
||||
"allowed_change_scope": {"modules": ["deployment", "work-agent-execution"], "paths": ["deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000159-agent-bridge-config-wiring.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "cmd/**", "modules/**", "adapters/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["AGENT_EXECUTION_BRIDGE_URL deployment variable", "agent-worker optional bridge behavior"], "may_change": []},
|
||||
"semantic_owner": "deployment projection; work-agent-execution remains owner",
|
||||
"canonical_state": ["none"],
|
||||
"state_transitions": ["unset URL -> recovery-only; configured URL -> existing typed HTTP executor path"],
|
||||
"side_effects_and_privileges": ["deployment privileged-surface review is declared; no effective new privilege; no credential forwarding; no outbound call when variable is empty"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["existing worker fail-closed and stale recovery behavior remains unchanged"],
|
||||
"assumptions": ["an independently operated bridge is still required before enabling the variable in production", "empty default preserves current recovery-only behavior"],
|
||||
"acceptance_scenarios": ["production compose renders with empty bridge URL", "agent-worker remains recovery-only when unset", "configured URL is available to the process without source secret", "no bridge runtime is fabricated"],
|
||||
"required_tests": ["JSON validation", "docker compose config", "go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000160",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0010",
|
||||
"base_repo_revision": "ada2da0c6c93d630f7699bd05abd32031c11544c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Synchronize project status with the latest bridge wiring deployment and preserve the explicit external capability gap.",
|
||||
"requirement_ids": ["AUDIT-TRUTHFUL-STATUS", "WP-301"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000160-status-after-bridge-wiring.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["production revision", "optional bridge configuration state"], "may_change": []},
|
||||
"semantic_owner": "none; documentation projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["evidence projection only"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["not applicable"],
|
||||
"assumptions": ["unset bridge URL remains truthful recovery-only state"],
|
||||
"acceptance_scenarios": ["current production revision is shown", "bridge wiring and missing runtime are distinguished", "no secret value appears"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000161",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0010",
|
||||
"base_repo_revision": "3cf44ada8afd75fb008cf8d5965caf9c3f5b0462",
|
||||
"risk_tier": "E",
|
||||
"intent": "Publish a requirement-by-requirement completion matrix for the audit objective, distinguishing repository proof, production proof and external gaps.",
|
||||
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-301", "WP-601", "WP-800"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000161-completion-audit-matrix.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["audit roadmap", "committed task packets", "production evidence artifacts"], "may_change": []},
|
||||
"semantic_owner": "none; audit evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["evidence projection only"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifact only"],
|
||||
"failure_retry_restart": ["not applicable"],
|
||||
"assumptions": ["unknown or externally blocked requirements are not marked complete", "matrix claims are limited to cited evidence scope"],
|
||||
"acceptance_scenarios": ["all seven objective areas have a status", "production and repository proof are distinguished", "remaining external gaps are explicit", "no secret value appears"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000162",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0010",
|
||||
"base_repo_revision": "bda71caecece67e5ef225ac250a7a80c4bc84d94",
|
||||
"risk_tier": "E",
|
||||
"intent": "Refresh current production revision in project status and completion evidence after the audit matrix deployment.",
|
||||
"requirement_ids": ["AUDIT-TRUTHFUL-STATUS"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000162-status-after-completion-matrix.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["current production revision", "completion matrix evidence"], "may_change": []},
|
||||
"semantic_owner": "none; documentation projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["evidence projection only"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["not applicable"],
|
||||
"assumptions": ["matrix remains NOT_COMPLETE until external gaps are actually verified"],
|
||||
"acceptance_scenarios": ["status and matrix point to bda71ca", "no completion claim is widened", "no secret value appears"],
|
||||
"required_tests": ["JSON validation", "go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000163",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "c25a6346d0615f66ddb411450fcc7a36c62d2b48",
|
||||
"risk_tier": "E",
|
||||
"intent": "Extend authenticated cabinet contract tests to cover truthful model-fleet and Agent Execution status surfaces already implemented in Creator Web.",
|
||||
"requirement_ids": ["WP-801", "WP-802", "WP-805", "Authenticated cabinet acceptance"],
|
||||
"allowed_change_scope": {"modules": ["creator-web"], "paths": ["tests/creator_web/cabinet_acceptance_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000163-operations-ui-contract-refresh.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", "contracts/**", ".gitea/**"]},
|
||||
"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": ["existing authenticated Creator Web status routes and static cabinet"], "may_change": ["acceptance tests only"]},
|
||||
"semantic_owner": "creator-web",
|
||||
"canonical_state": ["none; tests inspect existing UI contract"],
|
||||
"state_transitions": ["authenticated session gates truthful status requests"],
|
||||
"side_effects_and_privileges": ["no external mutation; no credentials embedded"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["missing status surface fails CI"],
|
||||
"assumptions": ["live authenticated browser visual acceptance remains a separate production verification"],
|
||||
"acceptance_scenarios": ["fleet and executor status controls exist", "server exposes authenticated routes", "console renders actual selected tier and worker mode", "responsive/reduced-motion tests remain active"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_AUTHENTICATED_BROWSER_SESSION", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000164",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "65a60d5f068e3d8f70733c4ee96bfbb9d9943d5e",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make live model-fleet readiness use the existing file-backed CPU provider API key so FAST status reflects the authenticated provider path.",
|
||||
"requirement_ids": ["WP-601", "Model fleet/failover"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker", "capability"], "paths": ["cmd/cognitive-worker/main.go", "cmd/cognitive-worker/fleet_status.go", "cmd/cognitive-worker/fleet_status_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000164-authorized-fleet-readiness.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "adapters/**", ".gitea/**"]},
|
||||
"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": ["existing provider API key resolver", "fleet readiness response"], "may_change": ["fleet readiness probe request headers"]},
|
||||
"semantic_owner": "capability/cognitive-worker",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["authenticated provider probe -> truthful FAST readiness"],
|
||||
"side_effects_and_privileges": ["uses existing in-memory resolved key only for the internal provider probe; no key logging or persistence"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["401 remains a typed unavailable result when key is absent or rejected; GPU/Medium behavior unchanged"],
|
||||
"assumptions": ["OpenAI-compatible local provider uses Bearer authentication as the existing model adapter does"],
|
||||
"acceptance_scenarios": ["fleet readiness sends Bearer header when CPU key is configured", "FAST is ready when authenticated /models succeeds", "no Authorization header is sent without a key", "Lifeboat fallback remains unchanged"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production fleet readiness"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000165",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "5d5afc88883c7f79e3b051c790642e41386666ec",
|
||||
"risk_tier": "E",
|
||||
"intent": "Use the existing file-backed CPU provider API key in cognitive-worker readiness probes so /readyz and fleet readiness report consistent provider state.",
|
||||
"requirement_ids": ["WP-601", "Model fleet/failover"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker", "platform-health"], "paths": ["runtime/health/server.go", "runtime/health/server_test.go", "cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000165-authorized-cpu-health-probe.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "adapters/**", ".gitea/**"]},
|
||||
"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": ["existing health Probe contract", "existing provider API key resolver"], "may_change": ["health probe header support"]},
|
||||
"semantic_owner": "cognitive-worker/platform health",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["authenticated CPU probe -> consistent readiness evidence"],
|
||||
"side_effects_and_privileges": ["resolved key is used only in outbound probe header; never logged or persisted"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["provider timeout or rejected key remains degraded; no fallback behavior changed"],
|
||||
"assumptions": ["the local provider uses Bearer authentication"],
|
||||
"acceptance_scenarios": ["health probe sends configured header", "cognitive /readyz uses authenticated CPU probe", "GPU remains optional", "no key is sent when absent"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000166",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "4086b7522460a78afc341d996ca7874505018871",
|
||||
"risk_tier": "E",
|
||||
"intent": "Give the authenticated CPU readiness probe the provider adapter timeout while retaining short opportunistic GPU probing.",
|
||||
"requirement_ids": ["WP-601", "Model fleet/failover"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker", "platform-health"], "paths": ["runtime/health/server.go", "runtime/health/server_test.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/fleet_status.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000166-per-provider-readiness-timeouts.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "adapters/**", ".gitea/**"]},
|
||||
"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": ["existing health Probe contract", "existing fleet readiness probe"], "may_change": ["per-probe timeout metadata"]},
|
||||
"semantic_owner": "cognitive-worker/platform health",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["provider-specific probe deadline -> truthful readiness result"],
|
||||
"side_effects_and_privileges": ["no new privilege; existing provider auth only"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["CPU gets bounded 10-second probe; GPU retains 2-second opportunistic timeout"],
|
||||
"assumptions": ["CPU provider catalog may take longer than opportunistic probes while remaining within adapter bounds"],
|
||||
"acceptance_scenarios": ["per-probe health timeout is honored", "CPU fleet client uses 10 seconds", "GPU remains bounded", "Lifeboat fallback remains unchanged"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000167",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "c7b57daaf8233b0f0e79c77c02e92040378921d5",
|
||||
"risk_tier": "E",
|
||||
"intent": "Use the bounded CPU provider HTTP client for the authenticated readiness probe so /readyz reports the same truthful result as fleet readiness.",
|
||||
"requirement_ids": ["WP-601", "Model fleet/failover"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000167-cpu-readiness-client-timeout.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "adapters/**", ".gitea/**"]},
|
||||
"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": ["existing readiness Probe contract", "existing authenticated CPU fleet endpoint"], "may_change": []},
|
||||
"semantic_owner": "cognitive-worker",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["CPU provider response -> consistent /readyz and fleet readiness state"],
|
||||
"side_effects_and_privileges": ["no new privilege; existing provider auth only"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["CPU readiness remains bounded at 10 seconds; opportunistic GPU remains 2 seconds"],
|
||||
"assumptions": ["The CPU provider is authenticated with the existing file-backed API key"],
|
||||
"acceptance_scenarios": ["/readyz can report CPU AVAILABLE when fleet readiness reports CPU AVAILABLE", "GPU remains bounded", "Lifeboat fallback remains unchanged"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000168",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "205d8f620603ccf3e0138f6beb93156450664314",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make the MEDIUM model-fleet slot a real optional OpenAI-compatible provider path with protected file credentials, discovery, readiness and gateway registration.",
|
||||
"requirement_ids": ["WP-200", "WP-301", "Model fleet/failover"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker", "deployment-policy", "tests"], "paths": ["cmd/cognitive-worker/main.go", "cmd/cognitive-worker/main_test.go", "deploy/docker-compose.prod.yml", "tests/cognitive_advisory/cognitive_worker_env_policy.ps1", "tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000168-configurable-medium-fleet-slot.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "adapters/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing provider adapter contract", "existing discovery registration", "existing fleet readiness contract", "existing CredentialRef file contract"], "may_change": []},
|
||||
"semantic_owner": "cognitive-worker/capability registration boundary",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["MEDIUM endpoint absent -> NOT_CONFIGURED", "MEDIUM endpoint configured -> discovery/readiness registration", "available provider -> eligible gateway binding subject to core registry policy"],
|
||||
"side_effects_and_privileges": ["declares one new deployment configuration surface for an existing read-only secret volume; provider key is resolved only through the existing protected file CredentialRef"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["medium probe has a bounded 10-second client timeout; absent endpoint remains NOT_CONFIGURED; fallback selection remains Lifeboat when preferred tiers are unavailable"],
|
||||
"assumptions": ["A real medium OpenAI-compatible runtime may be supplied later through deployment environment injection", "No production medium runtime is claimed by this change"],
|
||||
"acceptance_scenarios": ["empty medium URL produces NOT_CONFIGURED without startup failure", "configured medium URL is registered for discovery and fleet readiness", "direct medium secret environment is rejected", "medium CredentialRef file is accepted", "CPU/GPU/Lifeboat behavior remains unchanged"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "environment policy tests", "CI", "production fleet readiness"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000169",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "8f3920519b6a228d78b6ad775a7c19838dfbb70b",
|
||||
"risk_tier": "E",
|
||||
"intent": "Keep an absent optional MEDIUM provider out of runtime readiness while preserving its explicit NOT_CONFIGURED fleet member.",
|
||||
"requirement_ids": ["WP-200", "WP-301", "Model fleet/failover"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000169-optional-medium-readiness.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "adapters/**", ".gitea/**"]},
|
||||
"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": ["existing readiness handler", "existing fleet readiness contract"], "may_change": []},
|
||||
"semantic_owner": "cognitive-worker",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["MEDIUM absent -> fleet NOT_CONFIGURED without runtime readiness degradation", "MEDIUM configured -> readiness probe included"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["configured MEDIUM remains bounded by its 10-second client; absent MEDIUM is not probed"],
|
||||
"assumptions": ["Fleet status remains the explicit inventory surface for unconfigured optional tiers"],
|
||||
"acceptance_scenarios": ["production /readyz stays ready when MEDIUM URL is empty", "fleet readiness retains MEDIUM NOT_CONFIGURED", "configured MEDIUM is included in /readyz"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000170",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "51427a246e1ccc115a6c2640790d9220beca95e9",
|
||||
"risk_tier": "E",
|
||||
"intent": "Refresh audit completion and production fleet evidence after the configurable MEDIUM slot and optional readiness fixes were deployed.",
|
||||
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-200", "WP-301"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "docs/evidence/wp-200-wp-301-fleet-execution-status-2026-08-22-v2.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000170-refresh-fleet-audit-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["live fleet readiness evidence", "audit completion matrix"], "may_change": []},
|
||||
"semantic_owner": "none; audit evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["stale evidence -> current production evidence"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
|
||||
"failure_retry_restart": ["not applicable"],
|
||||
"assumptions": ["live probe output was captured after production deployment of 51427a24", "missing external capabilities remain explicitly incomplete"],
|
||||
"acceptance_scenarios": ["production revision is current", "MEDIUM configuration and readiness semantics are recorded", "agent bridge and authenticated UI gaps remain visible", "no secret value appears"],
|
||||
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000171",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "503fe840741ac99499e06bd93df977acf1baad41",
|
||||
"risk_tier": "E",
|
||||
"intent": "Connect cognitive-worker to the already-operated llm-local CPU model runtime as the configured MEDIUM fleet member through an explicit external Docker network.",
|
||||
"requirement_ids": ["WP-200", "WP-301", "Model fleet/failover"],
|
||||
"allowed_change_scope": {"modules": ["deployment-topology", "cognitive-worker"], "paths": ["deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000171-connect-existing-medium-runtime.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "adapters/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing OpenAI-compatible local provider adapter", "existing MEDIUM fleet readiness contract", "existing llm-local Docker network"], "may_change": []},
|
||||
"semantic_owner": "deployment topology/cognitive-worker registration",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["MEDIUM unconfigured -> live provider discovery/readiness", "preferred HEAVY unavailable -> highest available fallback selection"],
|
||||
"side_effects_and_privileges": ["one explicit read-only network attachment from cognitive-worker to existing llm-local runtime; no secret or database privilege added"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["if external llm-local network is absent deployment fails closed; provider timeout remains bounded; Lifeboat remains fallback"],
|
||||
"assumptions": ["production llm-local_default network and llm-local-api are independently operated and present", "llm-local-api exposes OpenAI-compatible /v1/models and chat routes on port 8080"],
|
||||
"acceptance_scenarios": ["compose config validates with external network declaration", "cognitive-worker resolves llm-local-api", "MEDIUM fleet member is AVAILABLE live", "runtime /readyz remains 200", "Lifeboat fallback remains available", "no secret value is added"],
|
||||
"required_tests": ["docker compose config", "go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production /readyz and fleet readiness"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000172",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "2c7a85779b530e3147fbf47b71742759cc010d58",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make fleet failover choose the best available non-Lifeboat tier below the preferred tier before using Lifeboat.",
|
||||
"requirement_ids": ["WP-301", "Model fleet/failover"],
|
||||
"allowed_change_scope": {"modules": ["capability", "cognitive-worker"], "paths": ["modules/capability/domain/fleet.go", "modules/capability/domain/fleet_test.go", "cmd/cognitive-worker/fleet_status_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000172-fleet-failover-priority.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "adapters/**", ".gitea/**"]},
|
||||
"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": ["existing FleetReadiness contract", "existing FleetTier selection contract"], "may_change": []},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["preferred unavailable -> next lower ready tier -> Lifeboat fallback"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["selection remains deterministic from observed readiness; invalid preferred tier fails closed"],
|
||||
"assumptions": ["HEAVY > MEDIUM > FAST is the quality/capability fallback order; LIFEBOAT is last-resort recovery"],
|
||||
"acceptance_scenarios": ["HEAVY unavailable and MEDIUM ready selects MEDIUM", "HEAVY and MEDIUM unavailable and FAST ready selects FAST", "all primary tiers unavailable selects Lifeboat", "invalid preferred tier is rejected"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production fleet readiness"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000173",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "64642f4baed28f9051efe6a269769c5838b547e4",
|
||||
"risk_tier": "E",
|
||||
"intent": "Refresh audit and production evidence after live MEDIUM runtime availability and deterministic fleet failover priority are deployed.",
|
||||
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-200", "WP-301"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "docs/evidence/wp-200-wp-301-fleet-execution-status-2026-08-22-v3.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000173-refresh-medium-failover-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["live fleet readiness evidence", "audit completion matrix"], "may_change": []},
|
||||
"semantic_owner": "none; audit evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["stale evidence -> current MEDIUM/failover evidence"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
|
||||
"failure_retry_restart": ["not applicable"],
|
||||
"assumptions": ["live probes were captured after production deployment of 64642f4b", "availability is not interpreted as qualification or activation"],
|
||||
"acceptance_scenarios": ["production revision is current", "MEDIUM model catalog and readiness are recorded", "preferred HEAVY falls back to MEDIUM", "remaining bridge/GPU/browser gaps remain explicit", "no secret value appears"],
|
||||
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"task_id": "DEV-000174",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "6e0d1a227918775b906e3462406c1505813f51c6",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record the bounded production qualification result for the reachable MEDIUM model without changing lifecycle state.",
|
||||
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-200", "WP-301"],
|
||||
"allowed_change_scope": {
|
||||
"modules": [],
|
||||
"paths": [
|
||||
"docs/evidence/wp-200-medium-qualification-2026-08-22.json",
|
||||
"docs/evidence/audit-completion-matrix-2026-08-22-v1.json",
|
||||
"docs/PROJECT_STATUS.md",
|
||||
"dev/ACTIVE_TASK.json",
|
||||
"dev/tasks/DEV-000174-medium-qualification-evidence.json"
|
||||
]
|
||||
},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["creator-advisory synthetic benchmark", "production cognitive registry revisions"], "may_change": []},
|
||||
"semantic_owner": "none; audit evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["unqualified MEDIUM availability -> recorded qualification result"],
|
||||
"side_effects_and_privileges": ["read-only registry lookup and one-shot benchmark invocation; no lifecycle mutation"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
|
||||
"failure_retry_restart": ["benchmark timeout is recorded as UNSUPPORTED; no retry changes lifecycle state"],
|
||||
"assumptions": ["the benchmark binary used the repository cognitive adapter and the production llm-local network", "binding and model revisions were obtained read-only from production registry"],
|
||||
"acceptance_scenarios": ["all four synthetic cases are recorded", "timeout and unsupported result are explicit", "no canary or activation is performed", "no secret value appears", "audit matrix retains qualification gap"],
|
||||
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000175",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "b29f8775e1217506098d5c9a0e99c1046494cba3",
|
||||
"risk_tier": "E",
|
||||
"intent": "Make local OpenAI-compatible provider invocation deadlines explicit per provider so slow but reachable fleet members can be qualified through the existing adapter path.",
|
||||
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
|
||||
"allowed_change_scope": {"modules": ["cognitive adapter", "cognitive worker", "benchmark runtime"], "paths": ["adapters/models/openai_compatible_local.go", "adapters/models/openai_compatible_local_test.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-benchmark/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000175-medium-invocation-timeout.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "modules/**", "adapters/postgres/**", ".gitea/**"]},
|
||||
"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": ["existing provider-neutral cognitive adapter contract", "existing provider environment configuration"], "may_change": ["provider-specific invocation timeout configuration"]},
|
||||
"semantic_owner": "cognitive adapter/runtime",
|
||||
"canonical_state": ["adapter request deadline"],
|
||||
"state_transitions": ["fixed local adapter deadline -> explicit positive provider-specific deadline"],
|
||||
"side_effects_and_privileges": ["none beyond existing provider requests"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["non-positive configuration falls back to safe default; request context remains authoritative"],
|
||||
"assumptions": ["the existing Medium runtime is reachable but may require more than 10 seconds for a complete response", "default CPU behavior remains 10 seconds unless separately configured"],
|
||||
"acceptance_scenarios": ["adapter accepts positive explicit timeout", "adapter rejects non-positive timeout", "benchmark passes its case timeout to the adapter", "production Medium defaults to 60 seconds", "no provider call bypasses the adapter"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000176",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "389dd0c23d818e3bde08d8353287938837179da8",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record the post-timeout-change production qualification retest for the reachable MEDIUM model.",
|
||||
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/wp-200-medium-qualification-2026-08-22-v2.json", "docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000176-medium-qualification-retest.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["production cognitive adapter qualification artifact"], "may_change": []},
|
||||
"semantic_owner": "none; audit evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["timeout-policy retest -> verified model-generation gap"],
|
||||
"side_effects_and_privileges": ["one-shot benchmark only; no lifecycle mutation"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
|
||||
"failure_retry_restart": ["45-second case deadline is recorded; unsupported result is not promoted"],
|
||||
"assumptions": ["retest ran through the updated cognitive adapter in production networks", "no activation or canary was attempted"],
|
||||
"acceptance_scenarios": ["all four cases record approximately 45-second timeout", "unsupported result remains explicit", "production status retains Medium qualification gap", "no secrets appear"],
|
||||
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000177",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "d60fb8697a577b113da2043bddca3b88e9e562fd",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record bounded production qualification results for currently readiness-available Heavy model candidates.",
|
||||
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/wp-200-heavy-qualification-2026-08-22.json", "docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000177-heavy-qualification-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["production Ollama adapter qualification artifact", "fleet readiness evidence"], "may_change": []},
|
||||
"semantic_owner": "none; audit evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["Heavy readiness available -> Heavy model candidates qualification recorded"],
|
||||
"side_effects_and_privileges": ["one-shot benchmark only; no lifecycle mutation"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
|
||||
"failure_retry_restart": ["MODEL_UNAVAILABLE is recorded as unsupported; no activation or promotion"],
|
||||
"assumptions": ["all four candidates were taken from the production registry read-only", "readiness availability does not imply model invocability"],
|
||||
"acceptance_scenarios": ["all four Heavy candidates are represented", "MODEL_UNAVAILABLE results are explicit", "audit status distinguishes readiness from qualification", "no secret values appear"],
|
||||
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000178",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "dc1ee7ff3c5e284548bf5d9e1e26c9dc9f2c88fd",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record refreshed Heavy registry discovery and successful bounded qualification for the currently installed model candidate.",
|
||||
"requirement_ids": ["WP-200", "WP-301", "AUDIT-COMPLETION-TRUTH"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/wp-200-heavy-qualification-2026-08-22-v2.json", "docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000178-heavy-qualification-refresh.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["provider discovery endpoint", "Ollama adapter qualification artifact"], "may_change": []},
|
||||
"semantic_owner": "none; audit evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["stale Heavy inventory -> current gemma4:26b supported qualification evidence"],
|
||||
"side_effects_and_privileges": ["provider discovery refresh and one-shot benchmark; no lifecycle promotion"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence artifacts only"],
|
||||
"failure_retry_restart": ["stale candidates remain historical evidence; current candidate is separately identified by immutable revisions"],
|
||||
"assumptions": ["discovery was invoked through cognitive-worker's Ollama adapter path", "qualification does not activate an INACTIVE binding"],
|
||||
"acceptance_scenarios": ["current gemma4:26b revisions are recorded", "all four synthetic cases pass", "stale qwen candidate result is not treated as current", "activation remains explicit and unclaimed", "no secrets appear"],
|
||||
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000179",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "5960844fcb7b9bfba9c24e18a5d15b569d0d3a08",
|
||||
"risk_tier": "E",
|
||||
"intent": "Synchronize audit projections with current production runtime evidence after the Heavy qualification deployment.",
|
||||
"requirement_ids": ["AUDIT-COMPLETION-TRUTH", "WP-301", "WP-400", "WP-102", "WP-600"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/audit-runtime-status-2026-08-22-v4.json", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000179-runtime-status-consolidation.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["live health/readiness/status endpoints"], "may_change": []},
|
||||
"semantic_owner": "none; audit projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["stale audit projection -> current production runtime evidence"],
|
||||
"side_effects_and_privileges": ["read-only live probes"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence only"],
|
||||
"failure_retry_restart": ["not applicable"],
|
||||
"assumptions": ["production revision is read from creator-web runtime environment", "runtime readiness is not treated as capability qualification"],
|
||||
"acceptance_scenarios": ["public health is current", "research/work-graph readiness is current", "agent recovery-only state is explicit", "fleet tiers and selected tier are current", "no secrets appear"],
|
||||
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000180",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "2ed2302526f6915c9ddef852ed17a8c47f716cc2",
|
||||
"risk_tier": "E",
|
||||
"intent": "Allow the existing synthetic qualification benchmark to bound maximum output bytes per case for slow local providers.",
|
||||
"requirement_ids": ["WP-200", "AUDIT-COMPLETION-TRUTH"],
|
||||
"allowed_change_scope": {"modules": ["cognitive benchmark"], "paths": ["cmd/cognitive-benchmark/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000180-bounded-medium-qualification-output.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/cognitive-worker/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["existing cognitive advisory request contract"], "may_change": ["benchmark CLI parameter only"]},
|
||||
"semantic_owner": "cognitive benchmark",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["fixed benchmark output cap -> explicit bounded qualification run"],
|
||||
"side_effects_and_privileges": ["none"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["non-positive output cap rejected; default preserves previous 4096-byte behavior"],
|
||||
"assumptions": ["bounded output is valid only for synthetic exact-answer corpus and does not alter production provider configuration"],
|
||||
"acceptance_scenarios": ["default behavior remains unchanged", "positive output cap is forwarded to advisory request", "invalid cap fails closed", "no provider boundary is bypassed"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000181",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "6f0e099632a79af90eab439b4f026cd31abaad26",
|
||||
"risk_tier": "E",
|
||||
"intent": "Record the final bounded Medium qualification retest after adding an explicit benchmark output cap.",
|
||||
"requirement_ids": ["WP-200", "AUDIT-COMPLETION-TRUTH"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["docs/evidence/wp-200-medium-qualification-2026-08-22-v3.json", "docs/PROJECT_STATUS.md", "docs/evidence/audit-completion-matrix-2026-08-22-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000181-medium-bounded-retest-evidence.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "contracts/**", "adapters/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["bounded cognitive benchmark artifact"], "may_change": []},
|
||||
"semantic_owner": "none; audit evidence projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["Medium output-cap hypothesis -> verified runtime performance gap"],
|
||||
"side_effects_and_privileges": ["one-shot benchmark only; no lifecycle mutation"],
|
||||
"persistence_and_migrations": ["immutable JSON evidence only"],
|
||||
"failure_retry_restart": ["60-second case timeout and 256-byte cap are recorded; unsupported result is not promoted"],
|
||||
"assumptions": ["benchmark used the existing cognitive adapter and current production Medium revisions", "a bounded exact-answer corpus is sufficient to disprove the output-cap hypothesis"],
|
||||
"acceptance_scenarios": ["all four cases record approximately 60-second timeout", "quality remains 0‰ and critical failure 1000‰", "Medium remains unqualified", "no secret values appear"],
|
||||
"required_tests": ["JSON validation", "go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl/independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000182",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "4709ef46076cf5c22522bbfea785fdb3c6220c14",
|
||||
"risk_tier": "E",
|
||||
"intent": "Expose an evidence-based model rating and adaptive routing policy so model category follows qualification and observed performance rather than provider or hardware names.",
|
||||
"requirement_ids": ["WP-200", "WP-601", "adaptive model routing", "CPU-first current operation"],
|
||||
"allowed_change_scope": {"modules": ["capability", "cognitive-worker"], "paths": ["contracts/capability/cognitive_registry.go", "modules/capability/domain/cognitive_registry.go", "modules/capability/domain/fleet.go", "modules/capability/domain/fleet_test.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/fleet_status_test.go", "modules/capability/domain/model_rating_test.go", "docs/MODEL_FLEET_POLICY.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000182-model-rating-and-adaptive-routing.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", ".gitea/**"]},
|
||||
"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": ["EvaluatedCapability", "PerformanceEvidence", "CognitiveRequirement"], "may_change": ["provider-neutral model rating projection"]},
|
||||
"semantic_owner": "capability",
|
||||
"canonical_state": ["existing owner-local qualification and performance evidence; rating is a derived projection"],
|
||||
"state_transitions": ["unrated -> rated only after supported qualification evidence; category is recomputed from current evidence; routing chooses an eligible rated candidate and falls back on invocation failure"],
|
||||
"side_effects_and_privileges": ["none; no provider invocation or external mutation"],
|
||||
"persistence_and_migrations": ["no migration; rating is derived and serializable"],
|
||||
"failure_retry_restart": ["missing or stale evidence yields UNRATED and cannot silently claim quality; existing gateway fallback remains authoritative"],
|
||||
"assumptions": ["CPU is the current operational choice, but routing remains provider- and hardware-neutral", "discovery never activates a binding"],
|
||||
"acceptance_scenarios": ["supported evidence produces a visible rating and category", "missing evidence remains UNRATED", "quality objective prefers higher evidence rating", "speed objective prefers lower observed latency", "provider identity alone cannot raise a rating", "CPU is preferred now but all ready capability classes remain valid failover candidates"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000184",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "16a0e8c4924a2e644294e12f048e747c00f940b9",
|
||||
"risk_tier": "E",
|
||||
"intent": "Connect an optional OpenAI-compatible GPU provider through protected Domovoy storage without literal provider credentials or hardwired environment configuration, while preserving CPU/Lifeboat degraded operation.",
|
||||
"requirement_ids": ["GPU provider endpoint is authenticated through protected storage", "GPU absence or invalid configuration does not stop cognitive-worker", "fleet readiness and discovery use OpenAI-compatible /v1/models", "CPU and Lifeboat remain available fallback paths"],
|
||||
"allowed_change_scope": {"modules": ["cognitive-worker", "deployment"], "paths": ["cmd/cognitive-worker/**", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000184-gpu-openai-protected-provider.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "contracts/**", "modules/**", "security/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["protected Domovoy secret files", "OpenAI-compatible /v1/models and /v1/chat/completions", "existing cognitive gateway and fleet readiness contracts"], "may_change": []},
|
||||
"semantic_owner": "capability provider adapter projection",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["missing/invalid protected GPU config -> provider omitted and degraded readiness", "valid config -> authenticated discovery/readiness and optional heavy-tier candidate"],
|
||||
"side_effects_and_privileges": ["outbound authenticated requests to the explicitly configured GPU endpoint; no credentials forwarded to Core or user responses"],
|
||||
"persistence_and_migrations": ["no schema changes; protected file storage only"],
|
||||
"failure_retry_restart": ["GPU timeout/unavailability falls back through existing router to CPU/Lifeboat", "restart reloads protected files", "invalid config disables only optional GPU provider"],
|
||||
"assumptions": ["the supplied endpoint implements OpenAI-compatible /v1/models and /v1/chat/completions", "the protected secret volume is mounted read-only into cognitive-worker", "docker-test remains prohibited"],
|
||||
"acceptance_scenarios": ["GPU /v1/models succeeds with protected key", "GPU key never appears in env/logs/responses", "GPU discovery creates provider-neutral records", "fleet readiness reports GPU heavy tier only when configured", "stopping GPU leaves CPU/Lifeboat ready", "chat remains available without GPU files"],
|
||||
"required_tests": ["go test ./cmd/cognitive-worker ./adapters/models", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000184-gpu-openai-protected-provider.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000184-gpu-openai-protected-provider.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check", "CI", "production protected-key models/readiness/live fallback checks"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000185",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0017",
|
||||
"base_repo_revision": "7ab4b02afa78d5c65eb1545bfd6e117cc1cdfad7",
|
||||
"risk_tier": "E",
|
||||
"intent": "Load the optional external Agent Execution bridge endpoint from Domovoy protected storage and preserve recovery-only operation when it is absent or invalid.",
|
||||
"requirement_ids": ["protected bridge configuration", "degraded recovery without bridge", "durable dispatch when bridge is configured"],
|
||||
"allowed_change_scope": {"modules": ["agent-worker", "deployment"], "paths": ["cmd/agent-worker/**", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000185-agent-bridge-protected-config.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "contracts/**", "security/**", ".gitea/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["existing agentbridge HTTP executor", "protected runtime configuration files", "existing work dispatch contract"], "may_change": []},
|
||||
"semantic_owner": "work agent execution adapter",
|
||||
"canonical_state": [],
|
||||
"state_transitions": ["missing/invalid protected bridge URL -> recovery-only", "valid protected bridge URL -> execution mode"],
|
||||
"side_effects_and_privileges": ["outbound HTTP calls only to protected configured bridge endpoint"],
|
||||
"persistence_and_migrations": ["no schema changes; protected file storage only"],
|
||||
"failure_retry_restart": ["bridge errors remain handled by existing worker cycle", "restart reloads protected URL", "missing bridge never prevents recovery"],
|
||||
"assumptions": ["protected secret volume is mounted read-only", "bridge URL file is deployment-owned"],
|
||||
"acceptance_scenarios": ["protected URL enables execution mode", "missing URL keeps recovery-only mode", "URL is not read from environment", "URL value never appears in status"],
|
||||
"required_tests": ["go test ./cmd/agent-worker/...", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000185-agent-bridge-protected-config.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000185-agent-bridge-protected-config.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI", "production status and recovery checks"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"task_id": "DEV-000186",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "cdbccb6",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add a typed, proposal-only ExtensionProposal generator from repeated or corroborated evidence without granting mutation, authority, deployment, or promotion capability.",
|
||||
"requirement_ids": ["WP-601", "Extension proposal generator", "Evidence deduplication", "Governed rollback plan"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["work-agent-execution"],
|
||||
"paths": ["contracts/work/self_development.go", "modules/work/domain/self_development.go", "modules/work/domain/self_development_test.go", "tests/semantic_evolution/self_development_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000186-extension-proposal-generator.json"]
|
||||
},
|
||||
"forbidden_change_scope": {
|
||||
"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "adapters/**", "deploy/**", "contracts/authority/**", ".gitea/**"]
|
||||
},
|
||||
"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": ["ADR-0022 governed self-development boundary", "existing Work DevelopmentGate"],
|
||||
"may_change": ["typed Work ExtensionProposal value objects"]
|
||||
},
|
||||
"semantic_owner": "work-agent-execution",
|
||||
"canonical_state": ["No durable canonical state; proposal value object only"],
|
||||
"state_transitions": ["repeated evidence -> PROPOSED ExtensionProposal", "insufficient/conflicting/incomplete evidence -> rejected result"],
|
||||
"side_effects_and_privileges": ["No source mutation, provider invocation, secret access, deployment, promotion, or authority expansion"],
|
||||
"persistence_and_migrations": ["No schema or migration changes"],
|
||||
"failure_retry_restart": ["Deterministic generation is rerunnable; unique evidence refs and signal keys preserve lineage; one failure cannot generate a proposal"],
|
||||
"assumptions": ["Human governance remains the promotion authority", "A later task may add durable proposal persistence only after owner and migration review"],
|
||||
"acceptance_scenarios": ["one failure is rejected", "repeated same-signal evidence produces one proposal with deduplicated signal keys and retained refs", "missing assumptions/evaluation/rollback fails closed", "proposal contains no executable authority or mutation field"],
|
||||
"required_tests": ["go test ./modules/work/domain ./tests/semantic_evolution", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000186-extension-proposal-generator.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000186-extension-proposal-generator.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"task_id": "DEV-000187",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "a09a7e8",
|
||||
"risk_tier": "E",
|
||||
"intent": "Add a fail-closed typed scaffold candidate factory that produces an isolated task-packet descriptor without creating files, reading secrets, or accessing protected main/production.",
|
||||
"requirement_ids": ["WP-602", "Scoped task packet candidate", "Isolated worktree provenance", "Protected path denial"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["work-agent-execution"],
|
||||
"paths": ["contracts/work/self_development.go", "modules/work/domain/self_development.go", "modules/work/domain/self_development_test.go", "tests/semantic_evolution/self_development_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000187-scaffold-task-packet-candidate.json"]
|
||||
},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "adapters/**", "deploy/**", "contracts/authority/**", ".gitea/**"]},
|
||||
"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": ["Existing ExtensionProposal", "ADR-0022 governed development boundary"], "may_change": ["typed Work scaffold candidate value objects"]},
|
||||
"semantic_owner": "work-agent-execution",
|
||||
"canonical_state": ["No durable state; isolated scaffold candidate value object only"],
|
||||
"state_transitions": ["proposal -> SANDBOXED task-packet candidate", "invalid scope/policy -> rejected result"],
|
||||
"side_effects_and_privileges": ["No filesystem mutation, source checkout, secret access, provider call, deployment, promotion, or authority expansion"],
|
||||
"persistence_and_migrations": ["No schema or migration changes"],
|
||||
"failure_retry_restart": ["Deterministic validation is rerunnable; invalid or protected scope fails closed"],
|
||||
"assumptions": ["A later external development control plane performs actual worktree creation", "Human governance remains promotion authority"],
|
||||
"acceptance_scenarios": ["valid proposal produces scoped SANDBOXED descriptor", "absolute/traversal/protected path is rejected", "production and secret access policies must be DENIED", "missing provenance or test plan is rejected", "descriptor contains no command or credential material"],
|
||||
"required_tests": ["go test ./modules/work/domain ./tests/semantic_evolution", "go vet ./...", "go run ./cmd/devctl preflight -task dev/tasks/DEV-000187-scaffold-task-packet-candidate.json", "go run ./cmd/devctl check-diff -task dev/tasks/DEV-000187-scaffold-task-packet-candidate.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check", "CI"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user