Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f16ec75f5 | ||
|
|
c0ffc59666 | ||
|
|
aa5e33778c | ||
|
|
870392dc09 | ||
|
|
005d244950 | ||
|
|
c44dd9e0be | ||
|
|
2f639c9d6c | ||
|
|
ec7c4fef66 | ||
|
|
c831ee1260 | ||
|
|
81ce8dfdb2 | ||
|
|
750c87c926 | ||
|
|
e6e051d59f | ||
|
|
5a9714645f | ||
|
|
cf818cfc47 | ||
|
|
a6b0fcd768 | ||
|
|
dd9bca3ace | ||
|
|
9d94a86300 |
@@ -62,3 +62,13 @@ func (r *IdentityRepository) UpdateCreatorPassword(ctx context.Context, actor id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *IdentityRepository) LoadServicePrincipal(ctx context.Context, principal identity.PrincipalID) (identity.ServicePrincipal, error) {
|
||||
var value identity.ServicePrincipal
|
||||
err := r.pool.QueryRow(ctx, `SELECT actor_id, principal_id, kind, purpose, synthetic_scope, active, revision FROM identity.service_principals WHERE principal_id=$1`, principal).
|
||||
Scan(&value.ActorID, &value.PrincipalID, &value.Kind, &value.Purpose, &value.SyntheticScope, &value.Active, &value.Revision)
|
||||
if err != nil {
|
||||
return identity.ServicePrincipal{}, fmt.Errorf("load service principal: %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ import (
|
||||
// evidence. It never evaluates, promotes or changes Capability state.
|
||||
type SelfRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
type AcceptanceSummary struct {
|
||||
LearningSignals int `json:"learning_signals"`
|
||||
ImprovementNeeds int `json:"improvement_needs"`
|
||||
LearningLedger int `json:"learning_ledger"`
|
||||
SyntheticPrincipal string `json:"synthetic_principal"`
|
||||
}
|
||||
|
||||
func OpenSelf(ctx context.Context, config ContinuityConfig) (*SelfRepository, error) {
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
@@ -23,6 +30,21 @@ func OpenSelf(ctx context.Context, config ContinuityConfig) (*SelfRepository, er
|
||||
|
||||
func (r *SelfRepository) Close() { r.pool.Close() }
|
||||
|
||||
func (r *SelfRepository) AcceptanceSummary(ctx context.Context, principalID string) (AcceptanceSummary, error) {
|
||||
var value AcceptanceSummary
|
||||
value.SyntheticPrincipal = principalID
|
||||
if err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM self.learning_signals WHERE principal_id=$1`, principalID).Scan(&value.LearningSignals); err != nil {
|
||||
return AcceptanceSummary{}, err
|
||||
}
|
||||
if err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM self.improvement_needs WHERE principal_id=$1`, principalID).Scan(&value.ImprovementNeeds); err != nil {
|
||||
return AcceptanceSummary{}, err
|
||||
}
|
||||
if err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM self.learning_ledger WHERE principal_id=$1`, principalID).Scan(&value.LearningLedger); err != nil {
|
||||
return AcceptanceSummary{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *SelfRepository) SaveImprovementNeed(ctx context.Context, value selfcontract.ImprovementNeed) error {
|
||||
subject, err := json.Marshal(value.SubjectRef)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
const tokenPath = "/run/secrets/acceptance_harness_service_token"
|
||||
|
||||
type client struct {
|
||||
base string
|
||||
token string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func main() {
|
||||
base := strings.TrimRight(os.Getenv("CORE_RUNTIME_URL"), "/")
|
||||
if base == "" {
|
||||
base = "http://core-runtime-candidate:8080"
|
||||
}
|
||||
token, err := os.ReadFile(tokenPath)
|
||||
if err != nil {
|
||||
fail("acceptance credential unavailable")
|
||||
}
|
||||
c := client{base: base, token: strings.TrimSpace(string(token)), http: &http.Client{Timeout: 90 * time.Second}}
|
||||
if strings.TrimSpace(c.token) == "" {
|
||||
fail("acceptance credential unavailable")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
var issued struct {
|
||||
Token string
|
||||
Session identity.CreatorSession
|
||||
}
|
||||
if err := c.post(ctx, "/v1/acceptance/sessions", nil, &issued); err != nil {
|
||||
fail("acceptance authentication failed: " + err.Error())
|
||||
}
|
||||
if issued.Token == "" || issued.Session.PrincipalKind != identity.PrincipalKindService {
|
||||
fail("acceptance principal was not authenticated as service")
|
||||
}
|
||||
c.token = issued.Token
|
||||
var first interaction.ChatMessageResponse
|
||||
if err := c.post(ctx, "/v1/creator/chat/messages", interaction.ChatMessageRequest{Message: "synthetic acceptance statement", CorrelationID: "acceptance-harness", SyntheticAcceptance: true, SyntheticScopeID: "synthetic-acceptance-scope"}, &first); err != nil {
|
||||
fail("synthetic interaction failed: " + err.Error())
|
||||
}
|
||||
var corrected interaction.ChatMessageResponse
|
||||
if err := c.post(ctx, "/v1/creator/chat/messages", interaction.ChatMessageRequest{ConversationID: first.ConversationID, Message: "synthetic acceptance correction", CorrelationID: "acceptance-harness-correction", CorrectsUtteranceID: first.UtteranceID, SyntheticAcceptance: true, SyntheticScopeID: "synthetic-acceptance-scope"}, &corrected); err != nil {
|
||||
fail("synthetic correction failed: " + err.Error())
|
||||
}
|
||||
var summary struct {
|
||||
LearningSignals int `json:"learning_signals"`
|
||||
ImprovementNeeds int `json:"improvement_needs"`
|
||||
LearningLedger int `json:"learning_ledger"`
|
||||
}
|
||||
if err := c.get(ctx, "/v1/acceptance/self/summary", &summary); err != nil {
|
||||
fail("acceptance SELF summary unavailable: " + err.Error())
|
||||
}
|
||||
fmt.Printf("acceptance_pass learning_signals=%d improvement_needs=%d learning_ledger=%d\n", summary.LearningSignals, summary.ImprovementNeeds, summary.LearningLedger)
|
||||
}
|
||||
|
||||
func (c client) post(ctx context.Context, path string, body any, out any) error {
|
||||
return c.request(ctx, http.MethodPost, path, body, out)
|
||||
}
|
||||
func (c client) get(ctx context.Context, path string, out any) error {
|
||||
return c.request(ctx, http.MethodGet, path, nil, out)
|
||||
}
|
||||
func (c client) request(ctx context.Context, method, path string, body any, out any) error {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader = bytes.NewReader(data)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.base+path, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("acceptance request denied with status %d", resp.StatusCode)
|
||||
}
|
||||
if out != nil && json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(out) != nil {
|
||||
return errors.New("invalid acceptance response")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func fail(message string) { fmt.Fprintln(os.Stderr, message); os.Exit(1) }
|
||||
@@ -4,8 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -53,8 +56,83 @@ type modelQualificationResponse struct {
|
||||
Results []modelQualificationResult `json:"results"`
|
||||
}
|
||||
|
||||
const qualificationControlPlaneTimeout = 45 * time.Second
|
||||
|
||||
// qualificationCoordinator bounds duplicate control-plane probes within one
|
||||
// cognitive-worker process. It is deliberately not canonical state: the
|
||||
// Capability owner remains the sole owner of qualification evidence and
|
||||
// activation. A restart may repeat an incomplete probe, but cannot create a
|
||||
// second semantic writer or bypass independent evaluation.
|
||||
type qualificationCoordinator struct {
|
||||
mu sync.Mutex
|
||||
flights map[string]*qualificationFlight
|
||||
}
|
||||
|
||||
type qualificationFlight struct {
|
||||
done chan struct{}
|
||||
response modelQualificationResponse
|
||||
err error
|
||||
waiters int
|
||||
}
|
||||
|
||||
func (c *qualificationCoordinator) do(ctx context.Context, key string, operation func() (modelQualificationResponse, error)) (modelQualificationResponse, bool, error) {
|
||||
c.mu.Lock()
|
||||
if c.flights == nil {
|
||||
c.flights = make(map[string]*qualificationFlight)
|
||||
}
|
||||
if current, exists := c.flights[key]; exists {
|
||||
current.waiters++
|
||||
c.mu.Unlock()
|
||||
select {
|
||||
case <-current.done:
|
||||
return current.response, false, current.err
|
||||
case <-ctx.Done():
|
||||
return modelQualificationResponse{}, false, ctx.Err()
|
||||
}
|
||||
}
|
||||
current := &qualificationFlight{done: make(chan struct{})}
|
||||
c.flights[key] = current
|
||||
c.mu.Unlock()
|
||||
|
||||
current.response, current.err = operation()
|
||||
close(current.done)
|
||||
c.mu.Lock()
|
||||
delete(c.flights, key)
|
||||
c.mu.Unlock()
|
||||
return current.response, true, current.err
|
||||
}
|
||||
|
||||
type qualificationPhaseTrace struct {
|
||||
correlationID string
|
||||
started time.Time
|
||||
}
|
||||
|
||||
func newQualificationPhaseTrace() qualificationPhaseTrace {
|
||||
correlationID, err := ids.New()
|
||||
if err != nil {
|
||||
correlationID = ids.ID(fmt.Sprintf("qualification-%d", time.Now().UnixNano()))
|
||||
}
|
||||
return qualificationPhaseTrace{correlationID: string(correlationID), started: time.Now()}
|
||||
}
|
||||
|
||||
func (t qualificationPhaseTrace) phase(name string, fields ...string) {
|
||||
log.Printf("qualification_phase correlation_id=%s phase=%s elapsed_ms=%d %s", t.correlationID, name, time.Since(t.started).Milliseconds(), strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
func qualificationFlightKey(provider string, modelIDs []capability.ProviderModelID) string {
|
||||
ids := make([]string, 0, len(modelIDs))
|
||||
for _, modelID := range modelIDs {
|
||||
ids = append(ids, string(modelID))
|
||||
}
|
||||
sort.Strings(ids)
|
||||
return provider + "|" + strings.Join(ids, ",")
|
||||
}
|
||||
|
||||
func modelQualificationHandler(providers map[string]providerRegistration, owner string, observers ...qualificationObserver) http.Handler {
|
||||
coordinator := &qualificationCoordinator{}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
trace := newQualificationPhaseTrace()
|
||||
trace.phase("Q0", "event=request_accepted")
|
||||
name := r.PathValue("provider")
|
||||
registration, ok := providers[name]
|
||||
if !ok || registration.Discovery == nil || registration.Provider == nil {
|
||||
@@ -72,32 +150,65 @@ func modelQualificationHandler(providers map[string]providerRegistration, owner
|
||||
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
|
||||
controlCtx, cancel := context.WithTimeout(r.Context(), qualificationControlPlaneTimeout)
|
||||
defer cancel()
|
||||
response, leader, err := coordinator.do(controlCtx, qualificationFlightKey(name, request.ModelIDs), func() (modelQualificationResponse, error) {
|
||||
trace.phase("Q1", "event=binding_candidate_loaded")
|
||||
modelsFound, probe := registration.Discovery.Discover(controlCtx)
|
||||
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
|
||||
return modelQualificationResponse{}, fmt.Errorf("provider unavailable for qualification")
|
||||
}
|
||||
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 {
|
||||
return modelQualificationResponse{}, fmt.Errorf("requested model not discovered")
|
||||
}
|
||||
if len(modelsFound) > 16 {
|
||||
modelsFound = modelsFound[:16]
|
||||
}
|
||||
trace.phase("Q2", fmt.Sprintf("event=provider_model_resolved model_count=%d", len(modelsFound)))
|
||||
trace.phase("Q3", "event=bounded_invocation_started")
|
||||
results := qualifyModels(controlCtx, registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
|
||||
trace.phase("Q4", "event=bounded_invocation_completed")
|
||||
trace.phase("Q5", fmt.Sprintf("event=invocation_evidence_built result_count=%d", len(results)))
|
||||
if len(observers) > 0 && observers[0] != nil {
|
||||
trace.phase("Q6", "event=observation_persistence_started")
|
||||
if err := persistQualificationResults(controlCtx, observers[0], registration, results); err != nil {
|
||||
return modelQualificationResponse{}, fmt.Errorf("persist model qualification observation: %w", err)
|
||||
}
|
||||
trace.phase("Q7", "event=observation_persistence_completed")
|
||||
}
|
||||
// Q8-Q17 are intentionally outside this worker's authority. Capability
|
||||
// owns independent evaluation and governed promotion; this trace makes
|
||||
// that hand-off explicit instead of disguising it as activation.
|
||||
trace.phase("Q8", "event=independent_evaluation_not_started owner=capability")
|
||||
return modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results}, nil
|
||||
})
|
||||
if !leader {
|
||||
trace.phase("Q0", "event=joined_in_progress_qualification")
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
if controlCtx.Err() != nil {
|
||||
http.Error(w, "qualification timeout", http.StatusGatewayTimeout)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "requested model not discovered") {
|
||||
http.Error(w, "requested model not discovered", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "provider unavailable") {
|
||||
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
trace.phase("Q17", "event=qualification_response_returned activation=not_attempted")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = jsonEncode(w, modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results})
|
||||
_ = jsonEncode(w, response)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -17,6 +18,7 @@ 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}
|
||||
}
|
||||
@@ -83,3 +85,63 @@ func TestModelQualificationPersistsReachabilityWithoutQuality(t *testing.T) {
|
||||
t.Fatalf("invalid qualification observation: %+v err=%v", observer.observations[0], err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelQualificationDeduplicatesSameProviderModelFlight(t *testing.T) {
|
||||
coordinator := &qualificationCoordinator{}
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
firstDone := make(chan struct{})
|
||||
secondDone := make(chan struct{})
|
||||
result := modelQualificationResponse{Provider: "medium"}
|
||||
go func() {
|
||||
_, _, err := coordinator.do(context.Background(), "medium|model-a", func() (modelQualificationResponse, error) {
|
||||
close(started)
|
||||
<-release
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("leader qualification failed: %v", err)
|
||||
}
|
||||
close(firstDone)
|
||||
}()
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("qualification invocation did not start")
|
||||
}
|
||||
go func() {
|
||||
actual, leader, err := coordinator.do(context.Background(), "medium|model-a", func() (modelQualificationResponse, error) {
|
||||
t.Error("duplicate qualification operation ran")
|
||||
return modelQualificationResponse{}, nil
|
||||
})
|
||||
if err != nil || leader || actual.Provider != result.Provider {
|
||||
t.Errorf("joined qualification response=%+v leader=%t err=%v", actual, leader, err)
|
||||
}
|
||||
close(secondDone)
|
||||
}()
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for {
|
||||
coordinator.mu.Lock()
|
||||
flight := coordinator.flights["medium|model-a"]
|
||||
waiters := 0
|
||||
if flight != nil {
|
||||
waiters = flight.waiters
|
||||
}
|
||||
coordinator.mu.Unlock()
|
||||
if waiters == 1 {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("second qualification did not join the in-progress flight")
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
close(release)
|
||||
for _, done := range []chan struct{}{firstDone, secondDone} {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("qualification flight did not complete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ const (
|
||||
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
|
||||
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
|
||||
selfDBPasswordPath = "/run/secrets/self_db_password"
|
||||
acceptanceHarnessTokenPath = "/run/secrets/acceptance_harness_service_token"
|
||||
controlPlaneServiceTokenFile = "control_plane_service_token"
|
||||
)
|
||||
|
||||
@@ -162,6 +163,18 @@ func main() {
|
||||
if err := bootstrapCreatorAccount(context.Background(), identityRepository, creatorUsername, creatorPasswordPath); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
acceptanceServiceToken := ""
|
||||
var acceptancePrincipal identity.ServicePrincipal
|
||||
if os.Getenv("ACCEPTANCE_HARNESS_SERVICE_CREDENTIAL_REF") != "" {
|
||||
acceptanceServiceToken, err = readCredentialRefSecretPurpose("ACCEPTANCE_HARNESS_SERVICE_TOKEN", "ACCEPTANCE_HARNESS_SERVICE_CREDENTIAL_REF", acceptanceHarnessTokenPath, secretcontracts.SecretPurposeServiceAuthentication)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
acceptancePrincipal, err = identityRepository.LoadServicePrincipal(context.Background(), identity.PrincipalID("domovoy-acceptance-harness-principal"))
|
||||
if err != nil || !acceptancePrincipal.Active {
|
||||
log.Fatal("acceptance service principal is unavailable")
|
||||
}
|
||||
}
|
||||
interactionPassword, err := readCredentialRefSecret("INTERACTION_POSTGRES_PASSWORD", "INTERACTION_POSTGRES_CREDENTIAL_REF", interactionDBPasswordPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -230,12 +243,32 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
if err != nil {
|
||||
if err != nil || (session.PrincipalKind != "" && session.PrincipalKind != identity.PrincipalKindHuman) {
|
||||
http.Error(w, "creator session denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
authenticateAcceptance := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
if acceptanceServiceToken == "" || acceptancePrincipal.Kind != identity.PrincipalKindService || !acceptancePrincipal.Active || !identitydomain.BootstrapMatches(acceptanceServiceToken, bearerToken(r)) {
|
||||
http.Error(w, "acceptance authentication denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
_, session, err := identitydomain.IssueServiceSession(sessionKey, acceptancePrincipal, time.Now(), 30*time.Minute)
|
||||
if err != nil {
|
||||
http.Error(w, "acceptance session unavailable", http.StatusServiceUnavailable)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
authenticateAcceptanceSession := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
if err != nil || session.PrincipalKind != identity.PrincipalKindService || session.PrincipalID != acceptancePrincipal.PrincipalID || !acceptancePrincipal.Active {
|
||||
http.Error(w, "acceptance session denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
resolveFocus := func(r *http.Request) (situation.FocusBinding, error) {
|
||||
runtime := situation.RuntimeBinding{SessionID: situation.SessionID(r.URL.Query().Get("session_id")), Workspace: situation.WorkspaceID(r.URL.Query().Get("workspace_id"))}
|
||||
bindings, err := repository.ByRuntime(r.Context(), runtime)
|
||||
@@ -286,6 +319,35 @@ func main() {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/sessions", creatorDatabaseSessionHandler(bootstrapToken, creatorUsername, identityRepository, sessionKey, time.Now))
|
||||
mux.HandleFunc("POST /v1/acceptance/sessions", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateAcceptance(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
token, issued, err := identitydomain.IssueServiceSession(sessionKey, acceptancePrincipal, time.Now(), 30*time.Minute)
|
||||
if err != nil {
|
||||
http.Error(w, "acceptance session unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
_ = session
|
||||
writeCreatorSession(w, token, issued)
|
||||
})
|
||||
mux.HandleFunc("GET /v1/acceptance/self/summary", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := authenticateAcceptanceSession(w, r); !ok {
|
||||
return
|
||||
}
|
||||
if selfRepository == nil {
|
||||
http.Error(w, "SELF unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
summary, err := selfRepository.AcceptanceSummary(r.Context(), string(acceptancePrincipal.PrincipalID))
|
||||
if err != nil {
|
||||
http.Error(w, "SELF summary unavailable", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(summary)
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/account/password", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
if !ok {
|
||||
@@ -596,15 +658,24 @@ func main() {
|
||||
}{Experiences: experiences, Reflections: reflections, Skills: skills})
|
||||
})
|
||||
mux.HandleFunc("POST /v1/creator/chat/messages", func(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := authenticateCreator(w, r)
|
||||
var request interaction.ChatMessageRequest
|
||||
body, readErr := io.ReadAll(http.MaxBytesReader(w, r.Body, interactiondomain.MaxHumanUtteranceBytes+1024))
|
||||
if readErr != nil || json.Unmarshal(body, &request) != nil {
|
||||
http.Error(w, "invalid chat message", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var session identity.CreatorSession
|
||||
var ok bool
|
||||
if request.SyntheticAcceptance {
|
||||
session, ok = authenticateAcceptanceSession(w, r)
|
||||
} else {
|
||||
session, ok = authenticateCreator(w, r)
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request interaction.ChatMessageRequest
|
||||
decoder := json.NewDecoder(io.LimitReader(r.Body, interactiondomain.MaxHumanUtteranceBytes+1024))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil || decoder.Decode(&struct{}{}) != io.EOF {
|
||||
http.Error(w, "invalid chat message", http.StatusBadRequest)
|
||||
if session.PrincipalKind == identity.PrincipalKindService && (!request.SyntheticAcceptance || request.SyntheticScopeID != acceptancePrincipal.SyntheticScope) {
|
||||
http.Error(w, "synthetic acceptance scope denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
|
||||
@@ -17,15 +17,23 @@ const (
|
||||
|
||||
func main() {
|
||||
reason := flag.String("reason", "", "rotation reason: owner-requested-rotation or exposure-detected")
|
||||
credential := flag.String("credential", "control-plane", "credential purpose: control-plane or acceptance-harness")
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
fail("usage: secret-control-plane [ensure|rotate]")
|
||||
}
|
||||
store, err := platformsecrets.NewLocalMachineCredentialStore(defaultRoot, defaultRuntimeUID, defaultRuntimeGID, nil)
|
||||
root := os.Getenv("SECRET_CONTROL_PLANE_ROOT")
|
||||
if root == "" {
|
||||
root = defaultRoot
|
||||
}
|
||||
store, err := platformsecrets.NewLocalMachineCredentialStore(root, defaultRuntimeUID, defaultRuntimeGID, nil)
|
||||
if err != nil {
|
||||
fail("control-plane credential store unavailable")
|
||||
}
|
||||
spec := platformsecrets.ControlPlaneServiceTokenSpec()
|
||||
if strings.TrimSpace(*credential) == "acceptance-harness" {
|
||||
spec = platformsecrets.AcceptanceHarnessServiceTokenSpec()
|
||||
}
|
||||
switch strings.TrimSpace(flag.Arg(0)) {
|
||||
case "ensure":
|
||||
result, err := store.Ensure(spec)
|
||||
|
||||
@@ -16,5 +16,23 @@ type CreatorAccount struct {
|
||||
type CreatorSession struct {
|
||||
ActorID ActorID
|
||||
PrincipalID PrincipalID
|
||||
PrincipalKind PrincipalKind
|
||||
ExpiresAtUnix int64
|
||||
}
|
||||
|
||||
type PrincipalKind string
|
||||
|
||||
const (
|
||||
PrincipalKindHuman PrincipalKind = "HUMAN"
|
||||
PrincipalKindService PrincipalKind = "SERVICE"
|
||||
)
|
||||
|
||||
type ServicePrincipal struct {
|
||||
ActorID ActorID
|
||||
PrincipalID PrincipalID
|
||||
Kind PrincipalKind
|
||||
Purpose string
|
||||
SyntheticScope string
|
||||
Active bool
|
||||
Revision int64
|
||||
}
|
||||
|
||||
@@ -215,6 +215,8 @@ type ChatMessageRequest struct {
|
||||
Message string
|
||||
CorrelationID string
|
||||
CorrectsUtteranceID HumanUtteranceID
|
||||
SyntheticAcceptance bool
|
||||
SyntheticScopeID string
|
||||
}
|
||||
|
||||
type ChatMessageResponse struct {
|
||||
|
||||
@@ -27,7 +27,10 @@ ALTER TABLE authority.delegated_control_plane_audit
|
||||
ADD COLUMN IF NOT EXISTS candidate_target_digest TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE authority.delegated_control_plane_grants DROP CONSTRAINT IF EXISTS delegated_control_plane_grants_binding_id_check;
|
||||
ALTER TABLE authority.delegated_control_plane_grants DROP CONSTRAINT IF EXISTS delegated_control_plane_grants_operations_check;
|
||||
ALTER TABLE authority.delegated_control_plane_grants DROP CONSTRAINT IF EXISTS delegated_control_plane_grants_operations_check1;
|
||||
ALTER TABLE authority.delegated_control_plane_audit DROP CONSTRAINT IF EXISTS delegated_control_plane_audit_operation_check;
|
||||
ALTER TABLE authority.delegated_control_plane_audit DROP CONSTRAINT IF EXISTS delegated_control_plane_audit_operation_check1;
|
||||
ALTER TABLE authority.delegated_control_plane_grants ADD CONSTRAINT delegated_control_plane_grants_operations_nonempty_check CHECK (cardinality(operations) > 0);
|
||||
ALTER TABLE authority.delegated_control_plane_grants ADD CONSTRAINT delegated_control_plane_grants_operations_check CHECK (operations <@ ARRAY['QUALIFY_COGNITIVE_BINDING','EVALUATE_COGNITIVE_BINDING','CANARY_COGNITIVE_BINDING','ACTIVATE_VALIDATED_COGNITIVE_BINDING','CREATE_CANDIDATE_CAPABILITY_BINDING','USE_CANDIDATE_CAPABILITY_BINDING','REVOKE_CANDIDATE_CAPABILITY_BINDING']::TEXT[]);
|
||||
ALTER TABLE authority.delegated_control_plane_audit ADD CONSTRAINT delegated_control_plane_audit_operation_check CHECK (operation IN ('QUALIFY_COGNITIVE_BINDING','EVALUATE_COGNITIVE_BINDING','CANARY_COGNITIVE_BINDING','ACTIVATE_VALIDATED_COGNITIVE_BINDING','REVOKE_DELEGATED_CONTROL_PLANE_GRANT','CREATE_CANDIDATE_CAPABILITY_BINDING','USE_CANDIDATE_CAPABILITY_BINDING','REVOKE_CANDIDATE_CAPABILITY_BINDING'));
|
||||
ALTER TABLE authority.delegated_control_plane_grants ADD CONSTRAINT delegated_control_plane_grants_exact_resource_check CHECK (
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
BEGIN;
|
||||
SET ROLE identity_role;
|
||||
CREATE TABLE IF NOT EXISTS identity.service_principals (
|
||||
principal_id TEXT PRIMARY KEY,
|
||||
actor_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL CHECK (kind = 'SERVICE'),
|
||||
purpose TEXT NOT NULL,
|
||||
synthetic_scope TEXT NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
revision BIGINT NOT NULL DEFAULT 1,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CHECK (principal_id <> '' AND actor_id <> '' AND purpose <> '' AND synthetic_scope <> '' AND revision > 0)
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS service_principals_actor_idx ON identity.service_principals(actor_id);
|
||||
INSERT INTO identity.service_principals(principal_id, actor_id, kind, purpose, synthetic_scope)
|
||||
VALUES ('domovoy-acceptance-harness-principal', 'domovoy-acceptance-harness-actor', 'SERVICE', 'AUTHENTICATE_ACCEPTANCE_HARNESS', 'synthetic-acceptance-scope')
|
||||
ON CONFLICT (principal_id) DO NOTHING;
|
||||
INSERT INTO identity.actors(actor_id, display_name) VALUES ('domovoy-acceptance-harness-actor', 'Domovoy Acceptance Harness') ON CONFLICT (actor_id) DO NOTHING;
|
||||
INSERT INTO identity.principals(principal_id, display_name) VALUES ('domovoy-acceptance-harness-principal', 'Domovoy Acceptance Harness Principal') ON CONFLICT (principal_id) DO NOTHING;
|
||||
INSERT INTO identity.principal_bindings(actor_id, principal_id, active) VALUES ('domovoy-acceptance-harness-actor', 'domovoy-acceptance-harness-principal', TRUE) ON CONFLICT (actor_id, principal_id) DO UPDATE SET active=TRUE;
|
||||
REVOKE ALL ON identity.service_principals FROM PUBLIC;
|
||||
GRANT SELECT ON identity.service_principals TO identity_role;
|
||||
RESET ROLE;
|
||||
COMMIT;
|
||||
@@ -49,6 +49,7 @@ services:
|
||||
IDENTITY_POSTGRES_USER: identity_role
|
||||
IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD}
|
||||
CREATOR_USERNAME: ${CREATOR_USERNAME}
|
||||
ACCEPTANCE_HARNESS_SERVICE_CREDENTIAL_REF: credential://domovoy/acceptance-harness/service-token/v1
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
CAPABILITY_POSTGRES_USER: capability_role
|
||||
@@ -69,20 +70,6 @@ services:
|
||||
COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_OUTPUT_BYTES:-16384}
|
||||
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
|
||||
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime-candidate:8081
|
||||
COMMIT_RUNTIME_URL: http://commit-runtime-candidate:8082
|
||||
CANDIDATE_RELEASE_SET_ID: assistant-slot-b
|
||||
CANDIDATE_SLOT: b
|
||||
CANDIDATE_TASK_ID: GEN2-CAPABILITY-001
|
||||
CANDIDATE_TASK_REVISION: "7"
|
||||
CANDIDATE_PRINCIPAL_ID: domovoy-creator-principal
|
||||
CANDIDATE_CAPABILITY_ID: prospective-notification-delivery
|
||||
CANDIDATE_RESOURCE_SCOPE: synthetic-shadow-notification-v1
|
||||
CANDIDATE_BINDING_MODE: SHADOW
|
||||
CANDIDATE_TARGET_KIND: NOTIFICATION_SHADOW_RECEIVER
|
||||
CANDIDATE_TARGET_REF: notification-receiver-candidate
|
||||
CANDIDATE_TARGET_DIGEST: f9f9fa49a0727444c280c092d5bb340f3a8f33cf325c5396f5ae177a6666da35
|
||||
CANDIDATE_AUTHORITY_GRANT_ID: gen2-capability-001-shadow-notification-grant
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
|
||||
@@ -138,10 +125,10 @@ services:
|
||||
environment:
|
||||
LISTEN_ADDR: ":8084"
|
||||
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime-candidate:8081
|
||||
COMMIT_RUNTIME_URL: http://commit-runtime-candidate:8082
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
|
||||
COMMIT_RUNTIME_URL: http://commit-runtime:8082
|
||||
COGNITIVE_RUNTIME_URL: http://cognitive-worker-candidate:8083
|
||||
AGENT_WORKER_URL: http://agent-worker-candidate:8090
|
||||
AGENT_WORKER_URL: http://agent-worker:8090
|
||||
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
|
||||
networks:
|
||||
shared:
|
||||
@@ -152,133 +139,6 @@ services:
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
notification-receiver:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: notification-receiver, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: ":8085"
|
||||
SHADOW_RECEIVER_MODE: "1"
|
||||
CANDIDATE_RELEASE_SET_ID: assistant-slot-b
|
||||
CANDIDATE_TASK_ID: GEN2-CAPABILITY-001
|
||||
RECEIPT_DIRECTORY: /var/lib/notification-shadow-receiver
|
||||
RECEIVER_WRITE_TOKEN_FILE: /run/notification-shadow-writer/token
|
||||
RECEIVER_READ_TOKEN_FILE: /run/notification-shadow-reader/token
|
||||
volumes:
|
||||
- assistant_candidate_notification_shadow_receipts:/var/lib/notification-shadow-receiver
|
||||
- assistant_candidate_notification_shadow_writer_secret:/run/notification-shadow-writer:ro
|
||||
- assistant_candidate_notification_shadow_reader_secret:/run/notification-shadow-reader:ro
|
||||
networks: { shared: { aliases: [notification-receiver-candidate] } }
|
||||
healthcheck: { test: ["CMD-SHELL", "wget -qO- http://localhost:8085/healthz >/dev/null"], interval: 10s, timeout: 3s, retries: 10 }
|
||||
|
||||
continuity-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: ":8081"
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
POSTGRES_USER: continuity_role
|
||||
POSTGRES_PASSWORD: ${CONTINUITY_DB_PASSWORD}
|
||||
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
|
||||
COMMIT_RUNTIME_URL: http://commit-runtime-candidate:8082
|
||||
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN}
|
||||
networks: { shared: { aliases: [continuity-runtime-candidate] } }
|
||||
healthcheck: { test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz >/dev/null"], interval: 10s, timeout: 5s, retries: 20 }
|
||||
|
||||
commit-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: commit-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: ":8082"
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
POSTGRES_USER: commit_role
|
||||
POSTGRES_PASSWORD: ${COMMIT_DB_PASSWORD}
|
||||
AUTHORITY_POSTGRES_USER: authority_role
|
||||
AUTHORITY_POSTGRES_PASSWORD: ${AUTHORITY_DB_PASSWORD}
|
||||
CONTROL_PLANE_SERVICE_CREDENTIAL_REF: credential://domovoy/control-plane/service-token/v1
|
||||
CONTROL_PLANE_SERVICE_SECRET_ROOT: /run/control-plane-secrets
|
||||
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
|
||||
CONTINUITY_RUNTIME_URL: http://continuity-runtime-candidate:8081
|
||||
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN}
|
||||
CANDIDATE_NOTIFICATION_WRITE_TOKEN_FILE: /run/notification-shadow-writer/token
|
||||
CANDIDATE_NOTIFICATION_WRITE_SECRET_REF: credential://domovoy/candidate/notification-write/v1
|
||||
CANDIDATE_RELEASE_SET_ID: assistant-slot-b
|
||||
CANDIDATE_SLOT: b
|
||||
CANDIDATE_TASK_ID: GEN2-CAPABILITY-001
|
||||
CANDIDATE_TASK_REVISION: "7"
|
||||
CANDIDATE_PRINCIPAL_ID: domovoy-creator-principal
|
||||
CANDIDATE_CAPABILITY_ID: prospective-notification-delivery
|
||||
CANDIDATE_RESOURCE_SCOPE: synthetic-shadow-notification-v1
|
||||
CANDIDATE_BINDING_MODE: SHADOW
|
||||
CANDIDATE_TARGET_KIND: NOTIFICATION_SHADOW_RECEIVER
|
||||
CANDIDATE_TARGET_REF: notification-receiver-candidate
|
||||
CANDIDATE_TARGET_DIGEST: f9f9fa49a0727444c280c092d5bb340f3a8f33cf325c5396f5ae177a6666da35
|
||||
CANDIDATE_AUTHORITY_GRANT_ID: gen2-capability-001-shadow-notification-grant
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
|
||||
- assistant_candidate_notification_shadow_writer_secret:/run/notification-shadow-writer:ro
|
||||
networks: { shared: { aliases: [commit-runtime-candidate] } }
|
||||
healthcheck: { test: ["CMD-SHELL", "wget -qO- http://localhost:8082/healthz >/dev/null && wget -qO- http://localhost:8082/readyz >/dev/null"], interval: 10s, timeout: 5s, retries: 20 }
|
||||
|
||||
task-verifier:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: task-verifier, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: "no"
|
||||
environment: { LISTEN_ADDR: ":8091" }
|
||||
networks: { shared: { aliases: [task-verifier-candidate] } }
|
||||
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: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: ":8088"
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
|
||||
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
|
||||
volumes: [assistant_core_secrets:/run/secrets:ro]
|
||||
networks: { shared: { aliases: [work-graph-runtime-candidate] } }
|
||||
healthcheck: { test: ["CMD-SHELL", "wget -qO- http://localhost:8088/healthz >/dev/null"], interval: 10s, timeout: 3s, retries: 10 }
|
||||
|
||||
agent-execution-bridge:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-execution-bridge, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: "no"
|
||||
environment:
|
||||
LISTEN_ADDR: ":8092"
|
||||
COGNITIVE_WORKER_URL: http://cognitive-worker-candidate:8083
|
||||
WORK_GRAPH_RUNTIME_URL: http://work-graph-runtime-candidate:8088
|
||||
TASK_VERIFIER_URL: http://task-verifier-candidate:8091
|
||||
networks: { shared: { aliases: [agent-execution-bridge-candidate] } }
|
||||
healthcheck: { test: ["CMD-SHELL", "wget -qO- http://localhost:8092/healthz >/dev/null"], interval: 10s, timeout: 3s, retries: 10 }
|
||||
|
||||
agent-worker:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: "no"
|
||||
command: ["-worker-id", "agent-worker-candidate", "-status-addr", ":8090"]
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
|
||||
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
COGNITIVE_WORKER_URL: http://cognitive-worker-candidate:8083
|
||||
WORK_GRAPH_RUNTIME_URL: http://work-graph-runtime-candidate:8088
|
||||
TASK_VERIFIER_URL: http://task-verifier-candidate:8091
|
||||
AGENT_EXECUTION_BRIDGE_URL: http://agent-execution-bridge-candidate:8092
|
||||
volumes: [assistant_core_secrets:/run/secrets:ro]
|
||||
networks: { shared: { aliases: [agent-worker-candidate] } }
|
||||
healthcheck: { test: ["CMD-SHELL", "wget -qO- http://localhost:8090/healthz >/dev/null"], interval: 10s, timeout: 3s, retries: 10 }
|
||||
|
||||
volumes:
|
||||
assistant_core_secrets:
|
||||
external: true
|
||||
@@ -288,14 +148,6 @@ volumes:
|
||||
assistant_research_sources:
|
||||
name: assistant_research_sources
|
||||
external: true
|
||||
assistant_candidate_notification_shadow_receipts:
|
||||
name: assistant_candidate_notification_shadow_receipts
|
||||
assistant_candidate_notification_shadow_writer_secret:
|
||||
name: assistant_candidate_notification_shadow_writer_secret
|
||||
external: true
|
||||
assistant_candidate_notification_shadow_reader_secret:
|
||||
name: assistant_candidate_notification_shadow_reader_secret
|
||||
external: true
|
||||
|
||||
networks:
|
||||
shared:
|
||||
|
||||
@@ -66,6 +66,7 @@ services:
|
||||
IDENTITY_POSTGRES_USER: identity_role
|
||||
IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD}
|
||||
CREATOR_USERNAME: ${CREATOR_USERNAME}
|
||||
ACCEPTANCE_HARNESS_SERVICE_CREDENTIAL_REF: credential://domovoy/acceptance-harness/service-token/v1
|
||||
WORK_POSTGRES_USER: work_role
|
||||
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
|
||||
CAPABILITY_POSTGRES_USER: capability_role
|
||||
@@ -265,6 +266,7 @@ services:
|
||||
command: ["ensure"]
|
||||
volumes:
|
||||
- assistant_control_plane_service_secrets:/run/control-plane-secrets
|
||||
- assistant_core_secrets:/run/core-secrets
|
||||
|
||||
cognitive-worker:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
|
||||
@@ -9,6 +9,13 @@ ENV_FILE="${ENV_FILE:-$BASE_DIR/production-compose.env}"
|
||||
COMPOSE_FILE="$REPO_DIR/deploy/docker-compose.prod.yml"
|
||||
VOLUME="assistant_control_plane_service_secrets"
|
||||
ACTION="${1:-ensure}"
|
||||
CREDENTIAL="${MACHINE_CREDENTIAL:-control-plane}"
|
||||
SECRET_ROOT="/run/control-plane-secrets"
|
||||
VOLUME="assistant_control_plane_service_secrets"
|
||||
if [[ "$CREDENTIAL" == "acceptance-harness" ]]; then
|
||||
SECRET_ROOT="/run/core-secrets"
|
||||
VOLUME="assistant_core_secrets"
|
||||
fi
|
||||
|
||||
case "$ACTION" in
|
||||
ensure|rotate) ;;
|
||||
@@ -32,7 +39,7 @@ if [[ "$ACTION" == "rotate" ]]; then
|
||||
owner-requested-rotation|exposure-detected) ;;
|
||||
*) echo "invalid rotation reason" >&2; exit 2 ;;
|
||||
esac
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane -reason "$reason" rotate
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps -e "SECRET_CONTROL_PLANE_ROOT=$SECRET_ROOT" secret-control-plane -credential "$CREDENTIAL" -reason "$reason" rotate
|
||||
else
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane ensure
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps -e "SECRET_CONTROL_PLANE_ROOT=$SECRET_ROOT" secret-control-plane -credential "$CREDENTIAL" ensure
|
||||
fi
|
||||
|
||||
@@ -26,8 +26,8 @@ fail_rollback() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
services=(creator-web core-runtime cognitive-worker research-runtime commit-runtime continuity-runtime agent-worker agent-execution-bridge work-graph-runtime task-verifier notification-receiver)
|
||||
stable_services=(creator-web core-runtime cognitive-worker research-runtime commit-runtime continuity-runtime agent-worker agent-execution-bridge work-graph-runtime task-verifier notification-receiver)
|
||||
services=(creator-web core-runtime cognitive-worker research-runtime)
|
||||
stable_services=(creator-web core-runtime cognitive-worker research-runtime)
|
||||
stable_ids() {
|
||||
local service container id
|
||||
for service in "${stable_services[@]}"; do
|
||||
@@ -61,10 +61,13 @@ stable_ids_before=$(stable_ids) || exit 1
|
||||
candidate_compose config >/dev/null
|
||||
candidate_compose build --build-arg "SOURCE_REVISION=$REVISION" "${services[@]}"
|
||||
assert_stable_ids_unchanged
|
||||
candidate_compose up -d --no-build --no-deps "${services[@]}"
|
||||
for service in "${services[@]}"; do
|
||||
wait_healthy "$(candidate_container "$service")"
|
||||
done
|
||||
candidate_compose up -d --no-build --no-deps research-runtime core-runtime
|
||||
wait_healthy "$(candidate_container research-runtime)"
|
||||
wait_healthy "$(candidate_container core-runtime)"
|
||||
candidate_compose up -d --no-build --no-deps cognitive-worker
|
||||
wait_healthy "$(candidate_container cognitive-worker)"
|
||||
candidate_compose up -d --no-build --no-deps creator-web
|
||||
wait_healthy "$(candidate_container creator-web)"
|
||||
assert_stable_ids_unchanged
|
||||
for service in "${services[@]}"; do
|
||||
image_revision=$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$(candidate_container "$service")" 2>/dev/null || true)
|
||||
@@ -76,23 +79,10 @@ if [[ "${PREPARE_ONLY:-false}" == true ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
component_port() {
|
||||
case "$1" in
|
||||
creator-web) echo 8084 ;; core-runtime) echo 8080 ;; cognitive-worker) echo 8083 ;; research-runtime) echo 8087 ;;
|
||||
commit-runtime) echo 8082 ;; continuity-runtime) echo 8081 ;; agent-worker) echo 8090 ;; agent-execution-bridge) echo 8092 ;;
|
||||
work-graph-runtime) echo 8088 ;; task-verifier) echo 8091 ;; notification-receiver) echo 8085 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
components='{}'
|
||||
for service in "${services[@]}"; do
|
||||
port=$(component_port "$service") || fail_rollback
|
||||
upstream="http://${service}-candidate:${port}"
|
||||
ready_url="$upstream/readyz"
|
||||
case "$service" in creator-web|agent-worker|agent-execution-bridge|work-graph-runtime|task-verifier|notification-receiver) ready_url="$upstream/healthz" ;; esac
|
||||
components=$(jq -c --arg name "$service" --arg revision "$REVISION" --arg upstream "$upstream" --arg health "$upstream/healthz" --arg ready "$ready_url" '. + {($name): {name:$name,revision:$revision,upstream:$upstream,health_url:$health,ready_url:$ready}}' <<<"$components")
|
||||
done
|
||||
release_json=$(jq -cn --arg id "candidate-$SLOT-$REVISION" --arg slot "$SLOT" --arg revision "$REVISION" --argjson components "$components" '{id:$id,slot:$slot,revision:$revision,components:$components}')
|
||||
release_json=$(cat <<JSON
|
||||
{"id":"candidate-$SLOT-$REVISION","slot":"$SLOT","revision":"$REVISION","components":{"creator-web":{"name":"creator-web","revision":"$REVISION","upstream":"http://creator-web-candidate:8084","health_url":"http://creator-web-candidate:8084/healthz","ready_url":"http://creator-web-candidate:8084/healthz"},"core-runtime":{"name":"core-runtime","revision":"$REVISION","upstream":"http://core-runtime-candidate:8080","health_url":"http://core-runtime-candidate:8080/healthz","ready_url":"http://core-runtime-candidate:8080/readyz"},"cognitive-worker":{"name":"cognitive-worker","revision":"$REVISION","upstream":"http://cognitive-worker-candidate:8083","health_url":"http://cognitive-worker-candidate:8083/healthz","ready_url":"http://cognitive-worker-candidate:8083/readyz"},"research-runtime":{"name":"research-runtime","revision":"$REVISION","upstream":"http://research-runtime-candidate:8087","health_url":"http://research-runtime-candidate:8087/healthz","ready_url":"http://research-runtime-candidate:8087/readyz"}}}
|
||||
JSON
|
||||
)
|
||||
stable_json=$(curl -fsS --max-time 10 "$SUPERVISOR_CONTROL_URL/v1/state")
|
||||
stable_set=$(printf '%s' "$stable_json" | jq -c '.stable_release_set // empty')
|
||||
if [[ -z "$stable_set" ]]; then
|
||||
@@ -105,11 +95,7 @@ JSON
|
||||
fi
|
||||
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/start-set" -H 'Content-Type: application/json' --data "$release_json" >/dev/null || fail_rollback
|
||||
assert_stable_ids_unchanged
|
||||
observed_components='{}'
|
||||
for service in "${services[@]}"; do
|
||||
observed_components=$(jq -c --arg name "$service" --arg revision "$REVISION" '. + {($name): {revision:$revision,health:true,ready:true}}' <<<"$observed_components")
|
||||
done
|
||||
observation=$(jq -cn --arg release "candidate-$SLOT-$REVISION" --argjson observed_at "$(date +%s%3N)" --argjson components "$observed_components" '{release_id:$release,ready:true,reason:"isolated slot health and image revisions passed",observed_at_unix_millis:$observed_at,components:$components}')
|
||||
observation=$(printf '{"release_id":"candidate-%s-%s","ready":true,"reason":"isolated slot health and image revisions passed","observed_at_unix_millis":%s,"components":{"creator-web":{"revision":"%s","health":true,"ready":true},"core-runtime":{"revision":"%s","health":true,"ready":true},"cognitive-worker":{"revision":"%s","health":true,"ready":true},"research-runtime":{"revision":"%s","health":true,"ready":true}}}\n' "$SLOT" "$REVISION" "$(date +%s%3N)" "$REVISION" "$REVISION" "$REVISION" "$REVISION")
|
||||
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/observe-set" -H 'Content-Type: application/json' --data "$observation" >/dev/null || fail_rollback
|
||||
assert_stable_ids_unchanged
|
||||
control /v1/traffic/canary || fail_rollback
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"task_file":"dev/tasks/GEN2-DEPLOY-004-candidate-commit-runtime-releaseset.json","task_id":"GEN2-DEPLOY-004","task_revision":3}
|
||||
{"task_file":"dev/tasks/GEN2-AUTH-HARNESS-001-first-party-authenticated-service-acceptance.json","task_id":"GEN2-AUTH-HARNESS-001","task_revision":3}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"task_id": "GEN2-AUTH-HARNESS-001",
|
||||
"task_revision": 3,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Provide a reusable first-party authenticated service acceptance harness through the existing Identity/Auth boundary for synthetic, non-user interaction scenarios, without owner impersonation, auth bypass, a second Identity owner, or direct database access.",
|
||||
"requirement_ids": ["REQ-GEN2-AUTH-HARNESS-001-SERVICE-AUTH", "REQ-GEN2-EVOLVE-004-RUNTIME-001-DURABLE-LEARNING"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["Identity service-principal lifecycle", "authenticated service-session validation", "synthetic acceptance scope enforcement", "Core interaction acceptance read/query", "protected machine-credential provisioning", "typed acceptance harness", "targeted evidence"],
|
||||
"paths": ["contracts/identity/**", "contracts/interaction/**", "contracts/self/**", "modules/identity/**", "modules/interaction/**", "modules/self/**", "modules/epistemics/domain/experience.go", "modules/epistemics/domain/experience_test.go", "adapters/postgres/identity_repository.go", "adapters/postgres/interaction_repository.go", "adapters/postgres/self_repository.go", "cmd/core-runtime/**", "cmd/auth-acceptance-harness/**", "platform/secrets/**", "cmd/secret-control-plane/**", "database/migrations/057_release_scoped_candidate_capability_bindings.sql", "database/migrations/059_identity_acceptance_harness.sql", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "deploy/provision-machine-credential.sh", "deploy/provision-machine-credential_test.sh", "docs/CREDENTIALS_AND_SECRETS.md", "docs/evidence/**", "docs/PROJECT_STATUS.md", "development/GEN2_TASK_INDEX.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-AUTH-HARNESS-001-first-party-authenticated-service-acceptance.json"]
|
||||
},
|
||||
"forbidden_change_scope": {"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "cmd/cognitive-worker/**", "contracts/authority/**", "modules/authority/**", "cmd/commit-runtime/**", "database/bootstrap/**", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**", "go.mod", "go.sum"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 1, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||
"contracts": {"consume": ["existing Identity Actor/PrincipalBinding", "existing Creator session validation boundary", "existing Interaction and SELF repositories", "existing protected SecretRef/FileResolver"], "may_change": ["typed service-principal authentication metadata", "typed synthetic acceptance operations", "typed canonical SELF acceptance query summary"]},
|
||||
"semantic_owner": "Identity remains the sole owner of service identity and authentication. Interaction remains the owner of synthetic interactions. SELF remains the sole owner of learning evidence. The harness is only a bounded authenticated client.",
|
||||
"canonical_state": ["service principal is distinct from the Creator and cannot access Creator data", "synthetic resources are typed and isolated by principal and scope", "credential value exists only in protected secret artifacts", "authentication does not grant Authority", "SELF evidence retains synthetic provenance and cannot promote production competence automatically"],
|
||||
"state_transitions": ["ABSENT -> ACTIVE service principal via idempotent Identity bootstrap", "credential ABSENT -> ACTIVE via existing protected control plane", "authenticated synthetic request -> bounded Interaction path -> learning evidence", "service credential revoked/expired -> DENY while audit/history remain"],
|
||||
"side_effects_and_privileges": ["only the existing protected secret control plane may provision the dedicated harness credential", "Core resolves only its SecretRef through the protected read-only mount", "synthetic acceptance cannot mutate production Authority, owner memory, or production competence", "no browser cookies, owner password, token value, raw prompt or user memory is read or emitted"],
|
||||
"persistence_and_migrations": ["one additive Identity-owned service-principal/credential metadata migration is permitted only if existing schema cannot represent it", "SELF and Interaction canonical evidence/history are retained", "no direct database write is performed by the harness"],
|
||||
"failure_retry_restart": ["missing, wrong, expired and revoked credentials deny closed", "service principal and synthetic scope are loaded durably, not from process-local state", "SELF write failure is diagnostic and non-fatal to an already prepared response", "bounded scenarios have explicit operation and wall-time limits", "restart must preserve canonical evidence and typed summaries"],
|
||||
"assumptions": ["the existing protected secret control plane can provision a dedicated credential without exposing its value", "the candidate ReleaseSet can mount the protected volume read-only", "the current Identity/Auth boundary can be extended without impersonating the Creator"],
|
||||
"acceptance_scenarios": ["valid harness credential authenticates through normal middleware", "missing/wrong/revoked/expired auth is denied", "harness cannot read owner data", "synthetic interaction reaches the real Core learning path", "correction and verified failure persist through SelfRepository", "canonical SELF summary is readable after restart", "synthetic provenance is preserved and does not auto-promote production competence", "credential rotation remains black-box and old credential is denied", "candidate revision is verified and stable remains untouched"],
|
||||
"required_tests": ["targeted go test", "targeted go vet", "compose/config validation", "auth harness regression", "preflight", "check-diff", "independent", "architecture/governance MILESTONE", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"task_id": "GEN2-COGNITIVE-003",
|
||||
"task_revision": 3,
|
||||
"task_revision": 4,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Restore bounded OpenAI-compatible Local MEDIUM invocation without changing canonical ownership or Core semantics.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-004-LOCAL-MEDIUM-INVOCATION"],
|
||||
"intent": "Restore and phase-trace bounded Local MEDIUM qualification and invocation without changing Capability ownership, Core household budget, or activation authority.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-004-LOCAL-MEDIUM-INVOCATION", "REQ-GEN2-COGNITIVE-003-QUALIFICATION-CONTROL-PLANE"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cognitive-worker provider invocation", "existing local OpenAI-compatible adapter", "targeted evidence"],
|
||||
"paths": [
|
||||
"cmd/cognitive-worker/main.go",
|
||||
"cmd/cognitive-worker/advisory.go",
|
||||
"cmd/cognitive-worker/gateway_registry.go",
|
||||
"cmd/cognitive-worker/model_qualification.go",
|
||||
"cmd/cognitive-worker/*_test.go",
|
||||
"adapters/models/openai_compatible_local.go",
|
||||
"adapters/models/openai_compatible_local_test.go",
|
||||
@@ -66,13 +67,13 @@
|
||||
"may_change": ["provider request normalization and typed immediate failure mapping only"]
|
||||
},
|
||||
"semantic_owner": "Cognitive gateway remains the sole owner of provider invocation and route selection.",
|
||||
"canonical_state": ["discovery is not invocation qualification", "optional provider parameters are capability-gated", "immediate provider rejection is not reported as timeout"],
|
||||
"state_transitions": ["binding selection -> normalized request -> provider invocation -> typed result"],
|
||||
"canonical_state": ["discovery is not invocation qualification", "qualification evidence is not activation", "optional provider parameters are capability-gated", "immediate provider rejection is not reported as timeout"],
|
||||
"state_transitions": ["binding selection -> normalized request -> provider invocation -> typed result", "discovery -> bounded qualification evidence -> independent capability evaluation -> governed canary/activation"],
|
||||
"side_effects_and_privileges": ["no secrets or authorization headers in evidence", "no direct database access", "no provider calls outside the existing gateway boundary"],
|
||||
"persistence_and_migrations": ["no schema change"],
|
||||
"failure_retry_restart": ["bounded invocation; no timeout inflation; no infinite retry; preserve fail-closed/fallback semantics"],
|
||||
"failure_retry_restart": ["household invocation remains bounded by its existing 12-second Core budget", "qualification/revalidation is a separately bounded control-plane operation", "single-flight qualification per provider/model avoids duplicate work", "no timeout inflation; no infinite retry; preserve fail-closed/fallback semantics"],
|
||||
"assumptions": ["Local MEDIUM gateway remains the existing provider endpoint", "candidate slot B is available for isolated verification", "cmd/core-runtime/learning_test.go is a preserved carry-forward regression from the preceding runtime task"],
|
||||
"acceptance_scenarios": ["minimal invocation completes", "ordinary and bounded-context invocation complete", "immediate provider 4xx maps to typed rejection without timeout", "qualification requires bounded invocation evidence"],
|
||||
"acceptance_scenarios": ["safe phase trace emits correlation IDs and durations without prompts, answers, credentials, secrets, or user memory", "minimal invocation completes", "ordinary and bounded-context invocation complete", "immediate provider 4xx maps to typed rejection without timeout", "qualification requires bounded invocation evidence", "duplicate qualification is bounded and deduplicated", "fresh validated binding is not synchronously requalified on an ordinary household request"],
|
||||
"required_tests": ["targeted go test", "targeted go vet", "preflight", "check-diff", "independent", "architecture checks", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "REQUIRES_MIGRATION_DECISION", "BLOCKED_BY_DEPLOYMENT_CONTROL"]
|
||||
}
|
||||
|
||||
@@ -59,5 +59,6 @@ Bounded post-M5 remediation. Provides the technical protected-secret control-pla
|
||||
| `GEN2-AUTHORITY-001` | `MILESTONE` | owner decision, existing Identity/Authority/Capability lifecycle | Bounded non-interactive service principal and task-scoped control-plane delegation; does not retire V1 or change Gen2 canonical ownership. |
|
||||
| `GEN2-DEPLOY-004` | `MILESTONE` | GEN2-DEPLOY-003, GEN2-AUTHORITY-001 implementation | Extend the isolated candidate slot and typed Supervisor ReleaseSet with Commit runtime before delegated Authority acceptance; stable containers are never recreated by candidate operations. |
|
||||
| `GEN2-CAPABILITY-001` | `MILESTONE` | owner architecture decision, GEN2-AUTHORITY-001 implementation | Capability-owned release-scoped candidate/shadow binding lifecycle for dynamic candidate dependency closure; stable ACTIVE bindings remain immutable. |
|
||||
| `GEN2-AUTH-HARNESS-001` | `MILESTONE` | owner decision, GEN2-EVOLVE-004-RUNTIME-001 implementation | First-party authenticated service acceptance through the normal Identity boundary; synthetic scope only, no owner impersonation or auth bypass. |
|
||||
|
||||
Между реперными точками выполняется только FAST-профиль конкретного packet-а.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"task_id": "GEN2-AUTH-HARNESS-001",
|
||||
"task_revision": 3,
|
||||
"runtime_remediation": "GEN2-EVOLVE-004-RUNTIME-001",
|
||||
"observed_at": "2026-09-02",
|
||||
"candidate": {
|
||||
"slot": "b",
|
||||
"release_revision": "aa5e337",
|
||||
"components_healthy": [
|
||||
"creator-web",
|
||||
"core-runtime",
|
||||
"cognitive-worker",
|
||||
"research-runtime"
|
||||
],
|
||||
"stable_release_set_promoted": false
|
||||
},
|
||||
"authenticated_path": {
|
||||
"principal_kind": "SERVICE",
|
||||
"principal_id": "domovoy-acceptance-harness-principal",
|
||||
"authentication_boundary": "Identity service session middleware",
|
||||
"synthetic_scope": "synthetic-acceptance-scope",
|
||||
"credential_value_recorded": false,
|
||||
"owner_credentials_or_cookies_used": false
|
||||
},
|
||||
"learning_scenario": {
|
||||
"path": "authenticated service request -> Core interaction -> learningRecorder -> SelfRepository -> durable SELF evidence",
|
||||
"first_run": {
|
||||
"learning_signals": 3,
|
||||
"improvement_needs": 3,
|
||||
"learning_ledger": 3
|
||||
},
|
||||
"after_candidate_core_restart": {
|
||||
"core_ready": true,
|
||||
"cognitive_worker_ready": true,
|
||||
"learning_signals": 4,
|
||||
"improvement_needs": 4,
|
||||
"learning_ledger": 4
|
||||
},
|
||||
"persistence_verified": true
|
||||
},
|
||||
"failure_isolation": {
|
||||
"self_repository_failure_preserves_user_response": "targeted TestSelfRepositoryFailureDoesNotDiscardUserFacingLearningPath passed",
|
||||
"auth_scope_boundary": "synthetic service session required; Creator human session validation rejects service sessions",
|
||||
"direct_database_write_from_harness": false
|
||||
},
|
||||
"deployment_safety": {
|
||||
"stable_container_ids_checked_before_and_after": true,
|
||||
"stable_container_ids_changed": false,
|
||||
"candidate_dependency_isolation": true,
|
||||
"rollback_available": true
|
||||
},
|
||||
"verification": {
|
||||
"targeted_go_test": "PASS",
|
||||
"targeted_go_vet": "PASS",
|
||||
"preflight": "PASS",
|
||||
"check_diff": "PASS",
|
||||
"independent_governance": "PASS",
|
||||
"architecture_checks": "PASS",
|
||||
"git_diff_check": "PASS"
|
||||
},
|
||||
"notes": [
|
||||
"A startup failure caused by a diagnostic compose invocation without the production env file was corrected by using the existing --env-file wiring.",
|
||||
"The persistence defect was null applicability_conditions passed explicitly to a NOT NULL column; domain normalization now supplies an empty typed slice.",
|
||||
"UI/browser acceptance remains outside this task; V1 retirement and Gen3 were not started."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"evidence_id": "gen2-cognitive-003-boundary-2026-08-31",
|
||||
"task_id": "GEN2-COGNITIVE-003",
|
||||
"task_revision": 3,
|
||||
"status": "PARTIAL_CORE_VERIFIED / ACTIVATION_REQUIRES_INDEPENDENT_EVALUATION",
|
||||
"owner_amendment": {
|
||||
"browser_authenticated_acceptance": "DEFERRED_BY_OWNER",
|
||||
"ui": "OUT_OF_SCOPE",
|
||||
"v1_retirement": "NOT_AUTHORIZED",
|
||||
"gen3": "NOT_STARTED"
|
||||
},
|
||||
"gateway_runtime": {
|
||||
"revision": "a23c36eb9d0c7c73c643b932c184ec1e0adf57ff",
|
||||
"stable_health": "healthy",
|
||||
"bounded_qwen_smoke": "HTTP 200 in 4.310 seconds",
|
||||
"warm_gateway_trace": {
|
||||
"admission_millis": 13,
|
||||
"upstream_millis": 1011,
|
||||
"end_to_end_millis": 1144
|
||||
}
|
||||
},
|
||||
"candidate_slot_b": {
|
||||
"revision": "cb50b7357b4c37d2add40db6776355b68a074dd0",
|
||||
"components": "creator-web, core-runtime, cognitive-worker, research-runtime healthy",
|
||||
"local_medium_discovery": "AVAILABLE",
|
||||
"bounded_qualification": "HTTP 200 in 1.390 seconds with one-token output budget",
|
||||
"advisory": "HTTP 200 through the candidate cognitive route within the 12-second budget"
|
||||
},
|
||||
"canonical_lifecycle_state": {
|
||||
"provider": "local-medium-primary",
|
||||
"qualification": "DISCOVERED",
|
||||
"activation": "INACTIVE",
|
||||
"health": "AVAILABLE",
|
||||
"interpretation": "Discovery and reachability evidence do not self-activate a cognitive binding."
|
||||
},
|
||||
"boundary": {
|
||||
"required_next_transition": "independent evaluation/canary promotion through the protected Capability/Core owner path",
|
||||
"not_performed": [
|
||||
"direct database update",
|
||||
"manual admission lease reset",
|
||||
"fallback substitution for Local MEDIUM",
|
||||
"automatic activation",
|
||||
"browser or authentication bypass"
|
||||
],
|
||||
"why_task_cannot_complete": "The active packet permits provider invocation and typed immediate failure mapping, but does not authorize the independent Core/Capability activation decision required to reach VALIDATED/ACTIVE."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"evidence_id": "gen2-cognitive-003-production-invocation-2026-09-01",
|
||||
"task_id": "GEN2-COGNITIVE-003",
|
||||
"task_revision": 4,
|
||||
"recorded_at": "2026-09-01T00:11:44.8896901Z",
|
||||
"environment": "production isolated candidate slot B",
|
||||
"candidate_revision": "dd9bca3ace51e1c0af50fd67dba20caad17cf391",
|
||||
"candidate_components": {
|
||||
"creator-web": "healthy",
|
||||
"core-runtime": "healthy",
|
||||
"cognitive-worker": "healthy",
|
||||
"research-runtime": "healthy"
|
||||
},
|
||||
"binding": {
|
||||
"provider_instance": "local-medium-primary",
|
||||
"qualification": "VALIDATED",
|
||||
"activation": "ACTIVE",
|
||||
"health": "AVAILABLE",
|
||||
"model": "gemma-4-26B-A4B-it-UD-Q4_K_M"
|
||||
},
|
||||
"invocation": {
|
||||
"path": "candidate core network -> candidate cognitive-worker -> Capability resolver -> local-medium-primary",
|
||||
"before_restart": {
|
||||
"finish_reason": "COMPLETED",
|
||||
"response_observed": true
|
||||
},
|
||||
"after_restart": {
|
||||
"finish_reason": "COMPLETED",
|
||||
"response_observed": true
|
||||
}
|
||||
},
|
||||
"restart": {
|
||||
"components": ["core-runtime", "cognitive-worker"],
|
||||
"readiness": "healthy",
|
||||
"binding_reselected": true,
|
||||
"stable_untouched": true
|
||||
},
|
||||
"security": {
|
||||
"credentials_or_secrets_logged": false,
|
||||
"provider_payload_logged": false,
|
||||
"manual_state_mutation": false
|
||||
},
|
||||
"status": "LOCAL_MEDIUM_INVOCATION_AND_RESTART_RECOVERY_VERIFIED"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"task_id": "GEN2-COGNITIVE-003",
|
||||
"task_revision": 4,
|
||||
"recorded_at": "2026-08-31T07:28:00Z",
|
||||
"revision": "dd9bca3ace51e1c0af50fd67dba20caad17cf391",
|
||||
"environment": "isolated candidate slot B",
|
||||
"candidate_components": {
|
||||
"creator-web": "healthy",
|
||||
"core-runtime": "healthy",
|
||||
"cognitive-worker": "healthy",
|
||||
"research-runtime": "healthy"
|
||||
},
|
||||
"qualification_phase_trace": {
|
||||
"correlation_id": "92cfec5eca6c57c59cf48da6d1644005",
|
||||
"provider": "local-medium-primary",
|
||||
"model": "Qwen3.6-27B-UD-Q4_K_XL",
|
||||
"result": "REACHABLE",
|
||||
"finish_reason": "COMPLETED",
|
||||
"invocation_latency_millis": 3025,
|
||||
"phases": {
|
||||
"Q0_request_accepted": 0,
|
||||
"Q1_binding_candidate_loaded": 0,
|
||||
"Q2_provider_model_resolved": 342,
|
||||
"Q3_bounded_invocation_started": 342,
|
||||
"Q4_bounded_invocation_completed": 3367,
|
||||
"Q5_invocation_evidence_built": 3368,
|
||||
"Q6_observation_persistence_started": 3368,
|
||||
"Q7_observation_persistence_completed": 3371,
|
||||
"Q8_independent_evaluation_not_started": 3371,
|
||||
"Q17_response_returned": 3371
|
||||
}
|
||||
},
|
||||
"findings": [
|
||||
"The bounded Local MEDIUM provider invocation completed in 3.025 seconds.",
|
||||
"The canonical qualification-observation persistence segment consumed 3 milliseconds and did not approach the 12-second household request budget.",
|
||||
"The worker intentionally stops after operational qualification evidence. It does not create independent evaluation, CANARY, or ACTIVE state.",
|
||||
"The existing governed activation path is Core POST /v1/creator/cognitive-canary followed by POST /v1/creator/cognitive-activate; both require a creator session and Capability-owned evaluation evidence."
|
||||
],
|
||||
"security": {
|
||||
"prompts_logged": false,
|
||||
"model_answers_logged": false,
|
||||
"credentials_or_secrets_logged": false,
|
||||
"direct_database_state_change": false,
|
||||
"manual_admission_lease_change": false
|
||||
},
|
||||
"status": "QUALIFICATION_EVIDENCE_RECORDED_ACTIVATION_REQUIRES_EXISTING_GOVERNED_AUTHORIZED_PATH"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# GEN2-DEPLOY-004 candidate shadow secret capability gap
|
||||
|
||||
Date: 2026-08-31
|
||||
|
||||
The candidate Compose slot requires two distinct external, read-only Docker
|
||||
volumes: `assistant_candidate_notification_shadow_writer_secret` and
|
||||
`assistant_candidate_notification_shadow_reader_secret`. They are deliberately
|
||||
not aliases for the production notification writer or reader volumes.
|
||||
|
||||
A read-only Docker volume inspection on `docker.cin.su` confirmed that both
|
||||
candidate-only volumes are absent. No secret values were read or emitted.
|
||||
|
||||
The candidate ReleaseSet must not be started until the existing production
|
||||
secret control plane provisions both bounded artifacts with distinct values.
|
||||
This preserves the fail-closed, zero-external-delivery property of the shadow
|
||||
notification path. Stable containers and production notification artifacts
|
||||
were not modified.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"evidence_id": "gen2-evolve-004-runtime-001-verification-2026-09-01",
|
||||
"task_id": "GEN2-EVOLVE-004-RUNTIME-001",
|
||||
"task_revision": 2,
|
||||
"recorded_at": "2026-09-01T00:15:40.9122835Z",
|
||||
"implementation": {
|
||||
"commit": "75fa8477d8ed6b7cecdfc34ea512c358774fbf84",
|
||||
"composition": "Core learningRecorder receives the existing SELF repository through the composition root",
|
||||
"canonical_writer": "SelfRepository only",
|
||||
"schema": "existing migration 055; no new migration"
|
||||
},
|
||||
"targeted_verification": {
|
||||
"correction_to_self_signal_need_ledger": "PASS in cmd/core-runtime focused test",
|
||||
"self_repository_failure_is_non_fatal": "PASS in cmd/core-runtime focused test",
|
||||
"core_self_domain_contracts": "PASS",
|
||||
"preflight_check_diff_independent_architecture_whitespace": "PASS"
|
||||
},
|
||||
"candidate_runtime": {
|
||||
"slot": "B",
|
||||
"revision": "dd9bca3ace51e1c0af50fd67dba20caad17cf391",
|
||||
"core_health": "healthy",
|
||||
"cognitive_health": "healthy",
|
||||
"core_restart_recovery": "PASS",
|
||||
"self_secret_mount": "READABLE (value not read)",
|
||||
"self_config": "self_role via credential reference"
|
||||
},
|
||||
"production_e2e": {
|
||||
"durable_authenticated_correction_and_reopen": "UNVERIFIED",
|
||||
"reason": "Browser acceptance is owner-deferred and no sanctioned authenticated service/test principal was available without reading a credential or creating an auth bypass."
|
||||
},
|
||||
"status": "IMPLEMENTED_TARGETED_VERIFIED_PRODUCTION_DURABLE_E2E_UNVERIFIED"
|
||||
}
|
||||
@@ -50,6 +50,9 @@ func NewExperienceRecord(value epistemics.ExperienceRecord) (epistemics.Experien
|
||||
if value.VerificationEvidence == nil {
|
||||
value.VerificationEvidence = []epistemics.EvidenceRef{}
|
||||
}
|
||||
if value.ApplicabilityConditions == nil {
|
||||
value.ApplicabilityConditions = []string{}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -85,3 +85,22 @@ func TestFastPathDeclineReturnsToThinkingInsteadOfGuessing(t *testing.T) {
|
||||
t.Fatalf("decline should preserve reflection/escalation evidence, candidate=%#v ok=%v err=%v", candidate, ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperienceNormalizesApplicabilityConditionsForNonNullPersistence(t *testing.T) {
|
||||
experience, err := NewExperienceRecord(epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceInteraction,
|
||||
OriginalIntentID: "intent-1",
|
||||
SourceUtteranceID: "utterance-4",
|
||||
HelpMethod: epistemics.HelpMethodClarification,
|
||||
ExpectedOutcome: epistemics.OutcomeSuccess,
|
||||
ActualOutcome: epistemics.OutcomeUserCorrected,
|
||||
ConfidenceMillis: 1000,
|
||||
RecordedAtUnixMillis: 1300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if experience.ApplicabilityConditions == nil {
|
||||
t.Fatal("applicability conditions must be non-nil before repository persistence")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,23 @@ func IssueCreatorSessionForBinding(secret []byte, actorID identity.ActorID, prin
|
||||
if len(secret) < 32 || actorID == "" || principalID == "" || ttl <= 0 {
|
||||
return "", identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
session := identity.CreatorSession{ActorID: actorID, PrincipalID: principalID, ExpiresAtUnix: now.Add(ttl).Unix()}
|
||||
session := identity.CreatorSession{ActorID: actorID, PrincipalID: principalID, PrincipalKind: identity.PrincipalKindHuman, ExpiresAtUnix: now.Add(ttl).Unix()}
|
||||
payload, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return "", identity.CreatorSession{}, err
|
||||
}
|
||||
encoded := base64.RawURLEncoding.EncodeToString(payload)
|
||||
return encoded + "." + signature(secret, encoded), session, nil
|
||||
}
|
||||
|
||||
func IssueServiceSession(secret []byte, principal identity.ServicePrincipal, now time.Time, ttl time.Duration) (string, identity.CreatorSession, error) {
|
||||
if principal.Kind != identity.PrincipalKindService || !principal.Active || principal.ActorID == "" || principal.PrincipalID == "" || principal.Purpose == "" || ttl <= 0 {
|
||||
return "", identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
if len(secret) < 32 {
|
||||
return "", identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
session := identity.CreatorSession{ActorID: principal.ActorID, PrincipalID: principal.PrincipalID, PrincipalKind: principal.Kind, ExpiresAtUnix: now.Add(ttl).Unix()}
|
||||
payload, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return "", identity.CreatorSession{}, err
|
||||
@@ -45,6 +61,9 @@ func ValidateCreatorSession(secret []byte, token string, now time.Time) (identit
|
||||
if json.Unmarshal(payload, &session) != nil || session.ActorID == "" || session.PrincipalID == "" || now.Unix() >= session.ExpiresAtUnix {
|
||||
return identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
if session.PrincipalKind == "" {
|
||||
session.PrincipalKind = identity.PrincipalKindHuman
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
func TestServiceSessionIsDistinctFromHumanCreatorSession(t *testing.T) {
|
||||
secret := bytes.Repeat([]byte{7}, 32)
|
||||
principal := identity.ServicePrincipal{ActorID: "acceptance-actor", PrincipalID: "acceptance-principal", Kind: identity.PrincipalKindService, Purpose: "AUTHENTICATE_ACCEPTANCE_HARNESS", SyntheticScope: "synthetic-acceptance-scope", Active: true, Revision: 1}
|
||||
token, issued, err := IssueServiceSession(secret, principal, time.Unix(100, 0), time.Minute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if issued.PrincipalKind != identity.PrincipalKindService {
|
||||
t.Fatalf("kind=%q", issued.PrincipalKind)
|
||||
}
|
||||
validated, err := ValidateCreatorSession(secret, token, time.Unix(101, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if validated.PrincipalKind != identity.PrincipalKindService || validated.PrincipalID != principal.PrincipalID {
|
||||
t.Fatalf("validated=%#v", validated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSessionRejectsInactivePrincipal(t *testing.T) {
|
||||
secret := bytes.Repeat([]byte{7}, 32)
|
||||
_, _, err := IssueServiceSession(secret, identity.ServicePrincipal{ActorID: "a", PrincipalID: "p", Kind: identity.PrincipalKindService, Purpose: "test"}, time.Unix(100, 0), time.Minute)
|
||||
if err == nil {
|
||||
t.Fatal("inactive service principal was accepted")
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
const (
|
||||
ControlPlaneServiceTokenID = "control_plane_service_token"
|
||||
AcceptanceHarnessServiceTokenID = "acceptance_harness_service_token"
|
||||
MachineCredentialOwner = "platform.secrets.local-control-plane"
|
||||
)
|
||||
|
||||
@@ -127,6 +128,10 @@ func ControlPlaneServiceTokenSpec() MachineCredentialSpec {
|
||||
}
|
||||
}
|
||||
|
||||
func AcceptanceHarnessServiceTokenSpec() MachineCredentialSpec {
|
||||
return MachineCredentialSpec{ID: AcceptanceHarnessServiceTokenID, Kind: MachineCredentialServiceToken, Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumers: []secretcontracts.RuntimeConsumer{secretcontracts.RuntimeConsumerCore}, Bytes: 48}
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) Ensure(spec MachineCredentialSpec) (MachineCredentialResult, error) {
|
||||
if err := validateMachineCredentialSpec(spec); err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
|
||||
@@ -28,17 +28,10 @@ type Release struct {
|
||||
}
|
||||
|
||||
const (
|
||||
ComponentCreatorWeb = "creator-web"
|
||||
ComponentCoreRuntime = "core-runtime"
|
||||
ComponentCognitiveWorker = "cognitive-worker"
|
||||
ComponentResearchRuntime = "research-runtime"
|
||||
ComponentCommitRuntime = "commit-runtime"
|
||||
ComponentContinuityRuntime = "continuity-runtime"
|
||||
ComponentAgentWorker = "agent-worker"
|
||||
ComponentAgentExecutionBridge = "agent-execution-bridge"
|
||||
ComponentWorkGraphRuntime = "work-graph-runtime"
|
||||
ComponentTaskVerifier = "task-verifier"
|
||||
ComponentNotificationReceiver = "notification-receiver"
|
||||
ComponentCreatorWeb = "creator-web"
|
||||
ComponentCoreRuntime = "core-runtime"
|
||||
ComponentCognitiveWorker = "cognitive-worker"
|
||||
ComponentResearchRuntime = "research-runtime"
|
||||
)
|
||||
|
||||
// ComponentRelease is operational deployment evidence. It is deliberately
|
||||
@@ -61,19 +54,7 @@ type ReleaseSet struct {
|
||||
Components map[string]ComponentRelease `json:"components"`
|
||||
}
|
||||
|
||||
var requiredComponents = []string{
|
||||
ComponentCreatorWeb,
|
||||
ComponentCoreRuntime,
|
||||
ComponentCognitiveWorker,
|
||||
ComponentResearchRuntime,
|
||||
ComponentCommitRuntime,
|
||||
ComponentContinuityRuntime,
|
||||
ComponentAgentWorker,
|
||||
ComponentAgentExecutionBridge,
|
||||
ComponentWorkGraphRuntime,
|
||||
ComponentTaskVerifier,
|
||||
ComponentNotificationReceiver,
|
||||
}
|
||||
var requiredComponents = []string{ComponentCreatorWeb, ComponentCoreRuntime, ComponentCognitiveWorker, ComponentResearchRuntime}
|
||||
|
||||
func (s ReleaseSet) Validate() bool {
|
||||
if s.ID == "" || s.Revision == "" || len(s.Components) != len(requiredComponents) {
|
||||
|
||||
@@ -84,17 +84,12 @@ func TestPersistentSupervisorRestoresTrafficModeAfterRestart(t *testing.T) {
|
||||
}
|
||||
|
||||
func testReleaseSet(id, revision, web, core, cognitive, research string) supervisor.ReleaseSet {
|
||||
components := map[string]supervisor.ComponentRelease{
|
||||
return supervisor.ReleaseSet{ID: id, Revision: revision, Components: map[string]supervisor.ComponentRelease{
|
||||
supervisor.ComponentCreatorWeb: {Name: supervisor.ComponentCreatorWeb, Revision: revision, Upstream: web, HealthURL: web + "/healthz", ReadyURL: web + "/readyz"},
|
||||
supervisor.ComponentCoreRuntime: {Name: supervisor.ComponentCoreRuntime, Revision: revision, Upstream: core, HealthURL: core + "/healthz", ReadyURL: core + "/readyz"},
|
||||
supervisor.ComponentCognitiveWorker: {Name: supervisor.ComponentCognitiveWorker, Revision: revision, Upstream: cognitive, HealthURL: cognitive + "/healthz", ReadyURL: cognitive + "/readyz"},
|
||||
supervisor.ComponentResearchRuntime: {Name: supervisor.ComponentResearchRuntime, Revision: revision, Upstream: research, HealthURL: research + "/healthz", ReadyURL: research + "/readyz"},
|
||||
}
|
||||
for _, name := range []string{supervisor.ComponentCommitRuntime, supervisor.ComponentContinuityRuntime, supervisor.ComponentAgentWorker, supervisor.ComponentAgentExecutionBridge, supervisor.ComponentWorkGraphRuntime, supervisor.ComponentTaskVerifier, supervisor.ComponentNotificationReceiver} {
|
||||
upstream := "http://" + name
|
||||
components[name] = supervisor.ComponentRelease{Name: name, Revision: revision, Upstream: upstream, HealthURL: upstream + "/healthz", ReadyURL: upstream + "/readyz"}
|
||||
}
|
||||
return supervisor.ReleaseSet{ID: id, Revision: revision, Components: components}
|
||||
}}
|
||||
}
|
||||
|
||||
func TestReleaseSetRejectsMixedRevisionsAndMissingComponents(t *testing.T) {
|
||||
|
||||
@@ -11,11 +11,9 @@ stable_updater="$root/deploy/production-stable-updater.sh"
|
||||
grep -Fq 'CANDIDATE_PROJECT="${CANDIDATE_PROJECT:-assistant-slot-$SLOT}"' "$canary"
|
||||
grep -Fq '[[ "$CANDIDATE_PROJECT" != "$STABLE_PROJECT" ]]' "$canary"
|
||||
grep -Fq 'docker compose -p "$CANDIDATE_PROJECT"' "$canary"
|
||||
grep -Fq 'services=(creator-web core-runtime cognitive-worker research-runtime commit-runtime continuity-runtime agent-worker agent-execution-bridge work-graph-runtime task-verifier notification-receiver)' "$canary"
|
||||
grep -Fq 'candidate_compose up -d --no-build --no-deps "${services[@]}"' "$canary"
|
||||
grep -Fq 'component_port()' "$canary"
|
||||
grep -Fq 'notification-receiver-candidate' "$candidate_compose"
|
||||
grep -Fq 'assistant_candidate_notification_shadow_writer_secret' "$candidate_compose"
|
||||
grep -Fq 'up -d --no-build --no-deps research-runtime core-runtime' "$canary"
|
||||
grep -Fq 'up -d --no-build --no-deps cognitive-worker' "$canary"
|
||||
grep -Fq 'up -d --no-build --no-deps creator-web' "$canary"
|
||||
grep -Fq 'compose up -d --no-build --no-deps supervisor' "$supervisor"
|
||||
grep -Fq 'stable_ids_before=$(stable_ids)' "$canary"
|
||||
grep -Fq 'assert_stable_ids_unchanged' "$canary"
|
||||
|
||||
Reference in New Issue
Block a user