Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f16ec75f5 | ||
|
|
c0ffc59666 | ||
|
|
aa5e33778c | ||
|
|
870392dc09 | ||
|
|
005d244950 | ||
|
|
c44dd9e0be | ||
|
|
2f639c9d6c | ||
|
|
ec7c4fef66 | ||
|
|
c831ee1260 | ||
|
|
81ce8dfdb2 | ||
|
|
750c87c926 | ||
|
|
e6e051d59f | ||
|
|
5a9714645f | ||
|
|
cf818cfc47 | ||
|
|
e21eae7a99 | ||
|
|
dec19e873d | ||
|
|
d1710907ec | ||
|
|
2f085858db | ||
|
|
a8be998cee | ||
|
|
f42a94ee7a | ||
|
|
35666c9923 | ||
|
|
da83248ac9 | ||
|
|
db139e6800 | ||
|
|
b4335d6735 | ||
|
|
44541b1ba3 | ||
|
|
9c42b18850 | ||
|
|
1c05619899 | ||
|
|
a6b0fcd768 | ||
|
|
dd9bca3ace | ||
|
|
9d94a86300 |
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -442,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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -62,3 +62,13 @@ func (r *IdentityRepository) UpdateCreatorPassword(ctx context.Context, actor id
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *IdentityRepository) LoadServicePrincipal(ctx context.Context, principal identity.PrincipalID) (identity.ServicePrincipal, error) {
|
||||
var value identity.ServicePrincipal
|
||||
err := r.pool.QueryRow(ctx, `SELECT actor_id, principal_id, kind, purpose, synthetic_scope, active, revision FROM identity.service_principals WHERE principal_id=$1`, principal).
|
||||
Scan(&value.ActorID, &value.PrincipalID, &value.Kind, &value.Purpose, &value.SyntheticScope, &value.Active, &value.Revision)
|
||||
if err != nil {
|
||||
return identity.ServicePrincipal{}, fmt.Errorf("load service principal: %w", err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,13 @@ import (
|
||||
// evidence. It never evaluates, promotes or changes Capability state.
|
||||
type SelfRepository struct{ pool *pgxpool.Pool }
|
||||
|
||||
type AcceptanceSummary struct {
|
||||
LearningSignals int `json:"learning_signals"`
|
||||
ImprovementNeeds int `json:"improvement_needs"`
|
||||
LearningLedger int `json:"learning_ledger"`
|
||||
SyntheticPrincipal string `json:"synthetic_principal"`
|
||||
}
|
||||
|
||||
func OpenSelf(ctx context.Context, config ContinuityConfig) (*SelfRepository, error) {
|
||||
pool, err := openPool(ctx, config)
|
||||
if err != nil {
|
||||
@@ -23,6 +30,21 @@ func OpenSelf(ctx context.Context, config ContinuityConfig) (*SelfRepository, er
|
||||
|
||||
func (r *SelfRepository) Close() { r.pool.Close() }
|
||||
|
||||
func (r *SelfRepository) AcceptanceSummary(ctx context.Context, principalID string) (AcceptanceSummary, error) {
|
||||
var value AcceptanceSummary
|
||||
value.SyntheticPrincipal = principalID
|
||||
if err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM self.learning_signals WHERE principal_id=$1`, principalID).Scan(&value.LearningSignals); err != nil {
|
||||
return AcceptanceSummary{}, err
|
||||
}
|
||||
if err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM self.improvement_needs WHERE principal_id=$1`, principalID).Scan(&value.ImprovementNeeds); err != nil {
|
||||
return AcceptanceSummary{}, err
|
||||
}
|
||||
if err := r.pool.QueryRow(ctx, `SELECT COUNT(*) FROM self.learning_ledger WHERE principal_id=$1`, principalID).Scan(&value.LearningLedger); err != nil {
|
||||
return AcceptanceSummary{}, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *SelfRepository) SaveImprovementNeed(ctx context.Context, value selfcontract.ImprovementNeed) error {
|
||||
subject, err := json.Marshal(value.SubjectRef)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,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.
|
||||
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
"git.cin.su/m/Assistant/contracts/interaction"
|
||||
)
|
||||
|
||||
const tokenPath = "/run/secrets/acceptance_harness_service_token"
|
||||
|
||||
type client struct {
|
||||
base string
|
||||
token string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
func main() {
|
||||
base := strings.TrimRight(os.Getenv("CORE_RUNTIME_URL"), "/")
|
||||
if base == "" {
|
||||
base = "http://core-runtime-candidate:8080"
|
||||
}
|
||||
token, err := os.ReadFile(tokenPath)
|
||||
if err != nil {
|
||||
fail("acceptance credential unavailable")
|
||||
}
|
||||
c := client{base: base, token: strings.TrimSpace(string(token)), http: &http.Client{Timeout: 90 * time.Second}}
|
||||
if strings.TrimSpace(c.token) == "" {
|
||||
fail("acceptance credential unavailable")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
var issued struct {
|
||||
Token string
|
||||
Session identity.CreatorSession
|
||||
}
|
||||
if err := c.post(ctx, "/v1/acceptance/sessions", nil, &issued); err != nil {
|
||||
fail("acceptance authentication failed: " + err.Error())
|
||||
}
|
||||
if issued.Token == "" || issued.Session.PrincipalKind != identity.PrincipalKindService {
|
||||
fail("acceptance principal was not authenticated as service")
|
||||
}
|
||||
c.token = issued.Token
|
||||
var first interaction.ChatMessageResponse
|
||||
if err := c.post(ctx, "/v1/creator/chat/messages", interaction.ChatMessageRequest{Message: "synthetic acceptance statement", CorrelationID: "acceptance-harness", SyntheticAcceptance: true, SyntheticScopeID: "synthetic-acceptance-scope"}, &first); err != nil {
|
||||
fail("synthetic interaction failed: " + err.Error())
|
||||
}
|
||||
var corrected interaction.ChatMessageResponse
|
||||
if err := c.post(ctx, "/v1/creator/chat/messages", interaction.ChatMessageRequest{ConversationID: first.ConversationID, Message: "synthetic acceptance correction", CorrelationID: "acceptance-harness-correction", CorrectsUtteranceID: first.UtteranceID, SyntheticAcceptance: true, SyntheticScopeID: "synthetic-acceptance-scope"}, &corrected); err != nil {
|
||||
fail("synthetic correction failed: " + err.Error())
|
||||
}
|
||||
var summary struct {
|
||||
LearningSignals int `json:"learning_signals"`
|
||||
ImprovementNeeds int `json:"improvement_needs"`
|
||||
LearningLedger int `json:"learning_ledger"`
|
||||
}
|
||||
if err := c.get(ctx, "/v1/acceptance/self/summary", &summary); err != nil {
|
||||
fail("acceptance SELF summary unavailable: " + err.Error())
|
||||
}
|
||||
fmt.Printf("acceptance_pass learning_signals=%d improvement_needs=%d learning_ledger=%d\n", summary.LearningSignals, summary.ImprovementNeeds, summary.LearningLedger)
|
||||
}
|
||||
|
||||
func (c client) post(ctx context.Context, path string, body any, out any) error {
|
||||
return c.request(ctx, http.MethodPost, path, body, out)
|
||||
}
|
||||
func (c client) get(ctx context.Context, path string, out any) error {
|
||||
return c.request(ctx, http.MethodGet, path, nil, out)
|
||||
}
|
||||
func (c client) request(ctx context.Context, method, path string, body any, out any) error {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader = bytes.NewReader(data)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.base+path, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("acceptance request denied with status %d", resp.StatusCode)
|
||||
}
|
||||
if out != nil && json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(out) != nil {
|
||||
return errors.New("invalid acceptance response")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func fail(message string) { fmt.Fprintln(os.Stderr, message); os.Exit(1) }
|
||||
@@ -4,8 +4,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -53,8 +56,83 @@ type modelQualificationResponse struct {
|
||||
Results []modelQualificationResult `json:"results"`
|
||||
}
|
||||
|
||||
const qualificationControlPlaneTimeout = 45 * time.Second
|
||||
|
||||
// qualificationCoordinator bounds duplicate control-plane probes within one
|
||||
// cognitive-worker process. It is deliberately not canonical state: the
|
||||
// Capability owner remains the sole owner of qualification evidence and
|
||||
// activation. A restart may repeat an incomplete probe, but cannot create a
|
||||
// second semantic writer or bypass independent evaluation.
|
||||
type qualificationCoordinator struct {
|
||||
mu sync.Mutex
|
||||
flights map[string]*qualificationFlight
|
||||
}
|
||||
|
||||
type qualificationFlight struct {
|
||||
done chan struct{}
|
||||
response modelQualificationResponse
|
||||
err error
|
||||
waiters int
|
||||
}
|
||||
|
||||
func (c *qualificationCoordinator) do(ctx context.Context, key string, operation func() (modelQualificationResponse, error)) (modelQualificationResponse, bool, error) {
|
||||
c.mu.Lock()
|
||||
if c.flights == nil {
|
||||
c.flights = make(map[string]*qualificationFlight)
|
||||
}
|
||||
if current, exists := c.flights[key]; exists {
|
||||
current.waiters++
|
||||
c.mu.Unlock()
|
||||
select {
|
||||
case <-current.done:
|
||||
return current.response, false, current.err
|
||||
case <-ctx.Done():
|
||||
return modelQualificationResponse{}, false, ctx.Err()
|
||||
}
|
||||
}
|
||||
current := &qualificationFlight{done: make(chan struct{})}
|
||||
c.flights[key] = current
|
||||
c.mu.Unlock()
|
||||
|
||||
current.response, current.err = operation()
|
||||
close(current.done)
|
||||
c.mu.Lock()
|
||||
delete(c.flights, key)
|
||||
c.mu.Unlock()
|
||||
return current.response, true, current.err
|
||||
}
|
||||
|
||||
type qualificationPhaseTrace struct {
|
||||
correlationID string
|
||||
started time.Time
|
||||
}
|
||||
|
||||
func newQualificationPhaseTrace() qualificationPhaseTrace {
|
||||
correlationID, err := ids.New()
|
||||
if err != nil {
|
||||
correlationID = ids.ID(fmt.Sprintf("qualification-%d", time.Now().UnixNano()))
|
||||
}
|
||||
return qualificationPhaseTrace{correlationID: string(correlationID), started: time.Now()}
|
||||
}
|
||||
|
||||
func (t qualificationPhaseTrace) phase(name string, fields ...string) {
|
||||
log.Printf("qualification_phase correlation_id=%s phase=%s elapsed_ms=%d %s", t.correlationID, name, time.Since(t.started).Milliseconds(), strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
func qualificationFlightKey(provider string, modelIDs []capability.ProviderModelID) string {
|
||||
ids := make([]string, 0, len(modelIDs))
|
||||
for _, modelID := range modelIDs {
|
||||
ids = append(ids, string(modelID))
|
||||
}
|
||||
sort.Strings(ids)
|
||||
return provider + "|" + strings.Join(ids, ",")
|
||||
}
|
||||
|
||||
func modelQualificationHandler(providers map[string]providerRegistration, owner string, observers ...qualificationObserver) http.Handler {
|
||||
coordinator := &qualificationCoordinator{}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
trace := newQualificationPhaseTrace()
|
||||
trace.phase("Q0", "event=request_accepted")
|
||||
name := r.PathValue("provider")
|
||||
registration, ok := providers[name]
|
||||
if !ok || registration.Discovery == nil || registration.Provider == nil {
|
||||
@@ -72,32 +150,65 @@ func modelQualificationHandler(providers map[string]providerRegistration, owner
|
||||
http.Error(w, "max_output_bytes exceeds qualification bound", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
modelsFound, probe := registration.Discovery.Discover(r.Context())
|
||||
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
|
||||
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
|
||||
return
|
||||
controlCtx, cancel := context.WithTimeout(r.Context(), qualificationControlPlaneTimeout)
|
||||
defer cancel()
|
||||
response, leader, err := coordinator.do(controlCtx, qualificationFlightKey(name, request.ModelIDs), func() (modelQualificationResponse, error) {
|
||||
trace.phase("Q1", "event=binding_candidate_loaded")
|
||||
modelsFound, probe := registration.Discovery.Discover(controlCtx)
|
||||
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
|
||||
return modelQualificationResponse{}, fmt.Errorf("provider unavailable for qualification")
|
||||
}
|
||||
if len(request.ModelIDs) > 0 {
|
||||
modelsFound = slices.DeleteFunc(modelsFound, func(model models.DiscoveredModel) bool {
|
||||
return !slices.Contains(request.ModelIDs, model.ProviderModelID)
|
||||
})
|
||||
}
|
||||
if len(modelsFound) == 0 {
|
||||
return modelQualificationResponse{}, fmt.Errorf("requested model not discovered")
|
||||
}
|
||||
if len(modelsFound) > 16 {
|
||||
modelsFound = modelsFound[:16]
|
||||
}
|
||||
trace.phase("Q2", fmt.Sprintf("event=provider_model_resolved model_count=%d", len(modelsFound)))
|
||||
trace.phase("Q3", "event=bounded_invocation_started")
|
||||
results := qualifyModels(controlCtx, registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
|
||||
trace.phase("Q4", "event=bounded_invocation_completed")
|
||||
trace.phase("Q5", fmt.Sprintf("event=invocation_evidence_built result_count=%d", len(results)))
|
||||
if len(observers) > 0 && observers[0] != nil {
|
||||
trace.phase("Q6", "event=observation_persistence_started")
|
||||
if err := persistQualificationResults(controlCtx, observers[0], registration, results); err != nil {
|
||||
return modelQualificationResponse{}, fmt.Errorf("persist model qualification observation: %w", err)
|
||||
}
|
||||
trace.phase("Q7", "event=observation_persistence_completed")
|
||||
}
|
||||
// Q8-Q17 are intentionally outside this worker's authority. Capability
|
||||
// owns independent evaluation and governed promotion; this trace makes
|
||||
// that hand-off explicit instead of disguising it as activation.
|
||||
trace.phase("Q8", "event=independent_evaluation_not_started owner=capability")
|
||||
return modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results}, nil
|
||||
})
|
||||
if !leader {
|
||||
trace.phase("Q0", "event=joined_in_progress_qualification")
|
||||
}
|
||||
if len(request.ModelIDs) > 0 {
|
||||
modelsFound = slices.DeleteFunc(modelsFound, func(model models.DiscoveredModel) bool {
|
||||
return !slices.Contains(request.ModelIDs, model.ProviderModelID)
|
||||
})
|
||||
}
|
||||
if len(modelsFound) == 0 {
|
||||
http.Error(w, "requested model not discovered", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if len(modelsFound) > 16 {
|
||||
modelsFound = modelsFound[:16]
|
||||
}
|
||||
results := qualifyModels(r.Context(), registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
|
||||
if len(observers) > 0 && observers[0] != nil {
|
||||
if err := persistQualificationResults(r.Context(), observers[0], registration, results); err != nil {
|
||||
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
|
||||
if err != nil {
|
||||
if controlCtx.Err() != nil {
|
||||
http.Error(w, "qualification timeout", http.StatusGatewayTimeout)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "requested model not discovered") {
|
||||
http.Error(w, "requested model not discovered", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "provider unavailable") {
|
||||
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
trace.phase("Q17", "event=qualification_response_returned activation=not_attempted")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = jsonEncode(w, modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results})
|
||||
_ = jsonEncode(w, response)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/adapters/models"
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
@@ -17,6 +18,7 @@ type qualificationProvider struct{}
|
||||
func (qualificationProvider) Probe(context.Context) models.ProviderProbe {
|
||||
return models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
|
||||
func (qualificationProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
|
||||
return []models.DiscoveredModel{{ProviderModelID: "model-a"}, {ProviderModelID: "model-b"}}, models.ProviderProbe{Health: capability.HealthAvailable}
|
||||
}
|
||||
@@ -83,3 +85,63 @@ func TestModelQualificationPersistsReachabilityWithoutQuality(t *testing.T) {
|
||||
t.Fatalf("invalid qualification observation: %+v err=%v", observer.observations[0], err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelQualificationDeduplicatesSameProviderModelFlight(t *testing.T) {
|
||||
coordinator := &qualificationCoordinator{}
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
firstDone := make(chan struct{})
|
||||
secondDone := make(chan struct{})
|
||||
result := modelQualificationResponse{Provider: "medium"}
|
||||
go func() {
|
||||
_, _, err := coordinator.do(context.Background(), "medium|model-a", func() (modelQualificationResponse, error) {
|
||||
close(started)
|
||||
<-release
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("leader qualification failed: %v", err)
|
||||
}
|
||||
close(firstDone)
|
||||
}()
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("qualification invocation did not start")
|
||||
}
|
||||
go func() {
|
||||
actual, leader, err := coordinator.do(context.Background(), "medium|model-a", func() (modelQualificationResponse, error) {
|
||||
t.Error("duplicate qualification operation ran")
|
||||
return modelQualificationResponse{}, nil
|
||||
})
|
||||
if err != nil || leader || actual.Provider != result.Provider {
|
||||
t.Errorf("joined qualification response=%+v leader=%t err=%v", actual, leader, err)
|
||||
}
|
||||
close(secondDone)
|
||||
}()
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for {
|
||||
coordinator.mu.Lock()
|
||||
flight := coordinator.flights["medium|model-a"]
|
||||
waiters := 0
|
||||
if flight != nil {
|
||||
waiters = flight.waiters
|
||||
}
|
||||
coordinator.mu.Unlock()
|
||||
if waiters == 1 {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("second qualification did not join the in-progress flight")
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
close(release)
|
||||
for _, done := range []chan struct{}{firstDone, secondDone} {
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("qualification flight did not complete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+339
-14
@@ -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,14 +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"
|
||||
selfDBPasswordPath = "/run/secrets/self_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() {
|
||||
@@ -105,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)
|
||||
@@ -137,6 +207,18 @@ func main() {
|
||||
} 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)
|
||||
@@ -161,12 +243,32 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
if err != nil {
|
||||
if err != nil || (session.PrincipalKind != "" && session.PrincipalKind != identity.PrincipalKindHuman) {
|
||||
http.Error(w, "creator session denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
authenticateAcceptance := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
if acceptanceServiceToken == "" || acceptancePrincipal.Kind != identity.PrincipalKindService || !acceptancePrincipal.Active || !identitydomain.BootstrapMatches(acceptanceServiceToken, bearerToken(r)) {
|
||||
http.Error(w, "acceptance authentication denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
_, session, err := identitydomain.IssueServiceSession(sessionKey, acceptancePrincipal, time.Now(), 30*time.Minute)
|
||||
if err != nil {
|
||||
http.Error(w, "acceptance session unavailable", http.StatusServiceUnavailable)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
authenticateAcceptanceSession := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
|
||||
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
|
||||
if err != nil || session.PrincipalKind != identity.PrincipalKindService || session.PrincipalID != acceptancePrincipal.PrincipalID || !acceptancePrincipal.Active {
|
||||
http.Error(w, "acceptance session denied", http.StatusUnauthorized)
|
||||
return identity.CreatorSession{}, false
|
||||
}
|
||||
return session, true
|
||||
}
|
||||
resolveFocus := func(r *http.Request) (situation.FocusBinding, error) {
|
||||
runtime := situation.RuntimeBinding{SessionID: situation.SessionID(r.URL.Query().Get("session_id")), Workspace: situation.WorkspaceID(r.URL.Query().Get("workspace_id"))}
|
||||
bindings, err := repository.ByRuntime(r.Context(), runtime)
|
||||
@@ -217,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 {
|
||||
@@ -527,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()
|
||||
@@ -1489,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 {
|
||||
@@ -1669,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))
|
||||
@@ -1963,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)
|
||||
}
|
||||
@@ -1980,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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,23 @@ const (
|
||||
|
||||
func main() {
|
||||
reason := flag.String("reason", "", "rotation reason: owner-requested-rotation or exposure-detected")
|
||||
credential := flag.String("credential", "control-plane", "credential purpose: control-plane or acceptance-harness")
|
||||
flag.Parse()
|
||||
if flag.NArg() != 1 {
|
||||
fail("usage: secret-control-plane [ensure|rotate]")
|
||||
}
|
||||
store, err := platformsecrets.NewLocalMachineCredentialStore(defaultRoot, defaultRuntimeUID, defaultRuntimeGID, nil)
|
||||
root := os.Getenv("SECRET_CONTROL_PLANE_ROOT")
|
||||
if root == "" {
|
||||
root = defaultRoot
|
||||
}
|
||||
store, err := platformsecrets.NewLocalMachineCredentialStore(root, defaultRuntimeUID, defaultRuntimeGID, nil)
|
||||
if err != nil {
|
||||
fail("control-plane credential store unavailable")
|
||||
}
|
||||
spec := platformsecrets.ControlPlaneServiceTokenSpec()
|
||||
if strings.TrimSpace(*credential) == "acceptance-harness" {
|
||||
spec = platformsecrets.AcceptanceHarnessServiceTokenSpec()
|
||||
}
|
||||
switch strings.TrimSpace(flag.Arg(0)) {
|
||||
case "ensure":
|
||||
result, err := store.Ensure(spec)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,5 +16,23 @@ type CreatorAccount struct {
|
||||
type CreatorSession struct {
|
||||
ActorID ActorID
|
||||
PrincipalID PrincipalID
|
||||
PrincipalKind PrincipalKind
|
||||
ExpiresAtUnix int64
|
||||
}
|
||||
|
||||
type PrincipalKind string
|
||||
|
||||
const (
|
||||
PrincipalKindHuman PrincipalKind = "HUMAN"
|
||||
PrincipalKindService PrincipalKind = "SERVICE"
|
||||
)
|
||||
|
||||
type ServicePrincipal struct {
|
||||
ActorID ActorID
|
||||
PrincipalID PrincipalID
|
||||
Kind PrincipalKind
|
||||
Purpose string
|
||||
SyntheticScope string
|
||||
Active bool
|
||||
Revision int64
|
||||
}
|
||||
|
||||
@@ -215,6 +215,8 @@ type ChatMessageRequest struct {
|
||||
Message string
|
||||
CorrelationID string
|
||||
CorrectsUtteranceID HumanUtteranceID
|
||||
SyntheticAcceptance bool
|
||||
SyntheticScopeID string
|
||||
}
|
||||
|
||||
type ChatMessageResponse struct {
|
||||
|
||||
@@ -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;
|
||||
@@ -23,7 +23,6 @@ services:
|
||||
SECRET_BACKEND_ROOT: /run/secrets
|
||||
volumes:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
|
||||
- assistant_research_sources:/var/lib/research-sources:ro
|
||||
networks:
|
||||
shared:
|
||||
@@ -50,10 +49,13 @@ 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
|
||||
@@ -70,6 +72,7 @@ services:
|
||||
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]
|
||||
|
||||
@@ -66,10 +66,13 @@ 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
|
||||
@@ -240,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}
|
||||
@@ -248,6 +254,7 @@ 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
|
||||
|
||||
@@ -259,6 +266,7 @@ services:
|
||||
command: ["ensure"]
|
||||
volumes:
|
||||
- assistant_control_plane_service_secrets:/run/control-plane-secrets
|
||||
- assistant_core_secrets:/run/core-secrets
|
||||
|
||||
cognitive-worker:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
|
||||
@@ -9,6 +9,13 @@ ENV_FILE="${ENV_FILE:-$BASE_DIR/production-compose.env}"
|
||||
COMPOSE_FILE="$REPO_DIR/deploy/docker-compose.prod.yml"
|
||||
VOLUME="assistant_control_plane_service_secrets"
|
||||
ACTION="${1:-ensure}"
|
||||
CREDENTIAL="${MACHINE_CREDENTIAL:-control-plane}"
|
||||
SECRET_ROOT="/run/control-plane-secrets"
|
||||
VOLUME="assistant_control_plane_service_secrets"
|
||||
if [[ "$CREDENTIAL" == "acceptance-harness" ]]; then
|
||||
SECRET_ROOT="/run/core-secrets"
|
||||
VOLUME="assistant_core_secrets"
|
||||
fi
|
||||
|
||||
case "$ACTION" in
|
||||
ensure|rotate) ;;
|
||||
@@ -32,7 +39,7 @@ if [[ "$ACTION" == "rotate" ]]; then
|
||||
owner-requested-rotation|exposure-detected) ;;
|
||||
*) echo "invalid rotation reason" >&2; exit 2 ;;
|
||||
esac
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane rotate -reason "$reason"
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps -e "SECRET_CONTROL_PLANE_ROOT=$SECRET_ROOT" secret-control-plane -credential "$CREDENTIAL" -reason "$reason" rotate
|
||||
else
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane ensure
|
||||
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps -e "SECRET_CONTROL_PLANE_ROOT=$SECRET_ROOT" secret-control-plane -credential "$CREDENTIAL" ensure
|
||||
fi
|
||||
|
||||
@@ -23,6 +23,8 @@ 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
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"task_file":"dev/tasks/GEN2-SECRETS-001-local-machine-credential-provisioning.json","task_id":"GEN2-SECRETS-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"]
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"task_id": "GEN2-COGNITIVE-003",
|
||||
"task_revision": 3,
|
||||
"task_revision": 4,
|
||||
"architecture_revision": "ARCH-GEN2-000",
|
||||
"base_repo_revision": "gen2-integration",
|
||||
"risk_tier": "E",
|
||||
"intent": "Restore bounded OpenAI-compatible Local MEDIUM invocation without changing canonical ownership or Core semantics.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-004-LOCAL-MEDIUM-INVOCATION"],
|
||||
"intent": "Restore and phase-trace bounded Local MEDIUM qualification and invocation without changing Capability ownership, Core household budget, or activation authority.",
|
||||
"requirement_ids": ["REQ-GEN2-EVOLVE-004-LOCAL-MEDIUM-INVOCATION", "REQ-GEN2-COGNITIVE-003-QUALIFICATION-CONTROL-PLANE"],
|
||||
"allowed_change_scope": {
|
||||
"modules": ["cognitive-worker provider invocation", "existing local OpenAI-compatible adapter", "targeted evidence"],
|
||||
"paths": [
|
||||
"cmd/cognitive-worker/main.go",
|
||||
"cmd/cognitive-worker/advisory.go",
|
||||
"cmd/cognitive-worker/gateway_registry.go",
|
||||
"cmd/cognitive-worker/model_qualification.go",
|
||||
"cmd/cognitive-worker/*_test.go",
|
||||
"adapters/models/openai_compatible_local.go",
|
||||
"adapters/models/openai_compatible_local_test.go",
|
||||
@@ -66,13 +67,13 @@
|
||||
"may_change": ["provider request normalization and typed immediate failure mapping only"]
|
||||
},
|
||||
"semantic_owner": "Cognitive gateway remains the sole owner of provider invocation and route selection.",
|
||||
"canonical_state": ["discovery is not invocation qualification", "optional provider parameters are capability-gated", "immediate provider rejection is not reported as timeout"],
|
||||
"state_transitions": ["binding selection -> normalized request -> provider invocation -> typed result"],
|
||||
"canonical_state": ["discovery is not invocation qualification", "qualification evidence is not activation", "optional provider parameters are capability-gated", "immediate provider rejection is not reported as timeout"],
|
||||
"state_transitions": ["binding selection -> normalized request -> provider invocation -> typed result", "discovery -> bounded qualification evidence -> independent capability evaluation -> governed canary/activation"],
|
||||
"side_effects_and_privileges": ["no secrets or authorization headers in evidence", "no direct database access", "no provider calls outside the existing gateway boundary"],
|
||||
"persistence_and_migrations": ["no schema change"],
|
||||
"failure_retry_restart": ["bounded invocation; no timeout inflation; no infinite retry; preserve fail-closed/fallback semantics"],
|
||||
"failure_retry_restart": ["household invocation remains bounded by its existing 12-second Core budget", "qualification/revalidation is a separately bounded control-plane operation", "single-flight qualification per provider/model avoids duplicate work", "no timeout inflation; no infinite retry; preserve fail-closed/fallback semantics"],
|
||||
"assumptions": ["Local MEDIUM gateway remains the existing provider endpoint", "candidate slot B is available for isolated verification", "cmd/core-runtime/learning_test.go is a preserved carry-forward regression from the preceding runtime task"],
|
||||
"acceptance_scenarios": ["minimal invocation completes", "ordinary and bounded-context invocation complete", "immediate provider 4xx maps to typed rejection without timeout", "qualification requires bounded invocation evidence"],
|
||||
"acceptance_scenarios": ["safe phase trace emits correlation IDs and durations without prompts, answers, credentials, secrets, or user memory", "minimal invocation completes", "ordinary and bounded-context invocation complete", "immediate provider 4xx maps to typed rejection without timeout", "qualification requires bounded invocation evidence", "duplicate qualification is bounded and deduplicated", "fresh validated binding is not synchronously requalified on an ordinary household request"],
|
||||
"required_tests": ["targeted go test", "targeted go vet", "preflight", "check-diff", "independent", "architecture checks", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "REQUIRES_MIGRATION_DECISION", "BLOCKED_BY_DEPLOYMENT_CONTROL"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -52,4 +52,13 @@ Bounded post-M5 remediation. Provides the technical protected-secret control-pla
|
||||
- **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-а.
|
||||
|
||||
@@ -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,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,60 @@
|
||||
# GEN2-DEPLOY-004 r2 — candidate dependency-closure finding
|
||||
|
||||
## Decision and graph evidence
|
||||
|
||||
The owner-approved r2 rule requires every versioned Assistant runtime reached
|
||||
by normal candidate operation to be in the candidate ReleaseSet. Static
|
||||
runtime configuration establishes this candidate closure:
|
||||
|
||||
```text
|
||||
creator-web -> core-runtime, continuity-runtime, commit-runtime,
|
||||
cognitive-worker, agent-worker
|
||||
core-runtime -> cognitive-worker, research-runtime, continuity-runtime,
|
||||
commit-runtime
|
||||
commit-runtime -> core-runtime, continuity-runtime
|
||||
continuity-runtime -> core-runtime, commit-runtime
|
||||
agent-worker -> task-verifier, agent-execution-bridge
|
||||
agent-execution-bridge -> cognitive-worker, work-graph-runtime, task-verifier
|
||||
work-graph-runtime -> core-runtime
|
||||
```
|
||||
|
||||
Therefore the static closure is: Creator Web, Core Runtime, Cognitive Worker,
|
||||
Research Runtime, Commit Runtime, Continuity Runtime, Agent Worker, Agent
|
||||
Execution Bridge, Work Graph Runtime, and Task Verifier.
|
||||
|
||||
PostgreSQL, protected secret volumes, the local model gateway/lifeboat, and
|
||||
the configured web-search endpoint are classified as shared durable
|
||||
infrastructure or shared external services. They do not contain an Assistant
|
||||
semantic/runtime release revision.
|
||||
|
||||
## Blocking dynamic dependency
|
||||
|
||||
Commit's normal prospective-notification operation obtains the exact active
|
||||
notification Capability binding from Core. The binding's immutable
|
||||
`HTTPSOrigin`, delivery path, and revision select the receiver. This is not a
|
||||
Compose environment URL and cannot be safely redirected by a container alias.
|
||||
|
||||
Consequently, with the existing active binding, a candidate Commit executing a
|
||||
normal notification operation would call the stable notification receiver. A
|
||||
candidate notification-receiver container alone does not solve the problem:
|
||||
the candidate would still resolve the stable binding. Reusing the stable name
|
||||
would create ambiguous network routing, and changing the active binding would
|
||||
mutate Capability-owned canonical state.
|
||||
|
||||
No runtime, Capability state, migration, secret, or production container was
|
||||
changed while establishing this finding.
|
||||
|
||||
## Required owner decision
|
||||
|
||||
Choose one governed boundary before DEPLOY-004 implementation can prove full
|
||||
dependency isolation:
|
||||
|
||||
1. authorize a bounded Capability-owned candidate binding/shadow-binding
|
||||
lifecycle, including its authority, rollback and selection semantics; or
|
||||
2. explicitly classify the notification receiver as a shared external service
|
||||
for this ReleaseSet, despite it being an Assistant-managed runtime.
|
||||
|
||||
The first option requires a new packet revision with the exact Capability
|
||||
contract/owner paths. The second changes the release-boundary policy and must
|
||||
be accepted explicitly. No direct binding update, stable alias reuse, or
|
||||
candidate-to-stable shortcut is an acceptable substitute.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"evidence_id": "gen2-evolve-004-runtime-001-verification-2026-09-01",
|
||||
"task_id": "GEN2-EVOLVE-004-RUNTIME-001",
|
||||
"task_revision": 2,
|
||||
"recorded_at": "2026-09-01T00:15:40.9122835Z",
|
||||
"implementation": {
|
||||
"commit": "75fa8477d8ed6b7cecdfc34ea512c358774fbf84",
|
||||
"composition": "Core learningRecorder receives the existing SELF repository through the composition root",
|
||||
"canonical_writer": "SelfRepository only",
|
||||
"schema": "existing migration 055; no new migration"
|
||||
},
|
||||
"targeted_verification": {
|
||||
"correction_to_self_signal_need_ledger": "PASS in cmd/core-runtime focused test",
|
||||
"self_repository_failure_is_non_fatal": "PASS in cmd/core-runtime focused test",
|
||||
"core_self_domain_contracts": "PASS",
|
||||
"preflight_check_diff_independent_architecture_whitespace": "PASS"
|
||||
},
|
||||
"candidate_runtime": {
|
||||
"slot": "B",
|
||||
"revision": "dd9bca3ace51e1c0af50fd67dba20caad17cf391",
|
||||
"core_health": "healthy",
|
||||
"cognitive_health": "healthy",
|
||||
"core_restart_recovery": "PASS",
|
||||
"self_secret_mount": "READABLE (value not read)",
|
||||
"self_config": "self_role via credential reference"
|
||||
},
|
||||
"production_e2e": {
|
||||
"durable_authenticated_correction_and_reopen": "UNVERIFIED",
|
||||
"reason": "Browser acceptance is owner-deferred and no sanctioned authenticated service/test principal was available without reading a credential or creating an auth bypass."
|
||||
},
|
||||
"status": "IMPLEMENTED_TARGETED_VERIFIED_PRODUCTION_DURABLE_E2E_UNVERIFIED"
|
||||
}
|
||||
@@ -33,3 +33,21 @@ The following checks completed successfully before production execution:
|
||||
|
||||
No credential value, fingerprint, length, header, or plaintext environment
|
||||
value was read, printed, copied, committed, or included in this evidence.
|
||||
|
||||
## Production control-plane evidence
|
||||
|
||||
The one-shot provisioner was run from a detached `gen2-integration` worktree
|
||||
on `docker.cin.su`. It created the dedicated external volume and provisioned
|
||||
the credential once. A repeat `ensure` reported that the already active
|
||||
artifact was reused. A bounded owner-requested rotation then completed.
|
||||
|
||||
For creation, idempotent ensure, and rotation, the stable Core, Commit,
|
||||
Creator Web, Cognitive Worker, and Research Runtime container identities were
|
||||
recorded before and after the one-shot operation and were unchanged. The
|
||||
verification checked only artifact and separate metadata/audit file presence;
|
||||
it did not read their content.
|
||||
|
||||
At this point the credential is provisioned but not yet used by the stable
|
||||
runtime. Consumer mount and service-authentication acceptance are completed
|
||||
only when `GEN2-AUTHORITY-001` is merged and deployed as its own controlled
|
||||
candidate ReleaseSet.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
var ErrDelegatedAuthorityDenied = errors.New("delegated control-plane authority denied")
|
||||
|
||||
// ValidateDelegatedControlPlaneGrant proves only the Authority-owned decision.
|
||||
// It does not replace the receiving semantic owner's lifecycle preconditions.
|
||||
func ValidateDelegatedControlPlaneGrant(nowUnixMillis int64, binding identity.PrincipalBinding, grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest) error {
|
||||
if nowUnixMillis < 1 || binding.ActorID == "" || binding.PrincipalID == "" || !binding.Active ||
|
||||
grant.ID == "" || grant.Revision < 1 || grant.Status != authority.DelegatedGrantActive ||
|
||||
grant.ServiceActorID != binding.ActorID || grant.ServicePrincipalID != binding.PrincipalID ||
|
||||
grant.IssuerPrincipalID == "" || grant.ArchitectureRevision == "" || grant.Nonce == "" ||
|
||||
grant.IssuedAtUnixMillis < 1 || grant.ExpiresAtUnixMillis <= grant.IssuedAtUnixMillis || nowUnixMillis < grant.IssuedAtUnixMillis || nowUnixMillis >= grant.ExpiresAtUnixMillis || !RequestMatchesDelegatedControlPlaneGrant(grant, request) {
|
||||
return ErrDelegatedAuthorityDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RequestMatchesDelegatedControlPlaneGrant(grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest) bool {
|
||||
if grant.TaskID == "" || grant.TaskID != request.TaskID || grant.TaskRevision < 1 || grant.TaskRevision != request.TaskRevision ||
|
||||
request.GrantID != grant.ID || request.Operation == "" || !slices.Contains(grant.Operations, request.Operation) {
|
||||
return false
|
||||
}
|
||||
switch request.Operation {
|
||||
case authority.DelegatedOperationCreateCandidateCapabilityBinding, authority.DelegatedOperationUseCandidateCapabilityBinding, authority.DelegatedOperationRevokeCandidateCapabilityBinding:
|
||||
return validCandidateResource(grant.CandidateResource) && grant.CandidateResource == request.CandidateResource && request.Resource == (authority.CognitiveBindingResource{})
|
||||
default:
|
||||
return validCognitiveResource(grant.Resource) && grant.Resource == request.Resource && request.CandidateResource == (authority.CandidateCapabilityBindingResource{})
|
||||
}
|
||||
}
|
||||
|
||||
func validCognitiveResource(resource authority.CognitiveBindingResource) bool {
|
||||
return resource.BindingID != "" && resource.ProviderInstanceID != "" && resource.ModelDescriptorID != ""
|
||||
}
|
||||
|
||||
func validCandidateResource(resource authority.CandidateCapabilityBindingResource) bool {
|
||||
return resource.ReleaseSetID != "" && resource.Slot != "" && resource.CapabilityID != "" && resource.ResourceScope != "" &&
|
||||
(resource.Mode == "CANDIDATE" || resource.Mode == "SHADOW") && resource.TargetKind != "" && resource.TargetRef != "" && len(resource.TargetDigest) == 64
|
||||
}
|
||||
|
||||
func CanIssuerRevokeDelegatedGrant(issuer identity.PrincipalID, grant authority.DelegatedControlPlaneGrant) bool {
|
||||
return issuer != "" && issuer == grant.IssuerPrincipalID && grant.Status == authority.DelegatedGrantActive
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/authority"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
func delegatedGrant() authority.DelegatedControlPlaneGrant {
|
||||
return authority.DelegatedControlPlaneGrant{ID: "grant-1", ServiceActorID: "service-actor", ServicePrincipalID: "service-principal", IssuerPrincipalID: "owner", TaskID: "GEN2-COGNITIVE-003", TaskRevision: 4, Operations: []authority.DelegatedControlPlaneOperation{authority.DelegatedOperationCanaryCognitiveBinding}, Resource: authority.CognitiveBindingResource{BindingID: "binding-1", ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1"}, IssuedAtUnixMillis: 10, ExpiresAtUnixMillis: 100, ArchitectureRevision: "ADR-0021", Nonce: "nonce", Revision: 1, Status: authority.DelegatedGrantActive}
|
||||
}
|
||||
|
||||
func TestDelegatedControlPlaneGrantIsExactAndTimeBound(t *testing.T) {
|
||||
grant := delegatedGrant()
|
||||
binding := identity.PrincipalBinding{ActorID: "service-actor", PrincipalID: "service-principal", Active: true}
|
||||
request := authority.DelegatedControlPlaneRequest{GrantID: grant.ID, TaskID: grant.TaskID, TaskRevision: grant.TaskRevision, Operation: authority.DelegatedOperationCanaryCognitiveBinding, Resource: grant.Resource}
|
||||
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err != nil {
|
||||
t.Fatalf("valid delegation denied: %v", err)
|
||||
}
|
||||
request.Resource.BindingID = "other"
|
||||
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err == nil {
|
||||
t.Fatal("different resource was allowed")
|
||||
}
|
||||
request.Resource = grant.Resource
|
||||
if err := ValidateDelegatedControlPlaneGrant(100, binding, grant, request); err == nil {
|
||||
t.Fatal("expired grant was allowed")
|
||||
}
|
||||
grant.Status = authority.DelegatedGrantRevoked
|
||||
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err == nil {
|
||||
t.Fatal("revoked grant was allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegatedGrantCannotSelfRevokeOrBroaden(t *testing.T) {
|
||||
grant := delegatedGrant()
|
||||
if CanIssuerRevokeDelegatedGrant("service-principal", grant) {
|
||||
t.Fatal("service principal may not revoke its own grant")
|
||||
}
|
||||
if !CanIssuerRevokeDelegatedGrant("owner", grant) {
|
||||
t.Fatal("issuer could not revoke active grant")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateBindingGrantIsExactAndCannotBroaden(t *testing.T) {
|
||||
resource := authority.CandidateCapabilityBindingResource{ReleaseSetID: "assistant-slot-b", Slot: "b", CapabilityID: "prospective-notification-delivery", ResourceScope: "synthetic-shadow-notification-v1", Mode: "SHADOW", TargetKind: "NOTIFICATION_SHADOW_RECEIVER", TargetRef: "notification-receiver-candidate", TargetDigest: "271c79ca1b1a590f1309461a87ba39bc017dfeb8d99268d1a7bd3a16d1d4b4d3"}
|
||||
grant := authority.DelegatedControlPlaneGrant{ID: "candidate-grant", ServiceActorID: "service-actor", ServicePrincipalID: "service-principal", IssuerPrincipalID: "owner", TaskID: "GEN2-CAPABILITY-001", TaskRevision: 2, Operations: []authority.DelegatedControlPlaneOperation{authority.DelegatedOperationUseCandidateCapabilityBinding}, CandidateResource: resource, IssuedAtUnixMillis: 10, ExpiresAtUnixMillis: 100, ArchitectureRevision: "ADR-0021", Nonce: "nonce", Revision: 1, Status: authority.DelegatedGrantActive}
|
||||
binding := identity.PrincipalBinding{ActorID: "service-actor", PrincipalID: "service-principal", Active: true}
|
||||
request := authority.DelegatedControlPlaneRequest{GrantID: grant.ID, TaskID: grant.TaskID, TaskRevision: grant.TaskRevision, Operation: authority.DelegatedOperationUseCandidateCapabilityBinding, CandidateResource: resource}
|
||||
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err != nil {
|
||||
t.Fatalf("exact candidate grant denied: %v", err)
|
||||
}
|
||||
request.CandidateResource.ReleaseSetID = "assistant-slot-a"
|
||||
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err == nil {
|
||||
t.Fatal("different release set was allowed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
)
|
||||
|
||||
var ErrCandidateBindingRequired = errors.New("candidate capability binding required")
|
||||
|
||||
type canonicalCandidateTargetDescriptor struct {
|
||||
Version string `json:"version"`
|
||||
BindingRevision int64 `json:"binding_revision"`
|
||||
TargetKind string `json:"target_kind"`
|
||||
TargetRef string `json:"target_ref"`
|
||||
Protocol string `json:"protocol"`
|
||||
Origin string `json:"origin"`
|
||||
ReleaseSetID string `json:"release_set_id"`
|
||||
Slot string `json:"slot"`
|
||||
TaskID string `json:"task_id"`
|
||||
TaskRevision int64 `json:"task_revision"`
|
||||
CapabilityID string `json:"capability_id"`
|
||||
ResourceScope string `json:"resource_scope"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
func CandidateTargetDigest(value capability.CandidateBinding) (string, error) {
|
||||
origin, err := url.Parse(value.TargetOrigin)
|
||||
if err != nil || origin.Host == "" || origin.User != nil || origin.RawQuery != "" || origin.Fragment != "" || (origin.Path != "" && origin.Path != "/") {
|
||||
return "", ErrCandidateBindingRequired
|
||||
}
|
||||
origin.Scheme, origin.Host, origin.Path = strings.ToLower(origin.Scheme), strings.ToLower(origin.Host), ""
|
||||
descriptor := canonicalCandidateTargetDescriptor{Version: "v1", BindingRevision: value.Revision, TargetKind: value.Context.TargetKind, TargetRef: value.Context.TargetRef, Protocol: string(value.Protocol), Origin: origin.String(), ReleaseSetID: value.Context.ReleaseSetID, Slot: value.Context.Slot, TaskID: value.Context.TaskID, TaskRevision: value.Context.TaskRevision, CapabilityID: string(value.Context.CapabilityID), ResourceScope: value.Context.ResourceScope, Mode: string(value.Context.Mode)}
|
||||
encoded, err := json.Marshal(descriptor)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256(encoded)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
// ValidateCandidateBinding validates Capability-owned candidate state. Stable
|
||||
// Binding validation intentionally remains stricter and HTTPS-only.
|
||||
func ValidateCandidateBinding(value capability.CandidateBinding) error {
|
||||
context := value.Context
|
||||
if value.ID == "" || value.Revision < 1 || value.BaseActiveBindingID == "" || value.BaseActiveBindingRevision < 1 ||
|
||||
context.ReleaseSetID == "" || context.Slot == "" || context.TaskID == "" || context.TaskRevision < 1 || context.PrincipalID == "" ||
|
||||
context.CapabilityID == "" || context.ResourceScope == "" || context.TargetKind == "" || context.TargetRef == "" || len(context.TargetDigest) != 64 || context.AuthorityGrantID == "" ||
|
||||
(value.Status != capability.CandidateBindingActive && value.Status != capability.CandidateBindingRevoked) ||
|
||||
(value.Context.Mode != capability.CandidateBindingModeCandidate && value.Context.Mode != capability.CandidateBindingModeShadow) ||
|
||||
value.Protocol != capability.ProtocolIdempotentReceiptV1 || value.CreatedAtUnixMillis < 1 || value.ExpiresAtUnixMillis <= value.CreatedAtUnixMillis ||
|
||||
!fixedPath(value.DeliveryPath) || !fixedPath(value.ReceiptPath) || value.DeliveryPath == value.ReceiptPath {
|
||||
return ErrCandidateBindingRequired
|
||||
}
|
||||
origin, err := url.Parse(value.TargetOrigin)
|
||||
if err != nil || origin.Host == "" || origin.User != nil || origin.RawQuery != "" || origin.Fragment != "" || (origin.Path != "" && origin.Path != "/") {
|
||||
return ErrCandidateBindingRequired
|
||||
}
|
||||
if context.Mode == capability.CandidateBindingModeCandidate && origin.Scheme != "https" {
|
||||
return ErrCandidateBindingRequired
|
||||
}
|
||||
if context.Mode == capability.CandidateBindingModeShadow && origin.Scheme != "http" && origin.Scheme != "https" {
|
||||
return ErrCandidateBindingRequired
|
||||
}
|
||||
if strings.TrimSpace(value.TargetOrigin) != value.TargetOrigin {
|
||||
return ErrCandidateBindingRequired
|
||||
}
|
||||
digest, err := CandidateTargetDigest(value)
|
||||
if err != nil || digest != context.TargetDigest {
|
||||
return ErrCandidateBindingRequired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CandidateBindingInvocable(value capability.CandidateBinding, context capability.CandidateReleaseContext, nowUnixMillis int64) error {
|
||||
if ValidateCandidateBinding(value) != nil || value.Status != capability.CandidateBindingActive || nowUnixMillis < value.CreatedAtUnixMillis || nowUnixMillis >= value.ExpiresAtUnixMillis || value.Context != context {
|
||||
return ErrCandidateBindingRequired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
func validCandidateBinding() capability.CandidateBinding {
|
||||
context := capability.CandidateReleaseContext{ReleaseSetID: "assistant-slot-b", Slot: "b", TaskID: "GEN2-CAPABILITY-001", TaskRevision: 7, PrincipalID: identity.PrincipalID("owner-1"), CapabilityID: "prospective-notification-delivery", ResourceScope: "synthetic-shadow-notification-v1", Mode: capability.CandidateBindingModeShadow, TargetKind: "NOTIFICATION_SHADOW_RECEIVER", TargetRef: "notification-receiver-candidate", AuthorityGrantID: "grant-1"}
|
||||
binding := capability.CandidateBinding{ID: "candidate-binding-1", Revision: 1, BaseActiveBindingID: "stable-binding-1", BaseActiveBindingRevision: 1, Context: context, TargetOrigin: "http://notification-receiver-candidate:8085", DeliveryPath: "/delivery/v1/deliveries", ReceiptPath: "/delivery/v1/receipts", Protocol: capability.ProtocolIdempotentReceiptV1, CreatedAtUnixMillis: 10, ExpiresAtUnixMillis: 100, Status: capability.CandidateBindingActive}
|
||||
digest, err := CandidateTargetDigest(binding)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
binding.Context.TargetDigest = digest
|
||||
return binding
|
||||
}
|
||||
|
||||
func TestCandidateBindingIsExactAndFailsClosed(t *testing.T) {
|
||||
binding := validCandidateBinding()
|
||||
if err := CandidateBindingInvocable(binding, binding.Context, 20); err != nil {
|
||||
t.Fatalf("valid candidate binding denied: %v", err)
|
||||
}
|
||||
for _, mutate := range []func(*capability.CandidateReleaseContext){
|
||||
func(v *capability.CandidateReleaseContext) { v.ReleaseSetID = "assistant-slot-other" },
|
||||
func(v *capability.CandidateReleaseContext) { v.TaskID = "GEN2-OTHER" },
|
||||
func(v *capability.CandidateReleaseContext) { v.AuthorityGrantID = "other-grant" },
|
||||
} {
|
||||
request := binding.Context
|
||||
mutate(&request)
|
||||
if err := CandidateBindingInvocable(binding, request, 20); err == nil {
|
||||
t.Fatal("mismatched candidate context was allowed")
|
||||
}
|
||||
}
|
||||
if err := CandidateBindingInvocable(binding, binding.Context, 100); err == nil {
|
||||
t.Fatal("expired candidate binding was allowed")
|
||||
}
|
||||
binding.Status = capability.CandidateBindingRevoked
|
||||
if err := CandidateBindingInvocable(binding, binding.Context, 20); err == nil {
|
||||
t.Fatal("revoked candidate binding was allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandidateTargetDigestBindsSemanticDescriptor(t *testing.T) {
|
||||
binding := validCandidateBinding()
|
||||
digest, err := CandidateTargetDigest(binding)
|
||||
if err != nil || digest != binding.Context.TargetDigest {
|
||||
t.Fatalf("digest=%q err=%v", digest, err)
|
||||
}
|
||||
if digest != "f9f9fa49a0727444c280c092d5bb340f3a8f33cf325c5396f5ae177a6666da35" {
|
||||
t.Fatalf("canonical descriptor digest changed: %q", digest)
|
||||
}
|
||||
changed := binding
|
||||
changed.Context.ResourceScope = "different-resource"
|
||||
other, err := CandidateTargetDigest(changed)
|
||||
if err != nil || other == digest {
|
||||
t.Fatalf("scope did not affect digest: %q %q err=%v", digest, other, err)
|
||||
}
|
||||
changed = binding
|
||||
changed.Revision++
|
||||
other, err = CandidateTargetDigest(changed)
|
||||
if err != nil || other == digest {
|
||||
t.Fatalf("binding revision did not affect digest: %q %q err=%v", digest, other, err)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ type Repository interface {
|
||||
SaveBinding(context.Context, capability.Binding) error
|
||||
LoadBinding(context.Context, capability.CapabilityBindingID, int64) (capability.Binding, error)
|
||||
LoadActiveBinding(context.Context, capability.CapabilityID, identity.PrincipalID) (capability.Binding, error)
|
||||
SaveCandidateBinding(context.Context, capability.CandidateBinding) error
|
||||
ResolveCandidateBinding(context.Context, capability.CandidateReleaseContext, int64) (capability.CandidateBindingResolution, error)
|
||||
RevokeCandidateBinding(context.Context, capability.CapabilityBindingID, int64, int64) (bool, error)
|
||||
SaveProviderInstance(context.Context, capability.ProviderInstance) error
|
||||
SaveModelDescriptor(context.Context, capability.ModelDescriptor) error
|
||||
SaveCognitiveBinding(context.Context, capability.CognitiveBinding) error
|
||||
|
||||
@@ -98,19 +98,26 @@ func validDigest(value string) bool {
|
||||
// Execute records UNKNOWN_OUTCOME rather than silently retrying an ambiguous
|
||||
// external mutation.
|
||||
func Execute(action commitcontract.AuthorizedAction, attemptID commitcontract.ActionAttemptID, executor ports.Executor) (commitcontract.ActionAttempt, error) {
|
||||
attempt, _, err := ExecuteWithResult(action, attemptID, executor)
|
||||
return attempt, err
|
||||
}
|
||||
|
||||
// ExecuteWithResult preserves executor evidence for the Commit runtime while
|
||||
// keeping the original domain API compatible with existing callers.
|
||||
func ExecuteWithResult(action commitcontract.AuthorizedAction, attemptID commitcontract.ActionAttemptID, executor ports.Executor) (commitcontract.ActionAttempt, ports.ExecutionResult, error) {
|
||||
if action.State != commitcontract.ActionAuthorized || attemptID == "" {
|
||||
return commitcontract.ActionAttempt{}, ErrUnknownOutcome
|
||||
return commitcontract.ActionAttempt{}, ports.ExecutionResult{}, ErrUnknownOutcome
|
||||
}
|
||||
attempt := commitcontract.ActionAttempt{ID: attemptID, AuthorizedID: action.ID, IntentID: action.Proposal.IntentID, State: commitcontract.ActionExecuting}
|
||||
result := executor.Execute(action)
|
||||
if !result.Known {
|
||||
attempt.State = commitcontract.ActionUnknownOutcome
|
||||
return attempt, ErrUnknownOutcome
|
||||
return attempt, result, ErrUnknownOutcome
|
||||
}
|
||||
if result.Succeeded {
|
||||
attempt.State = commitcontract.ActionSucceeded
|
||||
return attempt, nil
|
||||
return attempt, result, nil
|
||||
}
|
||||
attempt.State = commitcontract.ActionFailed
|
||||
return attempt, nil
|
||||
return attempt, result, nil
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ type Reconciler interface {
|
||||
type ExecutionResult struct {
|
||||
Succeeded bool
|
||||
Known bool
|
||||
Receipt *commit.ExecutionReceipt
|
||||
}
|
||||
|
||||
// AuthorityValidator is the remote-capable boundary to the Authority owner.
|
||||
|
||||
@@ -50,6 +50,9 @@ func NewExperienceRecord(value epistemics.ExperienceRecord) (epistemics.Experien
|
||||
if value.VerificationEvidence == nil {
|
||||
value.VerificationEvidence = []epistemics.EvidenceRef{}
|
||||
}
|
||||
if value.ApplicabilityConditions == nil {
|
||||
value.ApplicabilityConditions = []string{}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -85,3 +85,22 @@ func TestFastPathDeclineReturnsToThinkingInsteadOfGuessing(t *testing.T) {
|
||||
t.Fatalf("decline should preserve reflection/escalation evidence, candidate=%#v ok=%v err=%v", candidate, ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExperienceNormalizesApplicabilityConditionsForNonNullPersistence(t *testing.T) {
|
||||
experience, err := NewExperienceRecord(epistemics.ExperienceRecord{
|
||||
SourceKind: epistemics.ExperienceSourceInteraction,
|
||||
OriginalIntentID: "intent-1",
|
||||
SourceUtteranceID: "utterance-4",
|
||||
HelpMethod: epistemics.HelpMethodClarification,
|
||||
ExpectedOutcome: epistemics.OutcomeSuccess,
|
||||
ActualOutcome: epistemics.OutcomeUserCorrected,
|
||||
ConfidenceMillis: 1000,
|
||||
RecordedAtUnixMillis: 1300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if experience.ApplicabilityConditions == nil {
|
||||
t.Fatal("applicability conditions must be non-nil before repository persistence")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,23 @@ func IssueCreatorSessionForBinding(secret []byte, actorID identity.ActorID, prin
|
||||
if len(secret) < 32 || actorID == "" || principalID == "" || ttl <= 0 {
|
||||
return "", identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
session := identity.CreatorSession{ActorID: actorID, PrincipalID: principalID, ExpiresAtUnix: now.Add(ttl).Unix()}
|
||||
session := identity.CreatorSession{ActorID: actorID, PrincipalID: principalID, PrincipalKind: identity.PrincipalKindHuman, ExpiresAtUnix: now.Add(ttl).Unix()}
|
||||
payload, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return "", identity.CreatorSession{}, err
|
||||
}
|
||||
encoded := base64.RawURLEncoding.EncodeToString(payload)
|
||||
return encoded + "." + signature(secret, encoded), session, nil
|
||||
}
|
||||
|
||||
func IssueServiceSession(secret []byte, principal identity.ServicePrincipal, now time.Time, ttl time.Duration) (string, identity.CreatorSession, error) {
|
||||
if principal.Kind != identity.PrincipalKindService || !principal.Active || principal.ActorID == "" || principal.PrincipalID == "" || principal.Purpose == "" || ttl <= 0 {
|
||||
return "", identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
if len(secret) < 32 {
|
||||
return "", identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
session := identity.CreatorSession{ActorID: principal.ActorID, PrincipalID: principal.PrincipalID, PrincipalKind: principal.Kind, ExpiresAtUnix: now.Add(ttl).Unix()}
|
||||
payload, err := json.Marshal(session)
|
||||
if err != nil {
|
||||
return "", identity.CreatorSession{}, err
|
||||
@@ -45,6 +61,9 @@ func ValidateCreatorSession(secret []byte, token string, now time.Time) (identit
|
||||
if json.Unmarshal(payload, &session) != nil || session.ActorID == "" || session.PrincipalID == "" || now.Unix() >= session.ExpiresAtUnix {
|
||||
return identity.CreatorSession{}, ErrInvalidCreatorSession
|
||||
}
|
||||
if session.PrincipalKind == "" {
|
||||
session.PrincipalKind = identity.PrincipalKindHuman
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/identity"
|
||||
)
|
||||
|
||||
func TestServiceSessionIsDistinctFromHumanCreatorSession(t *testing.T) {
|
||||
secret := bytes.Repeat([]byte{7}, 32)
|
||||
principal := identity.ServicePrincipal{ActorID: "acceptance-actor", PrincipalID: "acceptance-principal", Kind: identity.PrincipalKindService, Purpose: "AUTHENTICATE_ACCEPTANCE_HARNESS", SyntheticScope: "synthetic-acceptance-scope", Active: true, Revision: 1}
|
||||
token, issued, err := IssueServiceSession(secret, principal, time.Unix(100, 0), time.Minute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if issued.PrincipalKind != identity.PrincipalKindService {
|
||||
t.Fatalf("kind=%q", issued.PrincipalKind)
|
||||
}
|
||||
validated, err := ValidateCreatorSession(secret, token, time.Unix(101, 0))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if validated.PrincipalKind != identity.PrincipalKindService || validated.PrincipalID != principal.PrincipalID {
|
||||
t.Fatalf("validated=%#v", validated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSessionRejectsInactivePrincipal(t *testing.T) {
|
||||
secret := bytes.Repeat([]byte{7}, 32)
|
||||
_, _, err := IssueServiceSession(secret, identity.ServicePrincipal{ActorID: "a", PrincipalID: "p", Kind: identity.PrincipalKindService, Purpose: "test"}, time.Unix(100, 0), time.Minute)
|
||||
if err == nil {
|
||||
t.Fatal("inactive service principal was accepted")
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
const (
|
||||
ControlPlaneServiceTokenID = "control_plane_service_token"
|
||||
AcceptanceHarnessServiceTokenID = "acceptance_harness_service_token"
|
||||
MachineCredentialOwner = "platform.secrets.local-control-plane"
|
||||
)
|
||||
|
||||
@@ -127,6 +128,10 @@ func ControlPlaneServiceTokenSpec() MachineCredentialSpec {
|
||||
}
|
||||
}
|
||||
|
||||
func AcceptanceHarnessServiceTokenSpec() MachineCredentialSpec {
|
||||
return MachineCredentialSpec{ID: AcceptanceHarnessServiceTokenID, Kind: MachineCredentialServiceToken, Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumers: []secretcontracts.RuntimeConsumer{secretcontracts.RuntimeConsumerCore}, Bytes: 48}
|
||||
}
|
||||
|
||||
func (s *LocalMachineCredentialStore) Ensure(spec MachineCredentialSpec) (MachineCredentialResult, error) {
|
||||
if err := validateMachineCredentialSpec(spec); err != nil {
|
||||
return MachineCredentialResult{}, err
|
||||
|
||||
Reference in New Issue
Block a user