Compare commits

..
64 Commits
Author SHA1 Message Date
m 7f16ec75f5 merge: authenticated acceptance harness remediation
CI / test (push) Failing after 36s
2026-09-02 02:28:47 +03:00
m c0ffc59666 docs(evidence): record authenticated harness acceptance
CI / test (push) Failing after 34s
2026-09-02 02:28:33 +03:00
m aa5e33778c governance(auth): allow epistemics normalization fix
CI / test (push) Failing after 46s
2026-09-02 02:24:49 +03:00
m 870392dc09 fix(epistemics): normalize applicability conditions
CI / test (push) Failing after 37s
2026-09-01 23:35:27 +03:00
m 005d244950 fix(auth): select service middleware before response
CI / test (push) Failing after 57s
2026-09-01 23:24:59 +03:00
m c44dd9e0be test(auth): expose harness stage status
CI / test (push) Failing after 35s
2026-09-01 23:22:11 +03:00
m 2f639c9d6c test(auth): report bounded harness status
CI / test (push) Failing after 40s
2026-09-01 23:20:58 +03:00
m ec7c4fef66 fix(migrations): make capability constraints idempotent
CI / test (push) Failing after 35s
2026-09-01 23:17:05 +03:00
m c831ee1260 feat(auth): isolate harness secret artifact
CI / test (push) Failing after 34s
2026-09-01 23:14:31 +03:00
m 81ce8dfdb2 feat(auth): add first-party acceptance harness
CI / test (push) Failing after 1m4s
2026-09-01 23:13:15 +03:00
m 750c87c926 docs(evidence): verify SELF runtime wiring
CI / test (push) Failing after 33s
2026-09-01 03:16:04 +03:00
m e6e051d59f merge: complete GEN2-COGNITIVE-003 invocation remediation
CI / test (push) Failing after 35s
2026-09-01 03:12:33 +03:00
m 5a9714645f docs(evidence): record Local MEDIUM restart invocation
CI / test (push) Failing after 35s
2026-09-01 03:12:26 +03:00
m cf818cfc47 merge: sync cognitive remediation with integration 2026-09-01 03:06:51 +03:00
m e21eae7a99 merge: GEN2-CAPABILITY-001 release scoped shadow bindings
CI / test (push) Failing after 36s
2026-08-31 16:29:32 +03:00
m dec19e873d feat(capability): add release scoped shadow bindings
CI / test (push) Failing after 35s
2026-08-31 16:28:57 +03:00
m d1710907ec docs(deploy): record candidate dependency closure gap
CI / test (push) Failing after 34s
2026-08-31 14:13:40 +03:00
m 2f085858db chore(governance): add GEN2-DEPLOY-004 release set task
CI / test (push) Failing after 36s
2026-08-31 13:42:48 +03:00
m a8be998cee docs(authority): record candidate topology gap
CI / test (push) Failing after 32s
2026-08-31 13:28:15 +03:00
m f42a94ee7a fix(authority): use isolated service credential mount
CI / test (push) Failing after 36s
2026-08-31 13:26:50 +03:00
m 35666c9923 merge: GEN2-SECRETS-001 into delegated authority
# Conflicts:
#	deploy/docker-compose.prod.yml
#	dev/ACTIVE_TASK.json
#	docs/CREDENTIALS_AND_SECRETS.md
2026-08-31 13:26:28 +03:00
m da83248ac9 merge: GEN2-SECRETS-001 production evidence
CI / test (push) Failing after 39s
2026-08-31 13:22:28 +03:00
m db139e6800 docs(secrets): record provisioning evidence
CI / test (push) Failing after 1m14s
2026-08-31 13:22:23 +03:00
m b4335d6735 merge: GEN2-SECRETS-001 rotation wrapper fix
CI / test (push) Failing after 37s
2026-08-31 13:21:27 +03:00
m 44541b1ba3 fix(secrets): pass bounded rotation reason
CI / test (push) Failing after 39s
2026-08-31 13:21:23 +03:00
m 9c42b18850 merge: GEN2-SECRETS-001 local credential provisioning
CI / test (push) Failing after 43s
2026-08-31 13:18:10 +03:00
m 3a8c6d4827 feat(secrets): add protected credential store
CI / test (push) Failing after 37s
2026-08-31 13:17:56 +03:00
m 8b1d75f337 feat(secrets): provision local machine credentials
CI / test (push) Failing after 39s
2026-08-31 13:17:33 +03:00
m 1c05619899 feat(authority): add delegated control-plane grants
CI / test (push) Failing after 37s
2026-08-31 11:33:48 +03:00
m a6b0fcd768 docs(evidence): record Local MEDIUM qualification phase trace
CI / test (push) Failing after 36s
2026-08-31 10:29:03 +03:00
m dd9bca3ace fix(cognitive): trace and bound qualification flights
CI / test (push) Failing after 38s
2026-08-31 10:25:16 +03:00
m 9d94a86300 docs(evidence): record cognitive activation boundary
CI / test (push) Failing after 35s
2026-08-31 09:48:09 +03:00
m cb50b7357b bound local advisory output
CI / test (push) Failing after 39s
2026-08-31 00:05:57 +03:00
m dfbee7568f fix cognitive local model discovery
CI / test (push) Failing after 36s
2026-08-30 23:54:18 +03:00
m f803cb67f0 merge: GEN2-EVOLVE-004 runtime wiring
CI / test (push) Failing after 37s
2026-08-30 22:48:53 +03:00
m 75fa8477d8 feat(core): wire durable self learning evidence 2026-08-30 22:48:46 +03:00
m b4c3479614 merge: GEN2-EVOLVE-004 SELF learning loop
CI / test (push) Failing after 35s
2026-08-30 22:26:18 +03:00
m 82b89a37d3 feat(self): add bounded improvement learning loop 2026-08-30 22:26:09 +03:00
m 4559e0439c merge: GEN2-EVOLVE-004 task packet
CI / test (push) Failing after 35s
2026-08-30 22:03:03 +03:00
m b39c3ed153 chore(gen2): define self improvement loop task 2026-08-30 22:02:57 +03:00
m dd04a67731 merge: GEN2-EVOLVE-003 runtime evidence
CI / test (push) Failing after 33s
2026-08-30 21:58:51 +03:00
m a0bd2a1831 feat(gen2): seed candidate skill from verified delegation 2026-08-30 21:58:49 +03:00
m c39126e0c1 merge: GEN2-EVOLVE-003 experience skill reflex lifecycle
CI / test (push) Failing after 36s
2026-08-30 21:56:51 +03:00
m 8ec7c7419f feat(gen2): add experience skill reflex lifecycle 2026-08-30 21:56:33 +03:00
m a07aba056d merge: GEN2-EVOLVE-002 iterative retrieval
CI / test (push) Failing after 35s
2026-08-30 20:47:05 +03:00
m 901d5e011d feat: add bounded iterative cognitive retrieval 2026-08-30 20:46:52 +03:00
m 74f326f11a chore: start GEN2-EVOLVE-002 2026-08-30 20:38:55 +03:00
m 850fe92ecb merge: GEN2-EVOLVE-001 adaptive retrieval
CI / test (push) Failing after 47s
2026-08-30 20:25:40 +03:00
m 9312441d00 feat: evolve adaptive Gen2 retrieval 2026-08-30 20:25:23 +03:00
m a6273d0134 feat: add scoped adaptive experience foundation 2026-08-30 20:06:58 +03:00
m e648d0423d chore: start GEN2-EVOLVE-001 2026-08-30 20:04:58 +03:00
m 07f0f6e380 merge: GEN2-COGNITIVE-002 core verified
CI / test (push) Failing after 36s
2026-08-30 19:55:11 +03:00
m e1b9c7ca84 docs: record cognitive core verification amendment
CI / test (push) Failing after 33s
2026-08-30 19:54:48 +03:00
m 00af176a20 fix: compile scoped interaction assertions
CI / test (push) Failing after 34s
2026-08-30 19:12:52 +03:00
m ce7000e78e chore: revise cognitive retrieval task scope
CI / test (push) Failing after 44s
2026-08-30 18:47:58 +03:00
m 6204e14177 fix: isolate candidate core cognitive route
CI / test (push) Failing after 36s
2026-08-30 16:02:11 +03:00
m 185317ee6a fix: revalidate stale cognitive binding health
CI / test (push) Failing after 35s
2026-08-30 15:56:34 +03:00
m 2e824b3c1f chore: start cognitive route remediation trace
CI / test (push) Failing after 35s
2026-08-30 15:33:59 +03:00
m f0360a8e13 merge GEN2-DEPLOY-003 isolated release slots
CI / test (push) Failing after 30s
2026-08-30 14:10:34 +03:00
m 5593f2ac2c docs: record isolated release slot drill
CI / test (push) Failing after 41s
2026-08-30 14:10:18 +03:00
m 216d76f1d8 docs: clarify stable updater isolation
CI / test (push) Failing after 28s
2026-08-30 13:59:24 +03:00
m 499b9678e1 test: support isolated canary rollback drill
CI / test (push) Failing after 28s
2026-08-30 13:55:35 +03:00
m 78a39ec011 fix: roll back release sets atomically
CI / test (push) Failing after 29s
2026-08-30 13:55:01 +03:00
m d120cc0958 fix: isolate candidate release slot
CI / test (push) Failing after 29s
2026-08-30 13:51:32 +03:00
138 changed files with 7254 additions and 313 deletions
+31 -1
View File
@@ -32,6 +32,8 @@ type OpenAICompatibleLocalAdapter struct {
// qualification/evaluation in the capability registry.
const compatibleDefaultContextBytes int64 = 32 << 10
const compatibleAdvisoryMaxTokens = 32
func NewOpenAICompatibleLocalAdapter(raw string, transport http.RoundTripper) (*OpenAICompatibleLocalAdapter, error) {
return NewOpenAICompatibleLocalAdapterWithAPIKey(raw, "", transport)
}
@@ -65,6 +67,9 @@ type compatibleModelsResponse struct {
ID string `json:"id"`
Created int64 `json:"created"`
OwnedBy string `json:"owned_by"`
ModelKind string `json:"model_kind"`
Capabilities map[string]bool `json:"capabilities"`
Modalities map[string]bool `json:"modalities"`
Meta struct {
Context int64 `json:"n_ctx"`
TrainingContext int64 `json:"n_ctx_train"`
@@ -94,6 +99,12 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover
if item.ID == "" {
continue
}
// The gateway catalogue may contain image/audio runtimes beside local
// chat models. Discovery is not allowed to qualify those as text
// advisory models merely because they appear in /v1/models.
if !isTextAdvisoryModel(item.ModelKind, item.Capabilities, item.Modalities) {
continue
}
stableMetadata := fmt.Sprintf("%s\x00%s\x00%d\x00%d\x00%d\x00%d\x00%s", item.ID, item.OwnedBy, item.Meta.Context, item.Meta.TrainingContext, item.Meta.Parameters, item.Meta.Size, item.Meta.FileType)
digest := sha256.Sum256([]byte(stableMetadata))
contextBytes := item.Meta.Context * 4
@@ -105,6 +116,19 @@ func (a *OpenAICompatibleLocalAdapter) Discover(ctx context.Context) ([]Discover
return models, ProviderProbe{Health: capability.HealthAvailable}
}
func isTextAdvisoryModel(kind string, capabilities, modalities map[string]bool) bool {
if strings.EqualFold(strings.TrimSpace(kind), "image-generation") || strings.EqualFold(strings.TrimSpace(kind), "audio-transcription") || strings.EqualFold(strings.TrimSpace(kind), "text-to-speech") {
return false
}
if chat, known := capabilities["chat"]; known && !chat {
return false
}
if text, known := modalities["text"]; known && !text {
return false
}
return true
}
type compatibleChatRequest struct {
Model string `json:"model"`
Messages []compatibleMessage `json:"messages"`
@@ -135,7 +159,11 @@ func (a *OpenAICompatibleLocalAdapter) Advise(ctx context.Context, request cogni
if err := ValidateAdvisoryRequest(request); err != nil || request.ProviderModelID == "" {
return cognitive.AdvisoryResponse{}, ErrInvalidAdvisory
}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: request.MaxOutputBytes / 4, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
maxTokens := request.MaxOutputBytes / 4
if maxTokens <= 0 || maxTokens > compatibleAdvisoryMaxTokens {
maxTokens = compatibleAdvisoryMaxTokens
}
payload := compatibleChatRequest{Model: string(request.ProviderModelID), Messages: []compatibleMessage{{Role: "system", Content: "Ты отвечаешь от имени Домового — локального помощника пользователя. Не называй себя именем модели, поставщиком или внутренним runtime. Если спрашивают имя, ответь: «Меня зовут Домовой». Понимай смысл сообщения и помогай довести цель до полезного результата. По умолчанию отвечай кратко, примерно одним-тремя предложениями; подробности давай только по просьбе. Отвечай только некатегоричным текстом совета; не утверждай, что внешнее действие уже выполнено."}, {Role: "user", Content: localAdvisoryMessage(request)}}, Temperature: 0, Stream: false, MaxTokens: maxTokens, ChatTemplateKwargs: compatibleTemplateKwargs{EnableThinking: false}}
var result compatibleChatResponse
status, err := a.requestJSON(ctx, http.MethodPost, "/chat/completions", payload, &result)
if err != nil {
@@ -236,6 +264,8 @@ func mapCompatibleStatus(status int) cognitive.FinishReason {
return cognitive.FinishContextTooLarge
case 429:
return cognitive.FinishRateLimited
case 400, 422:
return cognitive.FinishInvalidProviderResponse
default:
return cognitive.FinishProviderUnavailable
}
@@ -29,6 +29,37 @@ func (modelsMetadataRoundTripper) RoundTrip(request *http.Request) (*http.Respon
}, nil
}
type mixedModelsRoundTripper struct{}
func (mixedModelsRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"data":[{"id":"image-model","model_kind":"image-generation","capabilities":{"chat":false,"image_generation":true},"modalities":{"text":false,"image_generation":true}},{"id":"text-model","model_kind":"chat","capabilities":{"chat":true},"modalities":{"text":true},"meta":{}}]}`)),
Header: make(http.Header),
Request: request,
}, nil
}
func TestOpenAICompatibleLocalAdapterDoesNotQualifyNonTextModels(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", mixedModelsRoundTripper{})
if err != nil {
t.Fatal(err)
}
models, probe := adapter.Discover(context.Background())
if probe.Health != capability.HealthAvailable || len(models) != 1 || models[0].ProviderModelID != "text-model" {
t.Fatalf("unexpected filtered discovery: probe=%+v models=%+v", probe, models)
}
}
func TestOpenAICompatibleLocalAdapterMapsImmediateBadRequest(t *testing.T) {
if got := mapCompatibleStatus(http.StatusBadRequest); got != cognitive.FinishInvalidProviderResponse {
t.Fatalf("400 finish=%s, want %s", got, cognitive.FinishInvalidProviderResponse)
}
if got := mapCompatibleStatus(http.StatusUnprocessableEntity); got != cognitive.FinishInvalidProviderResponse {
t.Fatalf("422 finish=%s, want %s", got, cognitive.FinishInvalidProviderResponse)
}
}
func TestOpenAICompatibleLocalAdapterNormalizesMinimalModelMetadata(t *testing.T) {
adapter, err := NewOpenAICompatibleLocalAdapter("http://gateway/v1", modelsMetadataRoundTripper{})
if err != nil {
@@ -109,6 +140,7 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
t.Fatal(err)
}
var payload struct {
MaxTokens int `json:"max_tokens"`
ChatTemplateKwargs struct {
EnableThinking bool `json:"enable_thinking"`
} `json:"chat_template_kwargs"`
@@ -116,6 +148,9 @@ func TestOpenAICompatibleLocalAdapterDisablesTemplateThinkingForBoundedAdvisory(
if err := json.Unmarshal(transport.body, &payload); err != nil {
t.Fatal(err)
}
if payload.MaxTokens != compatibleAdvisoryMaxTokens {
t.Fatalf("max_tokens=%d, want bounded %d", payload.MaxTokens, compatibleAdvisoryMaxTokens)
}
if payload.ChatTemplateKwargs.EnableThinking {
t.Fatal("expected bounded advisory to disable template thinking")
}
+45 -8
View File
@@ -37,10 +37,12 @@ type DeliveryRequest struct {
}
type Receipt struct {
ReceiptID string `json:"receipt_id"`
ActionIntentID commitcontract.ActionIntentID `json:"action_intent_id"`
RequestDigest string `json:"request_digest"`
AcceptedAt int64 `json:"accepted_at_unix_millis"`
ReceiptID string `json:"receipt_id"`
ActionIntentID commitcontract.ActionIntentID `json:"action_intent_id"`
RequestDigest string `json:"request_digest"`
AcceptedAt int64 `json:"accepted_at_unix_millis"`
DeliveryMode string `json:"delivery_mode,omitempty"`
ExternalDeliveryAttempted bool `json:"external_delivery_attempted"`
}
func Digest(payload Payload) (string, error) {
@@ -76,11 +78,27 @@ func PayloadFromAction(action commitcontract.AuthorizedAction) (Payload, error)
type Executor struct {
binding capability.Binding
candidate *capability.CandidateBinding
secret string
client *http.Client
executeAllowed bool
}
// NewShadow accepts only a Capability-owned SHADOW binding. Its caller does
// not supply an endpoint; the target remains inside the resolved binding.
func NewShadow(binding capability.CandidateBinding, secret string, client *http.Client) (*Executor, error) {
if capabilitydomain.ValidateCandidateBinding(binding) != nil || binding.Context.Mode != capability.CandidateBindingModeShadow || binding.Status != capability.CandidateBindingActive {
return nil, ErrInvalidNotification
}
stable := capability.Binding{ID: binding.BaseActiveBindingID, Revision: binding.BaseActiveBindingRevision, OwnerPrincipalID: binding.Context.PrincipalID}
value, err := newExecutor(stable, secret, client, true)
if err != nil {
return nil, err
}
value.candidate = &binding
return value, nil
}
func New(binding capability.Binding, secret string, client *http.Client) (*Executor, error) {
if err := capabilitydomain.UsableBinding(binding); err != nil {
return nil, ErrInvalidNotification
@@ -116,7 +134,7 @@ func (e *Executor) Execute(action commitcontract.AuthorizedAction) ports.Executi
if err != nil {
return ports.ExecutionResult{Known: true}
}
endpoint, err := e.endpoint(e.binding.DeliveryPath, string(action.Proposal.IntentID))
endpoint, err := e.endpoint(e.deliveryPath(), string(action.Proposal.IntentID))
if err != nil {
return ports.ExecutionResult{Known: true}
}
@@ -127,6 +145,10 @@ func (e *Executor) Execute(action commitcontract.AuthorizedAction) ports.Executi
request.Header.Set("Authorization", "Bearer "+e.secret)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json")
if e.candidate != nil {
request.Header.Set("X-Candidate-Release", e.candidate.Context.ReleaseSetID)
request.Header.Set("X-Candidate-Task", e.candidate.Context.TaskID)
}
response, err := e.client.Do(request)
if err != nil {
return ports.ExecutionResult{}
@@ -139,10 +161,14 @@ func (e *Executor) Execute(action commitcontract.AuthorizedAction) ports.Executi
return ports.ExecutionResult{}
}
receipt, err := decodeReceipt(response.Body)
if err != nil || !matchingReceipt(receipt, action) {
if err != nil || !matchingReceipt(receipt, action) || (e.candidate != nil && (receipt.DeliveryMode != "SHADOW" || receipt.ExternalDeliveryAttempted)) {
return ports.ExecutionResult{}
}
return ports.ExecutionResult{Known: true, Succeeded: true}
result := ports.ExecutionResult{Known: true, Succeeded: true}
if e.candidate != nil {
result.Receipt = &commitcontract.ExecutionReceipt{ID: receipt.ReceiptID, ExecutorImplementationRef: "notification-shadow-receiver-v1", ExternalOperationRef: "SHADOW_CAPTURED", ExternalRequestID: string(receipt.ActionIntentID), RequestDigest: receipt.RequestDigest, DispatchAtUnixMillis: receipt.AcceptedAt, ExecutorResult: commitcontract.ExecutorAccepted, ReconciliationKey: receipt.ReceiptID, DeliveryMode: "SHADOW", ExternalDeliveryAttempted: false, ReleaseSetID: e.candidate.Context.ReleaseSetID, TaskID: e.candidate.Context.TaskID, TaskRevision: e.candidate.Context.TaskRevision, CapabilityBindingID: string(e.candidate.ID), CapabilityBindingRevision: e.candidate.Revision, TargetReference: e.candidate.Context.TargetRef}
}
return result
}
func (e *Executor) Reconcile(action commitcontract.AuthorizedAction) ports.ExecutionResult {
@@ -186,7 +212,11 @@ func (e *Executor) requestFor(action commitcontract.AuthorizedAction) (Payload,
}
func (e *Executor) endpoint(path, intent string) (string, error) {
base, err := url.Parse(e.binding.HTTPSOrigin)
origin := e.binding.HTTPSOrigin
if e.candidate != nil {
origin = e.candidate.TargetOrigin
}
base, err := url.Parse(origin)
if err != nil {
return "", err
}
@@ -195,6 +225,13 @@ func (e *Executor) endpoint(path, intent string) (string, error) {
return base.String(), nil
}
func (e *Executor) deliveryPath() string {
if e.candidate != nil {
return e.candidate.DeliveryPath
}
return e.binding.DeliveryPath
}
func decodeReceipt(reader io.Reader) (Receipt, error) {
var receipt Receipt
decoder := json.NewDecoder(io.LimitReader(reader, maxResponseBytes+1))
@@ -9,6 +9,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/contracts/identity"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
)
func testAction(t *testing.T) commitcontract.AuthorizedAction {
@@ -55,6 +56,35 @@ func TestExecutorPutAndReconcileMatchReceipt(t *testing.T) {
}
}
func TestShadowExecutorRequiresShadowReceipt(t *testing.T) {
action := testAction(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Candidate-Release") != "assistant-slot-b" || r.Header.Get("X-Candidate-Task") != "GEN2-CAPABILITY-001" {
t.Fatalf("missing candidate context")
}
_ = json.NewEncoder(w).Encode(Receipt{ReceiptID: "shadow-receipt-1", ActionIntentID: action.Proposal.IntentID, RequestDigest: action.Proposal.NotificationSubject.RequestDigest, AcceptedAt: 1, DeliveryMode: "SHADOW", ExternalDeliveryAttempted: false})
}))
defer server.Close()
binding := capability.CandidateBinding{ID: "candidate-binding-1", Revision: 1, BaseActiveBindingID: "binding-1", BaseActiveBindingRevision: 1,
Context: capability.CandidateReleaseContext{ReleaseSetID: "assistant-slot-b", Slot: "b", TaskID: "GEN2-CAPABILITY-001", TaskRevision: 7, PrincipalID: "owner-1", CapabilityID: commitcontract.CapabilityProspectiveNotification, ResourceScope: "synthetic-shadow-notification-v1", Mode: capability.CandidateBindingModeShadow, TargetKind: "NOTIFICATION_SHADOW_RECEIVER", TargetRef: "notification-receiver-candidate", AuthorityGrantID: "grant"},
TargetOrigin: server.URL, DeliveryPath: "/delivery/v1/deliveries", ReceiptPath: "/delivery/v1/receipts", Protocol: capability.ProtocolIdempotentReceiptV1, CreatedAtUnixMillis: 1, ExpiresAtUnixMillis: 100, Status: capability.CandidateBindingActive}
// The httptest address is intentionally not the production candidate target;
// its digest is recomputed locally only for this bounded test binding.
digest, err := capabilitydomain.CandidateTargetDigest(binding)
if err != nil {
t.Fatal(err)
}
binding.Context.TargetDigest = digest
executor, err := NewShadow(binding, "shadow-secret", server.Client())
if err != nil {
t.Fatal(err)
}
result := executor.Execute(action)
if !result.Known || !result.Succeeded || result.Receipt == nil || result.Receipt.DeliveryMode != "SHADOW" || result.Receipt.ExternalDeliveryAttempted {
t.Fatalf("shadow result=%+v", result)
}
}
func TestExecutorAmbiguousAndIntegrityOutcomes(t *testing.T) {
action := testAction(t)
for name, handler := range map[string]http.HandlerFunc{
+26
View File
@@ -3,6 +3,7 @@ package postgres
import (
"context"
"fmt"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"github.com/jackc/pgx/v5/pgxpool"
@@ -27,3 +28,28 @@ func (r *AuthorityRepository) LoadGrant(ctx context.Context, id authority.GrantI
}
return grant, scope, nil
}
func (r *AuthorityRepository) LoadDelegatedControlPlaneGrant(ctx context.Context, id authority.GrantID) (authority.DelegatedControlPlaneGrant, error) {
var grant authority.DelegatedControlPlaneGrant
err := r.pool.QueryRow(ctx, `SELECT grant_id,service_actor_id,service_principal_id,issuer_principal_id,task_id,task_revision,operations,COALESCE(binding_id,''),COALESCE(provider_instance_id,''),COALESCE(model_descriptor_id,''),candidate_release_set_id,candidate_slot,candidate_capability_id,candidate_resource_scope,candidate_mode,candidate_target_kind,candidate_target_ref,candidate_target_digest,issued_at_unix_millis,expires_at_unix_millis,architecture_revision,nonce,revision,status FROM authority.delegated_control_plane_grants WHERE grant_id=$1`, id).Scan(&grant.ID, &grant.ServiceActorID, &grant.ServicePrincipalID, &grant.IssuerPrincipalID, &grant.TaskID, &grant.TaskRevision, &grant.Operations, &grant.Resource.BindingID, &grant.Resource.ProviderInstanceID, &grant.Resource.ModelDescriptorID, &grant.CandidateResource.ReleaseSetID, &grant.CandidateResource.Slot, &grant.CandidateResource.CapabilityID, &grant.CandidateResource.ResourceScope, &grant.CandidateResource.Mode, &grant.CandidateResource.TargetKind, &grant.CandidateResource.TargetRef, &grant.CandidateResource.TargetDigest, &grant.IssuedAtUnixMillis, &grant.ExpiresAtUnixMillis, &grant.ArchitectureRevision, &grant.Nonce, &grant.Revision, &grant.Status)
if err != nil {
return authority.DelegatedControlPlaneGrant{}, fmt.Errorf("load delegated control-plane grant: %w", err)
}
return grant, nil
}
func (r *AuthorityRepository) RecordDelegatedControlPlaneAudit(ctx context.Context, audit authority.DelegatedControlPlaneAudit) error {
_, err := r.pool.Exec(ctx, `INSERT INTO authority.delegated_control_plane_audit (audit_id,grant_id,service_actor_id,service_principal_id,task_id,task_revision,operation,binding_id,provider_instance_id,model_descriptor_id,candidate_release_set_id,candidate_slot,candidate_capability_id,candidate_resource_scope,candidate_mode,candidate_target_kind,candidate_target_ref,candidate_target_digest,decision,outcome,recorded_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,NULLIF($8,''),NULLIF($9,''),NULLIF($10,''),$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)`, audit.AuditID, audit.GrantID, audit.ServiceActorID, audit.ServicePrincipalID, audit.TaskID, audit.TaskRevision, audit.Operation, audit.Resource.BindingID, audit.Resource.ProviderInstanceID, audit.Resource.ModelDescriptorID, audit.CandidateResource.ReleaseSetID, audit.CandidateResource.Slot, audit.CandidateResource.CapabilityID, audit.CandidateResource.ResourceScope, audit.CandidateResource.Mode, audit.CandidateResource.TargetKind, audit.CandidateResource.TargetRef, audit.CandidateResource.TargetDigest, audit.Decision, audit.Outcome, audit.RecordedAtUnixMillis)
if err != nil {
return fmt.Errorf("record delegated control-plane audit: %w", err)
}
return nil
}
func (r *AuthorityRepository) RevokeDelegatedControlPlaneGrant(ctx context.Context, id authority.GrantID, issuer string) (bool, error) {
result, err := r.pool.Exec(ctx, `UPDATE authority.delegated_control_plane_grants SET status='REVOKED',revision=revision+1,revoked_at_unix_millis=$3 WHERE grant_id=$1 AND issuer_principal_id=$2 AND status='ACTIVE'`, id, issuer, time.Now().UnixMilli())
if err != nil {
return false, fmt.Errorf("revoke delegated control-plane grant: %w", err)
}
return result.RowsAffected() == 1, nil
}
+138 -7
View File
@@ -2,11 +2,13 @@ package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -305,12 +307,14 @@ func (r *CapabilityRepository) SaveSkillProfile(ctx context.Context, value capab
_, err = tx.Exec(ctx, `INSERT INTO capability.skill_profiles (
skill_profile_id,revision,name,description,applicable_context,implementation_ref,implementation_version,status,
supporting_experience_refs,known_failure_experience_refs,confidence_millis,last_validated_unix_millis,
observed_failure_count,observed_correction_count,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT DO NOTHING`,
observed_failure_count,observed_correction_count,recorded_at_unix_millis,applicability,dependency_refs,validation_evidence_refs,
independent_evaluation_refs,known_failure_conditions,verified_outcome_count,must_not_worsen_satisfied)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) ON CONFLICT DO NOTHING`,
value.ID, value.Revision, value.Name, value.Description, value.ApplicableContext, value.ImplementationRef,
value.ImplementationVersion, value.Status, value.SupportingExperienceRefs, value.KnownFailureExperienceRefs,
value.ConfidenceMillis, value.LastValidatedUnixMillis, value.ObservedFailureCount, value.ObservedCorrectionCount,
value.RecordedAtUnixMillis)
value.RecordedAtUnixMillis, mustJSON(value.Applicability), value.DependencyRefs, value.ValidationEvidenceRefs,
value.IndependentEvaluationRefs, value.KnownFailureConditions, value.VerifiedOutcomeCount, value.MustNotWorsenSatisfied)
if err != nil {
return err
}
@@ -329,13 +333,16 @@ func (r *CapabilityRepository) LoadSkillProfile(ctx context.Context, id capabili
err := r.pool.QueryRow(ctx, `SELECT
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis,p.applicability,p.dependency_refs,
p.validation_evidence_refs,p.independent_evaluation_refs,p.known_failure_conditions,p.verified_outcome_count,p.must_not_worsen_satisfied
FROM capability.skill_profile_heads h
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
WHERE p.skill_profile_id=$1`, id).Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis)
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis, newJSONTarget(&value.Applicability),
&value.DependencyRefs, &value.ValidationEvidenceRefs, &value.IndependentEvaluationRefs, &value.KnownFailureConditions,
&value.VerifiedOutcomeCount, &value.MustNotWorsenSatisfied)
if err != nil {
return capability.SkillProfile{}, fmt.Errorf("load skill profile: %w", err)
}
@@ -349,7 +356,8 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
rows, err := r.pool.Query(ctx, `SELECT
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis,p.applicability,p.dependency_refs,
p.validation_evidence_refs,p.independent_evaluation_refs,p.known_failure_conditions,p.verified_outcome_count,p.must_not_worsen_satisfied
FROM capability.skill_profile_heads h
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
ORDER BY h.updated_at DESC, p.skill_profile_id LIMIT $1`, limit)
@@ -363,7 +371,9 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
if err := rows.Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis); err != nil {
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis, newJSONTarget(&value.Applicability),
&value.DependencyRefs, &value.ValidationEvidenceRefs, &value.IndependentEvaluationRefs, &value.KnownFailureConditions,
&value.VerifiedOutcomeCount, &value.MustNotWorsenSatisfied); err != nil {
return nil, err
}
result = append(result, value)
@@ -371,6 +381,40 @@ func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit in
return result, rows.Err()
}
func (r *CapabilityRepository) SaveReflexRevision(ctx context.Context, reflex capability.Reflex, value capability.ReflexRevision) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.reflex_revisions (
reflex_id,revision,principal_id,source_skill_id,source_skill_revision,kind,applicability,implementation_ref,implementation_version,artifact_digest,
dependency_refs,validation_evidence_refs,known_failure_experience_refs,shadow_policy,revalidation_policy,last_validated_at,lifecycle,
supersedes_revision,execution_count,failure_count,divergence_count,recorded_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22) ON CONFLICT DO NOTHING`,
value.ReflexID, value.Revision, reflex.PrincipalID, reflex.SourceSkillRef, value.SourceSkillRevision, value.Kind, mustJSON(value.Applicability), value.ImplementationRef, value.ImplementationVersion, value.ArtifactDigest,
value.DependencyRefs, value.ValidationEvidenceRefs, value.KnownFailureExperienceRefs, mustJSON(value.Shadow), mustJSON(value.Revalidation), value.LastValidatedAt, value.Lifecycle,
value.SupersedesRevision, value.ExecutionCount, value.FailureCount, value.DivergenceCount, value.RecordedAt)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO capability.reflex_heads(reflex_id,revision,lifecycle) VALUES($1,$2,$3)
ON CONFLICT(reflex_id) DO UPDATE SET revision=EXCLUDED.revision,lifecycle=EXCLUDED.lifecycle,updated_at=CURRENT_TIMESTAMP
WHERE capability.reflex_heads.revision < EXCLUDED.revision`, value.ReflexID, value.Revision, value.Lifecycle)
return err
}
func mustJSON(value any) []byte { encoded, _ := json.Marshal(value); return encoded }
type jsonTarget struct{ target any }
func newJSONTarget(target any) *jsonTarget { return &jsonTarget{target: target} }
func (v *jsonTarget) Scan(source any) error {
if source == nil {
return nil
}
bytes, ok := source.([]byte)
if !ok {
bytes = []byte(fmt.Sprint(source))
}
return json.Unmarshal(bytes, v.target)
}
func (r *CapabilityRepository) LoadBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64) (capability.Binding, error) {
var binding capability.Binding
err := r.pool.QueryRow(ctx, `SELECT binding_id,capability_id,owner_principal_id,revision,state,https_origin,delivery_path,receipt_path,
@@ -399,6 +443,93 @@ func (r *CapabilityRepository) LoadActiveBinding(ctx context.Context, capability
return binding, nil
}
func (r *CapabilityRepository) SaveCandidateBinding(ctx context.Context, binding capability.CandidateBinding) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
result, err := tx.Exec(ctx, `INSERT INTO capability.release_candidate_bindings (
binding_id,revision,base_active_binding_id,base_active_binding_revision,release_set_id,slot,task_id,task_revision,principal_id,capability_id,resource_scope,mode,target_kind,target_ref,target_digest,authority_grant_id,
target_origin,delivery_path,receipt_path,protocol,created_at_unix_millis,expires_at_unix_millis,status)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23) ON CONFLICT DO NOTHING`,
binding.ID, binding.Revision, binding.BaseActiveBindingID, binding.BaseActiveBindingRevision, binding.Context.ReleaseSetID, binding.Context.Slot, binding.Context.TaskID,
binding.Context.TaskRevision, binding.Context.PrincipalID, binding.Context.CapabilityID, binding.Context.ResourceScope, binding.Context.Mode, binding.Context.TargetKind, binding.Context.TargetRef, binding.Context.TargetDigest, binding.Context.AuthorityGrantID,
binding.TargetOrigin, binding.DeliveryPath, binding.ReceiptPath, binding.Protocol, binding.CreatedAtUnixMillis, binding.ExpiresAtUnixMillis, binding.Status)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return ErrCapabilityBindingConflict
}
if binding.Status == capability.CandidateBindingActive {
result, err = tx.Exec(ctx, `INSERT INTO capability.release_candidate_binding_heads (
release_set_id,slot,task_id,principal_id,capability_id,resource_scope,mode,binding_id,revision)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
ON CONFLICT (release_set_id,slot,task_id,principal_id,capability_id,resource_scope,mode) DO UPDATE SET binding_id=EXCLUDED.binding_id,revision=EXCLUDED.revision
WHERE capability.release_candidate_binding_heads.revision < EXCLUDED.revision`, binding.Context.ReleaseSetID, binding.Context.Slot, binding.Context.TaskID,
binding.Context.PrincipalID, binding.Context.CapabilityID, binding.Context.ResourceScope, binding.Context.Mode, binding.ID, binding.Revision)
if err != nil || result.RowsAffected() != 1 {
if err != nil {
return err
}
return ErrCapabilityBindingConflict
}
}
_, err = tx.Exec(ctx, `INSERT INTO capability.release_candidate_binding_audit (binding_id,revision,release_set_id,task_id,capability_id,resource_scope,mode,decision,reason,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,'CREATED','CAPABILITY_OWNED_CANDIDATE_BINDING',$8)`, binding.ID, binding.Revision, binding.Context.ReleaseSetID, binding.Context.TaskID, binding.Context.CapabilityID, binding.Context.ResourceScope, binding.Context.Mode, binding.CreatedAtUnixMillis)
if err != nil {
return err
}
return tx.Commit(ctx)
}
func (r *CapabilityRepository) ResolveCandidateBinding(ctx context.Context, request capability.CandidateReleaseContext, nowUnixMillis int64) (capability.CandidateBindingResolution, error) {
var binding capability.CandidateBinding
err := r.pool.QueryRow(ctx, `SELECT b.binding_id,b.revision,b.base_active_binding_id,b.base_active_binding_revision,b.release_set_id,b.slot,b.task_id,b.task_revision,b.principal_id,b.capability_id,b.resource_scope,b.mode,b.target_kind,b.target_ref,b.target_digest,b.authority_grant_id,b.target_origin,b.delivery_path,b.receipt_path,b.protocol,b.created_at_unix_millis,b.expires_at_unix_millis,b.status
FROM capability.release_candidate_binding_heads h JOIN capability.release_candidate_bindings b ON b.binding_id=h.binding_id AND b.revision=h.revision
WHERE h.release_set_id=$1 AND h.slot=$2 AND h.task_id=$3 AND h.principal_id=$4 AND h.capability_id=$5 AND h.resource_scope=$6 AND h.mode=$7`,
request.ReleaseSetID, request.Slot, request.TaskID, request.PrincipalID, request.CapabilityID, request.ResourceScope, request.Mode).
Scan(&binding.ID, &binding.Revision, &binding.BaseActiveBindingID, &binding.BaseActiveBindingRevision, &binding.Context.ReleaseSetID, &binding.Context.Slot, &binding.Context.TaskID, &binding.Context.TaskRevision, &binding.Context.PrincipalID, &binding.Context.CapabilityID, &binding.Context.ResourceScope, &binding.Context.Mode, &binding.Context.TargetKind, &binding.Context.TargetRef, &binding.Context.TargetDigest, &binding.Context.AuthorityGrantID, &binding.TargetOrigin, &binding.DeliveryPath, &binding.ReceiptPath, &binding.Protocol, &binding.CreatedAtUnixMillis, &binding.ExpiresAtUnixMillis, &binding.Status)
if err != nil {
return capability.CandidateBindingResolution{}, fmt.Errorf("candidate binding required: %w", err)
}
if capabilitydomain.CandidateBindingInvocable(binding, request, nowUnixMillis) != nil {
return capability.CandidateBindingResolution{}, ErrCapabilityBindingNotFound
}
_, _ = r.pool.Exec(ctx, `INSERT INTO capability.release_candidate_binding_audit (binding_id,revision,release_set_id,task_id,capability_id,resource_scope,mode,decision,reason,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,'RESOLVED','EXACT_CONTEXT',$8)`, binding.ID, binding.Revision, request.ReleaseSetID, request.TaskID, request.CapabilityID, request.ResourceScope, request.Mode, nowUnixMillis)
return capability.CandidateBindingResolution{Binding: binding, Mode: binding.Context.Mode, Reason: "EXACT_CONTEXT"}, nil
}
func (r *CapabilityRepository) RevokeCandidateBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64, nowUnixMillis int64) (bool, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return false, err
}
defer func() { _ = tx.Rollback(ctx) }()
var binding capability.CandidateBinding
err = tx.QueryRow(ctx, `UPDATE capability.release_candidate_bindings SET status='REVOKED' WHERE binding_id=$1 AND revision=$2 AND status='ACTIVE'
RETURNING binding_id,revision,base_active_binding_id,base_active_binding_revision,release_set_id,slot,task_id,task_revision,principal_id,capability_id,resource_scope,mode,target_kind,target_ref,target_digest,authority_grant_id,target_origin,delivery_path,receipt_path,protocol,created_at_unix_millis,expires_at_unix_millis,status`, id, revision).
Scan(&binding.ID, &binding.Revision, &binding.BaseActiveBindingID, &binding.BaseActiveBindingRevision, &binding.Context.ReleaseSetID, &binding.Context.Slot, &binding.Context.TaskID, &binding.Context.TaskRevision, &binding.Context.PrincipalID, &binding.Context.CapabilityID, &binding.Context.ResourceScope, &binding.Context.Mode, &binding.Context.TargetKind, &binding.Context.TargetRef, &binding.Context.TargetDigest, &binding.Context.AuthorityGrantID, &binding.TargetOrigin, &binding.DeliveryPath, &binding.ReceiptPath, &binding.Protocol, &binding.CreatedAtUnixMillis, &binding.ExpiresAtUnixMillis, &binding.Status)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return false, nil
}
return false, err
}
_, err = tx.Exec(ctx, `DELETE FROM capability.release_candidate_binding_heads WHERE release_set_id=$1 AND slot=$2 AND task_id=$3 AND principal_id=$4 AND capability_id=$5 AND resource_scope=$6 AND mode=$7 AND binding_id=$8 AND revision=$9`, binding.Context.ReleaseSetID, binding.Context.Slot, binding.Context.TaskID, binding.Context.PrincipalID, binding.Context.CapabilityID, binding.Context.ResourceScope, binding.Context.Mode, binding.ID, binding.Revision)
if err != nil {
return false, err
}
_, err = tx.Exec(ctx, `INSERT INTO capability.release_candidate_binding_audit (binding_id,revision,release_set_id,task_id,capability_id,resource_scope,mode,decision,reason,recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,'REVOKED','EXPLICIT_REVOKE',$8)`, binding.ID, binding.Revision, binding.Context.ReleaseSetID, binding.Context.TaskID, binding.Context.CapabilityID, binding.Context.ResourceScope, binding.Context.Mode, nowUnixMillis)
if err != nil {
return false, err
}
return true, tx.Commit(ctx)
}
func (r *CapabilityRepository) SaveProviderInstance(ctx context.Context, value capability.ProviderInstance) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances
(provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros)
+23
View File
@@ -111,6 +111,29 @@ func (r *CommitRepository) FinishAttempt(ctx context.Context, a commitcontract.A
return nil
}
func (r *CommitRepository) SaveExecutionReceipt(ctx context.Context, receipt commitcontract.ExecutionReceipt) error {
if !receipt.Valid() {
return ErrActionAttemptConflict
}
completed := int64(0)
if receipt.CompletedAtUnixMillis != nil {
completed = *receipt.CompletedAtUnixMillis
}
result, err := r.pool.Exec(ctx, `INSERT INTO commit.execution_receipts (
receipt_id,action_attempt_id,executor_implementation_ref,external_operation_ref,external_request_id,request_digest,response_digest,dispatch_at_unix_millis,completed_at_unix_millis,
executor_result,reconciliation_key,raw_artifact_ref,delivery_mode,external_delivery_attempted,release_set_id,task_id,task_revision,capability_binding_id,capability_binding_revision,target_reference)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20) ON CONFLICT (receipt_id) DO NOTHING`,
receipt.ID, receipt.ActionAttemptRef, receipt.ExecutorImplementationRef, receipt.ExternalOperationRef, receipt.ExternalRequestID, receipt.RequestDigest, receipt.ResponseDigest, receipt.DispatchAtUnixMillis, completed,
receipt.ExecutorResult, receipt.ReconciliationKey, receipt.RawArtifactRef, receipt.DeliveryMode, receipt.ExternalDeliveryAttempted, receipt.ReleaseSetID, receipt.TaskID, receipt.TaskRevision, receipt.CapabilityBindingID, receipt.CapabilityBindingRevision, receipt.TargetReference)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return ErrActionAttemptConflict
}
return nil
}
func (r *CommitRepository) LoadAuthorized(ctx context.Context, id commitcontract.AuthorizedActionID) (commitcontract.AuthorizedAction, error) {
return loadAuthorized(ctx, r.pool, id)
}
+43 -9
View File
@@ -101,10 +101,10 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
return nil, fmt.Errorf("invalid experience limit")
}
rows, err := r.pool.Query(ctx, `SELECT
experience_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
verification_evidence,confidence_millis,recorded_at_unix_millis
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
FROM epistemics.experience_records ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $1`, limit)
if err != nil {
return nil, err
@@ -113,10 +113,44 @@ func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limi
var result []epistemics.ExperienceRecord
for rows.Next() {
var value epistemics.ExperienceRecord
if err := rows.Scan(&value.ID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis); err != nil {
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
// RecentExperienceRecordsForPrincipalAndScope is an owner-local Epistemics
// read. It intentionally applies Principal and ContextScope in SQL before any
// Core-side relevance ranking, so another realm's experience never becomes a
// retrieval candidate.
func (r *EpistemicsRepository) RecentExperienceRecordsForPrincipalAndScope(ctx context.Context, principalID, scopeRef string, limit int) ([]epistemics.ExperienceRecord, error) {
if principalID == "" || limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid scoped experience query")
}
rows, err := r.pool.Query(ctx, `SELECT
experience_id,principal_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
verification_evidence,confidence_millis,recorded_at_unix_millis,context_scope_ref,problem_statement,applicability_conditions,why_succeeded_or_failed
FROM epistemics.experience_records
WHERE principal_id=$1 AND ($2='' OR context_scope_ref=$2)
ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $3`, principalID, scopeRef, limit)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]epistemics.ExperienceRecord, 0)
for rows.Next() {
var value epistemics.ExperienceRecord
if err := rows.Scan(&value.ID, &value.PrincipalID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis, &value.ContextScopeRef, &value.ProblemStatement, &value.ApplicabilityConditions, &value.WhySucceededOrFailed); err != nil {
return nil, err
}
result = append(result, value)
@@ -205,15 +239,15 @@ func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, i
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
experience_id, principal_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
capability_implementation_ref, capability_usage_observation_ref, capability_gap_ref, skill_ref, expectation_id,
expected_outcome, actual_outcome, outcome_ref, outcome_detail, human_response_ref, correction_utterance_id,
verification_evidence, confidence_millis, recorded_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21) ON CONFLICT DO NOTHING`,
value.ID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
verification_evidence, confidence_millis, recorded_at_unix_millis, context_scope_ref, problem_statement, applicability_conditions, why_succeeded_or_failed)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26) ON CONFLICT DO NOTHING`,
value.ID, value.PrincipalID, value.SourceKind, value.OriginalIntentID, value.SourceUtteranceID, value.ContextRefs, value.HelpMethod, value.HelpMethodRef,
value.CapabilityImplementationRef, value.CapabilityUsageObservationRef, value.CapabilityGapRef, value.SkillRef, value.ExpectationID,
value.ExpectedOutcome, value.ActualOutcome, value.OutcomeRef, value.OutcomeDetail, value.HumanResponseRef, value.CorrectionUtteranceID,
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis)
value.VerificationEvidence, value.ConfidenceMillis, value.RecordedAtUnixMillis, value.ContextScopeRef, value.ProblemStatement, value.ApplicabilityConditions, value.WhySucceededOrFailed)
return err
}
+10
View File
@@ -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
}
@@ -100,6 +100,22 @@ func (r *InteractionRepository) Utterances(ctx context.Context, id interaction.C
return out, rows.Err()
}
// UtterancesWithProjectReferences returns canonical utterances together with
// their already-persisted Interaction-owned project references. The caller
// uses the pair only as a read projection; this method does not infer or write
// scope membership.
func (r *InteractionRepository) UtterancesWithProjectReferences(ctx context.Context, id interaction.ConversationID) ([]interaction.HumanUtterance, []interaction.ProjectReference, error) {
utterances, err := r.Utterances(ctx, id)
if err != nil {
return nil, nil, err
}
references, err := r.projectReferencesForUtterances(ctx, utterances)
if err != nil {
return nil, nil, err
}
return utterances, references, nil
}
// RecentUtterancesForPrincipal provides durable conversational context when a
// client starts a fresh conversation instead of carrying the previous ID.
func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, error) {
@@ -118,3 +134,52 @@ func (r *InteractionRepository) RecentUtterancesForPrincipal(ctx context.Context
}
return out, rows.Err()
}
// RecentUtterancesForPrincipalWithProjectReferences keeps a new conversation
// within the same durable Interaction read boundary as its scoped context.
func (r *InteractionRepository) RecentUtterancesForPrincipalWithProjectReferences(ctx context.Context, principalID identity.PrincipalID, limit int) ([]interaction.HumanUtterance, []interaction.ProjectReference, error) {
utterances, err := r.RecentUtterancesForPrincipal(ctx, principalID, limit)
if err != nil {
return nil, nil, err
}
references, err := r.projectReferencesForUtterances(ctx, utterances)
if err != nil {
return nil, nil, err
}
return utterances, references, nil
}
func (r *InteractionRepository) projectReferencesForUtterances(ctx context.Context, utterances []interaction.HumanUtterance) ([]interaction.ProjectReference, error) {
if len(utterances) == 0 {
return nil, nil
}
ids := make([]string, 0, len(utterances))
seen := make(map[interaction.HumanUtteranceID]struct{}, len(utterances))
for _, utterance := range utterances {
if utterance.ID == "" {
continue
}
if _, exists := seen[utterance.ID]; exists {
continue
}
seen[utterance.ID] = struct{}{}
ids = append(ids, string(utterance.ID))
}
if len(ids) == 0 {
return nil, nil
}
rows, err := r.pool.Query(ctx, `SELECT project_reference_id,utterance_id,project_id,project_display_name,role,provenance,confidence_millis,recorded_at_unix_millis FROM interaction.project_references WHERE utterance_id = ANY($1::text[]) ORDER BY recorded_at_unix_millis,project_reference_id`, ids)
if err != nil {
return nil, err
}
defer rows.Close()
refs := make([]interaction.ProjectReference, 0)
for rows.Next() {
var ref interaction.ProjectReference
if err := rows.Scan(&ref.ID, &ref.UtteranceID, &ref.ProjectID, &ref.ProjectDisplayName, &ref.Role, &ref.Provenance, &ref.ConfidenceMillis, &ref.RecordedAtUnixMillis); err != nil {
return nil, err
}
refs = append(refs, ref)
}
return refs, rows.Err()
}
+170
View File
@@ -0,0 +1,170 @@
package postgres
import (
"context"
"encoding/json"
"fmt"
selfcontract "git.cin.su/m/Assistant/contracts/self"
"github.com/jackc/pgx/v5/pgxpool"
)
// SelfRepository persists only SELF-owned derived assessments and ledger
// 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 {
return nil, err
}
return &SelfRepository{pool: pool}, nil
}
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 {
return err
}
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.improvement_needs(improvement_need_id,principal_id,subject_ref,problem_summary,evidence_refs,expected_benefit,priority,state,created_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, subject, value.ProblemSummary, evidence, value.ExpectedBenefit, value.Priority, value.State, value.CreatedAt)
return err
}
func (r *SelfRepository) SaveLearningLedger(ctx context.Context, value selfcontract.LearningLedgerEntry) error {
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
improvement, err := json.Marshal(value.ImprovementNeedRef)
if err != nil {
return err
}
proposed, err := json.Marshal(value.ProposedChangeRef)
if err != nil {
return err
}
evaluations, err := json.Marshal(value.ActualEvaluationRefs)
if err != nil {
return err
}
gate, err := json.Marshal(value.DevelopmentGateRef)
if err != nil {
return err
}
before, err := json.Marshal(value.BeforeVersionRefs)
if err != nil {
return err
}
after, err := json.Marshal(value.AfterVersionRefs)
if err != nil {
return err
}
sideEffects, err := json.Marshal(value.SideEffects)
if err != nil {
return err
}
rollback, err := json.Marshal(value.RollbackRef)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.learning_ledger(entry_id,principal_id,improvement_need_ref,observed_problem,evidence_refs,diagnosis,hypothesis,proposed_change_ref,expected_improvement,evaluation_refs,development_gate_ref,before_version_refs,after_version_refs,decision,side_effects,rollback_ref,recorded_at_unix_millis)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, improvement, value.ObservedProblem, evidence, value.Diagnosis, value.Hypothesis, proposed, value.ExpectedImprovement, evaluations, gate, before, after, value.Decision, sideEffects, rollback, value.RecordedAt)
return err
}
func (r *SelfRepository) SaveLearningSignal(ctx context.Context, value selfcontract.LearningSignal) error {
if err := value.Validate(); err != nil {
return err
}
subject, err := json.Marshal(value.SubjectRef)
if err != nil {
return err
}
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
deps, err := json.Marshal(value.DependencyRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.learning_signals(signal_id,principal_id,kind,task_pattern,scope_key,subject_ref,evidence_refs,expected_outcome,actual_outcome,dependency_refs,environment_version,observed_at_unix_millis,severity,user_impact,correction_count,decision_regret)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.Kind, value.TaskPattern, value.ScopeKey, subject, evidence, value.ExpectedOutcome, value.ActualOutcome, deps, value.EnvironmentVersion, value.ObservedAt, value.Severity, value.UserImpact, value.CorrectionCount, value.DecisionRegret)
return err
}
func (r *SelfRepository) SaveWeaknessPattern(ctx context.Context, value selfcontract.WeaknessPattern) error {
evidence, err := json.Marshal(value.DetectionEvidenceRefs)
if err != nil {
return err
}
deps, err := json.Marshal(value.DependencyRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.weakness_patterns(assessment_id,principal_id,revision,pattern_key,task_pattern,scope_key,environment_version,dependency_refs,failure_mode,detection_evidence_refs,frequency,last_observed_at_unix_millis,severity,affected_component,affected_strategy,confidence_millis,epistemic_status,state,recorded_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.PrincipalID, value.Revision, value.PatternKey, value.TaskPattern, value.ScopeKey, value.EnvironmentVersion, deps, value.FailureMode, evidence, value.Frequency, value.LastObservedAt, value.Severity, value.AffectedComponent, value.AffectedStrategy, value.ConfidenceMillis, value.EpistemicStatus, value.State, value.RecordedAt)
return err
}
func (r *SelfRepository) SaveImprovementCandidate(ctx context.Context, value selfcontract.ImprovementCandidate) error {
if err := value.Validate(); err != nil {
return err
}
need, err := json.Marshal(value.NeedRef)
if err != nil {
return err
}
must, err := json.Marshal(value.MustNotWorsen)
if err != nil {
return err
}
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.improvement_candidates(candidate_id,principal_id,need_ref,hypothesis,expected_effect,evaluation_plan,must_not_worsen,rollback_plan,resource_cost,creator,independent_evaluator,state,evidence_refs,created_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.PrincipalID, need, value.Hypothesis, value.ExpectedEffect, value.EvaluationPlan, must, value.RollbackPlan, value.ResourceCost, value.Creator, value.IndependentEvaluator, value.State, evidence, value.CreatedAt)
return err
}
func (r *SelfRepository) SaveBenchmarkCase(ctx context.Context, value selfcontract.BenchmarkCase) error {
if !value.Valid() {
return fmt.Errorf("benchmark case is incomplete")
}
evidence, err := json.Marshal(value.EvidenceRefs)
if err != nil {
return err
}
_, err = r.pool.Exec(ctx, `INSERT INTO self.personal_benchmark_cases(case_id,principal_id,task_class,evidence_refs,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`, value.ID, value.PrincipalID, value.TaskClass, evidence, value.RecordedAt)
return err
}
@@ -0,0 +1,62 @@
# ADR-0021 — Delegated Non-Interactive Control-Plane Authority
Status: Accepted
Date: 2026-08-31
Accepted under: owner decision for GEN2-AUTHORITY-001
## Context
The existing Creator session endpoints correctly require a human principal for
governed Cognitive CANARY and ACTIVE transitions. A bounded background task
cannot use those endpoints without either a browser session, impersonation, or
a direct Capability write. All three alternatives are invalid.
## Decision
1. Identity remains the sole owner of Actors, Principals and their bindings. A
service principal is a separately identified Actor/Principal binding; it is
neither a Human Principal nor an impersonation of one.
2. Authority remains the sole owner of delegated task grants, validation,
revocation and safe action audit. This extends `AuthorityGrant`; it does not
introduce a robot-authority owner or a second authorization registry.
3. A delegated grant is typed and narrow: service principal, task id and
revision, operation capability, exact Cognitive binding resource/revision,
issuer, issue/expiry times, grant revision and lifecycle status. It is
default-deny and cannot authorize grant creation, extension, policy change,
or administration.
4. A control-plane request is authorized in the existing `commit-runtime`, the
runtime topology location of Authority. It must pass two independent checks: service credential
authentication and a current delegated-grant authorization for its exact
task/operation/resource. Credential possession never expands authority.
5. Long-lived credential material remains only in the existing protected secret
backend and is addressed by a `CredentialRef` plus bootstrap file reference.
The runtime never logs, persists, or returns its token. Rotation changes the
protected artifact; revocation of a grant blocks new actions immediately.
6. Capability remains the only owner of Cognitive binding/evaluation lifecycle.
The delegated path invokes the existing owner validation and promotion
builders; it cannot synthesize an evaluation or mark a binding active.
7. Control-plane actions receive durable Authority audit records containing
identity, task, operation, resource, decision and safe result reference.
Secret values, request payloads, user memory and hidden reasoning are never
stored in that audit.
8. External mutations continue to use Commit/Executor/Receipt/Outcome where
the existing action is an external effect. Capability's owner-local binding
lifecycle remains an owner-local governed state transition, not a shortcut
around an external executor.
## Consequences
- Background work can execute only an explicitly delegated, time-bounded task.
- Restart reconstructs authorization from Identity/Authority durable state; no
process-local permission cache is authoritative.
- Each later task must receive a new delegated grant. A grant for
GEN2-COGNITIVE-003 cannot be reused for SELF development or code promotion.
- The first implementation deliberately covers only the Cognitive lifecycle
operations required by GEN2-COGNITIVE-003. Broader control-plane actions
need their own task-scoped authority and acceptance evidence.
## Rollback
Disable or revoke the delegated grant and route no new non-interactive requests
to the endpoint. Existing Authority audit and Capability history remain
append-only evidence; no binding state is rolled back by deleting history.
+108
View File
@@ -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) }
+16 -3
View File
@@ -16,15 +16,17 @@ func (p *admissionTestProvider) Advise(context.Context, cognitive.AdvisoryReques
}
type admissionTestClient struct {
decision capability.AdmissionDecision
releases int
decision capability.AdmissionDecision
releases int
releaseContextErr error
}
func (c *admissionTestClient) Admit(context.Context, string, capability.AdmissionRequest) (capability.AdmissionDecision, capability.AdmissionLease, error) {
return c.decision, capability.AdmissionLease{ID: "lease-1", PolicyID: "policy-1"}, nil
}
func (c *admissionTestClient) Release(context.Context, capability.AdmissionLease) error {
func (c *admissionTestClient) Release(ctx context.Context, _ capability.AdmissionLease) error {
c.releases++
c.releaseContextErr = ctx.Err()
return nil
}
@@ -47,3 +49,14 @@ func TestInvokeAdmittedReleasesDurableLeaseAfterProvider(t *testing.T) {
t.Fatalf("admitted lease was not released: response=%#v err=%v calls=%d releases=%d", response, err, provider.calls, client.releases)
}
}
func TestInvokeAdmittedReleasesLeaseAfterCallerCancellation(t *testing.T) {
provider := &admissionTestProvider{}
client := &admissionTestClient{decision: capability.AdmissionDecision{State: capability.AdmissionAdmitted, Reason: capability.AdmissionReasonNone, PolicyID: "policy-1"}}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _ = (&cognitiveGateway{admission: client}).invokeAdmitted(ctx, "request-1", provider, cognitive.AdvisoryRequest{Message: "hello"}, 100)
if client.releases != 1 || client.releaseContextErr != nil {
t.Fatalf("cancelled caller leaked durable lease: releases=%d release_context_err=%v", client.releases, client.releaseContextErr)
}
}
+16 -11
View File
@@ -1,7 +1,9 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sort"
"time"
@@ -22,17 +24,7 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
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)
return
}
discovered, probe := registration.Discovery.Discover(r.Context())
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
for _, model := range discovered {
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved})
}
result, err := s.resolver.RegisterDiscovery(r.Context(), snapshot)
result, err := s.register(r.Context(), r.PathValue("provider"))
if err != nil {
http.Error(w, "provider discovery registration unavailable", http.StatusBadGateway)
return
@@ -41,6 +33,19 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(result)
}
func (s discoveryService) register(ctx context.Context, provider string) (capability.CognitiveDiscoveryRegistration, error) {
registration, ok := s.providers[provider]
if !ok || registration.Discovery == nil {
return capability.CognitiveDiscoveryRegistration{}, fmt.Errorf("provider discovery not found")
}
discovered, probe := registration.Discovery.Discover(ctx)
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
for _, model := range discovered {
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved})
}
return s.resolver.RegisterDiscovery(ctx, snapshot)
}
type discoveryStatus struct {
Provider string `json:"provider"`
ProviderKind capability.ProviderKind `json:"provider_kind"`
+82 -4
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -20,6 +21,8 @@ import (
const creatorAdvisoryCapabilityID capability.CapabilityID = "creator-advisory"
var errNoInvocableCognitiveBinding = errors.New("no invocable cognitive binding")
type bindingResolver interface {
Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error)
Observe(context.Context, capability.CognitiveUsageObservation) error
@@ -56,6 +59,7 @@ type cognitiveGateway struct {
allowCanary bool
canaryMaxOutputBytes int
admission admissionClient
revalidatePrimary func(context.Context) error
}
func (g cognitiveGateway) policyHandler(w http.ResponseWriter, r *http.Request) {
@@ -89,7 +93,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
maxOutputBytes = g.canaryMaxOutputBytes
}
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes), Objective: selectionObjectiveForMeaning(request.Meaning, request.Message)}
selected, err := g.resolver.Resolve(r.Context(), requirement)
selected, err := g.resolveWithRevalidation(r.Context(), requirement)
if err != nil {
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
@@ -131,7 +135,51 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
}
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
writeGatewayResponse(w, gatewayAdvisoryResponse(request, response))
}
const iterativeRetrievalMarker = "<domovoy-retrieval>"
// gatewayAdvisoryResponse is the only model-facing extraction point. The
// model may express a bounded retrieval intent, but it cannot provide scope,
// identity, policy or storage details; Core supplies those on continuation.
func gatewayAdvisoryResponse(request cognitive.GatewayAdvisoryRequest, response cognitive.AdvisoryResponse) cognitive.GatewayAdvisoryResponse {
result := cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}
if !request.AllowIterativeRetrieval || response.FinishReason != cognitive.FinishCompleted {
return result
}
intent, text, ok := parseIterativeRetrievalIntent(response.AdvisoryText)
if !ok {
return result
}
result.AdvisoryText = text
result.IterativeRetrieval = &intent
return result
}
func parseIterativeRetrievalIntent(text string) (cognitive.IterativeRetrievalRequest, string, bool) {
start := strings.LastIndex(text, iterativeRetrievalMarker)
if start < 0 {
return cognitive.IterativeRetrievalRequest{}, text, false
}
payload := strings.TrimSpace(text[start+len(iterativeRetrievalMarker):])
var intent cognitive.IterativeRetrievalRequest
if err := json.Unmarshal([]byte(payload), &intent); err != nil || !validIterativeRetrievalIntent(intent) {
return cognitive.IterativeRetrievalRequest{}, text, false
}
return intent, strings.TrimSpace(text[:start]), true
}
func validIterativeRetrievalIntent(intent cognitive.IterativeRetrievalRequest) bool {
if strings.TrimSpace(intent.Query) == "" || strings.TrimSpace(intent.Reason) == "" || len([]rune(intent.Query)) > 512 || len([]rune(intent.Reason)) > 512 {
return false
}
switch intent.Lane {
case cognitive.IterativeRetrievalCurrentFact, cognitive.IterativeRetrievalPositiveExperience, cognitive.IterativeRetrievalNegativeExperience, cognitive.IterativeRetrievalContradiction, cognitive.IterativeRetrievalHistoricalAnalogy:
return true
default:
return false
}
}
func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, provider advisoryProvider, invocation cognitive.AdvisoryRequest, maxOutputBytes int) (cognitive.AdvisoryResponse, error) {
@@ -145,11 +193,33 @@ func (g cognitiveGateway) invokeAdmitted(ctx context.Context, leaseID string, pr
return cognitive.AdvisoryResponse{Version: invocation.Version, RequestID: invocation.RequestID, ProviderBindingID: invocation.ProviderBindingID, ProviderBindingRevision: invocation.ProviderBindingRevision, FinishReason: cognitive.FinishRateLimited}, nil
}
lease = admittedLease
defer func() { _ = g.admission.Release(ctx, lease) }()
defer func() {
releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
_ = g.admission.Release(releaseCtx, lease)
}()
}
return provider.Advise(ctx, invocation)
}
// resolveWithRevalidation makes one bounded recovery attempt when Capability
// has an active/validated binding but its durable health projection rejected it
// as non-invocable. Discovery remains the canonical Capability write path; the
// retry observes the configured primary local provider and re-runs selection
// once. Provider invocation, admission, and fallback remain governed elsewhere.
func (g cognitiveGateway) resolveWithRevalidation(ctx context.Context, requirement capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
selected, err := g.resolver.Resolve(ctx, requirement)
if err == nil || !errors.Is(err, errNoInvocableCognitiveBinding) || g.revalidatePrimary == nil {
return selected, err
}
revalidationCtx, cancel := context.WithTimeout(ctx, 45*time.Second)
defer cancel()
if revalidationErr := g.revalidatePrimary(revalidationCtx); revalidationErr != nil {
return selected, err
}
return g.resolver.Resolve(ctx, requirement)
}
func selectionObjectiveForMessage(message string) capability.SelectionObjective {
lower := strings.ToLower(message)
if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") {
@@ -374,6 +444,10 @@ type httpBindingResolver struct {
client *http.Client
}
type httpStatusError struct{ status int }
func (e httpStatusError) Error() string { return fmt.Sprintf("registry status %d", e.status) }
func newHTTPBindingResolver(raw string, client *http.Client) (*httpBindingResolver, error) {
base, err := url.Parse(raw)
if err != nil || base.Scheme == "" || base.Host == "" {
@@ -385,6 +459,10 @@ func newHTTPBindingResolver(raw string, client *http.Client) (*httpBindingResolv
func (r *httpBindingResolver) Resolve(ctx context.Context, value capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
var result capability.ResolvedCognitiveBinding
err := r.post(ctx, "/v1/cognitive-bindings/resolve", value, &result)
var statusErr httpStatusError
if errors.As(err, &statusErr) && statusErr.status == http.StatusNotFound {
return result, errNoInvocableCognitiveBinding
}
return result, err
}
@@ -437,7 +515,7 @@ func (r *httpBindingResolver) post(ctx context.Context, path string, value, dest
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
return fmt.Errorf("registry status %d", response.StatusCode)
return httpStatusError{status: response.StatusCode}
}
if destination != nil {
return json.NewDecoder(io.LimitReader(response.Body, 64<<10)).Decode(destination)
@@ -22,6 +22,26 @@ type registryStub struct {
observation *capability.CognitiveUsageObservation
}
type revalidatingRegistry struct {
selected capability.ResolvedCognitiveBinding
calls int
alwaysNo bool
}
func (s *revalidatingRegistry) Resolve(context.Context, capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
s.calls++
if s.alwaysNo || s.calls == 1 {
return capability.ResolvedCognitiveBinding{}, errNoInvocableCognitiveBinding
}
return s.selected, nil
}
func (*revalidatingRegistry) Observe(context.Context, capability.CognitiveUsageObservation) error {
return nil
}
func (*revalidatingRegistry) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
return capability.CognitiveDiscoveryRegistration{}, nil
}
type failoverRegistry struct {
primary, fallback capability.ResolvedCognitiveBinding
observations []capability.CognitiveUsageObservation
@@ -88,6 +108,73 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) {
}
}
func TestGatewayExtractsOnlyBoundedTypedRetrievalIntent(t *testing.T) {
request := cognitive.GatewayAdvisoryRequest{AllowIterativeRetrieval: true}
response := cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request", FinishReason: cognitive.FinishCompleted, AdvisoryText: "<domovoy-retrieval>{\"Query\":\"sync catalog\",\"SemanticSubject\":\"catalog sync\",\"Lane\":\"NEGATIVE_EXPERIENCE\",\"Reason\":\"need known failure\"}"}
got := gatewayAdvisoryResponse(request, response)
if got.IterativeRetrieval == nil || got.IterativeRetrieval.Lane != cognitive.IterativeRetrievalNegativeExperience || got.AdvisoryText != "" {
t.Fatalf("response=%+v", got)
}
invalid := response
invalid.AdvisoryText = "<domovoy-retrieval>{\"Query\":\"all memory\",\"Lane\":\"UNRESTRICTED\",\"Reason\":\"everything\"}"
if got := gatewayAdvisoryResponse(request, invalid); got.IterativeRetrieval != nil {
t.Fatalf("invalid intent accepted: %+v", got)
}
}
func TestStaleUnavailableBindingRevalidatesOnceBeforeInvocation(t *testing.T) {
selected := gatewaySelection()
selected.Binding.DataPolicy = capability.DataPolicyLocalOnly
registry := &revalidatingRegistry{selected: selected}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "ok", FinishReason: cognitive.FinishCompleted}}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{EndpointRef: "endpoint-1", CredentialRef: "credential-1", Provider: provider}}}, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return nil }}
response := httptest.NewRecorder()
gateway.advisoryHandler(response, httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if response.Code != http.StatusOK || !provider.called || revalidations != 1 || registry.calls != 2 {
t.Fatalf("status=%d provider_called=%t revalidations=%d resolves=%d", response.Code, provider.called, revalidations, registry.calls)
}
}
func TestRevalidationFailureDoesNotMakeBindingInvocable(t *testing.T) {
registry := &registryStub{err: errNoInvocableCognitiveBinding}
provider := &stubAdvisoryProvider{}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{Provider: provider}}}, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return errors.New("provider unavailable") }}
gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if provider.called || revalidations != 1 {
t.Fatalf("failed provider revalidation was treated as invocable: provider_called=%t revalidations=%d", provider.called, revalidations)
}
}
func TestRevalidationIsNotRetriedInALoop(t *testing.T) {
registry := &revalidatingRegistry{alwaysNo: true}
revalidations := 0
gateway := cognitiveGateway{resolver: registry, now: time.Now, revalidatePrimary: func(context.Context) error { revalidations++; return nil }}
gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
if revalidations != 1 || registry.calls != 2 {
t.Fatalf("revalidation loop: revalidations=%d resolves=%d", revalidations, registry.calls)
}
}
func TestHTTPResolverMarksNoInvocableBindingForSingleRevalidation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/cognitive-bindings/resolve" {
t.Fatalf("path=%s", r.URL.Path)
}
http.Error(w, "no invocable cognitive binding", http.StatusNotFound)
}))
defer server.Close()
resolver, err := newHTTPBindingResolver(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
_, err = resolver.Resolve(context.Background(), capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: "principal-1", MaxOutputBytes: 1})
if !errors.Is(err, errNoInvocableCognitiveBinding) {
t.Fatalf("resolve error=%v, want no-invocable sentinel", err)
}
}
func TestProviderUsesStableInstanceWhenCredentialMetadataIsStale(t *testing.T) {
provider := &stubAdvisoryProvider{}
gateway := cognitiveGateway{providers: map[capability.ProviderKind][]providerRegistration{
+6 -3
View File
@@ -113,11 +113,15 @@ func main() {
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})
}
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})}
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
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}), revalidatePrimary: func(ctx context.Context) error {
_, err := discovery.register(ctx, "medium")
return err
}}
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}
mediumReadinessTimeout := 120 * time.Second
mediumReadinessTimeout := 120 * time.Second
mediumReadinessClient := &http.Client{Timeout: mediumReadinessTimeout}
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
fleetClient := &http.Client{Timeout: 2 * time.Second}
@@ -199,7 +203,6 @@ func main() {
mux.HandleFunc("POST /v1/multimodal/ocr", multimodalGateway.handler(capability.MultimodalRecognizeOCR))
mux.HandleFunc("POST /v1/multimodal/video-understand", multimodalGateway.handler(capability.MultimodalUnderstandVideo))
mux.HandleFunc("POST /v1/multimodal/generate-image", multimodalGateway.handler(capability.MultimodalGenerateImage))
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)
+132 -21
View File
@@ -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")
}
}
}
@@ -0,0 +1,41 @@
package main
import (
"fmt"
"os"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
)
// candidateReleaseContextFromRuntime accepts only deployment-owned metadata.
// It never derives a candidate slot from an HTTP request or notification body.
func candidateReleaseContextFromRuntime() (*capability.CandidateReleaseContext, error) {
const prefix = "CANDIDATE_"
release := os.Getenv(prefix + "RELEASE_SET_ID")
if release == "" {
return nil, nil
}
context := &capability.CandidateReleaseContext{
ReleaseSetID: release,
Slot: os.Getenv(prefix + "SLOT"),
TaskID: os.Getenv(prefix + "TASK_ID"),
PrincipalID: identity.PrincipalID(os.Getenv(prefix + "PRINCIPAL_ID")),
CapabilityID: capability.CapabilityID(os.Getenv(prefix + "CAPABILITY_ID")),
ResourceScope: os.Getenv(prefix + "RESOURCE_SCOPE"),
Mode: capability.CandidateBindingMode(os.Getenv(prefix + "BINDING_MODE")),
TargetKind: os.Getenv(prefix + "TARGET_KIND"),
TargetRef: os.Getenv(prefix + "TARGET_REF"),
TargetDigest: os.Getenv(prefix + "TARGET_DIGEST"),
AuthorityGrantID: os.Getenv(prefix + "AUTHORITY_GRANT_ID"),
}
var revision int64
if _, err := fmt.Sscan(os.Getenv(prefix+"TASK_REVISION"), &revision); err != nil {
return nil, fmt.Errorf("candidate task revision: %w", err)
}
context.TaskRevision = revision
if context.Slot == "" || context.TaskID == "" || context.TaskRevision < 1 || context.PrincipalID == "" || context.CapabilityID == "" || context.ResourceScope == "" || context.Mode == "" || context.TargetKind == "" || context.TargetRef == "" || context.TargetDigest == "" || context.AuthorityGrantID == "" {
return nil, fmt.Errorf("incomplete candidate release metadata")
}
return context, nil
}
@@ -0,0 +1,25 @@
package main
import "testing"
func TestCandidateReleaseContextRequiresDeploymentMetadata(t *testing.T) {
t.Setenv("CANDIDATE_RELEASE_SET_ID", "assistant-slot-b")
if _, err := candidateReleaseContextFromRuntime(); err == nil {
t.Fatal("partial candidate metadata was accepted")
}
t.Setenv("CANDIDATE_SLOT", "b")
t.Setenv("CANDIDATE_TASK_ID", "GEN2-CAPABILITY-001")
t.Setenv("CANDIDATE_TASK_REVISION", "7")
t.Setenv("CANDIDATE_PRINCIPAL_ID", "domovoy-creator-principal")
t.Setenv("CANDIDATE_CAPABILITY_ID", "prospective-notification-delivery")
t.Setenv("CANDIDATE_RESOURCE_SCOPE", "synthetic-shadow-notification-v1")
t.Setenv("CANDIDATE_BINDING_MODE", "SHADOW")
t.Setenv("CANDIDATE_TARGET_KIND", "NOTIFICATION_SHADOW_RECEIVER")
t.Setenv("CANDIDATE_TARGET_REF", "notification-receiver-candidate")
t.Setenv("CANDIDATE_TARGET_DIGEST", "f9f9fa49a0727444c280c092d5bb340f3a8f33cf325c5396f5ae177a6666da35")
t.Setenv("CANDIDATE_AUTHORITY_GRANT_ID", "grant")
context, err := candidateReleaseContextFromRuntime()
if err != nil || context.ReleaseSetID != "assistant-slot-b" || context.TaskRevision != 7 {
t.Fatalf("context=%+v err=%v", context, err)
}
}
@@ -0,0 +1,170 @@
package main
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/identity"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
authoritydomain "git.cin.su/m/Assistant/modules/authority/domain"
"git.cin.su/m/Assistant/platform/ids"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
const (
delegatedControlPlaneActor identity.ActorID = "domovoy-control-plane-actor"
delegatedControlPlanePrincipal identity.PrincipalID = "domovoy-control-plane-principal"
delegatedControlPlaneTokenFile = "control_plane_service_token"
)
type delegatedControlPlaneAuthoritySource interface {
LoadDelegatedControlPlaneGrant(context.Context, authority.GrantID) (authority.DelegatedControlPlaneGrant, error)
RecordDelegatedControlPlaneAudit(context.Context, authority.DelegatedControlPlaneAudit) error
RevokeDelegatedControlPlaneGrant(context.Context, authority.GrantID, string) (bool, error)
}
type delegatedControlPlaneCore interface {
Session(context.Context, string) (identity.CreatorSession, error)
Binding(context.Context, identity.ActorID, identity.PrincipalID) (identity.PrincipalBinding, error)
}
type delegatedControlPlaneHandler struct {
authority delegatedControlPlaneAuthoritySource
core delegatedControlPlaneCore
credential func() (string, error)
now func() time.Time
newID func() (ids.ID, error)
}
func registerDelegatedControlPlaneRoutes(mux *http.ServeMux, handler delegatedControlPlaneHandler) {
mux.HandleFunc("POST /v1/internal/control-plane/authorize", handler.authorize)
mux.HandleFunc("POST /v1/internal/control-plane/completions", handler.complete)
mux.HandleFunc("POST /v1/creator/control-plane/grants/{grant}/revoke", handler.revoke)
}
func (h delegatedControlPlaneHandler) authorize(w http.ResponseWriter, r *http.Request) {
if !h.serviceAuthenticated(r) {
http.Error(w, "service principal denied", http.StatusUnauthorized)
return
}
var request authority.DelegatedControlPlaneRequest
decoder := json.NewDecoder(io.LimitReader(r.Body, 8<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid delegated control-plane request", http.StatusBadRequest)
return
}
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), request.GrantID)
if err != nil {
http.Error(w, "delegated authority denied", http.StatusForbidden)
return
}
binding, err := h.core.Binding(r.Context(), delegatedControlPlaneActor, delegatedControlPlanePrincipal)
if err != nil || authoritydomain.ValidateDelegatedControlPlaneGrant(h.now().UnixMilli(), binding, grant, request) != nil {
_ = h.audit(r.Context(), grant, request, "DENY", "FAILED")
http.Error(w, "delegated authority denied", http.StatusForbidden)
return
}
if err := h.audit(r.Context(), grant, request, "ALLOW", "PENDING"); err != nil {
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h delegatedControlPlaneHandler) complete(w http.ResponseWriter, r *http.Request) {
if !h.serviceAuthenticated(r) {
http.Error(w, "service principal denied", http.StatusUnauthorized)
return
}
var value authority.DelegatedControlPlaneCompletion
decoder := json.NewDecoder(io.LimitReader(r.Body, 8<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || (value.Outcome != "COMPLETED" && value.Outcome != "FAILED") {
http.Error(w, "invalid delegated completion", http.StatusBadRequest)
return
}
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), value.Request.GrantID)
if err != nil {
http.Error(w, "delegated authority unavailable", http.StatusServiceUnavailable)
return
}
if !authoritydomain.RequestMatchesDelegatedControlPlaneGrant(grant, value.Request) {
http.Error(w, "delegated completion denied", http.StatusForbidden)
return
}
if err := h.audit(r.Context(), grant, value.Request, "ALLOW", value.Outcome); err != nil {
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h delegatedControlPlaneHandler) revoke(w http.ResponseWriter, r *http.Request) {
session, err := h.core.Session(r.Context(), r.Header.Get("Authorization"))
if err != nil {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), authority.GrantID(r.PathValue("grant")))
if err != nil || !authoritydomain.CanIssuerRevokeDelegatedGrant(session.PrincipalID, grant) {
http.Error(w, "delegated grant revoke denied", http.StatusForbidden)
return
}
ok, err := h.authority.RevokeDelegatedControlPlaneGrant(r.Context(), grant.ID, string(session.PrincipalID))
if err != nil || !ok {
http.Error(w, "delegated grant revoke denied", http.StatusConflict)
return
}
request := authority.DelegatedControlPlaneRequest{GrantID: grant.ID, TaskID: grant.TaskID, TaskRevision: grant.TaskRevision, Operation: authority.DelegatedOperationRevokeControlPlaneGrant, Resource: grant.Resource}
if err := h.audit(r.Context(), grant, request, "REVOKE", "REVOKED"); err != nil {
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h delegatedControlPlaneHandler) serviceAuthenticated(r *http.Request) bool {
if h.credential == nil {
return false
}
expected, err := h.credential()
presented := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
return err == nil && expected != "" && presented != "" && subtle.ConstantTimeCompare([]byte(expected), []byte(presented)) == 1
}
func (h delegatedControlPlaneHandler) audit(ctx context.Context, grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest, decision, outcome string) error {
if h.newID == nil {
return errors.New("delegated audit id unavailable")
}
id, err := h.newID()
if err != nil {
return err
}
return h.authority.RecordDelegatedControlPlaneAudit(ctx, authority.DelegatedControlPlaneAudit{AuditID: id, GrantID: grant.ID, ServiceActorID: delegatedControlPlaneActor, ServicePrincipalID: delegatedControlPlanePrincipal, TaskID: request.TaskID, TaskRevision: request.TaskRevision, Operation: request.Operation, Resource: request.Resource, Decision: decision, Outcome: outcome, RecordedAtUnixMillis: h.now().UnixMilli()})
}
func delegatedControlPlaneCredential() (string, error) {
ref := os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF")
if ref == "" {
return "", errors.New("control-plane service credential ref is required")
}
root := os.Getenv("CONTROL_PLANE_SERVICE_SECRET_ROOT")
if root == "" {
root = "/run/control-plane-secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumer: secretcontracts.RuntimeConsumerCommit, Path: filepath.Join(root, delegatedControlPlaneTokenFile)})
}
@@ -0,0 +1,140 @@
package main
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type delegatedAuthorityFake struct {
grant authority.DelegatedControlPlaneGrant
audits []authority.DelegatedControlPlaneAudit
revoked bool
}
func TestCandidateBindingResolutionUsesServiceCredential(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/internal/capability-bindings/candidate/resolve" || r.Header.Get("Authorization") != "Bearer service-token" {
t.Fatalf("unexpected candidate resolution request: %s %q", r.URL.Path, r.Header.Get("Authorization"))
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"reason":"EXACT_CONTEXT"}`))
}))
defer server.Close()
client, err := newCreatorCoreClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
client.serviceCredential = func() (string, error) { return "service-token", nil }
if _, err := client.ResolveCandidateCapabilityBinding(context.Background(), capability.CandidateReleaseContext{}); err != nil {
t.Fatalf("resolve candidate binding: %v", err)
}
}
func TestDelegatedAuthorityCredentialRotationRejectsOldToken(t *testing.T) {
root := t.TempDir()
tokenPath := filepath.Join(root, "control_plane_service_token")
if err := os.WriteFile(tokenPath, []byte("old-token\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF", "credential://domovoy/control-plane/service-token/v1")
t.Setenv("CONTROL_PLANE_SERVICE_SECRET_ROOT", root)
h := delegatedControlPlaneHandler{credential: delegatedControlPlaneCredential}
oldRequest := httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", nil)
oldRequest.Header.Set("Authorization", "Bearer old-token")
if !h.serviceAuthenticated(oldRequest) {
t.Fatal("active credential was rejected before rotation")
}
if err := os.WriteFile(tokenPath, []byte("new-token\n"), 0o600); err != nil {
t.Fatal(err)
}
if h.serviceAuthenticated(oldRequest) {
t.Fatal("old credential remained valid after rotation")
}
newRequest := httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", nil)
newRequest.Header.Set("Authorization", "Bearer new-token")
if !h.serviceAuthenticated(newRequest) {
t.Fatal("new credential was rejected after rotation")
}
}
func (f *delegatedAuthorityFake) LoadDelegatedControlPlaneGrant(context.Context, authority.GrantID) (authority.DelegatedControlPlaneGrant, error) {
if f.grant.ID == "" {
return authority.DelegatedControlPlaneGrant{}, errors.New("missing")
}
return f.grant, nil
}
func (f *delegatedAuthorityFake) RecordDelegatedControlPlaneAudit(_ context.Context, audit authority.DelegatedControlPlaneAudit) error {
f.audits = append(f.audits, audit)
return nil
}
func (f *delegatedAuthorityFake) RevokeDelegatedControlPlaneGrant(context.Context, authority.GrantID, string) (bool, error) {
f.revoked = true
f.grant.Status = authority.DelegatedGrantRevoked
return true, nil
}
type delegatedCoreFake struct{}
func (delegatedCoreFake) Binding(context.Context, identity.ActorID, identity.PrincipalID) (identity.PrincipalBinding, error) {
return identity.PrincipalBinding{ActorID: delegatedControlPlaneActor, PrincipalID: delegatedControlPlanePrincipal, Active: true}, nil
}
func (delegatedCoreFake) Session(context.Context, string) (identity.CreatorSession, error) {
return identity.CreatorSession{PrincipalID: "owner"}, nil
}
func TestDelegatedAuthorityRejectsWrongResourceAndSurvivesRestart(t *testing.T) {
resource := authority.CognitiveBindingResource{BindingID: "binding", ProviderInstanceID: "provider", ModelDescriptorID: "model"}
fake := &delegatedAuthorityFake{grant: authority.DelegatedControlPlaneGrant{ID: "grant", ServiceActorID: delegatedControlPlaneActor, ServicePrincipalID: delegatedControlPlanePrincipal, IssuerPrincipalID: "owner", TaskID: "GEN2-COGNITIVE-003", TaskRevision: 4, Operations: []authority.DelegatedControlPlaneOperation{authority.DelegatedOperationCanaryCognitiveBinding}, Resource: resource, IssuedAtUnixMillis: 1, ExpiresAtUnixMillis: 100, ArchitectureRevision: "ADR-0021", Nonce: "nonce", Revision: 1, Status: authority.DelegatedGrantActive}}
h := delegatedControlPlaneHandler{authority: fake, core: delegatedCoreFake{}, credential: func() (string, error) { return "service-token", nil }, now: func() time.Time { return time.UnixMilli(10) }, newID: func() (ids.ID, error) { return "audit", nil }}
mux := http.NewServeMux()
registerDelegatedControlPlaneRoutes(mux, h)
valid := `{"GrantID":"grant","TaskID":"GEN2-COGNITIVE-003","TaskRevision":4,"Operation":"CANARY_COGNITIVE_BINDING","Resource":{"BindingID":"binding","ProviderInstanceID":"provider","ModelDescriptorID":"model"}}`
request := httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", strings.NewReader(valid))
request.Header.Set("Authorization", "Bearer service-token")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusNoContent {
t.Fatalf("valid delegation status=%d", response.Code)
}
wrong := strings.Replace(valid, `"binding"`, `"other"`, 1)
request = httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", strings.NewReader(wrong))
request.Header.Set("Authorization", "Bearer service-token")
response = httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("wrong resource status=%d", response.Code)
}
// A fresh handler reads the same durable grant source; no process-local allow survives restart.
request = httptest.NewRequest(http.MethodPost, "/v1/creator/control-plane/grants/grant/revoke", nil)
request.Header.Set("Authorization", "Bearer creator-session")
response = httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusNoContent || !fake.revoked {
t.Fatalf("revoke status=%d revoked=%v", response.Code, fake.revoked)
}
restarted := delegatedControlPlaneHandler{authority: fake, core: delegatedCoreFake{}, credential: h.credential, now: h.now, newID: h.newID}
mux = http.NewServeMux()
registerDelegatedControlPlaneRoutes(mux, restarted)
request = httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", strings.NewReader(valid))
request.Header.Set("Authorization", "Bearer service-token")
response = httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("revoked grant accepted after restart: %d", response.Code)
}
if len(fake.audits) < 3 {
t.Fatalf("audit trail missing: %#v", fake.audits)
}
}
+55 -2
View File
@@ -25,8 +25,9 @@ var (
)
type creatorCoreClient struct {
baseURL *url.URL
client *http.Client
baseURL *url.URL
client *http.Client
serviceCredential func() (string, error)
}
type creatorContinuityClient struct {
@@ -121,6 +122,58 @@ func (c creatorCoreClient) CapabilityBinding(ctx context.Context, id capability.
return binding, nil
}
func (c creatorCoreClient) ResolveCandidateCapabilityBinding(ctx context.Context, context capability.CandidateReleaseContext) (capability.CandidateBindingResolution, error) {
var resolved capability.CandidateBindingResolution
status, err := c.postServiceAuthenticated(ctx, "/v1/internal/capability-bindings/candidate/resolve", context, &resolved)
if err != nil || status != http.StatusOK {
return capability.CandidateBindingResolution{}, errCreatorQueryUnavailable
}
return resolved, nil
}
func (c creatorCoreClient) postServiceAuthenticated(ctx context.Context, path string, value any, destination any) (int, error) {
if c.serviceCredential == nil {
return 0, errCreatorQueryUnavailable
}
credential, err := c.serviceCredential()
if err != nil || credential == "" {
return 0, errCreatorQueryUnavailable
}
return c.postWithAuthorization(ctx, path, value, destination, "Bearer "+credential)
}
func (c creatorCoreClient) post(ctx context.Context, path string, value any, destination any) (int, error) {
return c.postWithAuthorization(ctx, path, value, destination, "")
}
func (c creatorCoreClient) postWithAuthorization(ctx context.Context, path string, value any, destination any, authorization string) (int, error) {
target := *c.baseURL
target.Path, target.RawQuery = path, ""
body, err := json.Marshal(value)
if err != nil {
return 0, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), strings.NewReader(string(body)))
if err != nil {
return 0, err
}
request.Header.Set("Content-Type", "application/json")
if authorization != "" {
request.Header.Set("Authorization", authorization)
}
response, err := c.client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusOK && destination != nil {
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<10)).Decode(destination); err != nil {
return 0, err
}
}
return response.StatusCode, nil
}
func (c creatorCoreClient) get(ctx context.Context, path, authorization string, destination any) (int, error) {
target := *c.baseURL
target.Path = path
+9 -1
View File
@@ -24,6 +24,7 @@ type executionRepository interface {
LoadLatestAttempt(context.Context, commitcontract.AuthorizedActionID) (commitcontract.ActionAttempt, error)
BeginAttempt(context.Context, commitcontract.ActionAttempt) error
FinishAttempt(context.Context, commitcontract.ActionAttempt) error
SaveExecutionReceipt(context.Context, commitcontract.ExecutionReceipt) error
}
type attemptRequest struct {
@@ -94,10 +95,17 @@ func executeAuthorized(ctx context.Context, repository executionRepository, exec
if err := repository.BeginAttempt(ctx, begun); err != nil {
return commitcontract.ActionAttempt{}, err
}
attempt, operationErr := commitdomain.Execute(action, attemptID, executor)
attempt, result, operationErr := commitdomain.ExecuteWithResult(action, attemptID, executor)
if err := repository.FinishAttempt(ctx, attempt); err != nil {
return commitcontract.ActionAttempt{}, err
}
if result.Receipt != nil {
receipt := *result.Receipt
receipt.ActionAttemptRef = attempt.ID
if err := repository.SaveExecutionReceipt(ctx, receipt); err != nil {
return commitcontract.ActionAttempt{}, err
}
}
return attempt, operationErr
}
+11 -1
View File
@@ -16,6 +16,12 @@ import (
type fakeExecutionRepository struct {
action commitcontract.AuthorizedAction
attempts []commitcontract.ActionAttempt
receipt commitcontract.ExecutionReceipt
}
func (f *fakeExecutionRepository) SaveExecutionReceipt(_ context.Context, receipt commitcontract.ExecutionReceipt) error {
f.receipt = receipt
return nil
}
func (f *fakeExecutionRepository) LoadAuthorized(context.Context, commitcontract.AuthorizedActionID) (commitcontract.AuthorizedAction, error) {
@@ -47,7 +53,8 @@ type knownExecutor struct{ calls int }
func (e *knownExecutor) Execute(commitcontract.AuthorizedAction) ports.ExecutionResult {
e.calls++
return ports.ExecutionResult{Known: true, Succeeded: true}
completed := int64(1)
return ports.ExecutionResult{Known: true, Succeeded: true, Receipt: &commitcontract.ExecutionReceipt{ID: "receipt-1", ExecutorImplementationRef: "test-executor", ExternalOperationRef: "SHADOW_CAPTURED", RequestDigest: "request", ExecutorResult: commitcontract.ExecutorReportedSuccess, ReconciliationKey: "receipt-1", DispatchAtUnixMillis: 1, CompletedAtUnixMillis: &completed}}
}
func (e *knownExecutor) Reconcile(commitcontract.AuthorizedAction) ports.ExecutionResult {
return ports.ExecutionResult{Known: true, Succeeded: true}
@@ -66,6 +73,9 @@ func TestExecutionIsDurablyBegunAndCannotBeBlindlyRepeated(t *testing.T) {
if result.Code != http.StatusOK || executor.calls != 1 || repository.attempts[0].State != commitcontract.ActionSucceeded {
t.Fatalf("status=%d calls=%d attempts=%#v", result.Code, executor.calls, repository.attempts)
}
if repository.receipt.ActionAttemptRef != repository.attempts[0].ID {
t.Fatalf("receipt attempt reference=%q want %q", repository.receipt.ActionAttemptRef, repository.attempts[0].ID)
}
repeated := httptest.NewRequest(http.MethodPost, "/v1/internal/authorized-actions/authorized-1/execute", strings.NewReader(`{"AttemptID":"attempt-2"}`))
repeated.Header.Set("Authorization", "Bearer secret")
+29 -5
View File
@@ -73,15 +73,18 @@ func main() {
if err != nil {
log.Fatal(err)
}
notificationSecret, err := readCommitSecretFile("NOTIFICATION_WRITE_TOKEN_FILE")
candidateContext, err := candidateReleaseContextFromRuntime()
if err != nil {
log.Fatal(err)
}
notificationSecretRef := os.Getenv("NOTIFICATION_WRITE_SECRET_REF")
if notificationSecretRef == "" {
log.Fatal("NOTIFICATION_WRITE_SECRET_REF is required")
notificationSecretRef, notificationSecret, err := notificationCredentialsFromRuntime(candidateContext != nil)
if err != nil {
log.Fatal(err)
}
dispatcher := operationDispatcher{continuityExecutor: continuityExecutor, continuityReconciler: continuityExecutor, bindings: creatorCore, secretRef: notificationSecretRef, secret: notificationSecret, client: creatorHTTPClient}
if candidateContext != nil {
creatorCore.serviceCredential = delegatedControlPlaneCredential
}
dispatcher := operationDispatcher{continuityExecutor: continuityExecutor, continuityReconciler: continuityExecutor, bindings: creatorCore, secretRef: notificationSecretRef, secret: notificationSecret, client: creatorHTTPClient, candidateContext: candidateContext}
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "commit-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
@@ -100,6 +103,9 @@ func main() {
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
executor: dispatcher, newID: ids.New, now: time.Now,
}, executorToken)
registerDelegatedControlPlaneRoutes(mux, delegatedControlPlaneHandler{
authority: authorityRepository, core: creatorCore, credential: delegatedControlPlaneCredential, now: time.Now, newID: ids.New,
})
mux.HandleFunc("POST /v1/authorized-actions", func(w http.ResponseWriter, r *http.Request) {
var request authorizationRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
@@ -159,6 +165,24 @@ func main() {
}
}
// notificationCredentialsFromRuntime keeps candidate shadow execution
// physically separate from the production notification credential mount.
func notificationCredentialsFromRuntime(candidate bool) (string, string, error) {
fileName, refName := "NOTIFICATION_WRITE_TOKEN_FILE", "NOTIFICATION_WRITE_SECRET_REF"
if candidate {
fileName, refName = "CANDIDATE_NOTIFICATION_WRITE_TOKEN_FILE", "CANDIDATE_NOTIFICATION_WRITE_SECRET_REF"
}
secret, err := readCommitSecretFile(fileName)
if err != nil {
return "", "", err
}
ref := os.Getenv(refName)
if ref == "" {
return "", "", fmt.Errorf("%s is required", refName)
}
return ref, secret, nil
}
func readCommitSecretFile(name string) (string, error) {
path := os.Getenv(name)
if path == "" {
@@ -14,6 +14,7 @@ import (
type notificationBindingSource interface {
ActiveCapabilityBinding(context.Context, capability.CapabilityID, identity.PrincipalID) (capability.Binding, error)
CapabilityBinding(context.Context, capability.CapabilityBindingID, int64) (capability.Binding, error)
ResolveCandidateCapabilityBinding(context.Context, capability.CandidateReleaseContext) (capability.CandidateBindingResolution, error)
}
type operationDispatcher struct {
@@ -23,6 +24,7 @@ type operationDispatcher struct {
secretRef string
secret string
client *http.Client
candidateContext *capability.CandidateReleaseContext
}
func (d operationDispatcher) Execute(action commitcontract.AuthorizedAction) ports.ExecutionResult {
@@ -30,6 +32,20 @@ func (d operationDispatcher) Execute(action commitcontract.AuthorizedAction) por
return d.continuityExecutor.Execute(action)
}
subject := action.Proposal.NotificationSubject
if d.candidateContext != nil {
resolved, err := d.bindings.ResolveCandidateCapabilityBinding(context.Background(), *d.candidateContext)
if err != nil || resolved.Mode != capability.CandidateBindingModeShadow || resolved.Binding.Context != *d.candidateContext {
return ports.ExecutionResult{Known: true}
}
if subject.BindingID != resolved.Binding.BaseActiveBindingID || subject.BindingRevision != resolved.Binding.BaseActiveBindingRevision {
return ports.ExecutionResult{Known: true}
}
executor, err := notificationexecutor.NewShadow(resolved.Binding, d.secret, d.client)
if err != nil {
return ports.ExecutionResult{Known: true}
}
return executor.Execute(action)
}
exact, err := d.bindings.CapabilityBinding(context.Background(), subject.BindingID, subject.BindingRevision)
if err != nil || exact.SecretRef != d.secretRef {
return ports.ExecutionResult{Known: true}
@@ -49,6 +65,11 @@ func (d operationDispatcher) Reconcile(action commitcontract.AuthorizedAction) p
if action.Proposal.Operation != commitcontract.OperationDeliverProspectiveNotification {
return d.continuityReconciler.Reconcile(action)
}
if d.candidateContext != nil {
// A candidate shadow delivery is never reconciled against the ACTIVE
// production target. Its typed receipt is the sole candidate evidence.
return ports.ExecutionResult{Known: true}
}
subject := action.Proposal.NotificationSubject
exact, err := d.bindings.CapabilityBinding(context.Background(), subject.BindingID, subject.BindingRevision)
if err != nil || exact.SecretRef != d.secretRef {
@@ -3,21 +3,43 @@ package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
"testing"
"git.cin.su/m/Assistant/adapters/notificationexecutor"
"git.cin.su/m/Assistant/contracts/capability"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/contracts/identity"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
)
func TestCandidateNotificationCredentialsDoNotFallbackToProductionCredential(t *testing.T) {
path := t.TempDir() + "/candidate-token"
if err := os.WriteFile(path, []byte("candidate-only-token"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CANDIDATE_NOTIFICATION_WRITE_TOKEN_FILE", path)
t.Setenv("CANDIDATE_NOTIFICATION_WRITE_SECRET_REF", "credential://domovoy/candidate/notification-write/v1")
ref, secret, err := notificationCredentialsFromRuntime(true)
if err != nil || ref == "" || secret != "candidate-only-token" {
t.Fatalf("candidate credential resolution failed: ref=%q err=%v", ref, err)
}
t.Setenv("CANDIDATE_NOTIFICATION_WRITE_TOKEN_FILE", "")
if _, _, err := notificationCredentialsFromRuntime(true); err == nil {
t.Fatal("candidate path fell back to production notification credential")
}
}
type fakeNotificationBindings struct {
exact capability.Binding
active capability.Binding
exactErr error
activeErr error
exact capability.Binding
active capability.Binding
exactErr error
activeErr error
candidate capability.CandidateBindingResolution
candidateErr error
}
func (f fakeNotificationBindings) CapabilityBinding(context.Context, capability.CapabilityBindingID, int64) (capability.Binding, error) {
@@ -26,6 +48,9 @@ func (f fakeNotificationBindings) CapabilityBinding(context.Context, capability.
func (f fakeNotificationBindings) ActiveCapabilityBinding(context.Context, capability.CapabilityID, identity.PrincipalID) (capability.Binding, error) {
return f.active, f.activeErr
}
func (f fakeNotificationBindings) ResolveCandidateCapabilityBinding(context.Context, capability.CandidateReleaseContext) (capability.CandidateBindingResolution, error) {
return f.candidate, f.candidateErr
}
func dispatcherAction(t *testing.T, binding capability.Binding) commitcontract.AuthorizedAction {
t.Helper()
@@ -81,3 +106,28 @@ func TestOperationDispatcherReconcilesRetiredHistoricalBinding(t *testing.T) {
t.Fatalf("reconcile=%+v", result)
}
}
func TestCandidateDispatcherUsesShadowBindingWithoutActiveFallback(t *testing.T) {
var action commitcontract.AuthorizedAction
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Candidate-Release") != "assistant-slot-b" || r.Header.Get("X-Candidate-Task") != "GEN2-CAPABILITY-001" {
t.Fatalf("candidate headers missing")
}
_ = json.NewEncoder(w).Encode(notificationexecutor.Receipt{ReceiptID: "shadow-1", ActionIntentID: action.Proposal.IntentID, RequestDigest: action.Proposal.NotificationSubject.RequestDigest, AcceptedAt: 1, DeliveryMode: "SHADOW"})
}))
defer server.Close()
stable := dispatcherBinding("https://stable.invalid")
action = dispatcherAction(t, stable)
context := capability.CandidateReleaseContext{ReleaseSetID: "assistant-slot-b", Slot: "b", TaskID: "GEN2-CAPABILITY-001", TaskRevision: 7, PrincipalID: stable.OwnerPrincipalID, CapabilityID: stable.CapabilityID, ResourceScope: "synthetic-shadow-notification-v1", Mode: capability.CandidateBindingModeShadow, TargetKind: "NOTIFICATION_SHADOW_RECEIVER", TargetRef: "notification-receiver-candidate", AuthorityGrantID: "grant"}
candidate := capability.CandidateBinding{ID: "candidate-1", Revision: 1, BaseActiveBindingID: stable.ID, BaseActiveBindingRevision: stable.Revision, Context: context, TargetOrigin: server.URL, DeliveryPath: "/delivery/v1/deliveries", ReceiptPath: "/delivery/v1/receipts", Protocol: capability.ProtocolIdempotentReceiptV1, CreatedAtUnixMillis: 1, ExpiresAtUnixMillis: 100, Status: capability.CandidateBindingActive}
digest, err := capabilitydomain.CandidateTargetDigest(candidate)
if err != nil {
t.Fatal(err)
}
context.TargetDigest, candidate.Context.TargetDigest = digest, digest
dispatcher := operationDispatcher{continuityExecutor: &knownExecutor{}, continuityReconciler: &knownExecutor{}, bindings: fakeNotificationBindings{activeErr: errors.New("stable fallback must not run"), candidate: capability.CandidateBindingResolution{Binding: candidate, Mode: capability.CandidateBindingModeShadow}}, secret: "shadow-secret", client: server.Client(), candidateContext: &context}
result := dispatcher.Execute(action)
if !result.Known || !result.Succeeded || result.Receipt == nil || result.Receipt.DeliveryMode != "SHADOW" {
t.Fatalf("candidate result=%+v", result)
}
}
+199 -21
View File
@@ -16,8 +16,10 @@ import (
"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/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
"git.cin.su/m/Assistant/platform/ids"
)
@@ -28,6 +30,145 @@ type cognitiveInterpretationClient struct {
media *http.Client
}
// compileInteractionContext is the Core-side handoff from the existing
// Interaction retrieval projection to the existing Context Compiler. The
// compiler receives only already-authorized, typed fragments and applies its
// scope and byte-budget guards before the cognitive gateway sees them.
func compileInteractionContext(projection interactiondomain.ContextProjection, focusProjectID string) (string, error) {
return compileInteractionContextWithExperience(projection, focusProjectID, nil, "")
}
func compileInteractionContextWithExperience(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) (string, error) {
if len(projection.Fragments) == 0 && len(experiences) == 0 {
return projection.Text, nil
}
candidates := interactionContextCandidates(projection, focusProjectID, experiences, query)
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: candidates, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: 2048, ContextScopeRef: focusProjectID, FreshnessRequirement: "conversation-current", RiskClass: "low"})
if err != nil {
return "", err
}
parts := make([]string, 0, len(bundle.Excerpts))
for _, excerpt := range bundle.Excerpts {
parts = append(parts, excerpt.Text)
}
return strings.Join(parts, "\n"), nil
}
func interactionContextCandidates(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) []cognitive.ContextCandidate {
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments)+len(experiences))
for _, fragment := range projection.Fragments {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
Relevance: fragment.Relevance,
Lane: "current-fact",
ContextScopeRef: fragment.ScopeRef,
FreshnessState: "CURRENT",
})
}
if needsHistoricalAnalogy(query) {
for _, fragment := range projection.Historical {
candidates = append(candidates, cognitive.ContextCandidate{
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:historical:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
Relevance: fragment.Relevance - 1, Lane: "historical-analogy", ContextScopeRef: fragment.ScopeRef, FreshnessState: "HISTORICAL_ONLY",
})
}
}
retrieved := epistemicsdomain.RetrieveExperience(experiences, "", focusProjectID, query, 4)
for _, record := range retrieved.Positive {
candidates = append(candidates, experienceContextCandidate(record, "positive-experience", false))
}
for _, record := range retrieved.Negative {
candidates = append(candidates, experienceContextCandidate(record, "negative-experience", true))
}
for _, record := range retrieved.Contradictions {
candidates = append(candidates, experienceContextCandidate(record, "contradiction", true))
}
return candidates
}
func needsHistoricalAnalogy(query string) bool {
for _, marker := range []string{"истор", "раньше", "предыдущ", "старое", "аналог"} {
if strings.Contains(strings.ToLower(query), marker) {
return true
}
}
return false
}
func experienceContextCandidate(record epistemics.ExperienceRecord, lane string, negative bool) cognitive.ContextCandidate {
text := strings.TrimSpace(record.ProblemStatement + ": " + record.OutcomeDetail)
if detail := strings.TrimSpace(record.WhySucceededOrFailed); detail != "" {
text += " (" + detail + ")"
}
if len(record.ApplicabilityConditions) > 0 {
text += " [conditions: " + strings.Join(record.ApplicabilityConditions, ", ") + "]"
}
return cognitive.ContextCandidate{Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("experience:" + string(record.ID)), SourceKind: "epistemics-experience", SourceID: "epistemics:experience-records", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: text}, Relevance: 1, Lane: lane, ContextScopeRef: record.ContextScopeRef, FreshnessState: "CURRENT", Negative: negative}
}
const (
iterativeRetrievalMaxRounds = 1
iterativeRetrievalMaxFragments = 2
iterativeRetrievalMaxAddedBytes = 1024
iterativeRetrievalMaxLatency = 1500 * time.Millisecond
)
func newIterativeContextRetriever(history []interaction.HumanUtterance, references []interaction.ProjectReference, principalID, focusProjectID string, experiences []epistemics.ExperienceRecord) iterativeContextRetriever {
seen := map[string]struct{}{}
return func(ctx context.Context, request cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse {
started := time.Now()
if request.Pass < 0 || request.Pass >= iterativeRetrievalMaxRounds || request.RemainingBytes <= 0 || request.RemainingBytes > iterativeRetrievalMaxAddedBytes {
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "budget"}
}
projection := interactiondomain.BuildScopedGen2ContextForQuery(history, references, principalID, focusProjectID, request.Query)
candidates := interactionContextCandidates(projection, focusProjectID, experiences, request.Query)
filtered := make([]cognitive.ContextCandidate, 0, len(candidates))
for _, candidate := range candidates {
if !iterativeLaneMatches(request.Lane, candidate.Lane) {
continue
}
key := string(request.Lane) + ":" + string(candidate.Excerpt.ID)
if _, duplicate := seen[key]; duplicate {
continue
}
filtered = append(filtered, candidate)
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: filtered, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: request.RemainingBytes, ContextScopeRef: focusProjectID, TaskRef: "chat", FocusFrameRef: focusProjectID, FreshnessRequirement: "conversation-current"})
if err != nil || len(bundle.Excerpts) == 0 {
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "no_additional_evidence"}
}
out := make([]cognitive.ContextCandidate, 0, iterativeRetrievalMaxFragments)
for _, excerpt := range bundle.Excerpts {
if len(out) == iterativeRetrievalMaxFragments || time.Since(started) > iterativeRetrievalMaxLatency {
break
}
seen[string(request.Lane)+":"+string(excerpt.ID)] = struct{}{}
for _, candidate := range filtered {
if candidate.Excerpt.ID == excerpt.ID {
out = append(out, candidate)
break
}
}
}
if len(out) == 0 {
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "latency_budget"}
}
addedBytes := 0
for _, candidate := range out {
addedBytes += len([]byte(candidate.Excerpt.Text))
}
return cognitive.IterativeRetrievalResponse{Candidates: out, Exhausted: true, Reason: "completed", AddedBytes: addedBytes}
}
}
func iterativeLaneMatches(request cognitive.IterativeRetrievalLane, actual string) bool {
return (request == cognitive.IterativeRetrievalCurrentFact && actual == "current-fact") ||
(request == cognitive.IterativeRetrievalPositiveExperience && actual == "positive-experience") ||
(request == cognitive.IterativeRetrievalNegativeExperience && actual == "negative-experience") ||
(request == cognitive.IterativeRetrievalContradiction && actual == "contradiction") ||
(request == cognitive.IterativeRetrievalHistoricalAnalogy && actual == "historical-analogy")
}
const (
// Household interpretation and response review are intentionally compact
// calls. Keeping their output bounded prevents a loaded local model from
@@ -95,43 +236,53 @@ func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, sessi
// non-current question only after evidence retrieval did not produce a usable
// result. The model decides whether its knowledge is stable enough; it must
// explicitly decline changing or source-dependent facts instead of guessing.
type iterativeContextRetriever func(context.Context, cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
return c.AnswerStableKnowledgeWithContinuation(ctx, session, userMessage, personalMemory, meaning, nil)
}
// AnswerStableKnowledgeWithContinuation permits one model-requested, Core-run
// retrieval round. The callback receives only a typed intent and cannot alter
// identity, scope or provider selection.
func (c cognitiveInterpretationClient) AnswerStableKnowledgeWithContinuation(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning, retrieve iterativeContextRetriever) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
if retrieve != nil {
prompt += "\n\nЕсли именно для ответа не хватает ограниченного контекста, вместо ответа верни единственную строку <domovoy-retrieval>{\"Query\":\"...\",\"SemanticSubject\":\"...\",\"Lane\":\"CURRENT_FACT|POSITIVE_EXPERIENCE|NEGATIVE_EXPERIENCE|CONTRADICTION|HISTORICAL_ANALOGY\",\"Reason\":\"...\"}. Не запрашивай широкий дамп памяти."
}
context := []cognitive.ContextExcerpt{}
if strings.TrimSpace(personalMemory) != "" {
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
}
target := *c.baseURL
target.Path = "/v1/advisories"
payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{
requestValue := cognitive.GatewayAdvisoryRequest{
Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: prompt, Context: context, Locale: "ru", MaxOutputBytes: stableKnowledgeMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
})
AllowIterativeRetrieval: retrieve != nil,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
}
result, err := c.requestAdvisory(ctx, requestValue)
if err != nil {
return "", err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
}
var result cognitive.GatewayAdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return "", err
if result.IterativeRetrieval != nil && retrieve != nil {
additional := retrieve(ctx, *result.IterativeRetrieval)
for _, candidate := range additional.Candidates {
context = append(context, candidate.Excerpt)
}
if len(additional.Candidates) == 0 {
context = append(context, cognitive.ContextExcerpt{ID: "continuation:no-additional-evidence", SourceKind: "iterative-retrieval", SourceID: "core", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "No additional evidence was found; answer with uncertainty rather than inventing facts."})
}
requestValue.Context = context
requestValue.AllowIterativeRetrieval = false
result, err = c.requestAdvisory(ctx, requestValue)
if err != nil {
return "", err
}
}
if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" {
return "", fmt.Errorf("cognitive stable knowledge incomplete: %s", result.FinishReason)
@@ -139,6 +290,33 @@ func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context
return strings.TrimSpace(result.AdvisoryText), nil
}
func (c cognitiveInterpretationClient) requestAdvisory(ctx context.Context, value cognitive.GatewayAdvisoryRequest) (cognitive.GatewayAdvisoryResponse, error) {
target := *c.baseURL
target.Path = "/v1/advisories"
payload, err := json.Marshal(value)
if err != nil {
return cognitive.GatewayAdvisoryResponse{}, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return cognitive.GatewayAdvisoryResponse{}, err
}
request.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(request)
if err != nil {
return cognitive.GatewayAdvisoryResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return cognitive.GatewayAdvisoryResponse{}, fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
}
var result cognitive.GatewayAdvisoryResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return cognitive.GatewayAdvisoryResponse{}, err
}
return result, nil
}
func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) {
base, err := url.Parse(rawURL)
if err != nil || base.Scheme == "" || base.Host == "" {
@@ -11,6 +11,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
@@ -70,6 +71,84 @@ func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
}
}
func TestCompileInteractionContextAppliesTypedScopeBeforeGateway(t *testing.T) {
context, err := compileInteractionContext(interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{
{UtteranceID: "a", Text: "контрольный код = ALPHA", ScopeRef: "company-a", Relevance: 2},
{UtteranceID: "b", Text: "контрольный код = BETA", ScopeRef: "company-b", Relevance: 3},
}}, "company-a")
if err != nil || context != "контрольный код = ALPHA" {
t.Fatalf("context=%q err=%v", context, err)
}
}
func TestCompileInteractionContextUsesHistoricalOnlyWhenRequested(t *testing.T) {
projection := interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{{UtteranceID: "current", Text: "код = NEW", Relevance: 2}}, Historical: []interactiondomain.ContextFragment{{UtteranceID: "old", Text: "код = OLD", Relevance: 1, Historical: true}}}
current, err := compileInteractionContextWithExperience(projection, "", nil, "Какой текущий код?")
if err != nil || strings.Contains(current, "OLD") {
t.Fatalf("current context=%q err=%v", current, err)
}
historical, err := compileInteractionContextWithExperience(projection, "", nil, "Какой старый код был раньше?")
if err != nil || !strings.Contains(historical, "OLD") {
t.Fatalf("historical context=%q err=%v", historical, err)
}
}
func TestStableKnowledgeContinuesOnceWithBoundedAdditionalContext(t *testing.T) {
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
if calls == 1 {
if !request.AllowIterativeRetrieval {
t.Fatal("initial advisory did not permit bounded continuation")
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted, IterativeRetrieval: &cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "initial context is insufficient", RemainingBytes: 512}})
return
}
if request.AllowIterativeRetrieval || len(request.Context) != 1 || request.Context[0].Text != "контрольный код = ORBIT-731" {
t.Fatalf("continuation request=%+v", request)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted, AdvisoryText: "Контрольный код — ORBIT-731."})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
answer, err := client.AnswerStableKnowledgeWithContinuation(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Какой контрольный код?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"}, func(context.Context, cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse {
return cognitive.IterativeRetrievalResponse{Candidates: []cognitive.ContextCandidate{{Excerpt: cognitive.ContextExcerpt{ID: "interaction:code", Text: "контрольный код = ORBIT-731", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact"}}, Exhausted: true, AddedBytes: 28}
})
if err != nil || answer != "Контрольный код — ORBIT-731." || calls != 2 {
t.Fatalf("answer=%q err=%v calls=%d", answer, err, calls)
}
}
func TestIterativeRetrieverPreservesScopeLanesAndBudgets(t *testing.T) {
history := []interaction.HumanUtterance{{ID: "a", PrincipalID: "p", Content: "контрольный код = ALPHA", ReceivedAtUnixMillis: 1}, {ID: "b", PrincipalID: "p", Content: "контрольный код = BETA", ReceivedAtUnixMillis: 2}}
references := []interaction.ProjectReference{{ID: "a-ref", UtteranceID: "a", ProjectID: "company-a"}, {ID: "b-ref", UtteranceID: "b", ProjectID: "company-b"}}
experiences := []epistemics.ExperienceRecord{{ID: "failure", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "catalog sync", ActualOutcome: epistemics.OutcomeFailed, OutcomeDetail: "old connector failed", RecordedAtUnixMillis: 1}, {ID: "conflict", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "catalog sync", ActualOutcome: epistemics.OutcomeConflict, OutcomeDetail: "conflicting result", RecordedAtUnixMillis: 2}}
retrieve := newIterativeContextRetriever(history, references, "p", "company-a", experiences)
current := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "need scoped fact", RemainingBytes: 512})
if len(current.Candidates) != 1 || !strings.Contains(current.Candidates[0].Excerpt.Text, "ALPHA") || strings.Contains(current.Candidates[0].Excerpt.Text, "BETA") {
t.Fatalf("scope leaked: %#v", current)
}
if duplicate := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "repeat", RemainingBytes: 512}); duplicate.Reason != "no_additional_evidence" {
t.Fatalf("duplicate was re-added: %#v", duplicate)
}
if negative := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "catalog sync", Lane: cognitive.IterativeRetrievalNegativeExperience, Reason: "need failure", RemainingBytes: 512}); len(negative.Candidates) < 1 || !strings.Contains(strings.Join([]string{negative.Candidates[0].Excerpt.Text, negative.Candidates[len(negative.Candidates)-1].Excerpt.Text}, " "), "old connector failed") {
t.Fatalf("negative lane=%#v", negative)
}
if contradiction := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "catalog sync", Lane: cognitive.IterativeRetrievalContradiction, Reason: "need conflict", RemainingBytes: 512}); len(contradiction.Candidates) != 1 || !strings.Contains(contradiction.Candidates[0].Excerpt.Text, "conflicting result") {
t.Fatalf("contradiction lane=%#v", contradiction)
}
if budget := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "anything", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "bad", RemainingBytes: 2048}); budget.Reason != "budget" {
t.Fatalf("budget=%#v", budget)
}
}
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
@@ -0,0 +1,76 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/authority"
)
var ErrControlPlaneUnavailable = errors.New("delegated control-plane authority unavailable")
type controlPlaneAuthority struct {
baseURL string
client *http.Client
credential func() (string, error)
}
func (a controlPlaneAuthority) authorize(ctx context.Context, request authority.DelegatedControlPlaneRequest) error {
return a.post(ctx, "/v1/internal/control-plane/authorize", request)
}
func (a controlPlaneAuthority) completed(ctx context.Context, request authority.DelegatedControlPlaneRequest, success bool) {
outcome := "FAILED"
if success {
outcome = "COMPLETED"
}
_ = a.post(ctx, "/v1/internal/control-plane/completions", authority.DelegatedControlPlaneCompletion{Request: request, Outcome: outcome})
}
func (a controlPlaneAuthority) revoke(ctx context.Context, creatorAuthorization string, grantID authority.GrantID) error {
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(a.baseURL, "/")+"/v1/creator/control-plane/grants/"+string(grantID)+"/revoke", nil)
if err != nil {
return err
}
request.Header.Set("Authorization", creatorAuthorization)
response, err := a.client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusNoContent {
return fmt.Errorf("delegated grant revoke status %d", response.StatusCode)
}
return nil
}
func (a controlPlaneAuthority) post(ctx context.Context, path string, value any) error {
credential, err := a.credential()
if err != nil || credential == "" {
return ErrControlPlaneUnavailable
}
body, err := json.Marshal(value)
if err != nil {
return err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(a.baseURL, "/")+path, bytes.NewReader(body))
if err != nil {
return err
}
request.Header.Set("Authorization", "Bearer "+credential)
request.Header.Set("Content-Type", "application/json")
response, err := a.client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("delegated control-plane status %d", response.StatusCode)
}
return nil
}
@@ -0,0 +1,62 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"git.cin.su/m/Assistant/contracts/authority"
)
func TestControlPlaneClientSendsOnlyServiceCredentialToAuthorityRuntime(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/internal/control-plane/authorize" || r.Header.Get("Authorization") != "Bearer service-token" {
t.Fatalf("unexpected delegated request: %s %q", r.URL.Path, r.Header.Get("Authorization"))
}
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
client := controlPlaneAuthority{baseURL: server.URL, client: server.Client(), credential: func() (string, error) { return "service-token", nil }}
request := authority.DelegatedControlPlaneRequest{GrantID: "grant", TaskID: "task", TaskRevision: 1, Operation: authority.DelegatedOperationCanaryCognitiveBinding, Resource: authority.CognitiveBindingResource{BindingID: "binding", ProviderInstanceID: "provider", ModelDescriptorID: "model"}}
if err := client.authorize(context.Background(), request); err != nil {
t.Fatalf("authorize: %v", err)
}
}
func TestControlPlaneCredentialUsesDedicatedSecretRoot(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, controlPlaneServiceTokenFile), []byte("test-machine-token\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF", "credential://domovoy/control-plane/service-token/v1")
t.Setenv("CONTROL_PLANE_SERVICE_SECRET_ROOT", root)
if got, err := readControlPlaneServiceCredential(); err != nil || got != "test-machine-token" {
t.Fatalf("control-plane credential resolution failed: %v", err)
}
}
func TestCandidateControlPlaneAuthenticationRejectsMissingOrWrongCredential(t *testing.T) {
credential := func() (string, error) { return "service-token", nil }
for _, authorization := range []string{"", "Bearer other-token", "Basic service-token"} {
request := httptest.NewRequest(http.MethodPost, "/v1/internal/capability-bindings/candidate/resolve", nil)
request.Header.Set("Authorization", authorization)
if candidateControlPlaneAuthenticated(request, credential) {
t.Fatalf("authorization %q was accepted", authorization)
}
}
request := httptest.NewRequest(http.MethodPost, "/v1/internal/capability-bindings/candidate/resolve", nil)
request.Header.Set("Authorization", "Bearer service-token")
if !candidateControlPlaneAuthenticated(request, credential) {
t.Fatal("expected service credential to be accepted")
}
}
func TestCandidateReleaseContextRequiresCompleteRuntimeMetadata(t *testing.T) {
t.Setenv("CANDIDATE_RELEASE_SET_ID", "assistant-slot-b")
if _, err := candidateReleaseContextFromRuntime(); err == nil {
t.Fatal("partial candidate runtime metadata was accepted")
}
}
+103 -1
View File
@@ -11,8 +11,11 @@ import (
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
selfcontract "git.cin.su/m/Assistant/contracts/self"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
selfdomain "git.cin.su/m/Assistant/modules/self/domain"
"git.cin.su/m/Assistant/platform/ids"
"git.cin.su/m/Assistant/platform/telemetry"
)
@@ -29,15 +32,55 @@ type learningCapabilityStore interface {
SaveSkillProfile(context.Context, capability.SkillProfile) error
}
type learningSelfStore interface {
SaveLearningSignal(context.Context, selfcontract.LearningSignal) error
SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error
SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error
}
type learningRecorder struct {
epistemics learningEpistemicsStore
capability capabilityLearningStore
self learningSelfStore
}
type capabilityLearningStore interface {
learningCapabilityStore
}
func (r learningRecorder) recordSelfSignal(ctx context.Context, signal selfcontract.LearningSignal) {
if r.self == nil {
return
}
if err := signal.Validate(); err != nil {
log.Printf("learning SELF signal invalid: %v", err)
return
}
if err := r.self.SaveLearningSignal(ctx, signal); err != nil {
log.Printf("learning SELF signal unavailable: %v", err)
return
}
diagnosis := selfdomain.AttributeFailure(signal)
need, err := selfdomain.BuildImprovementNeed(signal, diagnosis, string(signal.Kind))
if err != nil {
log.Printf("learning SELF improvement need invalid: %v", err)
return
}
if err := r.self.SaveImprovementNeed(ctx, need); err != nil {
log.Printf("learning SELF improvement need unavailable: %v", err)
return
}
needRef := selfcontract.SemanticReference{OwnerNamespace: "self", SemanticKind: "improvement_need", StableID: ids.StableID(need.ID)}
ledger := selfcontract.LearningLedgerEntry{ID: selfcontract.LearningLedgerEntryID("ledger:observed:" + signal.ID), PrincipalID: signal.PrincipalID, ImprovementNeedRef: &needRef, ObservedProblem: need.ProblemSummary, EvidenceRefs: signal.EvidenceRefs, Diagnosis: string(diagnosis.Class), Hypothesis: diagnosis.Rationale, ProposedChangeRef: signal.SubjectRef, ExpectedImprovement: need.ExpectedBenefit, Decision: "OBSERVED_REQUIRES_GOVERNED_REVIEW", RecordedAt: signal.ObservedAt}
if err := r.self.SaveLearningLedger(ctx, ledger); err != nil {
log.Printf("learning SELF ledger unavailable: %v", err)
}
}
func selfEvidence(owner, kind, stable string) selfcontract.SemanticReference {
return selfcontract.SemanticReference{OwnerNamespace: owner, SemanticKind: kind, StableID: ids.StableID(stable)}
}
type reminderResponseLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
@@ -113,6 +156,7 @@ func (r learningRecorder) recordReminderResponse(ctx context.Context, input remi
actual, confidence := outcomeForHumanResponse(input.Result.Response)
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceContinuity,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs(
@@ -143,6 +187,7 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceInteraction,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodClarification,
@@ -155,6 +200,38 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:correction:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalUserCorrection,
TaskPattern: "interaction-correction", ScopeKey: "principal:" + string(input.Utterance.PrincipalID),
SubjectRef: selfEvidence("interaction", "intent", string(input.Intent.ID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))}, ObservedAt: input.At.UnixMilli(), Severity: 2, UserImpact: 2,
})
}
func (r learningRecorder) createDelegationCandidateSkill(ctx context.Context, input delegationLearningInput, experience epistemics.ExperienceRecord) {
if r.capability == nil || experience.ID == "" || len(experience.VerificationEvidence) == 0 {
return
}
// One verified result creates only a candidate. Repeated outcome evidence and
// independent validation are required by Capability before activation.
profile := capability.SkillProfile{
ID: capability.SkillProfileID("skill:delegation:" + string(input.ImplementationID)),
Revision: 1,
Name: "Verified bounded delegation strategy",
Description: "Observed verified delegation outcome; requires repeatability and independent validation.",
ApplicableContext: []string{"work:delegation"},
ImplementationRef: "work:agent-delegation-v1",
ImplementationVersion: "1",
Status: capability.SkillCandidate,
ConfidenceMillis: 500,
RecordedAtUnixMillis: experience.RecordedAtUnixMillis,
}
next, err := capabilitydomain.CandidateFromVerifiedExperience(profile, capability.SkillEvidenceRef("epistemics:experience:"+string(experience.ID)), true, experience.RecordedAtUnixMillis)
if err != nil {
return
}
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
log.Printf("learning candidate skill unavailable: %v", err)
}
}
func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) {
@@ -175,10 +252,16 @@ func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastP
if input.Result.Response.ID != "" {
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
}
experience.PrincipalID = string(input.Utterance.PrincipalID)
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
}
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:fast-path:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalSkillFallback,
TaskPattern: "fast-path", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "skill", string(reminderResponseSkillID)),
EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))}, ActualOutcome: string(input.Reason), ObservedAt: input.At.UnixMilli(), Severity: 1,
})
}
func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabilityLearningInput) {
@@ -196,6 +279,7 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodCapabilityExecution,
@@ -211,6 +295,13 @@ func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabil
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
if input.Outcome == capability.UsageFailed || input.Outcome == capability.UsageUnknown {
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:capability:" + string(input.Utterance.ID), PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalVerifiedFailure,
TaskPattern: "capability-use", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "implementation", string(input.ImplementationID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", string(input.Utterance.ID))},
ActualOutcome: string(input.Outcome), ObservedAt: input.At.UnixMilli(), Severity: 2,
})
}
}
func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input delegationLearningInput) {
@@ -232,6 +323,7 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
PrincipalID: string(input.Utterance.PrincipalID),
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs("work:delegation:"+input.DelegationID, "work:run:"+input.RunID),
@@ -246,7 +338,17 @@ func (r learningRecorder) recordDelegationOutcome(ctx context.Context, input del
ConfidenceMillis: confidence,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok && input.Verified {
r.createDelegationCandidateSkill(ctx, input, saved)
}
if !input.Verified {
r.recordSelfSignal(ctx, selfcontract.LearningSignal{
ID: "signal:delegation:" + input.DelegationID, PrincipalID: string(input.Utterance.PrincipalID), Kind: selfcontract.SignalVerifiedFailure,
TaskPattern: "delegation", ScopeKey: "principal:" + string(input.Utterance.PrincipalID), SubjectRef: selfEvidence("capability", "implementation", string(input.ImplementationID)), EvidenceRefs: []selfcontract.SemanticReference{selfEvidence("epistemics", "experience", input.OutcomeRef)},
ActualOutcome: string(actual), ObservedAt: input.At.UnixMilli(), Severity: 2,
})
}
}
func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
+76 -1
View File
@@ -2,12 +2,14 @@ package main
import (
"context"
"errors"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
selfcontract "git.cin.su/m/Assistant/contracts/self"
)
type learningEpistemicsStub struct {
@@ -28,17 +30,60 @@ func (s *learningEpistemicsStub) SaveReflectionCandidate(context.Context, episte
type learningCapabilityStub struct {
loadCalls int
saveCalls int
saved []capability.SkillProfile
}
type learningSelfStub struct {
signals int
needs int
ledgers int
}
type failingLearningSelfStub struct{}
func (failingLearningSelfStub) SaveLearningSignal(context.Context, selfcontract.LearningSignal) error {
return errors.New("self repository unavailable")
}
func (failingLearningSelfStub) SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error {
return errors.New("self repository unavailable")
}
func (failingLearningSelfStub) SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error {
return errors.New("self repository unavailable")
}
func (s *learningSelfStub) SaveLearningSignal(context.Context, selfcontract.LearningSignal) error {
s.signals++
return nil
}
func (s *learningSelfStub) SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error {
s.needs++
return nil
}
func (s *learningSelfStub) SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error {
s.ledgers++
return nil
}
func (s *learningCapabilityStub) LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error) {
s.loadCalls++
return capability.SkillProfile{}, nil
}
func (s *learningCapabilityStub) SaveSkillProfile(context.Context, capability.SkillProfile) error {
func (s *learningCapabilityStub) SaveSkillProfile(_ context.Context, value capability.SkillProfile) error {
s.saveCalls++
s.saved = append(s.saved, value)
return nil
}
func TestVerifiedDelegationCreatesCandidateNotActiveSkill(t *testing.T) {
e := &learningEpistemicsStub{}
c := &learningCapabilityStub{}
r := learningRecorder{epistemics: e, capability: c}
r.recordDelegationOutcome(context.Background(), delegationLearningInput{Intent: interaction.OriginalHumanIntent{ID: "i"}, Utterance: interaction.HumanUtterance{ID: "u"}, DelegationID: "d", RunID: "r", ImplementationID: "impl", OutcomeRef: "outcome", VerificationRefs: []epistemics.EvidenceRef{"verified"}, Verified: true, At: time.UnixMilli(1000)})
if len(c.saved) != 1 || c.saved[0].Status != capability.SkillCandidate || c.saved[0].VerifiedOutcomeCount != 1 {
t.Fatalf("candidate=%#v", c.saved)
}
}
func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
epistemicsStore := &learningEpistemicsStub{}
capabilityStore := &learningCapabilityStub{}
@@ -55,3 +100,33 @@ func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
t.Fatalf("generic correction must remain evidence without a reminder skill reference: %#v", epistemicsStore.experiences)
}
}
func TestCorrectionWritesBoundedSelfEvidenceWithoutChangingResponsePath(t *testing.T) {
epistemicsStore := &learningEpistemicsStub{}
selfStore := &learningSelfStub{}
recorder := learningRecorder{epistemics: epistemicsStore, self: selfStore}
recorder.recordCorrection(context.Background(), correctionLearningInput{
Intent: interaction.OriginalHumanIntent{ID: "intent-2"},
Utterance: interaction.HumanUtterance{ID: "utterance-2", PrincipalID: "principal-2"},
At: time.UnixMilli(2000),
})
if selfStore.signals != 1 || selfStore.needs != 1 || selfStore.ledgers != 1 {
t.Fatalf("self evidence counts: signals=%d needs=%d ledgers=%d", selfStore.signals, selfStore.needs, selfStore.ledgers)
}
if len(epistemicsStore.experiences) != 1 {
t.Fatalf("completed learning path was changed")
}
}
func TestSelfRepositoryFailureDoesNotDiscardUserFacingLearningPath(t *testing.T) {
epistemicsStore := &learningEpistemicsStub{}
recorder := learningRecorder{epistemics: epistemicsStore, self: failingLearningSelfStub{}}
recorder.recordCorrection(context.Background(), correctionLearningInput{
Intent: interaction.OriginalHumanIntent{ID: "intent-failure"},
Utterance: interaction.HumanUtterance{ID: "utterance-failure", PrincipalID: "principal-failure"},
At: time.UnixMilli(3000),
})
if len(epistemicsStore.experiences) != 1 {
t.Fatalf("SELF repository failure must not discard the completed epistemics response path")
}
}
+388 -23
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
@@ -31,6 +32,7 @@ import (
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
@@ -39,13 +41,70 @@ import (
)
const (
creatorPasswordPath = "/run/secrets/creator_password"
creatorBootstrapTokenPath = "/run/secrets/creator_bootstrap_token"
creatorSessionKeyPath = "/run/secrets/creator_session_signing_key"
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
creatorPasswordPath = "/run/secrets/creator_password"
creatorBootstrapTokenPath = "/run/secrets/creator_bootstrap_token"
creatorSessionKeyPath = "/run/secrets/creator_session_signing_key"
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"
)
func candidateAuthorityResource(context capability.CandidateReleaseContext, origin string) authority.CandidateCapabilityBindingResource {
digest := context.TargetDigest
if origin != "" {
calculated, err := capabilitydomain.CandidateTargetDigest(capability.CandidateBinding{Context: context, TargetOrigin: origin, Protocol: capability.ProtocolIdempotentReceiptV1})
if err != nil {
digest = ""
} else {
digest = calculated
}
}
return authority.CandidateCapabilityBindingResource{ReleaseSetID: context.ReleaseSetID, Slot: context.Slot, CapabilityID: string(context.CapabilityID), ResourceScope: context.ResourceScope, Mode: string(context.Mode), TargetKind: context.TargetKind, TargetRef: context.TargetRef, TargetDigest: digest}
}
// candidateReleaseContextFromRuntime accepts only deployment-owned metadata.
// Candidate HTTP callers cannot choose a release identity for this runtime.
func candidateReleaseContextFromRuntime() (*capability.CandidateReleaseContext, error) {
const prefix = "CANDIDATE_"
release := os.Getenv(prefix + "RELEASE_SET_ID")
if release == "" {
return nil, nil
}
context := &capability.CandidateReleaseContext{
ReleaseSetID: release,
Slot: os.Getenv(prefix + "SLOT"),
TaskID: os.Getenv(prefix + "TASK_ID"),
PrincipalID: identity.PrincipalID(os.Getenv(prefix + "PRINCIPAL_ID")),
CapabilityID: capability.CapabilityID(os.Getenv(prefix + "CAPABILITY_ID")),
ResourceScope: os.Getenv(prefix + "RESOURCE_SCOPE"),
Mode: capability.CandidateBindingMode(os.Getenv(prefix + "BINDING_MODE")),
TargetKind: os.Getenv(prefix + "TARGET_KIND"),
TargetRef: os.Getenv(prefix + "TARGET_REF"),
TargetDigest: os.Getenv(prefix + "TARGET_DIGEST"),
AuthorityGrantID: os.Getenv(prefix + "AUTHORITY_GRANT_ID"),
}
revision, err := strconv.ParseInt(os.Getenv(prefix+"TASK_REVISION"), 10, 64)
if err != nil {
return nil, fmt.Errorf("candidate task revision: %w", err)
}
context.TaskRevision = revision
if context.Slot == "" || context.TaskID == "" || context.TaskRevision < 1 || context.PrincipalID == "" || context.CapabilityID == "" || context.ResourceScope == "" || context.Mode == "" || context.TargetKind == "" || context.TargetRef == "" || context.TargetDigest == "" || context.AuthorityGrantID == "" {
return nil, errors.New("incomplete candidate release metadata")
}
return context, nil
}
func candidateControlPlaneAuthenticated(r *http.Request, credential func() (string, error)) bool {
if credential == nil {
return false
}
expected, err := credential()
presented := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
return err == nil && expected != "" && presented != "" && subtle.ConstantTimeCompare([]byte(expected), []byte(presented)) == 1
}
const maxCreatorCredentialsBytes = 4 << 10
func main() {
@@ -104,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)
@@ -117,6 +188,37 @@ func main() {
log.Fatal(err)
}
defer interactionRepository.Close()
var selfRepository *postgres.SelfRepository
selfUser := os.Getenv("SELF_POSTGRES_USER")
selfPassword := ""
if selfUser != "" && os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") != "" {
selfPassword, err = readCredentialRefSecret("SELF_POSTGRES_PASSWORD", "SELF_POSTGRES_CREDENTIAL_REF", selfDBPasswordPath)
if err != nil {
log.Printf("SELF learning repository unavailable; user responses remain unaffected: %v", err)
}
}
if selfUser != "" && selfPassword != "" {
selfRepository, err = postgres.OpenSelf(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: selfUser, Password: selfPassword})
if err != nil {
log.Printf("SELF learning repository unavailable; user responses remain unaffected: %v", err)
} else {
defer selfRepository.Close()
}
} else if selfUser == "" || os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") == "" {
log.Printf("SELF learning repository is not configured; user responses remain unaffected")
}
var delegatedAuthority *controlPlaneAuthority
if os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF") != "" {
delegatedAuthority = &controlPlaneAuthority{baseURL: envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"), client: &http.Client{Timeout: 5 * time.Second}, credential: func() (string, error) {
return readControlPlaneServiceCredential()
}}
} else {
log.Printf("delegated control-plane authority is not configured; existing human control remains available")
}
candidateContext, err := candidateReleaseContextFromRuntime()
if err != nil {
log.Fatal(err)
}
reminderClient, err := newContinuityReminderClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
@@ -137,16 +239,36 @@ func main() {
if err != nil {
log.Fatal(err)
}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository, self: selfRepository}
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)
@@ -197,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 {
@@ -507,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()
@@ -571,9 +731,10 @@ func main() {
travelContinuation := false
var previousTask *interaction.HumanUtterance
var conversationHistory []interaction.HumanUtterance
var contextProjectReferences []interaction.ProjectReference
if request.ConversationID != "" {
var historyErr error
conversationHistory, historyErr = interactionRepository.Utterances(r.Context(), conversation.ID)
conversationHistory, contextProjectReferences, historyErr = interactionRepository.UtterancesWithProjectReferences(r.Context(), conversation.ID)
if historyErr != nil {
log.Printf("interaction conversation history unavailable: %v", historyErr)
}
@@ -581,14 +742,29 @@ func main() {
// Keep durable user context available even when the web client carries a
// conversation ID; a new or rotated conversation must not erase learned
// preferences.
recentUserHistory, historyErr := interactionRepository.RecentUtterancesForPrincipal(r.Context(), session.PrincipalID, 64)
recentUserHistory, recentProjectReferences, historyErr := interactionRepository.RecentUtterancesForPrincipalWithProjectReferences(r.Context(), session.PrincipalID, 64)
if historyErr != nil {
log.Printf("interaction principal history unavailable: %v", historyErr)
} else {
log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory))
}
conversationHistory = append(conversationHistory, recentUserHistory...)
gen2Context := interactiondomain.BuildScopedGen2Context(conversationHistory, string(session.PrincipalID))
contextProjectReferences = append(contextProjectReferences, recentProjectReferences...)
focusProjectID := ""
if previousFocus != nil {
focusProjectID = string(previousFocus.ProjectID)
}
gen2Context := interactiondomain.BuildScopedGen2ContextForQuery(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, effectiveTaskMessage)
experienceRecords, experienceErr := epistemicsRepository.RecentExperienceRecordsForPrincipalAndScope(r.Context(), string(session.PrincipalID), focusProjectID, 16)
if experienceErr != nil {
log.Printf("epistemics experience context unavailable: %v", experienceErr)
}
if compiledContext, contextErr := compileInteractionContextWithExperience(gen2Context, focusProjectID, experienceRecords, effectiveTaskMessage); contextErr != nil {
log.Printf("interaction context compilation unavailable: %v", contextErr)
} else {
gen2Context.Text = compiledContext
}
iterativeRetriever := newIterativeContextRetriever(conversationHistory, contextProjectReferences, string(session.PrincipalID), focusProjectID, experienceRecords)
if gen2Context.UsedPersonalMemoryFallback {
telemetry.RecordCompatibility("personal-memory-fallback", "core-runtime", "read", "gen2-context-empty", true, true, false, "USED", utterance.CorrelationID, string(session.PrincipalID), now)
}
@@ -887,7 +1063,7 @@ func main() {
planningReply = retrievedMemoryReply
needsCognition = false
}
if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
if hasChatRoute(routePlan, chatRouteResearch) && len(gen2Context.Fragments) == 0 && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
researchAttempted = true
if planID, idErr := ids.New(); idErr == nil {
parts := interactiondomain.IndependentRequestParts(effectiveTaskMessage)
@@ -916,9 +1092,9 @@ func main() {
reply, backed := renderResearchReply(searchQuery, result, researchErr)
if !backed && cognitiveAvailable {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, part, gen2Context.Text, cognitive.TaskMeaning{
answer, answerErr := cognitiveClient.AnswerStableKnowledgeWithContinuation(answerCtx, session, part, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
})
}, iterativeRetriever)
cancelAnswer()
if answerErr == nil && stableKnowledgeAnswer(answer) {
reply, backed = answer, true
@@ -954,11 +1130,11 @@ func main() {
}
}
canGenerateTextReply := shouldGenerateNonMutatingTextDirective(semantic, routePlan, helpPath, creativeRequest) && planningReply == deterministicReply
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && (!hasChatRoute(routePlan, chatRouteResearch) || len(gen2Context.Fragments) > 0) && !creativeRequest {
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, gen2Context.Text, cognitive.TaskMeaning{
answer, answerErr := cognitiveClient.AnswerStableKnowledgeWithContinuation(answerCtx, session, effectiveTaskMessage, gen2Context.Text, cognitive.TaskMeaning{
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
})
}, iterativeRetriever)
cancelAnswer()
if answerErr == nil && stableKnowledgeAnswer(answer) {
planningReply = answer
@@ -1453,6 +1629,61 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(binding)
})
mux.HandleFunc("POST /v1/internal/capability-bindings/candidate", func(w http.ResponseWriter, r *http.Request) {
if delegatedAuthority == nil || candidateContext == nil {
http.Error(w, "delegated authority unavailable", http.StatusServiceUnavailable)
return
}
if !candidateControlPlaneAuthenticated(r, delegatedAuthority.credential) {
http.Error(w, "service principal denied", http.StatusUnauthorized)
return
}
var binding capability.CandidateBinding
decoder := json.NewDecoder(io.LimitReader(r.Body, 16*1024+1))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&binding); err != nil || binding.Context != *candidateContext || capabilitydomain.ValidateCandidateBinding(binding) != nil {
http.Error(w, "invalid candidate binding", http.StatusBadRequest)
return
}
resource := candidateAuthorityResource(binding.Context, binding.TargetOrigin)
request := authority.DelegatedControlPlaneRequest{GrantID: authority.GrantID(binding.Context.AuthorityGrantID), TaskID: binding.Context.TaskID, TaskRevision: binding.Context.TaskRevision, Operation: authority.DelegatedOperationCreateCandidateCapabilityBinding, CandidateResource: resource}
if err := delegatedAuthority.authorize(r.Context(), request); err != nil || capabilityRepository.SaveCandidateBinding(r.Context(), binding) != nil {
delegatedAuthority.completed(r.Context(), request, false)
http.Error(w, "candidate binding denied", http.StatusForbidden)
return
}
delegatedAuthority.completed(r.Context(), request, true)
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/internal/capability-bindings/candidate/resolve", func(w http.ResponseWriter, r *http.Request) {
if delegatedAuthority == nil || candidateContext == nil {
http.Error(w, "delegated authority unavailable", http.StatusServiceUnavailable)
return
}
if !candidateControlPlaneAuthenticated(r, delegatedAuthority.credential) {
http.Error(w, "service principal denied", http.StatusUnauthorized)
return
}
var context capability.CandidateReleaseContext
if err := json.NewDecoder(io.LimitReader(r.Body, 8*1024+1)).Decode(&context); err != nil || context != *candidateContext {
http.Error(w, "invalid candidate context", http.StatusBadRequest)
return
}
resource := candidateAuthorityResource(context, "")
request := authority.DelegatedControlPlaneRequest{GrantID: authority.GrantID(context.AuthorityGrantID), TaskID: context.TaskID, TaskRevision: context.TaskRevision, Operation: authority.DelegatedOperationUseCandidateCapabilityBinding, CandidateResource: resource}
if err := delegatedAuthority.authorize(r.Context(), request); err != nil {
http.Error(w, "candidate binding denied", http.StatusForbidden)
return
}
resolved, err := capabilityRepository.ResolveCandidateBinding(r.Context(), context, time.Now().UnixMilli())
delegatedAuthority.completed(r.Context(), request, err == nil)
if err != nil {
http.Error(w, "candidate binding required", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resolved)
})
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 {
@@ -1483,6 +1714,10 @@ func main() {
candidates, err = capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
}
if err != nil {
// The resolver is an internal capability boundary. Keep request identity
// and content out of logs, but preserve the repository error so a 503
// can be distinguished from a true empty/unsuitable binding set.
log.Printf("cognitive registry resolve unavailable: %v", err)
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
@@ -1629,6 +1864,119 @@ func main() {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/control-plane/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
if delegatedAuthority == nil {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
return
}
var value struct {
Delegation authority.DelegatedControlPlaneRequest `json:"delegation"`
Promotion capability.CognitiveCanaryPromotionRequest `json:"promotion"`
}
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.Delegation.Operation != authority.DelegatedOperationCanaryCognitiveBinding {
http.Error(w, "invalid delegated canary request", http.StatusBadRequest)
return
}
current, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), value.Promotion.BindingID, value.Promotion.ExpectedBindingRevision)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
resource := authority.CognitiveBindingResource{BindingID: string(current.Binding.ID), ProviderInstanceID: string(current.Provider.ID), ModelDescriptorID: string(current.Model.ID)}
if value.Delegation.Resource != resource {
http.Error(w, "delegated resource denied", http.StatusForbidden)
return
}
err = delegatedAuthority.authorize(r.Context(), value.Delegation)
if err != nil {
if errors.Is(err, ErrControlPlaneUnavailable) {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
} else {
http.Error(w, "delegated control-plane authority denied", http.StatusForbidden)
}
return
}
value.Promotion.RequestedByPrincipalID = current.Binding.OwnerPrincipalID
value.Promotion.CapabilityID = current.Binding.CapabilityID
promotion, err := capabilitydomain.BuildCanaryPromotion(value.Promotion, current)
if err == nil {
err = capabilityRepository.SaveCognitiveCanaryPromotion(r.Context(), promotion)
}
delegatedAuthority.completed(r.Context(), value.Delegation, err == nil)
if err != nil {
http.Error(w, "canary promotion precondition failed", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/control-plane/cognitive-activate", func(w http.ResponseWriter, r *http.Request) {
if delegatedAuthority == nil {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
return
}
var value struct {
Delegation authority.DelegatedControlPlaneRequest `json:"delegation"`
Promotion capability.CognitiveActivationPromotionRequest `json:"promotion"`
}
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.Delegation.Operation != authority.DelegatedOperationActivateCognitiveBinding {
http.Error(w, "invalid delegated activation request", http.StatusBadRequest)
return
}
current, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), value.Promotion.BindingID, value.Promotion.ExpectedBindingRevision)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
resource := authority.CognitiveBindingResource{BindingID: string(current.Binding.ID), ProviderInstanceID: string(current.Provider.ID), ModelDescriptorID: string(current.Model.ID)}
if value.Delegation.Resource != resource {
http.Error(w, "delegated resource denied", http.StatusForbidden)
return
}
err = delegatedAuthority.authorize(r.Context(), value.Delegation)
if err != nil {
if errors.Is(err, ErrControlPlaneUnavailable) {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
} else {
http.Error(w, "delegated control-plane authority denied", http.StatusForbidden)
}
return
}
value.Promotion.RequestedByPrincipalID = current.Binding.OwnerPrincipalID
value.Promotion.CapabilityID = current.Binding.CapabilityID
promotion, err := capabilitydomain.BuildActivationPromotion(value.Promotion, current)
if err == nil {
err = capabilityRepository.SaveCognitiveActivationPromotion(r.Context(), promotion)
}
delegatedAuthority.completed(r.Context(), value.Delegation, err == nil)
if err != nil {
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/creator/control-plane/grants/{grant}/revoke", func(w http.ResponseWriter, r *http.Request) {
if delegatedAuthority == nil {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
return
}
_, ok := authenticateCreator(w, r)
if !ok {
return
}
if err := delegatedAuthority.revoke(r.Context(), r.Header.Get("Authorization"), authority.GrantID(r.PathValue("grant"))); err != nil {
http.Error(w, "delegated grant revoke denied", http.StatusForbidden)
return
}
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("POST /v1/cognitive-registry/performance-evidence", func(w http.ResponseWriter, r *http.Request) {
var value capability.PerformanceEvidence
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
@@ -1923,6 +2271,23 @@ func normalizeSecretFile(name string, contents []byte) (string, error) {
}
func readCredentialRefSecret(name, refName, fileName string) (string, error) {
return readCredentialRefSecretPurpose(name, refName, fileName, secretcontracts.SecretPurposeDatabasePassword)
}
func readControlPlaneServiceCredential() (string, error) {
ref := os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF")
if ref == "" {
return "", errors.New("CONTROL_PLANE_SERVICE_CREDENTIAL_REF is required")
}
root := envOr("CONTROL_PLANE_SERVICE_SECRET_ROOT", "/run/control-plane-secrets")
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumer: secretcontracts.RuntimeConsumerCore, Path: filepath.Join(root, controlPlaneServiceTokenFile)})
}
func readCredentialRefSecretPurpose(name, refName, fileName string, purpose secretcontracts.SecretPurpose) (string, error) {
if _, directSet := os.LookupEnv(name); directSet {
return "", fmt.Errorf("%s direct environment secret is forbidden; use the standard secret file", name)
}
@@ -1940,7 +2305,7 @@ func readCredentialRefSecret(name, refName, fileName string) (string, error) {
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{
CredentialRef: secretcontracts.CredentialRef(ref),
Purpose: secretcontracts.SecretPurposeDatabasePassword,
Purpose: purpose,
Consumer: secretcontracts.RuntimeConsumerCore,
Path: fileName,
})
+6 -1
View File
@@ -19,7 +19,12 @@ func main() {
if err != nil {
log.Fatal(err)
}
server, err := newReceiverServer(envOr("RECEIPT_DIRECTORY", "/var/lib/notification-receiver"), writeToken, readToken, time.Now)
var server *receiverServer
if os.Getenv("SHADOW_RECEIVER_MODE") == "1" {
server, err = newShadowReceiverServer(envOr("RECEIPT_DIRECTORY", "/var/lib/notification-receiver"), writeToken, readToken, os.Getenv("CANDIDATE_RELEASE_SET_ID"), os.Getenv("CANDIDATE_TASK_ID"), time.Now)
} else {
server, err = newReceiverServer(envOr("RECEIPT_DIRECTORY", "/var/lib/notification-receiver"), writeToken, readToken, time.Now)
}
if err != nil {
log.Fatal(err)
}
+23 -1
View File
@@ -32,8 +32,11 @@ type receiverServer struct {
writeToken string
readToken string
now func() time.Time
shadow *shadowReceiverContext
}
type shadowReceiverContext struct{ releaseSetID, taskID string }
func newReceiverServer(directory, writeToken, readToken string, now func() time.Time) (*receiverServer, error) {
if directory == "" || strings.TrimSpace(writeToken) == "" || strings.TrimSpace(readToken) == "" ||
writeToken == readToken || now == nil {
@@ -45,6 +48,15 @@ func newReceiverServer(directory, writeToken, readToken string, now func() time.
return &receiverServer{directory: directory, writeToken: writeToken, readToken: readToken, now: now}, nil
}
func newShadowReceiverServer(directory, writeToken, readToken, releaseSetID, taskID string, now func() time.Time) (*receiverServer, error) {
server, err := newReceiverServer(directory, writeToken, readToken, now)
if err != nil || releaseSetID == "" || taskID == "" {
return nil, errors.New("invalid shadow receiver configuration")
}
server.shadow = &shadowReceiverContext{releaseSetID: releaseSetID, taskID: taskID}
return server, nil
}
func (s *receiverServer) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
@@ -62,6 +74,10 @@ func (s *receiverServer) put(w http.ResponseWriter, r *http.Request) {
http.Error(w, "receiver writer denied", http.StatusUnauthorized)
return
}
if s.shadow != nil && (r.Header.Get("X-Candidate-Release") != s.shadow.releaseSetID || r.Header.Get("X-Candidate-Task") != s.shadow.taskID) {
http.Error(w, "candidate shadow context denied", http.StatusForbidden)
return
}
intent := r.PathValue("intent")
if !safeIntent.MatchString(intent) {
http.Error(w, "invalid action intent", http.StatusBadRequest)
@@ -83,7 +99,13 @@ func (s *receiverServer) put(w http.ResponseWriter, r *http.Request) {
http.Error(w, "delivery digest mismatch", http.StatusConflict)
return
}
receipt := storedReceipt{Receipt: notificationexecutor.Receipt{ReceiptID: "receipt-" + intent, ActionIntentID: request.ActionIntentID, RequestDigest: digest, AcceptedAt: s.now().UnixMilli()}, Payload: request.Payload}
receipt := storedReceipt{Receipt: notificationexecutor.Receipt{ReceiptID: "receipt-" + intent, ActionIntentID: request.ActionIntentID, RequestDigest: digest, AcceptedAt: s.now().UnixMilli()}}
if s.shadow != nil {
receipt.DeliveryMode = "SHADOW"
receipt.ExternalDeliveryAttempted = false
} else {
receipt.Payload = request.Payload
}
created, existing, err := s.create(receipt)
if err != nil {
http.Error(w, "receipt unavailable", http.StatusInternalServerError)
+28
View File
@@ -120,3 +120,31 @@ func TestConcurrentDuplicateCreatesOneReceipt(t *testing.T) {
t.Fatalf("created=%d", created)
}
}
func TestShadowReceiverRequiresContextAndDoesNotPersistPayload(t *testing.T) {
server, err := newShadowReceiverServer(t.TempDir(), "shadow-write", "shadow-read", "assistant-slot-b", "GEN2-CAPABILITY-001", func() time.Time { return time.UnixMilli(100) })
if err != nil {
t.Fatal(err)
}
value := delivery(t, "shadow-intent", "must not persist")
body, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPut, "/delivery/v1/deliveries/shadow-intent", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer shadow-write")
req.Header.Set("X-Candidate-Release", "assistant-slot-b")
req.Header.Set("X-Candidate-Task", "GEN2-CAPABILITY-001")
response := httptest.NewRecorder()
server.routes().ServeHTTP(response, req)
if response.Code != http.StatusCreated || !bytes.Contains(response.Body.Bytes(), []byte(`"delivery_mode":"SHADOW"`)) || bytes.Contains(response.Body.Bytes(), []byte("must not persist")) {
t.Fatalf("shadow response status=%d body=%s", response.Code, response.Body.String())
}
req = httptest.NewRequest(http.MethodPut, "/delivery/v1/deliveries/shadow-intent", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer shadow-write")
response = httptest.NewRecorder()
server.routes().ServeHTTP(response, req)
if response.Code != http.StatusForbidden {
t.Fatalf("unscoped shadow request status=%d", response.Code)
}
}
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
const (
defaultRoot = "/run/control-plane-secrets"
defaultRuntimeUID = 100
defaultRuntimeGID = 101
)
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]")
}
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)
if err != nil {
fail("control-plane credential provisioning failed")
}
if result.Created {
fmt.Println("control-plane credential provisioned")
return
}
fmt.Println("control-plane credential already active")
case "rotate":
if !validRotationReason(*reason) {
fail("rotation reason is invalid")
}
if _, err := store.Rotate(spec, *reason); err != nil {
fail("control-plane credential rotation failed")
}
fmt.Println("control-plane credential rotated")
default:
fail("unsupported control-plane credential operation")
}
}
func validRotationReason(reason string) bool {
switch strings.TrimSpace(reason) {
case "owner-requested-rotation", "exposure-detected":
return true
default:
return false
}
}
func fail(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(1)
}
+15
View File
@@ -0,0 +1,15 @@
package main
import "testing"
func TestSecretControlPlaneDefaultsUseDedicatedProtectedRoot(t *testing.T) {
if defaultRoot != "/run/control-plane-secrets" || defaultRuntimeUID <= 0 || defaultRuntimeGID <= 0 {
t.Fatal("unsafe control-plane defaults")
}
}
func TestRotationReasonsAreBoundedAndNeverArbitraryDiagnosticText(t *testing.T) {
if !validRotationReason("owner-requested-rotation") || !validRotationReason("exposure-detected") || validRotationReason("Bearer secret-value") {
t.Fatal("unsafe rotation reason validation")
}
}
+14 -1
View File
@@ -261,7 +261,20 @@ func (s *server) promoteTraffic(w http.ResponseWriter, _ *http.Request) {
respond(w, ok, err)
}
func (s *server) rollbackTraffic(w http.ResponseWriter, _ *http.Request) {
ok, err := s.lifeboat.RollbackTraffic()
state, snapshotErr := s.lifeboat.Snapshot()
if snapshotErr != nil {
respond(w, false, snapshotErr)
return
}
// A ReleaseSet must roll back as a whole. The legacy release-only method
// would merely flip the traffic mode while retaining a candidate upstream.
var ok bool
var err error
if state.StableReleaseSet != nil {
ok, err = s.lifeboat.RollbackSet()
} else {
ok, err = s.lifeboat.RollbackTraffic()
}
respond(w, ok, err)
}
+14
View File
@@ -0,0 +1,14 @@
package authority
import "git.cin.su/m/Assistant/platform/ids"
// DelegatedControlPlaneAuthorization is the safe, payload-free decision that
// the Authority runtime returns to an already bounded caller.
type DelegatedControlPlaneAuthorization struct {
AuditID ids.ID
}
type DelegatedControlPlaneCompletion struct {
Request DelegatedControlPlaneRequest
Outcome string
}
+86
View File
@@ -13,6 +13,92 @@ type GrantClass string
const GrantClassCreator GrantClass = "CREATOR"
// DelegatedControlPlaneOperation is deliberately a closed, semantic operation
// set. A service credential cannot turn into arbitrary control-plane access.
type DelegatedControlPlaneOperation string
const (
DelegatedOperationQualifyCognitiveBinding DelegatedControlPlaneOperation = "QUALIFY_COGNITIVE_BINDING"
DelegatedOperationEvaluateCognitiveBinding DelegatedControlPlaneOperation = "EVALUATE_COGNITIVE_BINDING"
DelegatedOperationCanaryCognitiveBinding DelegatedControlPlaneOperation = "CANARY_COGNITIVE_BINDING"
DelegatedOperationActivateCognitiveBinding DelegatedControlPlaneOperation = "ACTIVATE_VALIDATED_COGNITIVE_BINDING"
DelegatedOperationCreateCandidateCapabilityBinding DelegatedControlPlaneOperation = "CREATE_CANDIDATE_CAPABILITY_BINDING"
DelegatedOperationUseCandidateCapabilityBinding DelegatedControlPlaneOperation = "USE_CANDIDATE_CAPABILITY_BINDING"
DelegatedOperationRevokeCandidateCapabilityBinding DelegatedControlPlaneOperation = "REVOKE_CANDIDATE_CAPABILITY_BINDING"
DelegatedOperationRevokeControlPlaneGrant DelegatedControlPlaneOperation = "REVOKE_DELEGATED_CONTROL_PLANE_GRANT"
)
type DelegatedGrantStatus string
const (
DelegatedGrantActive DelegatedGrantStatus = "ACTIVE"
DelegatedGrantRevoked DelegatedGrantStatus = "REVOKED"
)
// CognitiveBindingResource identifies the one governed Capability lifecycle
// resource to which a task delegation applies. It is intentionally not a
// general-purpose resource selector.
type CognitiveBindingResource struct {
BindingID string
ProviderInstanceID string
ModelDescriptorID string
}
// CandidateCapabilityBindingResource is an exact, bounded control-plane
// resource. Authority does not resolve targets or own Capability state.
type CandidateCapabilityBindingResource struct {
ReleaseSetID string
Slot string
CapabilityID string
ResourceScope string
Mode string
TargetKind string
TargetRef string
TargetDigest string
}
type DelegatedControlPlaneGrant struct {
ID GrantID
ServiceActorID identity.ActorID
ServicePrincipalID identity.PrincipalID
IssuerPrincipalID identity.PrincipalID
TaskID string
TaskRevision int64
Operations []DelegatedControlPlaneOperation
Resource CognitiveBindingResource
CandidateResource CandidateCapabilityBindingResource
IssuedAtUnixMillis int64
ExpiresAtUnixMillis int64
ArchitectureRevision string
Nonce string
Revision AuthorityRevision
Status DelegatedGrantStatus
}
type DelegatedControlPlaneRequest struct {
GrantID GrantID
TaskID string
TaskRevision int64
Operation DelegatedControlPlaneOperation
Resource CognitiveBindingResource
CandidateResource CandidateCapabilityBindingResource
}
type DelegatedControlPlaneAudit struct {
AuditID ids.ID
GrantID GrantID
ServiceActorID identity.ActorID
ServicePrincipalID identity.PrincipalID
TaskID string
TaskRevision int64
Operation DelegatedControlPlaneOperation
Resource CognitiveBindingResource
CandidateResource CandidateCapabilityBindingResource
Decision string
Outcome string
RecordedAtUnixMillis int64
}
type Scope struct {
ID ScopeID
Global bool
+18 -3
View File
@@ -27,15 +27,30 @@ type StrategyDecision struct {
Escalate bool
}
// StrategyEligibility is supplied by Capability lifecycle resolution. It keeps
// a requested cheap route from becoming an entitlement to use stale evidence.
type StrategyEligibility struct {
RuleEligible bool
ReflexEligible bool
SkillEligible bool
}
func (r StrategyRequest) Select() StrategyDecision {
return r.SelectEligible(StrategyEligibility{RuleEligible: true, ReflexEligible: true, SkillEligible: true})
}
func (r StrategyRequest) SelectEligible(eligible StrategyEligibility) StrategyDecision {
if r.RiskClass == "HIGH" || r.CostOfError >= 800 {
return StrategyDecision{Path: PathStrongLocal, Reason: "high consequence requires deeper reasoning", Escalate: true}
}
if r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
if eligible.RuleEligible && r.RequiredQuality <= 500 && r.CostBudgetMicros > 0 {
return StrategyDecision{Path: PathRule, Reason: "bounded low-risk path"}
}
if r.PreferLowLatency {
if eligible.ReflexEligible && r.PreferLowLatency {
return StrategyDecision{Path: PathReflex, Reason: "latency preference with later escalation"}
}
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
if eligible.SkillEligible {
return StrategyDecision{Path: PathSkill, Reason: "validated reusable strategy"}
}
return StrategyDecision{Path: PathSmallLocal, Reason: "no eligible reflex or skill; escalate to reasoning", Escalate: true}
}
+16
View File
@@ -10,6 +10,7 @@ const (
ReflexActive ReflexLifecycle = "ACTIVE"
ReflexDegraded ReflexLifecycle = "DEGRADED"
ReflexStale ReflexLifecycle = "STALE"
ReflexRevalidating ReflexLifecycle = "REVALIDATING"
ReflexSuspended ReflexLifecycle = "SUSPENDED"
ReflexRetired ReflexLifecycle = "RETIRED"
ReflexDeterministicCode ReflexKind = "DETERMINISTIC_CODE"
@@ -58,4 +59,19 @@ type ReflexRevision struct {
LastValidatedAt int64
Lifecycle ReflexLifecycle
RecordedAt int64
SupersedesRevision int64
ExecutionCount int64
FailureCount int64
DivergenceCount int64
}
// ShadowComparison is evidence produced by a bounded shadow sample. It is not
// permission to rewrite a Reflex; SELF/governed evaluation decide next steps.
type ShadowComparison struct {
ReflexID ReflexID
ReflexRevision int64
EvidenceRefs []SkillEvidenceRef
VerifierPassed bool
Diverged bool
ObservedAt int64
}
+10
View File
@@ -70,6 +70,7 @@ const (
SkillCandidate SkillLifecycleStatus = "CANDIDATE"
SkillEvaluating SkillLifecycleStatus = "EVALUATING"
SkillValidated SkillLifecycleStatus = "VALIDATED"
SkillActive SkillLifecycleStatus = "ACTIVE"
SkillDegraded SkillLifecycleStatus = "DEGRADED"
SkillSuspended SkillLifecycleStatus = "SUSPENDED"
SkillRetired SkillLifecycleStatus = "RETIRED"
@@ -202,6 +203,15 @@ type SkillProfile struct {
ObservedFailureCount int64
ObservedCorrectionCount int64
RecordedAtUnixMillis int64
// The fields below are the additive Gen2 lifecycle view. The older fields
// remain a compatibility projection; they are not another Skill registry.
Applicability ApplicabilitySpec
DependencyRefs []string
ValidationEvidenceRefs []SkillEvidenceRef
IndependentEvaluationRefs []SkillEvidenceRef
KnownFailureConditions []string
VerifiedOutcomeCount int64
MustNotWorsenSatisfied bool
}
type SkillOutcomeObservation struct {
+53
View File
@@ -12,10 +12,21 @@ type CapabilityKind string
type BindingState string
type BindingProtocol string
// CandidateBindingMode controls how a release-scoped binding may be used.
// It is deliberately separate from a production BindingState.
type CandidateBindingMode string
type CandidateBindingStatus string
const (
BindingActive BindingState = "ACTIVE"
BindingRetired BindingState = "RETIRED"
CandidateBindingModeCandidate CandidateBindingMode = "CANDIDATE"
CandidateBindingModeShadow CandidateBindingMode = "SHADOW"
CandidateBindingActive CandidateBindingStatus = "ACTIVE"
CandidateBindingRevoked CandidateBindingStatus = "REVOKED"
ProtocolIdempotentReceiptV1 BindingProtocol = "IDEMPOTENT_RECEIPT_V1"
CapabilityKindReadEmail CapabilityKind = "READ_EMAIL"
@@ -54,6 +65,48 @@ type Binding struct {
Health HealthState
}
// CandidateReleaseContext is supplied by a candidate runtime, not inferred
// from a host name or process-local state. It scopes dynamic capability
// resolution to one release slot and task.
type CandidateReleaseContext struct {
ReleaseSetID string
Slot string
TaskID string
TaskRevision int64
PrincipalID identity.PrincipalID
CapabilityID CapabilityID
ResourceScope string
Mode CandidateBindingMode
TargetKind string
TargetRef string
TargetDigest string
AuthorityGrantID string
}
// CandidateBinding is Capability-owned release-scoped state. TargetOrigin is
// intentionally distinct from Binding.HTTPSOrigin: a SHADOW target can be an
// internal candidate receiver and must never be promoted into the ACTIVE head.
type CandidateBinding struct {
ID CapabilityBindingID
Revision int64
BaseActiveBindingID CapabilityBindingID
BaseActiveBindingRevision int64
Context CandidateReleaseContext
TargetOrigin string
DeliveryPath string
ReceiptPath string
Protocol BindingProtocol
CreatedAtUnixMillis int64
ExpiresAtUnixMillis int64
Status CandidateBindingStatus
}
type CandidateBindingResolution struct {
Binding CandidateBinding
Mode CandidateBindingMode
Reason string
}
type HealthState string
const (
+87 -13
View File
@@ -36,6 +36,14 @@ type ContextOmission struct {
Reason string
}
// ContextInclusion records the bounded compiler decision without exposing
// source text. It is trace-only metadata and is never user-facing output.
type ContextInclusion struct {
ID ContextExcerptID
Lane string
Reason string
}
type ContextBundle struct {
Excerpts []ContextExcerpt
Omissions []ContextOmission
@@ -45,19 +53,60 @@ type ContextBundle struct {
type ContextAssemblyTrace struct {
Included []ContextExcerptID
Inclusions []ContextInclusion
Excluded []ContextOmission
Coverage string
CompilerVersion string
}
type IterativeRetrievalLane string
const (
IterativeRetrievalCurrentFact IterativeRetrievalLane = "CURRENT_FACT"
IterativeRetrievalPositiveExperience IterativeRetrievalLane = "POSITIVE_EXPERIENCE"
IterativeRetrievalNegativeExperience IterativeRetrievalLane = "NEGATIVE_EXPERIENCE"
IterativeRetrievalContradiction IterativeRetrievalLane = "CONTRADICTION"
IterativeRetrievalHistoricalAnalogy IterativeRetrievalLane = "HISTORICAL_ANALOGY"
)
// IterativeRetrievalRequest is the model-visible, storage-agnostic intent.
// Principal, Scope, Focus and policy are deliberately absent: Core binds them
// from the already-authorized request and never accepts them from a model.
type IterativeRetrievalRequest struct {
Query string
RemainingBytes int
Pass int
Query string
SemanticSubject string
Lane IterativeRetrievalLane
Reason string
RemainingBytes int
Pass int
}
// IterativeRetrievalBudget is a Core-enforced per-request budget.
type IterativeRetrievalBudget struct {
MaxRounds int
MaxFragments int
MaxAddedBytes int
MaxLatencyMillis int64
}
// IterativeRetrievalEnvelope is the trusted continuation input assembled by
// Core. It is never produced by a provider or model adapter.
type IterativeRetrievalEnvelope struct {
Request IterativeRetrievalRequest
PrincipalID string
ContextScopeRef string
FocusFrameRef string
TaskRef string
FreshnessRequirement string
DataPolicy capability.DataPolicy
Budget IterativeRetrievalBudget
SeenExcerptIDs []ContextExcerptID
}
type IterativeRetrievalResponse struct {
Candidates []ContextCandidate
Exhausted bool
Reason string
AddedBytes int
}
// CompileContext deterministically selects permitted, relevant excerpts for a
@@ -67,14 +116,9 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
return ContextBundle{}, ErrInvalidContextBudget
}
candidates := append([]ContextCandidate(nil), input.Candidates...)
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Relevance != candidates[j].Relevance {
return candidates[i].Relevance > candidates[j].Relevance
}
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
})
bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}}
seen := make(map[ContextExcerptID]struct{}, len(candidates))
eligible := make([]ContextCandidate, 0, len(candidates))
for _, candidate := range candidates {
excerpt := candidate.Excerpt
if _, ok := seen[excerpt.ID]; ok {
@@ -86,8 +130,7 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "context_scope"})
continue
}
reason := permittedContextReason(excerpt, input.DataPolicy)
if reason != "" {
if reason := permittedContextReason(excerpt, input.DataPolicy); reason != "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
continue
}
@@ -95,14 +138,45 @@ func CompileContext(input ContextCompileInput) (ContextBundle, error) {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"})
continue
}
eligible = append(eligible, candidate)
}
candidates = eligible
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Relevance != candidates[j].Relevance {
return candidates[i].Relevance > candidates[j].Relevance
}
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
})
included := make(map[ContextExcerptID]struct{}, len(candidates))
include := func(candidate ContextCandidate, reason string) bool {
excerpt := candidate.Excerpt
bytes := len([]byte(excerpt.Text))
if bundle.Bytes+bytes > input.MaxBytes {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"})
continue
return false
}
bundle.Excerpts = append(bundle.Excerpts, excerpt)
bundle.Bytes += bytes
bundle.Trace.Included = append(bundle.Trace.Included, excerpt.ID)
bundle.Trace.Inclusions = append(bundle.Trace.Inclusions, ContextInclusion{ID: excerpt.ID, Lane: candidate.Lane, Reason: reason})
included[excerpt.ID] = struct{}{}
return true
}
// Preserve one candidate from each semantic lane before filling by score so
// many similar current facts cannot erase a relevant failure or conflict.
for _, lane := range []string{"current-fact", "positive-experience", "negative-experience", "contradiction", "historical-analogy", "interaction"} {
for _, candidate := range candidates {
if candidate.Lane == lane && include(candidate, "lane_coverage") {
break
}
}
}
for _, candidate := range candidates {
if _, ok := included[candidate.Excerpt.ID]; ok {
continue
}
if !include(candidate, "salience") {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: candidate.Excerpt.ID, Reason: "budget"})
}
}
bundle.Trace.Excluded = append([]ContextOmission(nil), bundle.Omissions...)
bundle.Trace.CompilerVersion = "context-compiler-v2-additive"
@@ -12,3 +12,19 @@ func TestContextCompilerV2TraceAndBudgetRemainDeterministic(t *testing.T) {
t.Fatalf("bundle=%#v err=%v", b, err)
}
}
func TestContextCompilerPreservesNegativeLaneWithinBoundedBudget(t *testing.T) {
b, err := cognitive.CompileContext(cognitive.ContextCompileInput{MaxBytes: 27, DataPolicy: capability.DataPolicyLocalOnly, Candidates: []cognitive.ContextCandidate{
{Excerpt: cognitive.ContextExcerpt{ID: "current", Text: "current-value", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 100},
{Excerpt: cognitive.ContextExcerpt{ID: "failure", Text: "known-failure", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "negative-experience", Relevance: 1, Negative: true},
{Excerpt: cognitive.ContextExcerpt{ID: "crowd", Text: "similar-current", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact", Relevance: 99},
}})
if err != nil || len(b.Excerpts) != 2 || b.Excerpts[0].ID != "current" || b.Excerpts[1].ID != "failure" {
t.Fatalf("bundle=%#v err=%v", b, err)
}
// The negative record fits and is retained before a lower-value crowd of
// similar facts can consume the remaining bounded budget.
if len(b.Trace.Inclusions) != 2 || b.Trace.Inclusions[0].Reason != "lane_coverage" || b.Trace.Inclusions[1].Reason != "lane_coverage" {
t.Fatalf("trace=%#v", b.Trace)
}
}
+14 -12
View File
@@ -69,18 +69,19 @@ type AdvisoryRequest struct {
// GatewayAdvisoryRequest describes the task and policy requirement. It cannot
// name a provider, model or binding; Capability owns that selection.
type GatewayAdvisoryRequest struct {
Version ContractVersion
RequestID RequestID
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
Message string
Context []ContextExcerpt
Locale string
MaxOutputBytes int
DataPolicy capability.DataPolicy
EgressAcknowledged bool
Meaning TaskMeaning
Version ContractVersion
RequestID RequestID
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
Message string
Context []ContextExcerpt
Locale string
MaxOutputBytes int
DataPolicy capability.DataPolicy
EgressAcknowledged bool
AllowIterativeRetrieval bool
Meaning TaskMeaning
}
// TaskMeaning is the trusted semantic summary produced by Core. It is not a
@@ -177,4 +178,5 @@ type GatewayAdvisoryResponse struct {
ReferencedExcerptIDs []ContextExcerptID
FinishReason FinishReason
Usage *ProviderUsage
IterativeRetrieval *IterativeRetrievalRequest
}
+8
View File
@@ -25,6 +25,14 @@ type ExecutionReceipt struct {
Ambiguity AmbiguityState
ReconciliationKey string
RawArtifactRef string
DeliveryMode string
ExternalDeliveryAttempted bool
ReleaseSetID string
TaskID string
TaskRevision int64
CapabilityBindingID string
CapabilityBindingRevision int64
TargetReference string
}
func (r ExecutionReceipt) Valid() bool {
+18
View File
@@ -56,6 +56,24 @@ type FreshnessAssessment struct {
RevalidationAttemptRef string
AssessedAtUnixMillis int64
}
// FreshnessRequirement is supplied by the current task/focus owner. It is an
// evaluation input, not Epistemics-owned task state, so freshness remains
// relative to the decision being made.
type FreshnessRequirement struct {
Mode string
MaximumAgeMillis *int64
RequiredVersionRefs []SemanticReference
}
// WithExactVersionBinding is a typed contract helper for callers that need to
// attach an exact environment/version applicability constraint without taking
// a direct dependency on the World package.
func WithExactVersionBinding(value TemporalApplicability, subject, version SemanticReference) TemporalApplicability {
value.VersionBindings = append(value.VersionBindings, world.VersionBinding{SubjectRef: subject, VersionRef: version, MatchMode: world.VersionExact})
return value
}
type RevalidationRequest struct {
ID string
SourceKnowledgeRef string
+10
View File
@@ -102,6 +102,7 @@ type SystemExpectation struct {
// state or grants authority.
type ExperienceRecord struct {
ID ExperienceRecordID
PrincipalID string
SourceKind ExperienceSourceKind
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
@@ -133,6 +134,15 @@ type ExperienceRecord struct {
WhySucceededOrFailed string
}
// ConsolidationCandidate is a rebuildable Epistemics projection. It preserves
// source ExperienceRecord references and cannot activate a Skill by itself.
type ConsolidationCandidate struct {
EpisodeRef string
ExperienceRefs []ExperienceRecordID
PropositionText string
SkillCandidate bool
}
// ReflectionCandidate records that broader reflection may be warranted.
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
// produce candidates only; it cannot directly rewrite canonical state.
+18
View File
@@ -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
}
+2
View File
@@ -215,6 +215,8 @@ type ChatMessageRequest struct {
Message string
CorrelationID string
CorrectsUtteranceID HumanUtteranceID
SyntheticAcceptance bool
SyntheticScopeID string
}
type ChatMessageResponse struct {
+4 -2
View File
@@ -8,8 +8,9 @@ type CredentialRef string
type SecretPurpose string
const (
SecretPurposeDatabasePassword SecretPurpose = "DATABASE_PASSWORD"
SecretPurposeProviderAPIKey SecretPurpose = "PROVIDER_API_KEY"
SecretPurposeDatabasePassword SecretPurpose = "DATABASE_PASSWORD"
SecretPurposeProviderAPIKey SecretPurpose = "PROVIDER_API_KEY"
SecretPurposeServiceAuthentication SecretPurpose = "SERVICE_AUTHENTICATION"
)
// RuntimeConsumer identifies a trusted runtime allowed to resolve a credential.
@@ -17,6 +18,7 @@ type RuntimeConsumer string
const (
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerCommit RuntimeConsumer = "commit-runtime"
RuntimeConsumerWorkWorker RuntimeConsumer = "agent-worker"
RuntimeConsumerEpistemicsWorker RuntimeConsumer = "reflection-worker"
RuntimeConsumerCognitive RuntimeConsumer = "cognitive-worker"
+12
View File
@@ -1,6 +1,18 @@
package self
type BenchmarkCaseRef string
type BenchmarkCase struct {
ID string
PrincipalID string
TaskClass string
EvidenceRefs []SemanticReference
RecordedAt int64
}
func (c BenchmarkCase) Valid() bool {
return c.ID != "" && c.PrincipalID != "" && c.TaskClass != "" && len(c.EvidenceRefs) > 0 && c.RecordedAt > 0
}
type PersonalBenchmark struct {
ID string
PrincipalID string
+161 -9
View File
@@ -6,6 +6,58 @@ type AssessmentID string
type LearningLedgerEntryID string
type AssessmentState string
type LearningSignalKind string
type DiagnosisClass string
type EpistemicStatus string
type InterventionClass string
type DecisionRegretKind string
type CandidateState string
const (
SignalVerifiedFailure LearningSignalKind = "VERIFIED_FAILURE"
SignalUserCorrection LearningSignalKind = "USER_CORRECTION"
SignalRepeatedMistake LearningSignalKind = "REPEATED_KNOWN_MISTAKE"
SignalReflexDivergence LearningSignalKind = "REFLEX_DIVERGENCE"
SignalSkillFallback LearningSignalKind = "SKILL_FALLBACK"
SignalUnexpectedEscalation LearningSignalKind = "UNEXPECTED_ESCALATION"
SignalOutcomeMismatch LearningSignalKind = "OUTCOME_MISMATCH"
SignalRetryFallback LearningSignalKind = "RETRY_FALLBACK"
SignalStaleDependency LearningSignalKind = "STALE_DEPENDENCY"
SignalDecisionRegret LearningSignalKind = "DECISION_REGRET"
SignalBetterSolution LearningSignalKind = "BETTER_SOLUTION"
DiagnosisMemoryMissing DiagnosisClass = "MEMORY_MISSING"
DiagnosisMemoryNotRetrieved DiagnosisClass = "MEMORY_NOT_RETRIEVED"
DiagnosisWrongScope DiagnosisClass = "WRONG_SCOPE"
DiagnosisStaleKnowledge DiagnosisClass = "STALE_KNOWLEDGE"
DiagnosisContextSelection DiagnosisClass = "CONTEXT_SELECTION"
DiagnosisReasoningFailure DiagnosisClass = "REASONING_FAILURE"
DiagnosisSkillFailure DiagnosisClass = "SKILL_FAILURE"
DiagnosisReflexFailure DiagnosisClass = "REFLEX_FAILURE"
DiagnosisToolFailure DiagnosisClass = "TOOL_FAILURE"
DiagnosisProviderFailure DiagnosisClass = "PROVIDER_FAILURE"
DiagnosisOutcomeVerificationFailure DiagnosisClass = "OUTCOME_VERIFICATION_FAILURE"
DiagnosisRoutingInefficiency DiagnosisClass = "ROUTING_INEFFICIENCY"
DiagnosisUnknown DiagnosisClass = "UNKNOWN_CAUSE"
EpistemicKnown EpistemicStatus = "KNOWN"
EpistemicSupported EpistemicStatus = "SUPPORTED"
EpistemicHypothesis EpistemicStatus = "HYPOTHESIS"
EpistemicUnknown EpistemicStatus = "UNKNOWN"
InterventionConfiguration InterventionClass = "CONFIGURATION"
InterventionSkillReview InterventionClass = "SKILL_REVIEW"
InterventionReflexReview InterventionClass = "REFLEX_REVIEW"
InterventionRoutingReview InterventionClass = "ROUTING_REVIEW"
InterventionProviderReview InterventionClass = "PROVIDER_REVIEW"
InterventionContextReview InterventionClass = "CONTEXT_REVIEW"
RegretUnavailableThen DecisionRegretKind = "INFORMATION_UNAVAILABLE_THEN"
RegretAvailableUnused DecisionRegretKind = "INFORMATION_AVAILABLE_UNUSED"
RegretIgnoredInContext DecisionRegretKind = "EVIDENCE_IGNORED_IN_CONTEXT"
CandidateProposed CandidateState = "PROPOSED"
CandidateEvaluating CandidateState = "EVALUATING"
CandidateKept CandidateState = "KEPT"
CandidateRejected CandidateState = "REJECTED"
CandidateRolledBack CandidateState = "ROLLED_BACK"
)
const (
AssessmentCandidate AssessmentState = "CANDIDATE"
AssessmentConfirmed AssessmentState = "CONFIRMED"
@@ -37,6 +89,17 @@ type WeaknessPattern struct {
DetectionEvidenceRefs []SemanticReference
State AssessmentState
RecordedAt int64
TaskPattern string
ScopeKey string
EnvironmentVersion string
DependencyRefs []SemanticReference
Frequency int
LastObservedAt int64
Severity int
AffectedComponent string
AffectedStrategy string
ConfidenceMillis int64
EpistemicStatus EpistemicStatus
}
type DependencyAssessment struct {
ID AssessmentID
@@ -48,15 +111,83 @@ type DependencyAssessment struct {
AssessedAt int64
}
type ImprovementNeed struct {
ID AssessmentID
PrincipalID string
SubjectRef SemanticReference
ProblemSummary string
EvidenceRefs []SemanticReference
ExpectedBenefit string
Priority int
State string
CreatedAt int64
ID AssessmentID
PrincipalID string
SubjectRef SemanticReference
ProblemSummary string
EvidenceRefs []SemanticReference
ExpectedBenefit string
Priority int
State string
CreatedAt int64
Diagnosis DiagnosisClass
DiagnosisStatus EpistemicStatus
AffectedComponent string
ScopeKey string
Risk string
Intervention InterventionClass
Hypothesis string
EvaluationPlan string
MustNotWorsen string
RollbackPlan string
ResourceCost string
}
type LearningSignal struct {
ID string
PrincipalID string
Kind LearningSignalKind
TaskPattern string
ScopeKey string
SubjectRef SemanticReference
EvidenceRefs []SemanticReference
ExpectedOutcome string
ActualOutcome string
DependencyRefs []SemanticReference
EnvironmentVersion string
ObservedAt int64
Severity int
UserImpact int
CorrectionCount int
DecisionRegret DecisionRegretKind
}
type Diagnosis struct {
Class DiagnosisClass
Status EpistemicStatus
Rationale string
EvidenceRefs []SemanticReference
ConfidenceMillis int64
}
type ImprovementCandidate struct {
ID string
PrincipalID string
NeedRef SemanticReference
Hypothesis string
ExpectedEffect string
EvaluationPlan string
MustNotWorsen []string
RollbackPlan string
ResourceCost string
Creator string
IndependentEvaluator string
State CandidateState
EvidenceRefs []SemanticReference
CreatedAt int64
}
type ImprovementEvaluation struct {
CandidateRef SemanticReference
Evaluator string
EvidenceRefs []SemanticReference
ExpectedQuality int64
ActualQuality int64
ExpectedLatency int64
ActualLatency int64
MustNotWorsenPass bool
Decision CandidateState
EvaluatedAt int64
}
type LearningLedgerEntry struct {
ID LearningLedgerEntryID
@@ -90,3 +221,24 @@ func (l LearningLedgerEntry) Validate() error {
}
return nil
}
func (s LearningSignal) Validate() error {
if s.ID == "" || s.PrincipalID == "" || s.Kind == "" || s.TaskPattern == "" || s.ScopeKey == "" || !s.SubjectRef.Valid() || len(s.EvidenceRefs) == 0 || s.ObservedAt < 1 {
return fmt.Errorf("learning signal is incomplete")
}
return nil
}
func (d Diagnosis) Validate() error {
if d.Class == "" || d.Status == "" || len(d.EvidenceRefs) == 0 || d.ConfidenceMillis < 0 || d.ConfidenceMillis > 1000 {
return fmt.Errorf("diagnosis requires bounded evidence")
}
return nil
}
func (c ImprovementCandidate) Validate() error {
if c.ID == "" || c.PrincipalID == "" || !c.NeedRef.Valid() || c.Hypothesis == "" || c.ExpectedEffect == "" || c.EvaluationPlan == "" || c.MustNotWorsen == nil || c.RollbackPlan == "" || c.Creator == "" || c.IndependentEvaluator == "" || c.Creator == c.IndependentEvaluator || len(c.EvidenceRefs) == 0 || c.CreatedAt < 1 {
return fmt.Errorf("candidate lacks independent bounded evaluation")
}
return nil
}
@@ -0,0 +1,17 @@
BEGIN;
SET ROLE epistemics_role;
ALTER TABLE epistemics.experience_records
ADD COLUMN IF NOT EXISTS principal_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS context_scope_ref TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS problem_statement TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS applicability_conditions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
ADD COLUMN IF NOT EXISTS why_succeeded_or_failed TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS experience_records_principal_scope_recorded_idx
ON epistemics.experience_records (principal_id, context_scope_ref, recorded_at_unix_millis DESC);
RESET ROLE;
COMMIT;
@@ -0,0 +1,79 @@
BEGIN;
SET ROLE capability_role;
ALTER TABLE capability.skill_profiles
ADD COLUMN IF NOT EXISTS applicability JSONB NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS dependency_refs TEXT[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS validation_evidence_refs TEXT[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS independent_evaluation_refs TEXT[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS known_failure_conditions TEXT[] NOT NULL DEFAULT '{}',
ADD COLUMN IF NOT EXISTS verified_outcome_count BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS must_not_worsen_satisfied BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE capability.skill_profile_heads DROP CONSTRAINT IF EXISTS skill_profile_heads_status_check;
ALTER TABLE capability.skill_profile_heads
ADD CONSTRAINT skill_profile_heads_status_check CHECK (status IN ('CANDIDATE','EVALUATING','VALIDATED','ACTIVE','DEGRADED','SUSPENDED','RETIRED'));
CREATE TABLE IF NOT EXISTS capability.reflex_revisions (
reflex_id TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
principal_id TEXT NOT NULL,
source_skill_id TEXT NOT NULL,
source_skill_revision BIGINT NOT NULL CHECK (source_skill_revision > 0),
kind TEXT NOT NULL,
applicability JSONB NOT NULL,
implementation_ref TEXT NOT NULL,
implementation_version TEXT NOT NULL DEFAULT '',
artifact_digest TEXT NOT NULL DEFAULT '',
dependency_refs TEXT[] NOT NULL DEFAULT '{}',
validation_evidence_refs TEXT[] NOT NULL DEFAULT '{}',
known_failure_experience_refs TEXT[] NOT NULL DEFAULT '{}',
shadow_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
revalidation_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
last_validated_at BIGINT NOT NULL DEFAULT 0,
lifecycle TEXT NOT NULL,
supersedes_revision BIGINT NOT NULL DEFAULT 0,
execution_count BIGINT NOT NULL DEFAULT 0,
failure_count BIGINT NOT NULL DEFAULT 0,
divergence_count BIGINT NOT NULL DEFAULT 0,
recorded_at BIGINT NOT NULL CHECK (recorded_at > 0),
PRIMARY KEY (reflex_id, revision),
CHECK (lifecycle IN ('CANDIDATE','VALIDATING','ACTIVE','DEGRADED','STALE','REVALIDATING','SUSPENDED','RETIRED'))
);
CREATE TABLE IF NOT EXISTS capability.reflex_heads (
reflex_id TEXT PRIMARY KEY,
revision BIGINT NOT NULL,
lifecycle TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
GRANT SELECT, INSERT, UPDATE ON capability.reflex_revisions, capability.reflex_heads TO capability_role;
RESET ROLE;
SET ROLE self_role;
CREATE TABLE IF NOT EXISTS self.improvement_needs (
improvement_need_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
subject_ref JSONB NOT NULL,
problem_summary TEXT NOT NULL,
evidence_refs JSONB NOT NULL,
expected_benefit TEXT NOT NULL,
priority INTEGER NOT NULL,
state TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL
);
ALTER TABLE self.learning_ledger
ADD COLUMN IF NOT EXISTS improvement_need_ref JSONB,
ADD COLUMN IF NOT EXISTS proposed_change_ref JSONB,
ADD COLUMN IF NOT EXISTS expected_improvement TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS evaluation_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS development_gate_ref JSONB,
ADD COLUMN IF NOT EXISTS before_version_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS after_version_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS side_effects JSONB NOT NULL DEFAULT '[]'::jsonb,
ADD COLUMN IF NOT EXISTS rollback_ref JSONB;
GRANT SELECT, INSERT, UPDATE ON self.improvement_needs, self.learning_ledger TO self_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,74 @@
BEGIN;
SET ROLE self_role;
CREATE TABLE IF NOT EXISTS self.learning_signals (
signal_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
kind TEXT NOT NULL,
task_pattern TEXT NOT NULL,
scope_key TEXT NOT NULL,
subject_ref JSONB NOT NULL,
evidence_refs JSONB NOT NULL,
expected_outcome TEXT NOT NULL DEFAULT '',
actual_outcome TEXT NOT NULL DEFAULT '',
dependency_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
environment_version TEXT NOT NULL DEFAULT '',
observed_at_unix_millis BIGINT NOT NULL,
severity INTEGER NOT NULL DEFAULT 0,
user_impact INTEGER NOT NULL DEFAULT 0,
correction_count INTEGER NOT NULL DEFAULT 0,
decision_regret TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS self.weakness_patterns (
assessment_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
revision BIGINT NOT NULL CHECK (revision > 0),
pattern_key TEXT NOT NULL,
task_pattern TEXT NOT NULL,
scope_key TEXT NOT NULL,
environment_version TEXT NOT NULL DEFAULT '',
dependency_refs JSONB NOT NULL DEFAULT '[]'::jsonb,
failure_mode TEXT NOT NULL,
detection_evidence_refs JSONB NOT NULL,
frequency INTEGER NOT NULL,
last_observed_at_unix_millis BIGINT NOT NULL,
severity INTEGER NOT NULL,
affected_component TEXT NOT NULL,
affected_strategy TEXT NOT NULL DEFAULT '',
confidence_millis BIGINT NOT NULL,
epistemic_status TEXT NOT NULL,
state TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS self.improvement_candidates (
candidate_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
need_ref JSONB NOT NULL,
hypothesis TEXT NOT NULL,
expected_effect TEXT NOT NULL,
evaluation_plan TEXT NOT NULL,
must_not_worsen JSONB NOT NULL,
rollback_plan TEXT NOT NULL,
resource_cost TEXT NOT NULL,
creator TEXT NOT NULL,
independent_evaluator TEXT NOT NULL,
state TEXT NOT NULL,
evidence_refs JSONB NOT NULL,
created_at_unix_millis BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS self.personal_benchmark_cases (
case_id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
task_class TEXT NOT NULL,
evidence_refs JSONB NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL
);
GRANT SELECT, INSERT, UPDATE ON self.learning_signals, self.weakness_patterns, self.improvement_candidates, self.personal_benchmark_cases TO self_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,36 @@
BEGIN;
SET ROLE identity_role;
INSERT INTO identity.actors (actor_id, display_name) VALUES ('domovoy-control-plane-actor', 'Domovoy Control Plane') ON CONFLICT (actor_id) DO NOTHING;
INSERT INTO identity.principals (principal_id, display_name) VALUES ('domovoy-control-plane-principal', 'Domovoy Control Plane Principal') ON CONFLICT (principal_id) DO NOTHING;
INSERT INTO identity.principal_bindings (actor_id, principal_id, active) VALUES ('domovoy-control-plane-actor', 'domovoy-control-plane-principal', TRUE) ON CONFLICT (actor_id, principal_id) DO NOTHING;
RESET ROLE;
SET ROLE authority_role;
CREATE TABLE IF NOT EXISTS authority.delegated_control_plane_grants (
grant_id TEXT PRIMARY KEY, service_actor_id TEXT NOT NULL, service_principal_id TEXT NOT NULL, issuer_principal_id TEXT NOT NULL,
task_id TEXT NOT NULL, task_revision BIGINT NOT NULL, operations TEXT[] NOT NULL,
binding_id TEXT NOT NULL, provider_instance_id TEXT NOT NULL, model_descriptor_id TEXT NOT NULL,
issued_at_unix_millis BIGINT NOT NULL, expires_at_unix_millis BIGINT NOT NULL, architecture_revision TEXT NOT NULL, nonce TEXT NOT NULL,
revision BIGINT NOT NULL, status TEXT NOT NULL, revoked_at_unix_millis BIGINT NOT NULL DEFAULT 0,
CHECK (task_revision > 0), CHECK (cardinality(operations) > 0),
CHECK (operations <@ ARRAY['QUALIFY_COGNITIVE_BINDING','EVALUATE_COGNITIVE_BINDING','CANARY_COGNITIVE_BINDING','ACTIVATE_VALIDATED_COGNITIVE_BINDING']::TEXT[]),
CHECK (issued_at_unix_millis > 0), CHECK (expires_at_unix_millis > issued_at_unix_millis), CHECK (revision > 0),
CHECK (status IN ('ACTIVE','REVOKED')), CHECK (binding_id <> '' AND provider_instance_id <> '' AND model_descriptor_id <> ''),
CHECK (nonce <> '' AND architecture_revision <> '')
);
CREATE TABLE IF NOT EXISTS authority.delegated_control_plane_audit (
audit_id TEXT PRIMARY KEY, grant_id TEXT NOT NULL, service_actor_id TEXT NOT NULL, service_principal_id TEXT NOT NULL,
task_id TEXT NOT NULL, task_revision BIGINT NOT NULL, operation TEXT NOT NULL,
binding_id TEXT NOT NULL, provider_instance_id TEXT NOT NULL, model_descriptor_id TEXT NOT NULL,
decision TEXT NOT NULL, outcome TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (task_revision > 0), CHECK (recorded_at_unix_millis > 0),
CHECK (operation IN ('QUALIFY_COGNITIVE_BINDING','EVALUATE_COGNITIVE_BINDING','CANARY_COGNITIVE_BINDING','ACTIVATE_VALIDATED_COGNITIVE_BINDING','REVOKE_DELEGATED_CONTROL_PLANE_GRANT')),
CHECK (decision IN ('ALLOW','DENY','REVOKE')), CHECK (outcome IN ('PENDING','COMPLETED','FAILED','REVOKED'))
);
CREATE INDEX IF NOT EXISTS delegated_control_plane_audit_grant_idx ON authority.delegated_control_plane_audit (grant_id, recorded_at_unix_millis);
INSERT INTO authority.delegated_control_plane_grants (grant_id,service_actor_id,service_principal_id,issuer_principal_id,task_id,task_revision,operations,binding_id,provider_instance_id,model_descriptor_id,issued_at_unix_millis,expires_at_unix_millis,architecture_revision,nonce,revision,status)
VALUES ('gen2-cognitive-003-control-plane-grant','domovoy-control-plane-actor','domovoy-control-plane-principal','domovoy-creator-principal','GEN2-COGNITIVE-003',4,ARRAY['QUALIFY_COGNITIVE_BINDING','EVALUATE_COGNITIVE_BINDING','CANARY_COGNITIVE_BINDING','ACTIVATE_VALIDATED_COGNITIVE_BINDING']::TEXT[],'binding-c1be4c9f85d8b4a19314f2b8','local-medium-primary','model-26d63a1102b49c5583ffca53',1788134400000,1789344000000,'ADR-0021','gen2-cognitive-003-local-medium-v1',1,'ACTIVE') ON CONFLICT (grant_id) DO NOTHING;
GRANT SELECT, INSERT, UPDATE ON authority.delegated_control_plane_grants, authority.delegated_control_plane_audit TO authority_role;
RESET ROLE;
COMMIT;
@@ -0,0 +1,64 @@
BEGIN;
SET ROLE authority_role;
ALTER TABLE authority.delegated_control_plane_grants
ALTER COLUMN binding_id DROP NOT NULL,
ALTER COLUMN provider_instance_id DROP NOT NULL,
ALTER COLUMN model_descriptor_id DROP NOT NULL,
ADD COLUMN IF NOT EXISTS candidate_release_set_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_slot TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_capability_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_resource_scope TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_mode TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_target_kind TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_target_ref TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_target_digest TEXT NOT NULL DEFAULT '';
ALTER TABLE authority.delegated_control_plane_audit
ALTER COLUMN binding_id DROP NOT NULL,
ALTER COLUMN provider_instance_id DROP NOT NULL,
ALTER COLUMN model_descriptor_id DROP NOT NULL,
ADD COLUMN IF NOT EXISTS candidate_release_set_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_slot TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_capability_id TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_resource_scope TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_mode TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_target_kind TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS candidate_target_ref TEXT NOT NULL DEFAULT '',
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 (
(binding_id <> '' AND provider_instance_id <> '' AND model_descriptor_id <> '' AND candidate_release_set_id = '' AND candidate_slot = '' AND candidate_capability_id = '' AND candidate_resource_scope = '' AND candidate_mode = '' AND candidate_target_kind = '' AND candidate_target_ref = '') OR
(binding_id IS NULL AND provider_instance_id IS NULL AND model_descriptor_id IS NULL AND candidate_release_set_id <> '' AND candidate_slot <> '' AND candidate_capability_id <> '' AND candidate_resource_scope <> '' AND candidate_mode IN ('CANDIDATE','SHADOW') AND candidate_target_kind <> '' AND candidate_target_ref <> '' AND length(candidate_target_digest) = 64)
);
INSERT INTO authority.delegated_control_plane_grants (grant_id,service_actor_id,service_principal_id,issuer_principal_id,task_id,task_revision,operations,binding_id,provider_instance_id,model_descriptor_id,candidate_release_set_id,candidate_slot,candidate_capability_id,candidate_resource_scope,candidate_mode,candidate_target_kind,candidate_target_ref,candidate_target_digest,issued_at_unix_millis,expires_at_unix_millis,architecture_revision,nonce,revision,status)
VALUES ('gen2-capability-001-shadow-notification-grant','domovoy-control-plane-actor','domovoy-control-plane-principal','domovoy-creator-principal','GEN2-CAPABILITY-001',7,ARRAY['CREATE_CANDIDATE_CAPABILITY_BINDING','USE_CANDIDATE_CAPABILITY_BINDING','REVOKE_CANDIDATE_CAPABILITY_BINDING']::TEXT[],NULL,NULL,NULL,'assistant-slot-b','b','prospective-notification-delivery','synthetic-shadow-notification-v1','SHADOW','NOTIFICATION_SHADOW_RECEIVER','notification-receiver-candidate','f9f9fa49a0727444c280c092d5bb340f3a8f33cf325c5396f5ae177a6666da35',(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::BIGINT,(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP + INTERVAL '14 days') * 1000)::BIGINT,'ADR-0021','gen2-capability-001-shadow-notification-r7',1,'ACTIVE') ON CONFLICT (grant_id) DO NOTHING;
RESET ROLE;
SET ROLE capability_role;
CREATE TABLE IF NOT EXISTS capability.release_candidate_bindings (
binding_id TEXT NOT NULL, revision BIGINT NOT NULL, base_active_binding_id TEXT NOT NULL, base_active_binding_revision BIGINT NOT NULL,
release_set_id TEXT NOT NULL, slot TEXT NOT NULL, task_id TEXT NOT NULL, task_revision BIGINT NOT NULL, principal_id TEXT NOT NULL, capability_id TEXT NOT NULL, resource_scope TEXT NOT NULL, mode TEXT NOT NULL, target_kind TEXT NOT NULL, target_ref TEXT NOT NULL, target_digest TEXT NOT NULL, authority_grant_id TEXT NOT NULL,
target_origin TEXT NOT NULL, delivery_path TEXT NOT NULL, receipt_path TEXT NOT NULL, protocol TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL, expires_at_unix_millis BIGINT NOT NULL, status TEXT NOT NULL,
PRIMARY KEY (binding_id, revision),
CHECK (revision > 0 AND base_active_binding_revision > 0 AND task_revision > 0), CHECK (mode IN ('CANDIDATE','SHADOW')), CHECK (status IN ('ACTIVE','REVOKED')),
CHECK (expires_at_unix_millis > created_at_unix_millis), CHECK (target_origin <> '' AND delivery_path <> '' AND receipt_path <> '' AND target_kind <> '' AND target_ref <> '' AND length(target_digest) = 64 AND authority_grant_id <> '')
);
CREATE TABLE IF NOT EXISTS capability.release_candidate_binding_heads (
release_set_id TEXT NOT NULL, slot TEXT NOT NULL, task_id TEXT NOT NULL, principal_id TEXT NOT NULL, capability_id TEXT NOT NULL, resource_scope TEXT NOT NULL, mode TEXT NOT NULL,
binding_id TEXT NOT NULL, revision BIGINT NOT NULL, PRIMARY KEY (release_set_id,slot,task_id,principal_id,capability_id,resource_scope,mode)
);
CREATE TABLE IF NOT EXISTS capability.release_candidate_binding_audit (
audit_id BIGSERIAL PRIMARY KEY, binding_id TEXT NOT NULL, revision BIGINT NOT NULL, release_set_id TEXT NOT NULL, task_id TEXT NOT NULL, capability_id TEXT NOT NULL, resource_scope TEXT NOT NULL, mode TEXT NOT NULL, decision TEXT NOT NULL, reason TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (decision IN ('CREATED','RESOLVED','DENIED','REVOKED','EXPIRED'))
);
CREATE INDEX IF NOT EXISTS release_candidate_binding_audit_lookup_idx ON capability.release_candidate_binding_audit (release_set_id, task_id, capability_id, recorded_at_unix_millis DESC);
RESET ROLE;
COMMIT;
@@ -0,0 +1,15 @@
BEGIN;
SET ROLE commit_role;
CREATE TABLE IF NOT EXISTS commit.execution_receipts (
receipt_id TEXT PRIMARY KEY, action_attempt_id TEXT NOT NULL UNIQUE, executor_implementation_ref TEXT NOT NULL,
external_operation_ref TEXT NOT NULL DEFAULT '', external_request_id TEXT NOT NULL DEFAULT '', request_digest TEXT NOT NULL,
response_digest TEXT NOT NULL DEFAULT '', dispatch_at_unix_millis BIGINT NOT NULL, completed_at_unix_millis BIGINT NOT NULL DEFAULT 0,
executor_result TEXT NOT NULL, reconciliation_key TEXT NOT NULL, raw_artifact_ref TEXT NOT NULL DEFAULT '',
delivery_mode TEXT NOT NULL DEFAULT '', external_delivery_attempted BOOLEAN NOT NULL DEFAULT FALSE,
release_set_id TEXT NOT NULL DEFAULT '', task_id TEXT NOT NULL DEFAULT '', task_revision BIGINT NOT NULL DEFAULT 0,
capability_binding_id TEXT NOT NULL DEFAULT '', capability_binding_revision BIGINT NOT NULL DEFAULT 0, target_reference TEXT NOT NULL DEFAULT '',
CHECK (dispatch_at_unix_millis > 0), CHECK (executor_result <> ''), CHECK (delivery_mode IN ('','SHADOW')),
CHECK (delivery_mode <> 'SHADOW' OR external_delivery_attempted = FALSE)
);
RESET ROLE;
COMMIT;
@@ -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;
+158
View File
@@ -0,0 +1,158 @@
# This file deliberately owns only one inactive release slot. It joins the
# stable project's shared network as an external resource, but has a distinct
# Compose project name supplied by run-production-canary.sh. Do not add
# durable services or stable services here.
services:
research-runtime:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: research-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: :8087
RESEARCH_SOURCE_ROOT: /var/lib/research-sources
RESEARCH_HTTP_ALLOWED_HOSTS: assistant.cin.su,git.cin.su
RESEARCH_SEARCH_URL_TEMPLATE: ${RESEARCH_SEARCH_URL_TEMPLATE:-https://www.bing.com/search?q=%s}
RESEARCH_SEARCH_ALLOWED_HOSTS: ${RESEARCH_SEARCH_ALLOWED_HOSTS:-www.bing.com}
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
SECRET_BACKEND_ROOT: /run/secrets
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_research_sources:/var/lib/research-sources:ro
networks:
shared:
aliases: [research-runtime-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8087/healthz >/dev/null && wget -qO- http://localhost:8087/readyz >/dev/null"]
interval: 10s
timeout: 10s
retries: 20
core-runtime:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: core-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8080"
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
POSTGRES_USER: situation_role
POSTGRES_PASSWORD: ${SITUATION_DB_PASSWORD}
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
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
CONTROL_PLANE_SERVICE_CREDENTIAL_REF: credential://domovoy/control-plane/service-token/v1
CONTROL_PLANE_SERVICE_SECRET_ROOT: /run/control-plane-secrets
SELF_POSTGRES_USER: self_role
SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
INTERACTION_POSTGRES_USER: interaction_role
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
SECRET_BACKEND_ROOT: /run/secrets
COGNITIVE_ADMISSION_POLICY_ID: creator-advisory-local-v1
COGNITIVE_ADMISSION_MAX_CONCURRENT: ${COGNITIVE_ADMISSION_MAX_CONCURRENT:-1}
COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR: ${COGNITIVE_ADMISSION_MAX_ACCEPTED_PER_HOUR:-60}
COGNITIVE_ADMISSION_MAX_INPUT_BYTES: ${COGNITIVE_ADMISSION_MAX_INPUT_BYTES:-81920}
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
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
networks:
shared:
aliases: [core-runtime-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
cognitive-worker:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8083"
CORE_RUNTIME_URL: http://core-runtime-candidate: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-gateway:8080/v1}
LOCAL_MEDIUM_PROVIDER_API_KEY_FILE: ${LOCAL_MEDIUM_PROVIDER_API_KEY_FILE:-/run/secrets/llm_local_gateway_api_key}
LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF: ${LOCAL_MEDIUM_PROVIDER_CREDENTIAL_REF:-credential://domovoy/local-medium/provider-api-key/v1}
LOCAL_GPU_PROVIDER_TIMEOUT: ${LOCAL_GPU_PROVIDER_TIMEOUT:-60s}
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
LOCAL_IMAGE_PROVIDER_KIND: ${LOCAL_IMAGE_PROVIDER_KIND:-comfyui}
LOCAL_IMAGE_PROVIDER_URL: ${LOCAL_IMAGE_PROVIDER_URL:-http://comfyui:8188}
LOCAL_IMAGE_MODEL: ${LOCAL_IMAGE_MODEL:-z_image_turbo_bf16.safetensors}
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
COGNITIVE_ALLOW_CANARY: "true"
volumes:
- assistant_core_secrets:/run/secrets:ro
networks:
shared:
aliases: [cognitive-worker-candidate]
llm-local: {}
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8083/healthz >/dev/null && wget -qO- http://localhost:8083/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
creator-web:
build:
context: ..
dockerfile: deploy/images/runtime.Dockerfile
args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
restart: "no"
environment:
LISTEN_ADDR: ":8084"
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
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:8090
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
networks:
shared:
aliases: [creator-web-candidate]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
volumes:
assistant_core_secrets:
external: true
assistant_control_plane_service_secrets:
name: assistant_control_plane_service_secrets
external: true
assistant_research_sources:
name: assistant_research_sources
external: true
networks:
shared:
name: assistant_default
external: true
llm-local:
name: llm-local_default
external: true
+24 -52
View File
@@ -66,10 +66,15 @@ 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
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
CONTROL_PLANE_SERVICE_CREDENTIAL_REF: credential://domovoy/control-plane/service-token/v1
CONTROL_PLANE_SERVICE_SECRET_ROOT: /run/control-plane-secrets
SELF_POSTGRES_USER: self_role
SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
INTERACTION_POSTGRES_USER: interaction_role
@@ -83,23 +88,13 @@ services:
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null && wget -qO- http://localhost:8080/readyz >/dev/null"]
interval: 10s
timeout: 20s
retries: 20
core-runtime-candidate:
profiles: ["assistant-canary"]
extends:
service: core-runtime
restart: "no"
environment:
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
depends_on:
postgres: { condition: service_healthy }
research-runtime-candidate: { condition: service_started }
agent-canary:
profiles: ["agent-canary"]
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-canary, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
@@ -223,14 +218,6 @@ services:
timeout: 10s
retries: 20
research-runtime-candidate:
profiles: ["assistant-canary"]
extends:
service: research-runtime
restart: "no"
depends_on:
postgres: { condition: service_healthy }
continuity-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -256,6 +243,9 @@ services:
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
SECRET_BACKEND_ROOT: /run/secrets
CORE_RUNTIME_URL: http://core-runtime:8080
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN}
@@ -264,8 +254,20 @@ services:
# External mutation credentials belong here only via Portainer/Docker secrets/env injection.
depends_on: [core-runtime, continuity-runtime, notification-receiver]
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
- assistant_notification_writer_secret:/run/notification-writer:ro
secret-control-plane:
profiles: ["secret-control-plane"]
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: secret-control-plane, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
user: "0:0"
restart: "no"
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}" } }
restart: unless-stopped
@@ -299,17 +301,6 @@ services:
timeout: 20s
retries: 20
cognitive-worker-candidate:
profiles: ["assistant-canary"]
extends:
service: cognitive-worker
restart: "no"
environment:
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
depends_on:
core-runtime-candidate: { condition: service_started }
cognitive-lifeboat: { condition: service_healthy }
cognitive-lifeboat:
image: ghcr.io/ggml-org/llama.cpp:server
restart: unless-stopped
@@ -386,33 +377,14 @@ services:
timeout: 3s
retries: 10
creator-web-candidate:
profiles: ["assistant-canary"]
extends:
service: creator-web
restart: "no"
environment:
CORE_RUNTIME_URL: http://core-runtime-candidate:8080
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:8090
depends_on:
core-runtime-candidate: { condition: service_started }
continuity-runtime: { condition: service_started }
commit-runtime: { condition: service_started }
cognitive-worker-candidate: { condition: service_healthy }
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8084/healthz >/dev/null"]
interval: 10s
timeout: 5s
retries: 20
volumes:
assistant_pg:
assistant_nats:
assistant_core_secrets:
external: true
assistant_control_plane_service_secrets:
name: assistant_control_plane_service_secrets
external: true
assistant_notification_receipts:
assistant_notification_writer_secret:
external: true
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
# Stable-only production updater. Candidate slots are managed exclusively by
# their isolated release command and Supervisor; this controller never starts
# or reconciles a candidate service.
BASE_DIR="${BASE_DIR:-/home/m/assistant-deploy}"
REPO_DIR="${REPO_DIR:-$BASE_DIR/repo}"
ENV_FILE="${ENV_FILE:-$BASE_DIR/production-compose.env}"
COMPOSE_FILE="$REPO_DIR/deploy/docker-compose.prod.yml"
LOG_FILE="${LOG_FILE:-$BASE_DIR/deploy.log}"
LOCK_FILE="${LOCK_FILE:-/tmp/assistant-stable-deploy.lock}"
stable_services=(postgres core-runtime continuity-runtime commit-runtime cognitive-worker creator-web notification-receiver research-runtime agent-worker work-graph-runtime)
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
log() { printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*" >>"$LOG_FILE"; }
fail() { log "FAILED: $*"; exit 1; }
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
cd "$REPO_DIR"
git fetch --prune origin main >>"$LOG_FILE" 2>&1
git merge --ff-only origin/main >>"$LOG_FILE" 2>&1 || fail "stable checkout is not a fast-forward to origin/main"
revision="$(git rev-parse HEAD)"
[[ "$revision" =~ ^[0-9a-f]{40}$ ]] || fail "invalid source revision"
if ! REPO_DIR="$REPO_DIR" ENV_FILE="$ENV_FILE" bash "$REPO_DIR/deploy/apply-migrations.sh" >>"$LOG_FILE" 2>&1; then
fail "migration application failed"
fi
compose config >/dev/null 2>>"$LOG_FILE" || fail "stable compose config validation failed"
all_current=1
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
label="$(docker inspect "$container" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || true)"
[[ "$label" == "$revision" ]] || all_current=0
done
if [[ "$all_current" == 1 ]]; then log "SKIP stable revision=$revision"; exit 0; fi
log "BUILD stable revision=$revision"
ASSISTANT_SOURCE_REVISION="$revision" compose build --build-arg "SOURCE_REVISION=$revision" "${stable_services[@]}" >>"$LOG_FILE" 2>&1
log "UP stable revision=$revision"
ASSISTANT_SOURCE_REVISION="$revision" compose up -d --no-build "${stable_services[@]}" >>"$LOG_FILE" 2>&1
for attempt in $(seq 1 30); do
if curl -fsS --max-time 5 https://assistant.cin.su/healthz >/dev/null 2>&1; then break; fi
[[ "$attempt" == 30 ]] && fail "public health failed"
sleep 10
done
for service in "${stable_services[@]}"; do
container="assistant-${service}-1"
label="$(docker inspect "$container" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' 2>/dev/null || true)"
[[ "$label" == "$revision" ]] || fail "artifact drift container=$container"
done
log "OK stable revision=$revision"
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
# This is the only production writer for the dedicated control-plane machine
# credential volume. It never receives, prints, or exports a secret value.
BASE_DIR="${BASE_DIR:-/home/m/assistant-deploy}"
REPO_DIR="${REPO_DIR:-$BASE_DIR/repo}"
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) ;;
*) echo "unsupported machine credential action" >&2; exit 2 ;;
esac
[[ -f "$ENV_FILE" ]] || { echo "production compose environment is missing" >&2; exit 1; }
[[ -f "$COMPOSE_FILE" ]] || { echo "production compose file is missing" >&2; exit 1; }
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
# `docker volume create` is idempotent and does not inspect or expose content.
docker volume create "$VOLUME" >/dev/null
revision="$(git -C "$REPO_DIR" rev-parse HEAD)"
ASSISTANT_SOURCE_REVISION="$revision" compose build --build-arg "SOURCE_REVISION=$revision" secret-control-plane >/dev/null
if [[ "$ACTION" == "rotate" ]]; then
# Only bounded, non-secret rotation reasons can reach the audit trail.
reason="${ROTATION_REASON:-owner-requested-rotation}"
case "$reason" in
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 -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 -e "SECRET_CONTROL_PLANE_ROOT=$SECRET_ROOT" secret-control-plane -credential "$CREDENTIAL" ensure
fi
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
# Regression: the wrapper accepts only an operation name and never places a
# plaintext credential on a Docker/Compose command line.
script="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/provision-machine-credential.sh"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/bin" "$tmp/repo/deploy"
cp "$script" "$tmp/repo/deploy/provision-machine-credential.sh"
printf 'placeholder\n' > "$tmp/repo/deploy/docker-compose.prod.yml"
printf 'placeholder\n' > "$tmp/env"
cat > "$tmp/bin/docker" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "${CAPTURE:?}"
SH
cat > "$tmp/bin/git" <<'SH'
#!/usr/bin/env bash
printf '0123456789abcdef0123456789abcdef01234567\n'
SH
chmod +x "$tmp/bin/docker" "$tmp/bin/git" "$tmp/repo/deploy/provision-machine-credential.sh"
capture="$tmp/capture"
PATH="$tmp/bin:$PATH" CAPTURE="$capture" REPO_DIR="$tmp/repo" ENV_FILE="$tmp/env" "$tmp/repo/deploy/provision-machine-credential.sh" ensure >/dev/null
grep -qx 'volume create assistant_control_plane_service_secrets' "$capture"
grep -q -- '--profile secret-control-plane run --rm --no-deps secret-control-plane ensure' "$capture"
PATH="$tmp/bin:$PATH" CAPTURE="$capture" REPO_DIR="$tmp/repo" ENV_FILE="$tmp/env" ROTATION_REASON=owner-requested-rotation "$tmp/repo/deploy/provision-machine-credential.sh" rotate >/dev/null
grep -q -- '--profile secret-control-plane run --rm --no-deps secret-control-plane -reason owner-requested-rotation rotate' "$capture"
if grep -Eqi '(token=|password=|secret=|authorization:)' "$capture"; then
echo "provisioning wrapper passed credential material" >&2
exit 1
fi
+37 -51
View File
@@ -1,57 +1,54 @@
#!/usr/bin/env bash
set -euo pipefail
# Candidate services are never part of the stable Compose project. The slot
# Compose file may use only declared external networks and durable resources.
BASE_DIR="${REPO_DIR:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
ENV_FILE="${ENV_FILE:-/home/m/assistant-deploy/production-compose.env}"
COMPOSE_FILE="$BASE_DIR/deploy/docker-compose.prod.yml"
CANDIDATE_FILE="$BASE_DIR/deploy/docker-compose.candidate-slot.yml"
SUPERVISOR_CONTROL_URL="${SUPERVISOR_CONTROL_URL:-http://127.0.0.1:${SUPERVISOR_CONTROL_PORT:-18085}}"
REVISION="${1:?candidate revision is required}"
SLOT="${CANDIDATE_SLOT:-b}"
STABLE_PROJECT="${STABLE_PROJECT:-assistant}"
CANDIDATE_PROJECT="${CANDIDATE_PROJECT:-assistant-slot-$SLOT}"
HEALTH_ATTEMPTS="${HEALTH_ATTEMPTS:-360}"
PROMOTE_AFTER_CANARY="${PROMOTE_AFTER_CANARY:-true}"
set -a
. "$ENV_FILE"
set +a
[[ "$SLOT" =~ ^[a-z0-9-]+$ ]] || { echo "invalid candidate slot" >&2; exit 2; }
[[ "$CANDIDATE_PROJECT" != "$STABLE_PROJECT" ]] || { echo "candidate and stable Compose projects must differ" >&2; exit 2; }
compose() { docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"; }
candidate_compose() { docker compose -p "$CANDIDATE_PROJECT" --env-file "$ENV_FILE" -f "$CANDIDATE_FILE" "$@"; }
control() { curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL$1" "${@:2}" >/dev/null; }
fail_rollback() {
control /v1/traffic/rollback || true
control /v1/releases/rollback || true
echo "production ReleaseSet canary failed; stable ReleaseSet rollback requested" >&2
echo "isolated candidate ReleaseSet failed; stable ReleaseSet rollback requested" >&2
exit 1
}
services=(creator-web-candidate core-runtime-candidate cognitive-worker-candidate research-runtime-candidate)
containers=(assistant-creator-web-candidate-1 assistant-core-runtime-candidate-1 assistant-cognitive-worker-candidate-1 assistant-research-runtime-candidate-1)
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
container="assistant-${service}-1"
container="${STABLE_PROJECT}-${service}-1"
id=$(docker inspect -f '{{.Id}}' "$container" 2>/dev/null || true)
[[ -n "$id" ]] || { echo "stable container missing: $container" >&2; return 1; }
printf '%s=%s\n' "$service" "$id"
done
}
assert_stable_ids_unchanged() {
local after
after=$(stable_ids) || fail_rollback
[[ "$after" == "$stable_ids_before" ]] || {
echo "stable container IDs changed during candidate operation" >&2
echo "stable container IDs changed during isolated candidate operation" >&2
diff -u <(printf '%s\n' "$stable_ids_before") <(printf '%s\n' "$after") >&2 || true
fail_rollback
}
}
stable_ids_before=$(stable_ids) || exit 1
compose --profile assistant-canary build --build-arg "SOURCE_REVISION=$REVISION" "${services[@]}"
candidate_container() { printf '%s-%s-1' "$CANDIDATE_PROJECT" "$1"; }
wait_healthy() {
local container="$1" status="" ready=""
ready=""
for attempt in $(seq 1 "$HEALTH_ATTEMPTS"); do
status=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' "$container" 2>/dev/null || true)
if [[ "$status" == healthy ]]; then ready=true; break; fi
@@ -60,68 +57,57 @@ wait_healthy() {
[[ "$ready" == true ]] || fail_rollback
}
compose --profile assistant-canary up -d --no-build --no-deps research-runtime-candidate core-runtime-candidate
wait_healthy assistant-research-runtime-candidate-1
wait_healthy assistant-core-runtime-candidate-1
compose --profile assistant-canary up -d --no-build --no-deps cognitive-worker-candidate
wait_healthy assistant-cognitive-worker-candidate-1
compose --profile assistant-canary up -d --no-build --no-deps creator-web-candidate
wait_healthy assistant-creator-web-candidate-1
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
for container in "${containers[@]}"; do
image_revision=$(docker inspect -f '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$container" 2>/dev/null || true)
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)
[[ "$image_revision" == "$REVISION" ]] || fail_rollback
done
assert_stable_ids_unchanged
if [[ "${PREPARE_ONLY:-false}" == true ]]; then
echo "production ReleaseSet candidate prepared revision=$REVISION"
echo "isolated candidate ReleaseSet prepared slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
exit 0
fi
release_json=$(cat <<JSON
{
"id":"candidate-$REVISION",
"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"}
}
}
{"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
stable_revision=$(printf '%s' "$stable_json" | jq -r '.current.version // "unknown"')
stable_set=$(cat <<JSON
{"id":"stable-$stable_revision","revision":"$stable_revision","components":{
"creator-web":{"name":"creator-web","revision":"$stable_revision","upstream":"http://creator-web:8084","health_url":"http://creator-web:8084/healthz","ready_url":"http://creator-web:8084/readyz"},
"core-runtime":{"name":"core-runtime","revision":"$stable_revision","upstream":"http://core-runtime:8080","health_url":"http://core-runtime:8080/healthz","ready_url":"http://core-runtime:8080/readyz"},
"cognitive-worker":{"name":"cognitive-worker","revision":"$stable_revision","upstream":"http://cognitive-worker:8083","health_url":"http://cognitive-worker:8083/healthz","ready_url":"http://cognitive-worker:8083/readyz"},
"research-runtime":{"name":"research-runtime","revision":"$stable_revision","upstream":"http://research-runtime:8087","health_url":"http://research-runtime:8087/healthz","ready_url":"http://research-runtime:8087/readyz"}}}
{"id":"stable-$stable_revision","slot":"legacy-stable","revision":"$stable_revision","components":{"creator-web":{"name":"creator-web","revision":"$stable_revision","upstream":"http://creator-web:8084","health_url":"http://creator-web:8084/healthz","ready_url":"http://creator-web:8084/healthz"},"core-runtime":{"name":"core-runtime","revision":"$stable_revision","upstream":"http://core-runtime:8080","health_url":"http://core-runtime:8080/healthz","ready_url":"http://core-runtime:8080/readyz"},"cognitive-worker":{"name":"cognitive-worker","revision":"$stable_revision","upstream":"http://cognitive-worker:8083","health_url":"http://cognitive-worker:8083/healthz","ready_url":"http://cognitive-worker:8083/readyz"},"research-runtime":{"name":"research-runtime","revision":"$stable_revision","upstream":"http://research-runtime:8087","health_url":"http://research-runtime:8087/healthz","ready_url":"http://research-runtime:8087/readyz"}}}
JSON
)
curl -fsS --max-time 10 -X POST "$SUPERVISOR_CONTROL_URL/v1/releases/baseline-set" -H 'Content-Type: application/json' --data "$stable_set" >/dev/null || fail_rollback
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
observation=$(printf '{"release_id":"candidate-%s","ready":true,"reason":"all ReleaseSet healthchecks 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' "$REVISION" "$(date +%s%3N)" "$REVISION" "$REVISION" "$REVISION" "$REVISION")
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
assert_stable_ids_unchanged
headers=$(mktemp)
trap 'rm -f "$headers"' EXIT
if ! curl -fsS --max-time 10 -D "$headers" -H 'X-Assistant-Canary: true' "https://assistant.cin.su/login" -o /dev/null; then fail_rollback; fi
grep -qi '^X-Assistant-Traffic-Target: candidate' "$headers" || fail_rollback
if [[ "$PROMOTE_AFTER_CANARY" != true ]]; then
echo "isolated candidate ReleaseSet canary ready slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
exit 0
fi
control /v1/releases/promote || fail_rollback
assert_stable_ids_unchanged
echo "production ReleaseSet canary promoted revision=$REVISION"
echo "isolated candidate ReleaseSet promoted slot=$SLOT project=$CANDIDATE_PROJECT revision=$REVISION"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
# Safe diagnostics only: artifact existence/readability and mount topology.
# This script never reads or prints credential content.
TOKEN_PATH="/run/control-plane-secrets/control_plane_service_token"
VOLUME="assistant_control_plane_service_secrets"
authorized=(assistant-core-runtime-1 assistant-commit-runtime-1)
unauthorized=(assistant-creator-web-1 assistant-cognitive-worker-1 assistant-research-runtime-1 assistant-continuity-runtime-1 assistant-agent-worker-1)
for container in "${authorized[@]}"; do
docker inspect "$container" >/dev/null
docker exec "$container" sh -c "test -r '$TOKEN_PATH'"
docker inspect -f '{{range .Mounts}}{{if eq .Name "assistant_control_plane_service_secrets"}}{{.Destination}} {{.RW}}{{end}}{{end}}' "$container" | grep -qx '/run/control-plane-secrets false'
done
for container in "${unauthorized[@]}"; do
docker inspect "$container" >/dev/null
if docker inspect -f '{{range .Mounts}}{{println .Name}}{{end}}' "$container" | grep -qx "$VOLUME"; then
echo "unauthorized runtime has the control-plane credential mount" >&2
exit 1
fi
done
echo "machine credential mount topology verified"
+1 -1
View File
@@ -1 +1 @@
{"task_file":"dev/tasks/GEN2-COMPAT-OBS-001-production-compatibility-observation.json","task_id":"GEN2-COMPAT-OBS-001","task_revision":2}
{"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"]
}
@@ -0,0 +1,28 @@
{
"task_id": "GEN2-AUTHORITY-001",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Add a bounded, non-interactive service-principal and task-scoped delegated Authority path for existing governed Cognitive binding lifecycle operations without browser impersonation, a second Authority owner, or direct Capability mutation.",
"requirement_ids": ["REQ-GEN2-AUTHORITY-001-DELEGATED-CONTROL-PLANE", "REQ-GEN2-COGNITIVE-003-Q8-Q17"],
"allowed_change_scope": {
"modules": ["Identity service principal bootstrap", "Authority delegated grant validation and audit", "Core composition and typed control-plane endpoint", "existing Capability lifecycle invocation", "production secret-reference wiring", "targeted evidence"],
"paths": ["architecture/adrs/ADR-0021-delegated-noninteractive-control-plane-authority.md", "contracts/authority/types.go", "contracts/authority/control_plane.go", "contracts/authority/*_test.go", "contracts/secrets/types.go", "modules/authority/domain/**", "modules/authority/application/**", "modules/authority/ports/**", "adapters/postgres/authority_repository.go", "adapters/postgres/authority_repository_test.go", "cmd/core-runtime/main.go", "cmd/core-runtime/control_plane_authority.go", "cmd/core-runtime/control_plane_authority_test.go", "cmd/commit-runtime/main.go", "cmd/commit-runtime/control_plane_authority.go", "cmd/commit-runtime/control_plane_authority_test.go", "database/migrations/056_delegated_control_plane_authority.sql", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "docs/CREDENTIALS_AND_SECRETS.md", "docs/evidence/**", "docs/PROJECT_STATUS.md", "development/GEN2_TASK_INDEX.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-AUTHORITY-001-delegated-noninteractive-control-plane-authority.json"]
},
"forbidden_change_scope": {
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "contracts/identity/**", "modules/identity/**", "adapters/postgres/identity_repository.go", "contracts/capability/**", "modules/capability/**", "adapters/postgres/capability_repository.go", "cmd/cognitive-worker/**", "database/bootstrap/**", "security/**", "go.mod", "go.sum", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**"]
},
"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 Capability cognitive promotion requests/builders", "existing CredentialRef file resolver"], "may_change": ["typed Authority delegated-grant and audit contract", "typed secret purpose for service authentication"]},
"semantic_owner": "Identity remains the sole owner of service identity. Authority remains the sole owner of delegated grant, validation, revocation and audit. Capability remains the sole owner of cognitive binding/evaluation lifecycle.",
"canonical_state": ["service authentication is distinct from delegated authority", "delegation is task/capability/resource/time scoped and default-deny", "service principal cannot issue, extend, revoke or broaden its own authority", "Capability activation continues to require independent evaluation and owner-local lifecycle validation", "audit contains no secret or user payload"],
"state_transitions": ["service credential authentication -> delegated grant validation -> existing Capability CANARY/ACTIVE validation -> owner-local lifecycle persistence -> Authority audit", "grant ACTIVE -> REVOKED/EXPIRED denies new privileged actions while retaining audit history"],
"side_effects_and_privileges": ["Core receives only an Authority CredentialRef and service authentication SecretRef through the protected file backend", "the positive new_privileges and new_database_owner_roles values are devctl compatibility gates for deploy/database surfaces; no new role, broad administrator privilege, or ownership change is granted", "no external effect bypasses Commit/Executor where those owners already govern it"],
"persistence_and_migrations": ["additive owner-local Authority tables only; seed exactly one bounded service identity and GEN2-COGNITIVE-003 grant through owner roles", "historical audit is retained on rollback"],
"failure_retry_restart": ["authentication or validation failure is fail-closed", "grant state is read durably on each request", "no in-memory permission cache", "revocation prevents new actions immediately", "existing Capability revision preconditions prevent replay or duplicate promotion"],
"assumptions": ["the protected assistant_core_secrets control plane can materialize a non-empty control-plane service credential without exposing its value", "the current production schema has migrations through 055", "the owner decision authorizes a bounded GEN2-COGNITIVE-003 grant only, not future task grants"],
"acceptance_scenarios": ["valid grant permits only its declared Cognitive lifecycle operation and binding revision", "different binding, missing grant, expired grant and revoked grant are denied", "service principal cannot create or extend grants", "restart uses durable grant state", "audit is recorded without payload/secret", "browser session is not required", "existing independent Capability evaluation remains a mandatory promotion precondition"],
"required_tests": ["targeted go test", "targeted go vet", "migration/compose validation", "preflight", "check-diff", "independent", "architecture/security 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"]
}
@@ -0,0 +1,37 @@
{
"task_id": "GEN2-CAPABILITY-001",
"task_revision": 7,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Add a Capability-owned, revisioned and time-bounded candidate/shadow binding lifecycle with typed CandidateReleaseContext and delegated task authority, so candidate side-effecting operations cannot fall back to stable ACTIVE bindings.",
"requirement_ids": ["REQ-GEN2-CAPABILITY-001-RELEASE-SCOPED-BINDING", "REQ-GEN2-DEPLOY-004-DYNAMIC-CLOSURE", "REQ-GEN2-AUTHORITY-001-TASK-SCOPED-DELEGATION"],
"allowed_change_scope": {
"modules": ["Capability candidate/shadow binding lifecycle and resolution", "Authority delegated grant validation/audit for candidate binding operations", "Core and Commit runtime typed composition", "candidate deployment/supervisor readiness integration", "notification shadow receiver acceptance"],
"paths": [
"contracts/capability/**", "contracts/authority/types.go", "contracts/authority/control_plane.go", "contracts/authority/*_test.go", "contracts/commit/execution_receipt.go", "modules/commit/ports/executor.go",
"modules/capability/domain/**", "modules/capability/ports/**", "modules/authority/domain/**", "modules/authority/ports/**", "modules/commit/domain/commit.go", "modules/commit/domain/commit_test.go",
"adapters/postgres/capability_repository.go", "adapters/postgres/capability_repository_test.go", "adapters/postgres/authority_repository.go", "adapters/postgres/authority_repository_test.go", "adapters/postgres/commit_repository.go", "adapters/postgres/commit_repository_test.go", "adapters/notificationexecutor/**",
"cmd/core-runtime/main.go", "cmd/core-runtime/control_plane_authority.go", "cmd/core-runtime/control_plane_authority_test.go",
"cmd/commit-runtime/main.go", "cmd/commit-runtime/control_plane_authority.go", "cmd/commit-runtime/control_plane_authority_test.go", "cmd/commit-runtime/creator_clients.go", "cmd/commit-runtime/notification_executor.go", "cmd/commit-runtime/notification_executor_test.go", "cmd/commit-runtime/execution.go", "cmd/commit-runtime/execution_test.go", "cmd/commit-runtime/candidate_release_context.go", "cmd/commit-runtime/candidate_release_context_test.go",
"cmd/notification-receiver/**", "database/migrations/057_release_scoped_candidate_capability_bindings.sql", "database/migrations/058_commit_execution_receipts.sql",
"deploy/docker-compose.candidate-slot.yml", "deploy/run-production-canary.sh", "runtime/supervisor/**", "cmd/supervisor/**", "tests/deployment/**",
"docs/evidence/**", "docs/DEPLOYMENT.md", "docs/PROJECT_STATUS.md", "development/GEN2_TASK_INDEX.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-CAPABILITY-001-release-scoped-candidate-shadow-bindings.json", "dev/tasks/GEN2-DEPLOY-004-candidate-commit-runtime-releaseset.json",
"architecture/adrs/ADR-0021-delegated-noninteractive-control-plane-authority.md", "contracts/secrets/types.go", "modules/authority/application/**",
"cmd/core-runtime/control_plane_authority.go", "cmd/commit-runtime/control_plane_authority.go", "database/migrations/056_delegated_control_plane_authority.sql", "deploy/docker-compose.prod.yml", "docs/CREDENTIALS_AND_SECRETS.md", "dev/tasks/GEN2-AUTHORITY-001-delegated-noninteractive-control-plane-authority.json"
]
},
"forbidden_change_scope": {"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "contracts/identity/**", "modules/identity/**", "adapters/postgres/identity_repository.go", "database/bootstrap/**", "security/**", "go.mod", "go.sum", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**"]},
"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 Capability Binding and repository owner", "existing Authority delegated service principal/grant/audit", "existing Commit executor and receipt/outcome contracts", "existing Supervisor ReleaseSet control plane"], "may_change": ["typed CandidateReleaseContext and candidate binding resolution contract", "typed delegated Authority operation/resource for create/use/revoke candidate binding", "Capability owner repository ports", "candidate ReleaseSet readiness evidence"]},
"semantic_owner": "Capability remains the sole owner of capability binding, revision, resolution, lifecycle and binding promotion/retirement. Authority remains the sole owner of delegated grant validation/revocation/audit. Commit remains the sole owner of external action commit/execution state.",
"canonical_state": ["stable ACTIVE binding remains immutable during candidate acceptance", "candidate/shadow binding is a Capability-owned separate release-scoped revision, not a second owner", "candidate side-effecting resolution is fail-closed when the required candidate/shadow binding is absent", "no raw endpoint or secret crosses Core/Commit to bypass Capability", "shadow receipt is never a delivery WorldOutcome"],
"state_transitions": ["Authority grant -> Capability candidate/shadow binding create -> candidate typed resolve/use -> audit/receipt -> explicit revoke or expiry", "candidate binding never automatically becomes ACTIVE", "stable resolution without CandidateReleaseContext -> stable ACTIVE only"],
"side_effects_and_privileges": ["service authentication remains separate from Authority", "create/use/revoke operations require a task/release/capability/resource/time-scoped delegated grant", "candidate notification uses a synthetic shadow target and no production destination", "candidate binding cannot extend TTL, alter resource/release, grant itself rights, or promote itself"],
"persistence_and_migrations": ["additive Capability-owned release-scoped candidate binding and auditable resolution persistence only", "Authority audit stays owner-local", "no secret value is persisted", "rollback revokes/expires candidate binding and retains historical audit/evidence"],
"failure_retry_restart": ["missing/wrong-release/wrong-task/expired/revoked candidate binding denies without ACTIVE fallback", "restart reloads durable candidate binding and Authority state subject to TTL", "candidate binding creation is revisioned/idempotent", "shadow receiver failure produces a non-delivery receipt/outcome and does not create a real external action"],
"assumptions": ["migration 057 is globally free after existing migration 056", "the existing protected service credential is usable only as authentication and is never read or logged", "the notification receiver can be given a bounded shadow non-delivery mode without modifying production delivery state"],
"acceptance_scenarios": ["stable resolution returns only ACTIVE binding even when candidate bindings exist", "valid candidate release context plus grant resolves candidate/shadow binding", "wrong release/task, expired or revoked candidate binding denies", "missing required candidate binding denies with no stable fallback", "candidate binding cannot self-promote or alter its scope", "restart retains valid candidate binding/audit and revoked state", "synthetic candidate notification yields SHADOW_CAPTURED/NOT_DELIVERED_SHADOW with zero external delivery", "candidate dynamic dependency evidence identifies the notification receiver target without endpoint or secret logging"],
"required_tests": ["FAST: targeted go test ./modules/capability/... ./modules/authority/... ./adapters/postgres ./cmd/core-runtime ./cmd/commit-runtime ./cmd/notification-receiver ./runtime/supervisor ./cmd/supervisor", "FAST: targeted go vet for changed packages", "FAST: migration and compose config validation", "FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "FAST independent: go run ./cmd/devctl/independent", "FAST whitespace: git diff --check gen2-integration --", "MILESTONE: Capability/Deploy/Authority candidate-shadow lifecycle and rollback evidence"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,64 @@
{
"task_id": "GEN2-COGNITIVE-002",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Восстановить доказанно рабочий production cognitive route для обычного household и memory-recall запроса через существующие Capability и Cognitive Gateway boundaries, без подмены primary route Lifeboat-ом.",
"requirement_ids": ["REQ-GEN2-COGNITIVE-002-TRACE", "REQ-GEN2-COGNITIVE-002-ROUTING", "REQ-GEN2-COGNITIVE-002-RECOVERY"],
"allowed_change_scope": {
"modules": ["capability", "cognitive-worker", "core-runtime", "interaction", "production deployment"],
"paths": [
"cmd/cognitive-worker/**",
"cmd/core-runtime/**",
"modules/interaction/domain/gen2_context.go",
"modules/interaction/domain/gen2_context_test.go",
"adapters/postgres/interaction_repository.go",
"contracts/capability/**",
"contracts/cognitive/**",
"modules/capability/**",
"adapters/models/**",
"adapters/postgres/capability_repository.go",
"deploy/run-production-canary.sh",
"deploy/docker-compose.candidate-slot.yml",
"tests/**",
"docs/evidence/**",
"docs/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-COGNITIVE-002-restore-production-cognitive-route.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "database/**", "go.mod", "go.sum", ".gitea/workflows/**", "GEN2-028", "GEN2-029", "GEN2-030"]
},
"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 Capability binding lifecycle", "existing cognitive gateway/advisory contracts", "existing Supervisor ReleaseSet control plane"],
"may_change": ["typed capability/provider selection and invocation wiring only within declared paths", "the existing Interaction Gen2 retrieval projection for bounded, scoped human assertions; no new Memory owner or reader"]
},
"semantic_owner": "Capability remains the sole owner of provider/model/binding lifecycle; cognitive-worker invokes providers through adapters; Core consumes the governed gateway; Supervisor alone controls traffic promotion and rollback.",
"canonical_state": ["existing Capability provider/model/binding/evaluation evidence", "canonical Interaction human utterance history and existing typed ProjectReference read through the existing Gen2 retrieval projection", "no new canonical writer, owner, schema, role, or migration"],
"state_transitions": ["trace stable and candidate loaded binding heads -> correct stale/divergent lifecycle or invocation defect -> governed repair/qualification -> scoped Interaction projection selects bounded current human assertions -> isolated candidate validation -> Supervisor canary -> promote or rollback"],
"side_effects_and_privileges": ["production candidate deployment and Supervisor canary/promotion are explicitly authorized", "adapters/models/** may change only after trace proves a model adapter, provider endpoint, injection, timeout, rate-limit, context budget, or invocation defect there", "cmd/core-runtime/** may change only after trace proves Core-to-worker request construction, route selection, or response handling is causal", "modules/interaction/domain/gen2_context.go may change only to extend the existing bounded Gen2 read projection over canonical Interaction utterances with Principal hard filtering, current applicability and relevance selection; it must not write state, become a Memory owner, or promote PersonalMemory V1 to primary", "adapters/postgres/interaction_repository.go may only read the already-persisted typed interaction.ProjectReference records for the selected canonical Interaction utterances; it may not create schema, write state, or bypass Interaction ownership", "no direct SQL semantic mutation", "no credential, cookie, token, prompt, or user content read/emitted", "Lifeboat may be observed as fallback but cannot mask Local MEDIUM primary failure"],
"persistence_and_migrations": ["no schema/migration change is authorized", "adapters/postgres/capability_repository.go may change only after trace proves durable Capability head/state loading diverges from the correct persisted state; reuse its existing repository boundary"],
"failure_retry_restart": ["discovery health, invocation health, and availability remain distinct", "CPU/GPU gaps must not suppress a valid Local MEDIUM", "restart reloads a durable active/validated binding head", "failed candidate rolls traffic to Supervisor LKG without stable recreation"],
"assumptions": ["isolated candidate slot B and Supervisor controls from GEN2-DEPLOY-003 are available", "an existing authenticated production session may be used only through ordinary runtime interaction", "GEN2-COMPAT-OBS-001 remains paused and V1 paths remain unchanged"],
"acceptance_scenarios": [
"COGNITIVE-002-SCN-01: factual stable/candidate trace records Core request, requirement, binding selection, loaded binding head, fleet availability, provider eligibility, invocation result, and response/error without sensitive content",
"COGNITIVE-002-SCN-02: Local MEDIUM is shown ACTIVE, VALIDATED, AVAILABLE, and actually invoked for bounded OK, Russian household, and prepared-context recall requests",
"COGNITIVE-002-SCN-03: health endpoint success alone is not used as inference proof; discovery and invocation failures are separately classified",
"COGNITIVE-002-SCN-04: candidate four-component health/revision check, real canary household request, scoped bounded arbitrary-assertion memory write/multiple turns/recall, and candidate Core plus cognitive-worker restart/recovery pass",
"COGNITIVE-002-SCN-05: only after candidate success, Supervisor promotes it; evidence retains LKG and rollback readiness",
"COGNITIVE-002-EXIT: ordinary household and memory-recall response use Local MEDIUM through the governed route and remain usable after candidate restart"
],
"required_tests": [
"FAST: targeted go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/models/... ./adapters/postgres/... only for changed packages",
"FAST: targeted go vet for changed packages",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"production: stable/candidate trace, isolated candidate health/canary, Local MEDIUM invocation, restart recovery, and Supervisor promotion/rollback evidence"
],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,79 @@
{
"task_id": "GEN2-COGNITIVE-003",
"task_revision": 4,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"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",
"cmd/core-runtime/learning_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-COGNITIVE-003-restore-local-medium-invocation.json",
"docs/evidence/**"
]
},
"forbidden_change_scope": {
"paths": [
"cmd/core-runtime/capability_chat.go",
"cmd/core-runtime/capability_chat_test.go",
"cmd/core-runtime/chat_orchestrator.go",
"cmd/core-runtime/chat_orchestrator_test.go",
"cmd/core-runtime/cognitive_interpretation.go",
"cmd/core-runtime/cognitive_interpretation_test.go",
"cmd/core-runtime/development_gate.go",
"cmd/core-runtime/development_gate_test.go",
"cmd/core-runtime/household_scenario_test.go",
"cmd/core-runtime/human_response_gate_test.go",
"cmd/core-runtime/information_request_test.go",
"cmd/core-runtime/learning.go",
"cmd/core-runtime/main.go",
"cmd/core-runtime/main_test.go",
"cmd/core-runtime/research_client.go",
"cmd/core-runtime/research_client_test.go",
"adapters/postgres/**",
"modules/**",
"contracts/**",
"architecture/**",
"database/**",
"security/**",
"deploy/**",
"go.mod",
"go.sum",
"UI",
"V1 retirement",
"GEN2-EVOLVE-005",
"Gen3/**"
]
},
"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 contract", "existing capability binding/profile metadata", "existing OpenAI-compatible local adapter"],
"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", "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": ["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": ["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"]
}
@@ -0,0 +1,59 @@
{
"task_id": "GEN2-DEPLOY-003",
"task_revision": 1,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Изолировать stable и candidate ReleaseSet в независимые Compose deployment projects/slots, чтобы подготовка candidate не могла пересоздать stable containers, сохранив Supervisor как единственный traffic/promotion/rollback control plane.",
"requirement_ids": ["REQ-GEN2-DEPLOY-003-SLOT-ISOLATION", "REQ-GEN2-DEPLOY-003-UPDATER-OWNERSHIP", "REQ-GEN2-DEPLOY-003-ROLLBACK"],
"allowed_change_scope": {
"modules": ["deployment control", "runtime supervisor"],
"paths": [
"deploy/**",
"runtime/supervisor/**",
"cmd/supervisor/**",
"docs/evidence/**",
"docs/DEPLOYMENT.md",
"tests/deployment/**",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DEPLOY-003-isolated-release-slots.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "database/**", "modules/**", "contracts/**", "security/**", "adapters/**", "go.mod", "go.sum", ".gitea/workflows/**", "GEN2-028", "GEN2-029", "GEN2-030"]
},
"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 Supervisor durable ReleaseSet state", "existing production compose stack", "existing shared durable PostgreSQL/volumes"],
"may_change": ["deployment slot topology", "deployment-controller scripts", "Supervisor operational ReleaseSet identity fields"]
},
"semantic_owner": "Operations/Supervisor; slot and ReleaseSet identity are runtime-only operational control-plane state, not semantic canonical state.",
"canonical_state": ["no semantic owner, canonical writer, schema or data migration change", "Supervisor durable release evidence/LKG remains retained outside candidate containers"],
"state_transitions": ["stable slot retained -> isolated candidate slot start -> four-component health/revision validation -> Supervisor canary -> promote or traffic rollback -> inactive slot retained for rollback"],
"side_effects_and_privileges": ["production operations only through Docker/Compose and Supervisor control plane explicitly authorized for this remediation", "candidate preparation must never directly replace/recreate stable", "no credential reads or outputs", "external updater pause is allowed only as an explicit bounded operation with restore evidence"],
"persistence_and_migrations": ["no database migration", "shared PostgreSQL, named volumes and durable services are attached as existing external resources and are never duplicated or deleted by candidate cleanup"],
"failure_retry_restart": ["capture stable container IDs before every candidate operation and abort before canary when any changes", "candidate component failure returns traffic to LKG without container recreation", "Supervisor restart reloads ReleaseSet/slot/LKG evidence", "updater ownership must be project-scoped so it cannot reconcile the other slot"],
"assumptions": ["current stable may remain in its existing project during the one-time transition; it must not be recreated merely to rename a slot", "external deployment controller can be changed only when its exact owner/process is evidenced", "GEN2-COMPAT-OBS-001, V1 retirement and Gen3 remain stopped until this task passes"],
"acceptance_scenarios": [
"DEPLOY-003-SCN-01: stable and candidate Compose projects, service/container identities and updater ownership are distinct",
"DEPLOY-003-SCN-02: candidate dependencies resolve only to candidate services plus declared shared external resources",
"DEPLOY-003-SCN-03: candidate operation leaves every captured stable container ID unchanged; deviation aborts before canary",
"DEPLOY-003-SCN-04: all four candidate components report health/readiness and one revision",
"DEPLOY-003-SCN-05: Supervisor records ReleaseSet slot identity/revision/LKG and canaries the candidate",
"DEPLOY-003-SCN-06: forced candidate component failure rolls all traffic back to previous stable without stable recreate",
"DEPLOY-003-SCN-07: Supervisor restart preserves LKG/rollback target and candidate recovery does not alter stable IDs",
"DEPLOY-003-EXIT: isolated production ReleaseSet drill passes, after which GEN2-COMPAT-OBS-001 may resume without V1 retirement"
],
"required_tests": [
"FAST: compose config for stable and candidate slots",
"FAST: targeted go test ./runtime/supervisor ./cmd/supervisor",
"FAST: targeted go vet ./runtime/supervisor ./cmd/supervisor",
"FAST: deployment shell regression for project/identity isolation and stable ID invariance",
"FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"FAST independent: go run ./cmd/devctl/independent",
"FAST whitespace: git diff --check gen2-integration --",
"PRODUCTION: isolated candidate ReleaseSet health, canary, forced rollback, Supervisor restart/LKG and stable-ID drill"
],
"terminal_gap_states": ["BLOCKED_BY_DEPLOYMENT_CONTROL", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,63 @@
{
"task_id": "GEN2-DEPLOY-004",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Close the isolated candidate slot over static and Capability-resolved Assistant runtime dependencies, using GEN2-CAPABILITY-001 candidate/shadow binding resolution, and keep stable containers untouched so candidate side effects cannot fall back to production targets.",
"requirement_ids": ["REQ-GEN2-DEPLOY-004-DEPENDENCY-CLOSURE", "REQ-GEN2-DEPLOY-004-DYNAMIC-CAPABILITY-CLOSURE", "REQ-GEN2-CAPABILITY-001-RELEASE-SCOPED-BINDING", "REQ-GEN2-AUTHORITY-001-ISOLATED-COMMIT", "REQ-GEN2-DEPLOY-003-STABLE-ID-GUARD"],
"allowed_change_scope": {
"modules": ["deployment control", "runtime supervisor", "candidate Assistant runtime dependency closure", "inherited delegated Authority prerequisite only"],
"paths": [
"deploy/docker-compose.candidate-slot.yml",
"deploy/run-production-canary.sh",
"runtime/supervisor/**",
"cmd/supervisor/**",
"tests/deployment/**",
"docs/evidence/**",
"docs/DEPLOYMENT.md",
"docs/PROJECT_STATUS.md",
"development/GEN2_TASK_INDEX.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DEPLOY-004-candidate-commit-runtime-releaseset.json",
"architecture/adrs/ADR-0021-delegated-noninteractive-control-plane-authority.md",
"contracts/authority/types.go",
"contracts/authority/control_plane.go",
"contracts/authority/*_test.go",
"contracts/secrets/types.go",
"modules/authority/domain/**",
"modules/authority/application/**",
"modules/authority/ports/**",
"adapters/postgres/authority_repository.go",
"adapters/postgres/authority_repository_test.go",
"cmd/core-runtime/main.go",
"cmd/core-runtime/control_plane_authority.go",
"cmd/core-runtime/control_plane_authority_test.go",
"cmd/commit-runtime/main.go",
"cmd/commit-runtime/control_plane_authority.go",
"cmd/commit-runtime/control_plane_authority_test.go",
"database/migrations/056_delegated_control_plane_authority.sql",
"deploy/docker-compose.prod.yml",
"docs/CREDENTIALS_AND_SECRETS.md",
"dev/tasks/GEN2-AUTHORITY-001-delegated-noninteractive-control-plane-authority.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "contracts/identity/**", "modules/identity/**", "adapters/postgres/identity_repository.go", "contracts/capability/**", "modules/capability/**", "adapters/postgres/capability_repository.go", "cmd/cognitive-worker/**", "database/bootstrap/**", "security/**", "go.mod", "go.sum", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**"]
},
"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 typed Supervisor ReleaseSet and ComponentRelease", "existing runtime health/readiness/revision endpoints", "GEN2-CAPABILITY-001 CandidateReleaseContext and candidate/shadow binding resolution", "existing delegated Authority service credential and task-scoped grant contracts", "existing shared PostgreSQL and external secret volumes"],
"may_change": ["typed Supervisor explicit required ComponentKind set and static/dynamic dependency evidence", "candidate Compose dependency wiring", "candidate deployment control script"]
},
"semantic_owner": "Operations/Supervisor owns operational ReleaseSet evidence; Commit remains the sole canonical owner of durable Commit state; Authority and Capability ownership do not change.",
"canonical_state": ["stable and candidate instances of each owner use the same transactional canonical repository and do not form two semantic owners", "no direct stable container recreation", "no new canonical writer, schema, role, credential value, or provider routing owner", "pre-existing GEN2-AUTHORITY-001 paths are inherited only so devctl can validate the committed prerequisite diff against gen2-integration; this packet grants no new write authority for them"],
"state_transitions": ["stable dependency-closed ReleaseSet evidence retained -> isolated candidate static and Capability dependency closure start -> all component health/readiness/revision and dependency-target observation -> Supervisor canary -> promote or whole ReleaseSet rollback", "missing, unhealthy, mixed-revision, stable-targeted, or missing required candidate/shadow binding -> candidate ReleaseSet unhealthy -> traffic stable without stable container recreation"],
"side_effects_and_privileges": ["production operations are limited to Docker/Compose candidate slot B and Supervisor control endpoints", "candidate Creator, Core, Commit, Continuity, Agent Worker, Agent Execution Bridge, Work Graph and Task Verifier use only candidate Assistant runtime URLs", "PostgreSQL, protected secret volumes, model/gateway services and other explicitly classified non-Assistant infrastructure remain shared", "candidate components receive only required existing SecretRefs through read-only protected mounts", "stable IDs are captured before every candidate operation and any change aborts before canary"],
"persistence_and_migrations": ["no migration or durable semantic schema change", "shared PostgreSQL and existing protected volumes remain external resources and are never deleted", "Supervisor persists stable/candidate/LKG ReleaseSet evidence across restart"],
"failure_retry_restart": ["missing, unhealthy, revision-mismatched, or stable-targeted candidate component rejects the whole candidate ReleaseSet", "rollback changes Supervisor traffic only and retains stable containers", "Supervisor restart reloads the complete dependency-closed LKG and candidate state", "candidate and stable owner runtimes rely on existing repository idempotency/revision preconditions for bounded test resources"],
"assumptions": ["GEN2-AUTHORITY-001 implementation commits through a8be998 remain unmodified prerequisites on this task branch", "the protected control-plane secret artifact exists and can be mounted read-only without displaying it", "existing owner repositories support concurrent stable/candidate instances over bounded test resources", "any runtime target absent from the static configuration graph is classified before it is admitted as shared infrastructure"],
"acceptance_scenarios": ["candidate ReleaseSet contains the transitive static closure of creator-web, core-runtime, cognitive-worker, research-runtime, commit-runtime, continuity-runtime, agent-worker, agent-execution-bridge, work-graph-runtime, task-verifier, and every classified Capability target ReleaseComponent, each at one revision", "every candidate Assistant runtime URL targets a component in the same candidate ReleaseSet; stable Assistant runtime URLs abort before canary", "candidate side-effect resolution requires a valid same-ReleaseSet candidate/shadow binding and never falls back to ACTIVE production", "missing, unhealthy, revision-mismatched, unknown, or unbound required component prevents release READY", "candidate start/update leaves every captured stable Assistant runtime container ID unchanged", "forced candidate Continuity or Commit failure causes whole ReleaseSet rollback to stable traffic without recreating stable", "Supervisor restart preserves the complete component set, binding context and LKG evidence", "synthetic shadow notification produces no external delivery"],
"required_tests": ["FAST: targeted go test ./runtime/supervisor ./cmd/supervisor", "FAST: targeted go vet ./runtime/supervisor ./cmd/supervisor", "FAST: compose config candidate slot", "FAST: targeted deployment shell regression for dependency isolation and stable ID invariance", "FAST governance: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "FAST scope: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "FAST independent: go run ./cmd/devctl/independent", "FAST whitespace: git diff --check gen2-integration --", "PRODUCTION: isolated dependency-closed ReleaseSet health, revision, candidate Continuity/Commit rollback, Supervisor restart/LKG, and stable-ID drill"],
"terminal_gap_states": ["BLOCKED_BY_DEPLOYMENT_CONTROL", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,78 @@
{
"task_id": "GEN2-EVOLVE-001",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Extend the existing Gen2 retrieval fabric with task-relative freshness, bounded salience, scoped positive/negative experience, contradiction candidates and rebuildable consolidation candidates without creating a Memory owner or retiring V1 compatibility.",
"requirement_ids": ["REQ-GEN2-EVOLVE-001-ADAPTIVE-RETRIEVAL", "REQ-GEN2-EVOLVE-001-EXPERIENCE", "REQ-GEN2-EVOLVE-001-CONSOLIDATION"],
"allowed_change_scope": {
"modules": ["interaction", "epistemics", "core-runtime", "cognitive-context", "production evidence"],
"paths": [
"modules/interaction/domain/gen2_context.go",
"modules/interaction/domain/gen2_context_test.go",
"modules/epistemics/domain/freshness.go",
"modules/epistemics/domain/freshness_test.go",
"modules/epistemics/domain/experience.go",
"modules/epistemics/domain/experience_test.go",
"modules/epistemics/domain/adaptive_retrieval.go",
"modules/epistemics/domain/adaptive_retrieval_test.go",
"modules/epistemics/domain/consolidation.go",
"modules/epistemics/domain/consolidation_test.go",
"contracts/epistemics/freshness.go",
"contracts/epistemics/types.go",
"contracts/cognitive/context_compiler.go",
"contracts/cognitive/context_compiler_v2_test.go",
"adapters/postgres/epistemics_repository.go",
"adapters/postgres/interaction_repository.go",
"cmd/core-runtime/main.go",
"cmd/core-runtime/learning.go",
"cmd/core-runtime/cognitive_interpretation.go",
"cmd/core-runtime/cognitive_interpretation_test.go",
"database/migrations/053_gen2_adaptive_experience_scope.sql",
"development/GEN2_MIGRATION_MAP.md",
"docs/evidence/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-EVOLVE-001-adaptive-memory-freshness-salience-and-experience.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", ".gitea/workflows/**", "cmd/supervisor/**", "deploy/**", "GEN2-028", "GEN2-029", "GEN2-030", "PersonalMemory write paths", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {
"consume": ["existing Interaction utterance and ProjectReference projection", "existing Epistemics ExperienceRecord and Freshness contracts", "existing Cognitive Context Compiler V2"],
"may_change": ["typed Epistemics retrieval and temporal fields", "typed Context Compiler lane and omission metadata", "no public or cross-owner contract"]
},
"semantic_owner": "Interaction remains canonical owner of human utterances and ProjectReference; Epistemics remains canonical owner of ExperienceRecord and task-relative freshness evidence; Context Compiler remains a rebuildable cognitive projection; Core only composes existing owner reads through established repository boundaries.",
"canonical_state": ["Interaction utterance history and ProjectReference", "Epistemics ExperienceRecord and temporal applicability", "no new Memory canonical state", "no direct cross-owner SQL join or write"],
"persistence_and_migrations": ["add only owner-local Epistemics columns for principal and ContextScope references required to retrieve ExperienceRecord without cross-owner joins", "existing rows remain historical and are not deleted", "migration must be additive and rollback-safe", "migration number 053 is assigned because global migrations 051 and 052 already exist on the task base; no existing migration is changed", "cmd/core-runtime/learning.go is included only to persist the existing authenticated principal into the additive Epistemics metadata written by this task"],
"side_effects_and_privileges": ["no external mutation", "no provider override", "no autonomous Skill or Reflex activation", "no scheduler", "no V1 retirement", "no browser acceptance required"],
"state_transitions": [
"task-relative freshness uses observed/verified/validity/volatility/version/provenance inputs and never turns a stale assessment into permanent deletion",
"current versus historical assertion resolution preserves historical lineage and evaluates applicability before recency",
"ProjectReference and principal filtering precede semantic relevance ranking",
"Epistemics positive and negative ExperienceRecord retrieval is separately lane-classified and preserves failure applicability conditions",
"bounded contradiction candidates are retrieved after the primary candidates without confirmation-only selection",
"Context Compiler receives bounded current-fact, experience, contradiction and historical-analogy lanes with explainable inclusions and omissions",
"consolidation is a deterministic rebuildable projection from raw records to episode/proposition/experience/Skill-candidate evidence; it does not write or activate Skills"
],
"failure_retry_restart": ["bounded retrieval never loops", "missing or old Experience metadata remains historical or excluded rather than treated as current", "rebuild/restart recomputes projections from canonical utterance and Epistemics evidence", "no candidate failure changes stable production traffic"],
"assumptions": ["GEN2-COGNITIVE-002 scoped Interaction retrieval remains the primary assertion reader", "Epistemics owns additive ExperienceRecord metadata", "COMPAT observation and V1 retirement remain deferred by owner", "no browser or production promotion is required for this task"],
"acceptance_scenarios": [
"EVOLVE-001-SCN-01 current versus historical value returns the applicable current value while retaining historical evidence",
"EVOLVE-001-SCN-02 fast-aging and slow-aging inputs receive distinct task-relative freshness assessments",
"EVOLVE-001-SCN-03 Company A and Company B are hard-isolated through typed scope references before ranking",
"EVOLVE-001-SCN-04 relevant memory is selected and irrelevant memory is excluded under a bounded context budget with an explainable omission",
"EVOLVE-001-SCN-05 similar work retrieves both successful and failed applicable ExperienceRecord lanes",
"EVOLVE-001-SCN-06 contradiction retrieval emits a bounded opposing candidate without replacing canonical evidence",
"EVOLVE-001-SCN-07 rebuildable consolidation retains source evidence references and survives repository reload without raw-evidence loss"
],
"required_tests": [
"FAST: targeted go test and go vet for changed interaction, epistemics, cognitive context, core-runtime and postgres packages",
"FAST: devctl preflight, check-diff, independent governance, git diff --check",
"MILESTONE: changed owners plus migration consistency and focused restart/rebuild retrieval check; no browser or FULL suite"
],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "GEN2-EVOLVE-002",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Wire one bounded cognitive memory-continuation round through the existing Interaction/Epistemics retrieval fabric and Context Compiler without a new Memory owner, V1 retirement, UI change, direct model storage access or an unrestricted memory tool.",
"requirement_ids": ["REQ-GEN2-EVOLVE-002-ITERATIVE-RETRIEVAL"],
"allowed_change_scope": {"modules": ["cognitive contracts", "core-runtime", "cognitive-worker", "Gen2 governance and evidence"], "paths": ["contracts/cognitive/context_compiler.go", "contracts/cognitive/context_compiler_v2_test.go", "contracts/cognitive/types.go", "cmd/core-runtime/cognitive_interpretation.go", "cmd/core-runtime/cognitive_interpretation_test.go", "cmd/core-runtime/main.go", "cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-002-iterative-retrieval-and-cognitive-memory-continuation.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "database/**", "deploy/**", "cmd/supervisor/**", "cmd/creator-web/**", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]},
"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 Interaction and Epistemics retrieval projections", "existing Context Compiler V2"], "may_change": ["Cognitive iterative-retrieval request, inherited continuation envelope and advisory response fields only"]},
"semantic_owner": "Interaction remains canonical owner of utterance assertions and ProjectReference; Epistemics remains canonical owner of experience. Core validates and binds continuation identity/scope/policy before calling existing owner reads; Context Compiler remains the only context selector; cognitive-worker may emit only a typed retrieval intent and never reads storage.",
"canonical_state": ["no new state is written", "no new Memory owner", "no direct model or cognitive-worker storage access", "no cross-owner SQL join"],
"persistence_and_migrations": ["no schema or migration change"],
"side_effects_and_privileges": ["no external mutation", "no provider override", "no UI/browser change", "no V1 retirement"],
"state_transitions": ["initial Context Compiler context -> cognitive-worker may emit a typed retrieval intent -> Core binds inherited Principal/Scope/Focus/task/freshness/policy -> existing owner retrieval hard-filters scope before ranking -> Context Compiler compiles unique bounded additions -> one continuation advisory request", "no result -> explicit no-additional-evidence continuation", "contradiction or negative experience is lane-classified rather than suppressed"],
"failure_retry_restart": ["one bounded continuation round only", "round/bytes/fragments/latency and duplicate budgets fail closed", "missing evidence remains unknown", "continuation state is request-scoped; durable Work/Investigation remains responsible for any restartable task"],
"assumptions": ["the task branch starts at gen2-integration 850fe92", "UI/browser acceptance remains deferred by owner", "the traced existing IterativeRetrievalRequest lacks the required structured lane/reason and inherited runtime envelope, so this revision changes only those Cognitive contract fields"],
"acceptance_scenarios": ["EVOLVE-002-SCN-01 insufficient initial context can obtain one additional current fact", "EVOLVE-002-SCN-02 negative and contradiction lanes are separately requested", "EVOLVE-002-SCN-03 inherited Company A scope rejects Company B regardless of request", "EVOLVE-002-SCN-04 duplicate excerpts and rounds/byte limits are rejected", "EVOLVE-002-SCN-05 no result preserves an explicit unknown/no-additional-evidence state"],
"required_tests": ["FAST: targeted go test and go vet for changed cognitive contract, core-runtime and cognitive-worker packages", "FAST: devctl preflight, check-diff, independent governance, git diff --check", "MILESTONE: changed owner/runtime tests plus focused bounded continuation and restart-state check; no browser or FULL suite"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,39 @@
{
"task_id": "GEN2-EVOLVE-003",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Implement the bounded evidence-governed Experience to Skill to Reflex learning lifecycle using existing Epistemics, Capability and SELF owners: candidate, independent validation, applicable routing, bounded shadow/revalidation, downgrade/retirement and Learning Ledger evidence, without autonomous production mutation.",
"requirement_ids": ["REQ-GEN2-EVOLVE-003-EXPERIENCE-SKILL-REFLEX"],
"allowed_change_scope": {
"modules": ["capability", "self", "core-runtime learning", "Gen2 evidence and governance"],
"paths": [
"contracts/capability/skill_v2.go", "contracts/capability/reflex_v2.go", "contracts/capability/resolution.go", "contracts/capability/intelligence.go", "contracts/self/types.go",
"modules/capability/domain/skill.go", "modules/capability/domain/skill_test.go", "modules/capability/domain/reflex.go", "modules/capability/domain/reflex_test.go", "modules/capability/domain/intelligence_test.go",
"modules/self/domain/self.go", "modules/self/domain/self_test.go",
"adapters/postgres/capability_repository.go", "adapters/postgres/self_repository.go", "adapters/postgres/capability_repository_test.go", "adapters/postgres/self_repository_test.go",
"database/migrations/054_experience_skill_reflex_learning_lifecycle.sql",
"cmd/core-runtime/learning.go", "cmd/core-runtime/learning_test.go",
"dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-003-experience-skill-reflex-learning-lifecycle.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "deploy/**", "cmd/creator-web/**", "cmd/cognitive-worker/**", "adapters/models/**", "modules/epistemics/**", "adapters/postgres/epistemics_repository.go", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {
"consume": ["existing Epistemics ExperienceRecord/ReflectionCandidate", "existing Capability Skill V2/Reflex V2", "existing SELF LearningLedger/ImprovementNeed", "existing Intelligence Strategy"],
"may_change": ["Capability and SELF typed lifecycle/evidence fields necessary for bounded candidate, validation, routing and revalidation semantics only"]
},
"semantic_owner": "Epistemics remains canonical owner of Experience and Outcome evidence; Capability remains sole canonical owner of Skill and Reflex lifecycle; SELF remains canonical owner of ImprovementNeed, competence/weakness assessments and Learning Ledger. Core Runtime only records evidence through owner repositories and never self-promotes production behavior.",
"canonical_state": ["Experience/Outcome evidence is referenced, never copied as a new source of truth", "Capability writes Skill/Reflex revisions and heads; SELF writes Learning Ledger and ImprovementNeed", "candidate, validation, shadow and revalidation never grant independent production mutation authority", "no new Memory owner, parallel Skill registry or cross-owner SQL join"],
"side_effects_and_privileges": ["No external mutation may bypass Authority -> Commit -> Executor -> verified Outcome.", "SELF may observe, evaluate and propose only; production promotion remains behind the existing governed development gates and independent evaluation.", "No provider invocation, privilege expansion, UI/browser change or V1 retirement is authorized."],
"persistence_and_migrations": ["054 is the next verified global migration number after 053; it is additive and uses existing capability_role and self_role only", "revision 3 sets devctl's database-surface compatibility delta to one solely because devctl requires it for any migration; no database owner role is created", "persist Skill/Reflex revision/head and SELF learning evidence needed for restart-safe lifecycle state; existing historical records remain untouched", "rollback disables/refuses non-active Reflex selection and returns to existing Skill/reasoning path without deleting evidence"],
"state_transitions": ["verified positive Experience with a bounded pattern and provenance -> Candidate Skill", "independent validation/replay evidence and must-not-worsen criteria -> Validated Skill -> Active eligible routing", "frequent stable Active Skill with deterministic bounded implementation and verifier -> Candidate Reflex -> Validated/Active Reflex", "dependency change, stale evidence, correction, failure trend or shadow divergence -> Stale/Suspicious -> revalidation request -> Active, rewrite proposal or Retired", "Reflex decline/failure -> Skill -> reasoning; no execution can bypass Authority/Commit/Executor"],
"failure_retry_restart": ["one success never promotes Candidate directly to Active Skill or Reflex", "shadow is sampled/bounded and never delays ordinary low-risk response; divergence records evidence and ImprovementNeed rather than rewriting Reflex", "revalidation is bounded and never retries endlessly; real failures remain fail-closed and escalate", "durable revisions, validation evidence and Ledger survive restart; no direct provider invocation or process-local canonical state"],
"assumptions": ["task branch starts at gen2-integration a07aba0", "UI/browser acceptance remains deferred by owner; V1 compatibility remains retained", "a verified inspection found 054 free, existing SkillProfile persistence, Reflex V2 contract without durable lifecycle persistence, and SELF typed ledger without a repository", "revision 2 adds only contracts/capability/resolution.go because the existing canonical SkillProfile is declared there and its durable evidence fields must be additive rather than creating a parallel Skill registry", "revision 3 satisfies devctl's migration compatibility gate without creating any role or privilege", "automatic arbitrary production code generation is not in scope; candidate executable strategies must use an existing bounded implementation reference and governed development remains the only production-promotion route"],
"acceptance_scenarios": ["EVOLVE-003-SCN-01 verified outcome and pattern create Candidate Skill with source Experience/Outcome references", "EVOLVE-003-SCN-02 independent replay/evaluation evidence promotes Skill only after repeatability and must-not-worsen checks", "EVOLVE-003-SCN-03 applicable fresh Active Skill is routed; inapplicable, stale or weak Skill escalates to reasoning", "EVOLVE-003-SCN-04 sufficient stable evidence creates Candidate Reflex; verified bounded Reflex becomes Active and retains fallback", "EVOLVE-003-SCN-05 dependency change, correction, failure trend and stale validation trigger bounded revalidation/downgrade rather than silent use", "EVOLVE-003-SCN-06 sampled shadow divergence records ImprovementNeed/LearningLedger evidence and does not rewrite production Reflex", "EVOLVE-003-SCN-07 user correction links the affected Skill/Reflex revision and preserves conditional known failure evidence", "EVOLVE-003-SCN-08 retired Reflex is excluded from routing while all evidence/history remains queryable"],
"required_tests": ["FAST: targeted go test and go vet for changed capability, self, core-runtime and postgres packages", "FAST: devctl preflight, check-diff, independent governance, architecture check, git diff --check", "MILESTONE: focused lifecycle/persistence/restart and escalation tests only; no browser, production canary or FULL suite"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "GEN2-EVOLVE-004-RUNTIME-001",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Wire the existing bounded SELF learning loop into the Core composition root without creating a new owner or bypassing existing ports.",
"requirement_ids": ["REQ-GEN2-EVOLVE-004-SELF-RUNTIME-WIRING"],
"allowed_change_scope": {"modules": ["core-runtime learning composition", "SELF persistence integration", "production secret reference wiring", "targeted evidence"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/learning.go", "cmd/core-runtime/learning_test.go", "adapters/postgres/self_repository.go", "adapters/postgres/self_repository_test.go", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "docs/CREDENTIALS_AND_SECRETS.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-EVOLVE-004-RUNTIME-001-wire-self-learning-loop.json", "docs/evidence/**", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "cmd/cognitive-worker/**", "modules/**", "adapters/models/**", "database/**", "deploy/caddy/**", "deploy/images/**", "deploy/windows/**", "deploy/PORTAINER.md", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**"]},
"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 learningRecorder", "existing SELF repository methods", "existing SELF domain contracts"], "may_change": ["Core composition wiring only"]},
"semantic_owner": "SELF remains the sole owner of derived learning state; Core only coordinates evidence recording.",
"canonical_state": ["SELF write failures never replace a completed user response with an error", "no direct database access outside SelfRepository", "no automatic promotion or production mutation"],
"state_transitions": ["learning signal -> SelfRepository -> bounded derived evidence -> ledger"],
"side_effects_and_privileges": ["use existing self_role repository boundary; do not read or print secrets", "the positive new_privileges value is only the devctl deployment-surface compatibility gate; no privilege is granted and self_role grants are unchanged"],
"persistence_and_migrations": ["no schema change; consume migration 055 tables"],
"failure_retry_restart": ["recording is best-effort after response completion and durable when repository is available; restart reads remain repository-owned"],
"assumptions": ["existing external assistant_core_secrets control plane can materialize self_db_password without exposing its value", "existing production database has migration 055"],
"acceptance_scenarios": ["correction/failure reaches SelfRepository", "repository failure is diagnostic and non-fatal", "durable evidence remains available after repository reopen"],
"required_tests": ["targeted go test", "targeted go vet", "preflight", "check-diff", "independent", "git diff --check", "focused MILESTONE"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,42 @@
{
"task_id": "GEN2-EVOLVE-004",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Implement a bounded evidence-only SELF learning loop: observed signals -> contextual weakness diagnosis -> ImprovementNeed -> candidate proposal -> independent evaluation plan -> Learning Ledger, without autonomous code generation, production promotion or a new owner.",
"requirement_ids": ["REQ-GEN2-EVOLVE-004-SELF-IMPROVEMENT-LOOP"],
"allowed_change_scope": {
"modules": ["self", "self benchmark", "core-runtime learning evidence", "Gen2 evidence and governance"],
"paths": [
"contracts/self/types.go",
"contracts/self/benchmark.go",
"modules/self/domain/self.go",
"modules/self/domain/self_test.go",
"adapters/postgres/self_repository.go",
"adapters/postgres/self_repository_test.go",
"database/migrations/055_self_weakness_diagnosis_and_benchmark.sql",
"cmd/core-runtime/learning.go",
"cmd/core-runtime/learning_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-EVOLVE-004-self-improvement-loop-weakness-diagnosis-and-governed-learning.json",
"docs/evidence/**",
"docs/PROJECT_STATUS.md"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "go.mod", "go.sum", "deploy/**", "cmd/creator-web/**", "cmd/cognitive-worker/**", "adapters/models/**", "modules/capability/**", "modules/epistemics/**", "database/migrations/001-054", "PersonalMemory write paths", "GEN2-028", "GEN2-029", "GEN2-030", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["existing Epistemics Experience/Outcome evidence", "existing Capability Skill/Reflex references", "existing SELF CompetenceAssessment/WeaknessPattern/ImprovementNeed/LearningLedger/PersonalBenchmark", "existing Work governed-evolution gates"], "may_change": ["SELF diagnosis, bounded candidate-proposal and benchmark case typed fields only"]},
"semantic_owner": "SELF remains the sole owner of derived competence, weakness, ImprovementNeed, PersonalBenchmark and Learning Ledger state. Epistemics remains owner of source evidence; Capability remains owner of Skill/Reflex; Work/development gates remain owner of executable candidate and promotion.",
"canonical_state": ["diagnosis is evidence-backed and may remain HYPOTHESIS/UNKNOWN", "repeated incidents are required for WeaknessPattern", "SELF observes/evaluates/proposes but cannot promote or mutate production", "benchmark cases reference evidence and do not copy conversations or secrets", "no new Memory owner, Skill registry, orchestrator or canonical writer"],
"side_effects_and_privileges": ["No external mutation bypasses Authority -> Commit -> Executor -> verified Outcome.", "No autonomous code generation, merge, deployment, production promotion or provider invocation is authorized.", "Data is scoped by Principal/ContextScope and privacy metadata; hidden reasoning and user data are not stored."],
"persistence_and_migrations": ["055 is the next free global migration after 054 and is additive under existing self_role only", "the positive new_database_owner_roles value is a devctl database-surface compatibility gate, not authorization for a new role; no new owner role is created", "persist contextual weakness incidents, ImprovementNeed diagnosis/candidate metadata, bounded benchmark references and Learning Ledger outcomes; preserve all historical evidence", "rollback disables proposal generation and leaves observations/evidence queryable"],
"state_transitions": ["verified failure/correction/fallback/divergence signal -> scoped evidence", "repeated scoped evidence -> WeaknessPattern; one random failure remains an incident", "supported diagnosis -> ImprovementNeed with intervention class, risk and expected effect", "ImprovementNeed -> candidate proposal/evaluation plan only", "independent evaluation and existing governed development gates decide promotion; actual result is appended to Learning Ledger"],
"failure_retry_restart": ["bounded diagnosis/candidate/evaluation budgets and deduplication prevent self-review loops", "provider outage is not competence loss; diagnosis distinguishes unavailable dependency", "stale/unknown/hypothesis diagnosis is never presented as established fact", "durable SELF state survives restart; no process-local canonical state"],
"assumptions": ["task branch starts at gen2-integration dd04a67", "UI/browser acceptance remains deferred and V1 compatibility remains retained", "existing Work governed-evolution contract is consumed rather than duplicated", "automatic executable production changes remain a future governed task"],
"acceptance_scenarios": ["EVOLVE-004-SCN-01 repeated user corrections attach to the affected scoped Skill/Reflex decision", "EVOLVE-004-SCN-02 repeated incidents create a WeaknessPattern while one random failure does not", "EVOLVE-004-SCN-03 diagnosis distinguishes retrieval/context/skill/reflex/provider/tool/outcome causes and preserves confidence/evidence", "EVOLVE-004-SCN-04 competence is contextual and evidence-derived, never self-declared", "EVOLVE-004-SCN-05 ImprovementNeed produces a bounded candidate proposal with risk, expected effect, evaluation and rollback", "EVOLVE-004-SCN-06 Creator and Evaluator are distinct; must-not-worsen can reject a candidate", "EVOLVE-004-SCN-07 Personal Benchmark cases reference confirmed evidence for memory, scope, failure avoidance, household, technical, continuity and authority/outcome classes", "EVOLVE-004-SCN-08 expected versus actual benefit is appended to Learning Ledger; no autonomous promotion occurs", "EVOLVE-004-SCN-09 diagnosis/candidate budgets and deduplication prevent infinite self-review"],
"required_tests": ["FAST: targeted go test and go vet for changed self, core-runtime, contracts and postgres packages", "FAST: devctl preflight, check-diff, independent governance, architecture check, git diff --check", "MILESTONE: focused evidence attribution, diagnosis, deduplication, contextual competence, candidate/evaluation, benchmark and Learning Ledger tests; no FULL suite, deployment or browser acceptance"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,28 @@
{
"task_id": "GEN2-SECRETS-001",
"task_revision": 2,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Provide an idempotent, local-only, cryptographically secure machine-credential provisioner and rotation mechanism on the existing protected Docker secret control plane, initially for the delegated control-plane service token, without creating a second semantic owner or exposing a secret value.",
"requirement_ids": ["REQ-GEN2-SECRETS-001-LOCAL-MACHINE-CREDENTIALS", "REQ-GEN2-AUTHORITY-001-DELEGATED-CONTROL-PLANE"],
"allowed_change_scope": {
"modules": ["technical protected secret file backend", "production deployment/bootstrap secret provisioning", "targeted Core/Commit service-authentication mount wiring", "targeted tests and evidence"],
"paths": ["platform/secrets/**", "contracts/secrets/types.go", "cmd/secret-control-plane/**", "cmd/core-runtime/main.go", "cmd/commit-runtime/control_plane_authority.go", "deploy/images/runtime.Dockerfile", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "deploy/provision-machine-credential.sh", "deploy/provision-machine-credential_test.sh", "deploy/verify-machine-credential.sh", "docs/CREDENTIALS_AND_SECRETS.md", "docs/evidence/**", "development/GEN2_TASK_INDEX.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-SECRETS-001-local-machine-credential-provisioning.json"]
},
"forbidden_change_scope": {
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "contracts/authority/**", "modules/authority/**", "adapters/postgres/**", "database/**", "modules/identity/**", "modules/capability/**", "cmd/cognitive-worker/**", "cmd/creator-web/**", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**", "go.mod", "go.sum"]
},
"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 CredentialRef and FileResolver", "existing Docker protected-volume deployment mechanism"], "may_change": ["typed SecretPurpose/consumer metadata only if required for the provisioner"]},
"semantic_owner": "No new semantic owner is introduced. platform/secrets remains a technical bootstrap/resolution mechanism; Identity, Authority, Capability and Configuration ownership remain unchanged.",
"canonical_state": ["credential values live only in protected local artifacts, never in Configuration, Memory, context, PostgreSQL, Compose environment, Git, logs or evidence", "machine authentication is distinct from Authority grants", "metadata/audit is separate from the value and contains no plaintext or reversible material", "the control-plane token is mounted read-only only to Core and Commit runtimes"],
"state_transitions": ["ABSENT -> ACTIVE version one via idempotent CSPRNG provisioning", "ACTIVE -> ROTATING -> ACTIVE new version after atomic artifact replacement and controlled verification", "ACTIVE -> COMPROMISED/REVOKED -> replacement version with audit retained"],
"side_effects_and_privileges": ["a one-shot local control-plane container may create or rotate only its dedicated external Docker volume", "Core and Commit get read-only mounts; all other runtimes receive no mount", "no plaintext environment variable is added"],
"persistence_and_migrations": ["no application database schema or canonical writer changes", "secret metadata and append-only audit reside separately from the secret value inside the protected control-plane volume", "rollback retains prior metadata and revokes only after a verified replacement"],
"failure_retry_restart": ["restart is idempotent and preserves the active artifact", "partial writes are avoided by atomic replacement", "a missing or unreadable artifact fails service authentication closed", "rotation never emits a credential and leaves a valid active artifact on failure"],
"assumptions": ["docker.cin.su permits creation of a dedicated external Docker volume by the existing production deployment control plane", "the runtime image can execute a one-shot local provisioner as the control-plane writer", "the existing protected secret volume pattern is the approved production mechanism"],
"acceptance_scenarios": ["missing credential is generated once with crypto/rand and then reused across restart", "metadata and audit are separate from the value", "Core and Commit mounts are read-only and no other runtime mounts the dedicated token volume", "rotation accepts the new credential and rejects the old credential", "logs and diagnostics redact known secret headers/fields", "a valid service credential alone does not bypass the existing delegated Authority grant checks"],
"required_tests": ["targeted go test ./platform/secrets ./cmd/secret-control-plane", "targeted go vet ./platform/secrets ./cmd/secret-control-plane", "compose config validation", "provisioner shell regression", "preflight", "check-diff", "independent", "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 -1
View File
@@ -1641,7 +1641,7 @@ Historical migrations are evidence. Gen2 migration starts at the next available
# 41. New Gen2 schema migration groups
Exact migration numbers are assigned after current repository HEAD is fixed.
Exact migration numbers are assigned after current repository HEAD is fixed. The repository uses one global sequence: `051_identity_principal_realms.sql` and `052_identity_creator_accounts.sql` occupy their numbers. GEN2-EVOLVE-001 therefore adds owner-local Epistemics metadata as additive migration `053_gen2_adaptive_experience_scope.sql`; it does not alter either existing migration.
Recommended groups:
+13
View File
@@ -1,5 +1,9 @@
# DOMOVOY Gen2 — Task Index
## GEN2-SECRETS-001 — Local Machine Credential Provisioning & Rotation
Bounded post-M5 remediation. Provides the technical protected-secret control-plane mechanism required by `GEN2-AUTHORITY-001`; it does not change semantic ownership or grant Authority.
Это исполняемый индекс task packets. `ARCH-GEN2-000` соответствует разделу `GEN2-000` Master Plan: префикс `ARCH-` нужен текущему `devctl`, чтобы последующие packets могли ссылаться на него через `architecture_revision`.
| Порядок | Task | Milestone | Gate | Base ref | Зависимость | Файл |
@@ -48,4 +52,13 @@
- **M6 retirement:** `GEN2-029` — FULL before irreversible retirement.
- **M6 final:** `GEN2-030` — FULL final acceptance.
## Bounded post-M5 remediation
| Task | Gate | Depends on | Purpose |
|---|---|---|---|
| `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-а.
+6
View File
@@ -21,6 +21,12 @@ Do not guess Portainer credentials and do not search other project source trees
## Production services
Each privileged runtime gets only its required secret/capability bindings. `commit-runtime` is intentionally isolated from the general Core Runtime.
The Core SELF learning repository uses the existing `assistant_core_secrets` volume and credential-reference mechanism with `self_role` and `credential://domovoy/self/db-password/v1`. The secret is consumed from the fixed `/run/secrets/self_db_password` path; its value is never placed in Compose environment, Git, logs or ordinary configuration. The role and schema already exist; this wiring does not create a role or broaden privileges.
Delegated non-interactive control-plane work is separately authenticated. The local `secret-control-plane` one-shot control-plane service provisions `control_plane_service_token` with `crypto/rand` into the dedicated external `assistant_control_plane_service_secrets` volume. The token artifact is mode `0400`; metadata and audit are separate protected files and contain no value or reversible fingerprint. Only `core-runtime` and the existing Authority/Commit runtime mount that volume read-only at `/run/control-plane-secrets`; no model, Skill, SELF, creator-web, cognitive-worker or research-runtime receives the mount. Compose carries only the non-secret credential reference `credential://domovoy/control-plane/service-token/v1`.
The provisioner is idempotent: restart reuses an existing safe active artifact. Rotation creates a new value by atomic replacement, records non-secret metadata/audit, and never accepts a plaintext environment variable. The service token is not a Creator session, cannot impersonate a human, and never grants Authority by itself. Removing or rotating the token disables/refreshes service authentication; revoking the durable Authority grant also blocks new delegated operations without deleting audit history.
Creator authentication secrets are stored in the external Docker volume `assistant_core_secrets` and mounted read-only only at `/run/secrets` in `core-runtime`. The production container environment contains `_FILE` paths, not the secret values. Files are owned by runtime uid 100/gid 101 with mode 0400. `CREATOR_USERNAME` is not secret and remains an environment value. Development may continue to use direct environment values, but a direct value and its matching `_FILE` setting must never be configured together.
The PostgreSQL off-host backup passphrase is generated randomly and stored as CurrentUser DPAPI ciphertext at `%LOCALAPPDATA%\Domovoy\postgres-backup-key.dpapi.json`. An atomic copy of that DPAPI ciphertext, never the plaintext passphrase, is retained at `\\nas\MST\codex\_backups\domovoy\keys\postgres-backup-key.dpapi.json`. The passphrase is never stored in Git, the production Docker host, a scheduled-task argument, or NAS plaintext. The NAS backup directory receives only GnuPG AES-256 ciphertext and a non-secret integrity manifest. Recovery therefore requires both the NAS artifacts and the authorized Windows DPAPI profile.
+11 -2
View File
@@ -2,6 +2,14 @@
## Gen2 M6 checkpoint — 2026-08-30
- `GEN2-EVOLVE-004 = MILESTONE_ACCEPTED`: bounded SELF learning signals, evidence-backed attribution, contextual weakness/competence, ImprovementNeed, independently evaluated candidate, benchmark and expected-vs-actual Learning Ledger primitives are implemented with durable SELF persistence in migration 055. Evidence: `docs/evidence/gen2-evolve-004-milestone-2026-08-30.md`. No autonomous promotion, production deployment, new owner, UI work, V1 retirement or GEN2-EVOLVE-005 was started.
- `GEN2-EVOLVE-003 = MILESTONE_ACCEPTED`: Experience evidence now enters an evidence-governed Candidate Skill → independent validation → Active Skill → bounded Candidate Reflex lifecycle. Reflex divergence/revalidation records SELF proposals and Learning Ledger evidence; it cannot autonomously rewrite or promote production. Evidence: `docs/evidence/gen2-evolve-003-milestone-2026-08-30.md`. UI and V1 compatibility remain unchanged.
- `GEN2-EVOLVE-002 = MILESTONE_ACCEPTED`: one bounded typed cognitive memory-continuation round is available through Core and the existing retrieval fabric. Evidence: `docs/evidence/gen2-evolve-002-milestone-2026-08-30.md`. UI and V1 compatibility remain unchanged.
- `GEN2-EVOLVE-001 = MILESTONE_ACCEPTED`: bounded adaptive retrieval adds task-relative freshness, computed salience, positive/negative/contradiction experience lanes and provenance-preserving consolidation candidates. Evidence: `docs/evidence/gen2-evolve-001-milestone-2026-08-30.md`. No V1 path was retired and UI/browser acceptance remains deferred by owner.
- `GEN2-028 = COMPLETED_WITH_EVIDENCE`: V1 compatibility inventory is recorded in `docs/evidence/gen2-028-v1-compatibility-inventory-2026-08-30.json`. Current production callers remain for PersonalMemory fallback, ActiveProjectFocus, truth_bundles, ordinary ENV reads, SkillProfile/FastPath; no path was proven `SAFE_TO_RETIRE`.
- `GEN2-029 = COMPLETED_WITH_SAFE_NOOP`: no V1 runtime path or historical evidence was deleted or disabled. The decision and rollback point are recorded in `docs/evidence/gen2-029-v1-retirement-decision-2026-08-30.json`; `FULL_RETIREMENT` was not applicable because no irreversible retirement was performed.
- `GEN2-030_STATUS = ACCEPTANCE_IN_PROGRESS`: this status is a documentation checkpoint, not a final Gen2 PASS. The final gate must confirm current production and repository evidence; the M6 retirement gap remains that active V1 compatibility callers have not yet been proven unused.
@@ -9,8 +17,9 @@
- `PROVIDER_GAPS = CPU_FAST_UNAVAILABLE; GPU_UNAVAILABLE`; Local MEDIUM remains the primary route and Lifeboat remains fallback per existing M5 evidence.
- `GEN2_M6_FINAL_STATUS = NOT_FINAL_PASS_RETIREMENT_GAP_REMAINS`; FULL_FINAL technical checks passed, but no final M6 baseline tag is created because active V1 compatibility callers were not proven safe to retire.
- `GEN2_M6_FINAL_EVIDENCE = docs/evidence/gen2-030-final-acceptance-2026-08-30.json`.
- `GEN2_FINAL_STATUS = GEN2_CORE_ACCEPTED / V1_COMPATIBILITY_RETAINED / UI_DEFERRED`.
- `GEN2-COMPAT-OBS-001 = INSTRUMENTATION_COMMITTED; PRODUCTION_OBSERVATION_BLOCKED_BY_MISSING_CAPABILITY`; evidence: `docs/evidence/gen2-compat-obs-001-blocked-2026-08-30.json`. No V1 path is `SAFE_TO_RETIRE`; no retirement task was created.
- `GEN2-COGNITIVE-002 = IMPLEMENTED / CORE_VERIFIED / BROWSER_ACCEPTANCE_DEFERRED_BY_OWNER`; non-browser evidence: `docs/evidence/gen2-cognitive-002-core-verified-2026-08-30.json`.
- `GEN2-COMPAT-OBS-001 = OBSERVATION_DEFERRED_BY_OWNER`; instrumentation remains committed, but no production observation window or retirement decision is claimed. No V1 path is `SAFE_TO_RETIRE`; no retirement task was created.
- `GEN2_FINAL_STATUS = GEN2_CORE_ACCEPTED / V1_COMPATIBILITY_RETAINED / BROWSER_ACCEPTANCE_DEFERRED / COMPAT_OBSERVATION_DEFERRED`.
- `NEXT_EVOLUTION_QUEUE = instrument bounded compatibility usage, observe, then retire only individually proven paths under a future FULL_RETIREMENT gate; no Gen3 or new feature work is started.
## Current audit delta — 2026-08-24
@@ -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,50 @@
# GEN2-AUTHORITY-001 — delegated control-plane deployment evidence
Date: 2026-08-31
The former missing-credential prerequisite is resolved by
`GEN2-SECRETS-001`. A local one-shot provisioner created the dedicated
protected artifact and verified idempotent ensure/rotation without reading or
emitting its value. Core and Commit are wired to resolve it from the dedicated
read-only mount, not the shared secret volume.
## Implemented and verified locally
The bounded implementation is present on branch
`gen2/authority-001-delegated-control-plane`. It keeps Identity as the service
identity owner, Authority as the durable grant/revocation/audit owner, and
Capability as the cognitive lifecycle owner. Core is an Authority-runtime
client; the Authority endpoint is served by the existing Commit runtime that
already owns the Authority repository in the V2 runtime topology.
On 2026-08-31 the following checks completed successfully without secret
values:
- targeted `go test` for Authority contracts/domain, secret resolver, Core,
Commit runtime, and the Authority PostgreSQL adapter;
- targeted `go vet` for the same packages;
- production and candidate Compose static validation;
- task preflight, diff scope, independent governance, and architecture checks;
- `git diff --check`.
## Current deployment-control gap
No delegated lifecycle operation has been attempted in production.
On 2026-08-31 the stable Core and Commit runtimes both reported release
revision `2ee5da81145cdad2da1c92c24a91145ad4af61a5`. The existing candidate
slot script declares exactly four components: Creator Web, Core, Cognitive
Worker, and Research Runtime. It has no candidate Commit runtime.
The Authority endpoint is intentionally served by Commit runtime. Therefore a
candidate Core cannot perform a real delegated Authority request against the
required matching candidate Commit runtime without either recreating stable
Commit (forbidden) or extending the candidate ReleaseSet/Supervisor contract
to contain Commit as an independently healthy and rollback-aware component.
This is `BLOCKED_BY_SPEC_GAP`, not a credential or implementation failure. A
new explicit packet/revision must authorize the minimum deployment paths:
`deploy/docker-compose.candidate-slot.yml`, `deploy/run-production-canary.sh`,
and the Supervisor release-set contract/tests if its fixed four-component
schema must change. The change must define Commit candidate health, revision
consistency, traffic/promotion behavior, and whole-ReleaseSet rollback.
@@ -0,0 +1,22 @@
# GEN2-CAPABILITY-001 notification receiver classification
## Evidence
The notification target is implemented by this repository's
`cmd/notification-receiver` runtime. It exposes the bounded delivery and
receipt protocol used by `adapters/notificationexecutor`; it is therefore an
Assistant ReleaseComponent, not a shared external service.
The current stable receiver persists idempotent receipts and is configured
with separate read/write secrets. Candidate acceptance must not reuse its
runtime instance, secret mount, or stable Capability ACTIVE binding.
## Candidate decision
The candidate ReleaseSet requires its own `notification-receiver-candidate`
instance in explicit shadow mode. That instance accepts only synthetic,
task-scoped delivery traffic and returns a shadow receipt. It has no
production delivery credential or production destination route.
`SHADOW_CAPTURED` proves only receipt capture. It is not delivery, a sent
notification, or a verified World outcome.
@@ -0,0 +1,43 @@
{
"evidence_id": "gen2-cognitive-002-core-verified-2026-08-30",
"task_id": "GEN2-COGNITIVE-002",
"task_revision": 3,
"commit": "00af176a200142bd3de67ba914e542b2299f43d5",
"status": "IMPLEMENTED / CORE_VERIFIED / BROWSER_ACCEPTANCE_DEFERRED_BY_OWNER",
"owner_amendment": {
"browser_authenticated_acceptance": "DEFERRED_BY_OWNER",
"compatibility_observation": "DEFERRED_BY_OWNER",
"v1_retirement": "NOT_AUTHORIZED"
},
"non_browser_acceptance": {
"targeted_tests": "PASS: cmd/cognitive-worker, cmd/core-runtime, modules/interaction/domain, adapters/postgres",
"targeted_vet": "PASS: same packages",
"governance": ["preflight PASS", "check-diff PASS", "independent PASS", "git diff --check PASS"],
"scoped_retrieval": [
"arbitrary assertion retrieval",
"current value selected while historical value is retained",
"ProjectReference hard scope isolation before relevance ranking",
"irrelevant assertions excluded from bounded context"
],
"context_compiler": "PASS: bounded typed Interaction fragments are compiled before the cognitive gateway request",
"candidate_slot_b": {
"components": "creator-web, core-runtime, cognitive-worker, research-runtime healthy",
"restart": "candidate core-runtime and cognitive-worker restarted; both health and ready endpoints recovered",
"local_medium": "governed gateway advisory completed after restart"
}
},
"preserved_invariants": [
"Capability remains canonical owner of binding lifecycle",
"Interaction remains canonical owner of utterances and ProjectReference",
"no direct semantic SQL update",
"PersonalMemory V1 is not restored as primary retrieval",
"Lifeboat is fallback, not proof of Local MEDIUM"
],
"not_claimed": [
"browser/authenticated production acceptance",
"production promotion",
"compatibility telemetry observation",
"V1 retirement readiness"
],
"rollback": "Supervisor release rollback returns traffic to the stable last-known-good ReleaseSet; stable containers are not recreated."
}
@@ -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"
}
@@ -0,0 +1,51 @@
{
"task": "GEN2-DEPLOY-003",
"status": "PASS",
"timestamp_utc": "2026-08-30T11:09:35Z",
"root_cause": {
"stable_and_candidate_project": "assistant",
"stable_and_candidate_working_directory": "/home/m/assistant-deploy/repo/deploy",
"updater": "/home/m/assistant-deploy/update.sh",
"updater_schedule": "cron every five minutes",
"unsafe_operation": "the updater ran full docker compose up for the stable project and invoked the old in-project candidate canary"
},
"remediation": {
"stable_project": "assistant",
"candidate_project": "assistant-slot-b",
"candidate_file": "deploy/docker-compose.candidate-slot.yml",
"shared_resources": ["assistant_default external network", "llm-local_default external network", "existing secret and research volumes"],
"updater_ownership": "stable-only updater; candidate release command is not invoked by the updater",
"supervisor_release_set_slot": "ReleaseSet.slot persisted in durable Supervisor state"
},
"candidate_revision": "216d76f1d8b4eeba6ee9427da4c87c3e5c728c02",
"stable_revision": "2ee5da81145cdad2da1c92c24a91145ad4af61a5",
"candidate_components": {
"creator-web": "healthy",
"core-runtime": "healthy",
"cognitive-worker": "healthy",
"research-runtime": "healthy"
},
"drill": {
"stable_ids_captured_before_candidate": true,
"stable_ids_unchanged_after_candidate_start": true,
"header_canary": "candidate slot b",
"forced_failure": "candidate cognitive-worker stopped",
"rollback": "Supervisor release rollback returned traffic to legacy stable without recreating stable containers",
"stable_ids_unchanged_after_rollback": true,
"supervisor_restart": "last-known-good legacy stable release set preserved",
"candidate_recovery": "candidate cognitive-worker restored healthy",
"stable_ids_unchanged_after_candidate_recovery": true
},
"final_state": {
"traffic_mode": "STABLE",
"stable_upstream": "http://creator-web:8084",
"last_known_good_slot": "legacy-stable",
"public_health": 200,
"public_ready": 200
},
"limits": [
"The existing stable project remains legacy-stable during the non-disruptive transition; it was not recreated merely to rename it.",
"Local MEDIUM discovery was slow/unavailable during candidate cognitive startup; it delayed startup but did not affect stable traffic or the isolated release-set drill.",
"GEN2-COMPAT-OBS-001 has not resumed and no V1 path has been retired."
]
}

Some files were not shown because too many files have changed in this diff Show More