feat(capability): add release scoped shadow bindings
CI / test (push) Failing after 35s

This commit is contained in:
m
2026-08-31 16:28:57 +03:00
parent d1710907ec
commit dec19e873d
36 changed files with 1078 additions and 50 deletions
+45 -8
View File
@@ -37,10 +37,12 @@ type DeliveryRequest struct {
}
type Receipt struct {
ReceiptID string `json:"receipt_id"`
ActionIntentID commitcontract.ActionIntentID `json:"action_intent_id"`
RequestDigest string `json:"request_digest"`
AcceptedAt int64 `json:"accepted_at_unix_millis"`
ReceiptID string `json:"receipt_id"`
ActionIntentID commitcontract.ActionIntentID `json:"action_intent_id"`
RequestDigest string `json:"request_digest"`
AcceptedAt int64 `json:"accepted_at_unix_millis"`
DeliveryMode string `json:"delivery_mode,omitempty"`
ExternalDeliveryAttempted bool `json:"external_delivery_attempted"`
}
func Digest(payload Payload) (string, error) {
@@ -76,11 +78,27 @@ func PayloadFromAction(action commitcontract.AuthorizedAction) (Payload, error)
type Executor struct {
binding capability.Binding
candidate *capability.CandidateBinding
secret string
client *http.Client
executeAllowed bool
}
// NewShadow accepts only a Capability-owned SHADOW binding. Its caller does
// not supply an endpoint; the target remains inside the resolved binding.
func NewShadow(binding capability.CandidateBinding, secret string, client *http.Client) (*Executor, error) {
if capabilitydomain.ValidateCandidateBinding(binding) != nil || binding.Context.Mode != capability.CandidateBindingModeShadow || binding.Status != capability.CandidateBindingActive {
return nil, ErrInvalidNotification
}
stable := capability.Binding{ID: binding.BaseActiveBindingID, Revision: binding.BaseActiveBindingRevision, OwnerPrincipalID: binding.Context.PrincipalID}
value, err := newExecutor(stable, secret, client, true)
if err != nil {
return nil, err
}
value.candidate = &binding
return value, nil
}
func New(binding capability.Binding, secret string, client *http.Client) (*Executor, error) {
if err := capabilitydomain.UsableBinding(binding); err != nil {
return nil, ErrInvalidNotification
@@ -116,7 +134,7 @@ func (e *Executor) Execute(action commitcontract.AuthorizedAction) ports.Executi
if err != nil {
return ports.ExecutionResult{Known: true}
}
endpoint, err := e.endpoint(e.binding.DeliveryPath, string(action.Proposal.IntentID))
endpoint, err := e.endpoint(e.deliveryPath(), string(action.Proposal.IntentID))
if err != nil {
return ports.ExecutionResult{Known: true}
}
@@ -127,6 +145,10 @@ func (e *Executor) Execute(action commitcontract.AuthorizedAction) ports.Executi
request.Header.Set("Authorization", "Bearer "+e.secret)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json")
if e.candidate != nil {
request.Header.Set("X-Candidate-Release", e.candidate.Context.ReleaseSetID)
request.Header.Set("X-Candidate-Task", e.candidate.Context.TaskID)
}
response, err := e.client.Do(request)
if err != nil {
return ports.ExecutionResult{}
@@ -139,10 +161,14 @@ func (e *Executor) Execute(action commitcontract.AuthorizedAction) ports.Executi
return ports.ExecutionResult{}
}
receipt, err := decodeReceipt(response.Body)
if err != nil || !matchingReceipt(receipt, action) {
if err != nil || !matchingReceipt(receipt, action) || (e.candidate != nil && (receipt.DeliveryMode != "SHADOW" || receipt.ExternalDeliveryAttempted)) {
return ports.ExecutionResult{}
}
return ports.ExecutionResult{Known: true, Succeeded: true}
result := ports.ExecutionResult{Known: true, Succeeded: true}
if e.candidate != nil {
result.Receipt = &commitcontract.ExecutionReceipt{ID: receipt.ReceiptID, ExecutorImplementationRef: "notification-shadow-receiver-v1", ExternalOperationRef: "SHADOW_CAPTURED", ExternalRequestID: string(receipt.ActionIntentID), RequestDigest: receipt.RequestDigest, DispatchAtUnixMillis: receipt.AcceptedAt, ExecutorResult: commitcontract.ExecutorAccepted, ReconciliationKey: receipt.ReceiptID, DeliveryMode: "SHADOW", ExternalDeliveryAttempted: false, ReleaseSetID: e.candidate.Context.ReleaseSetID, TaskID: e.candidate.Context.TaskID, TaskRevision: e.candidate.Context.TaskRevision, CapabilityBindingID: string(e.candidate.ID), CapabilityBindingRevision: e.candidate.Revision, TargetReference: e.candidate.Context.TargetRef}
}
return result
}
func (e *Executor) Reconcile(action commitcontract.AuthorizedAction) ports.ExecutionResult {
@@ -186,7 +212,11 @@ func (e *Executor) requestFor(action commitcontract.AuthorizedAction) (Payload,
}
func (e *Executor) endpoint(path, intent string) (string, error) {
base, err := url.Parse(e.binding.HTTPSOrigin)
origin := e.binding.HTTPSOrigin
if e.candidate != nil {
origin = e.candidate.TargetOrigin
}
base, err := url.Parse(origin)
if err != nil {
return "", err
}
@@ -195,6 +225,13 @@ func (e *Executor) endpoint(path, intent string) (string, error) {
return base.String(), nil
}
func (e *Executor) deliveryPath() string {
if e.candidate != nil {
return e.candidate.DeliveryPath
}
return e.binding.DeliveryPath
}
func decodeReceipt(reader io.Reader) (Receipt, error) {
var receipt Receipt
decoder := json.NewDecoder(io.LimitReader(reader, maxResponseBytes+1))
@@ -9,6 +9,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
commitcontract "git.cin.su/m/Assistant/contracts/commit"
"git.cin.su/m/Assistant/contracts/identity"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
)
func testAction(t *testing.T) commitcontract.AuthorizedAction {
@@ -55,6 +56,35 @@ func TestExecutorPutAndReconcileMatchReceipt(t *testing.T) {
}
}
func TestShadowExecutorRequiresShadowReceipt(t *testing.T) {
action := testAction(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Candidate-Release") != "assistant-slot-b" || r.Header.Get("X-Candidate-Task") != "GEN2-CAPABILITY-001" {
t.Fatalf("missing candidate context")
}
_ = json.NewEncoder(w).Encode(Receipt{ReceiptID: "shadow-receipt-1", ActionIntentID: action.Proposal.IntentID, RequestDigest: action.Proposal.NotificationSubject.RequestDigest, AcceptedAt: 1, DeliveryMode: "SHADOW", ExternalDeliveryAttempted: false})
}))
defer server.Close()
binding := capability.CandidateBinding{ID: "candidate-binding-1", Revision: 1, BaseActiveBindingID: "binding-1", BaseActiveBindingRevision: 1,
Context: capability.CandidateReleaseContext{ReleaseSetID: "assistant-slot-b", Slot: "b", TaskID: "GEN2-CAPABILITY-001", TaskRevision: 7, PrincipalID: "owner-1", CapabilityID: commitcontract.CapabilityProspectiveNotification, ResourceScope: "synthetic-shadow-notification-v1", Mode: capability.CandidateBindingModeShadow, TargetKind: "NOTIFICATION_SHADOW_RECEIVER", TargetRef: "notification-receiver-candidate", AuthorityGrantID: "grant"},
TargetOrigin: server.URL, DeliveryPath: "/delivery/v1/deliveries", ReceiptPath: "/delivery/v1/receipts", Protocol: capability.ProtocolIdempotentReceiptV1, CreatedAtUnixMillis: 1, ExpiresAtUnixMillis: 100, Status: capability.CandidateBindingActive}
// The httptest address is intentionally not the production candidate target;
// its digest is recomputed locally only for this bounded test binding.
digest, err := capabilitydomain.CandidateTargetDigest(binding)
if err != nil {
t.Fatal(err)
}
binding.Context.TargetDigest = digest
executor, err := NewShadow(binding, "shadow-secret", server.Client())
if err != nil {
t.Fatal(err)
}
result := executor.Execute(action)
if !result.Known || !result.Succeeded || result.Receipt == nil || result.Receipt.DeliveryMode != "SHADOW" || result.Receipt.ExternalDeliveryAttempted {
t.Fatalf("shadow result=%+v", result)
}
}
func TestExecutorAmbiguousAndIntegrityOutcomes(t *testing.T) {
action := testAction(t)
for name, handler := range map[string]http.HandlerFunc{
+2 -2
View File
@@ -31,7 +31,7 @@ func (r *AuthorityRepository) LoadGrant(ctx context.Context, id authority.GrantI
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,binding_id,provider_instance_id,model_descriptor_id,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.IssuedAtUnixMillis, &grant.ExpiresAtUnixMillis, &grant.ArchitectureRevision, &grant.Nonce, &grant.Revision, &grant.Status)
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)
}
@@ -39,7 +39,7 @@ func (r *AuthorityRepository) LoadDelegatedControlPlaneGrant(ctx context.Context
}
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,decision,outcome,recorded_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`, audit.AuditID, audit.GrantID, audit.ServiceActorID, audit.ServicePrincipalID, audit.TaskID, audit.TaskRevision, audit.Operation, audit.Resource.BindingID, audit.Resource.ProviderInstanceID, audit.Resource.ModelDescriptorID, audit.Decision, audit.Outcome, audit.RecordedAtUnixMillis)
_, 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)
}
@@ -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)
+23
View File
@@ -111,6 +111,29 @@ func (r *CommitRepository) FinishAttempt(ctx context.Context, a commitcontract.A
return nil
}
func (r *CommitRepository) SaveExecutionReceipt(ctx context.Context, receipt commitcontract.ExecutionReceipt) error {
if !receipt.Valid() {
return ErrActionAttemptConflict
}
completed := int64(0)
if receipt.CompletedAtUnixMillis != nil {
completed = *receipt.CompletedAtUnixMillis
}
result, err := r.pool.Exec(ctx, `INSERT INTO commit.execution_receipts (
receipt_id,action_attempt_id,executor_implementation_ref,external_operation_ref,external_request_id,request_digest,response_digest,dispatch_at_unix_millis,completed_at_unix_millis,
executor_result,reconciliation_key,raw_artifact_ref,delivery_mode,external_delivery_attempted,release_set_id,task_id,task_revision,capability_binding_id,capability_binding_revision,target_reference)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20) ON CONFLICT (receipt_id) DO NOTHING`,
receipt.ID, receipt.ActionAttemptRef, receipt.ExecutorImplementationRef, receipt.ExternalOperationRef, receipt.ExternalRequestID, receipt.RequestDigest, receipt.ResponseDigest, receipt.DispatchAtUnixMillis, completed,
receipt.ExecutorResult, receipt.ReconciliationKey, receipt.RawArtifactRef, receipt.DeliveryMode, receipt.ExternalDeliveryAttempted, receipt.ReleaseSetID, receipt.TaskID, receipt.TaskRevision, receipt.CapabilityBindingID, receipt.CapabilityBindingRevision, receipt.TargetReference)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return ErrActionAttemptConflict
}
return nil
}
func (r *CommitRepository) LoadAuthorized(ctx context.Context, id commitcontract.AuthorizedActionID) (commitcontract.AuthorizedAction, error) {
return loadAuthorized(ctx, r.pool, id)
}
@@ -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)
}
}
@@ -12,6 +12,7 @@ import (
"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"
)
@@ -22,6 +23,25 @@ type delegatedAuthorityFake struct {
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")
+55 -2
View File
@@ -25,8 +25,9 @@ var (
)
type creatorCoreClient struct {
baseURL *url.URL
client *http.Client
baseURL *url.URL
client *http.Client
serviceCredential func() (string, error)
}
type creatorContinuityClient struct {
@@ -121,6 +122,58 @@ func (c creatorCoreClient) CapabilityBinding(ctx context.Context, id capability.
return binding, nil
}
func (c creatorCoreClient) ResolveCandidateCapabilityBinding(ctx context.Context, context capability.CandidateReleaseContext) (capability.CandidateBindingResolution, error) {
var resolved capability.CandidateBindingResolution
status, err := c.postServiceAuthenticated(ctx, "/v1/internal/capability-bindings/candidate/resolve", context, &resolved)
if err != nil || status != http.StatusOK {
return capability.CandidateBindingResolution{}, errCreatorQueryUnavailable
}
return resolved, nil
}
func (c creatorCoreClient) postServiceAuthenticated(ctx context.Context, path string, value any, destination any) (int, error) {
if c.serviceCredential == nil {
return 0, errCreatorQueryUnavailable
}
credential, err := c.serviceCredential()
if err != nil || credential == "" {
return 0, errCreatorQueryUnavailable
}
return c.postWithAuthorization(ctx, path, value, destination, "Bearer "+credential)
}
func (c creatorCoreClient) post(ctx context.Context, path string, value any, destination any) (int, error) {
return c.postWithAuthorization(ctx, path, value, destination, "")
}
func (c creatorCoreClient) postWithAuthorization(ctx context.Context, path string, value any, destination any, authorization string) (int, error) {
target := *c.baseURL
target.Path, target.RawQuery = path, ""
body, err := json.Marshal(value)
if err != nil {
return 0, err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), strings.NewReader(string(body)))
if err != nil {
return 0, err
}
request.Header.Set("Content-Type", "application/json")
if authorization != "" {
request.Header.Set("Authorization", authorization)
}
response, err := c.client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusOK && destination != nil {
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<10)).Decode(destination); err != nil {
return 0, err
}
}
return response.StatusCode, nil
}
func (c creatorCoreClient) get(ctx context.Context, path, authorization string, destination any) (int, error) {
target := *c.baseURL
target.Path = path
+9 -1
View File
@@ -24,6 +24,7 @@ type executionRepository interface {
LoadLatestAttempt(context.Context, commitcontract.AuthorizedActionID) (commitcontract.ActionAttempt, error)
BeginAttempt(context.Context, commitcontract.ActionAttempt) error
FinishAttempt(context.Context, commitcontract.ActionAttempt) error
SaveExecutionReceipt(context.Context, commitcontract.ExecutionReceipt) error
}
type attemptRequest struct {
@@ -94,10 +95,17 @@ func executeAuthorized(ctx context.Context, repository executionRepository, exec
if err := repository.BeginAttempt(ctx, begun); err != nil {
return commitcontract.ActionAttempt{}, err
}
attempt, operationErr := commitdomain.Execute(action, attemptID, executor)
attempt, result, operationErr := commitdomain.ExecuteWithResult(action, attemptID, executor)
if err := repository.FinishAttempt(ctx, attempt); err != nil {
return commitcontract.ActionAttempt{}, err
}
if result.Receipt != nil {
receipt := *result.Receipt
receipt.ActionAttemptRef = attempt.ID
if err := repository.SaveExecutionReceipt(ctx, receipt); err != nil {
return commitcontract.ActionAttempt{}, err
}
}
return attempt, operationErr
}
+11 -1
View File
@@ -16,6 +16,12 @@ import (
type fakeExecutionRepository struct {
action commitcontract.AuthorizedAction
attempts []commitcontract.ActionAttempt
receipt commitcontract.ExecutionReceipt
}
func (f *fakeExecutionRepository) SaveExecutionReceipt(_ context.Context, receipt commitcontract.ExecutionReceipt) error {
f.receipt = receipt
return nil
}
func (f *fakeExecutionRepository) LoadAuthorized(context.Context, commitcontract.AuthorizedActionID) (commitcontract.AuthorizedAction, error) {
@@ -47,7 +53,8 @@ type knownExecutor struct{ calls int }
func (e *knownExecutor) Execute(commitcontract.AuthorizedAction) ports.ExecutionResult {
e.calls++
return ports.ExecutionResult{Known: true, Succeeded: true}
completed := int64(1)
return ports.ExecutionResult{Known: true, Succeeded: true, Receipt: &commitcontract.ExecutionReceipt{ID: "receipt-1", ExecutorImplementationRef: "test-executor", ExternalOperationRef: "SHADOW_CAPTURED", RequestDigest: "request", ExecutorResult: commitcontract.ExecutorReportedSuccess, ReconciliationKey: "receipt-1", DispatchAtUnixMillis: 1, CompletedAtUnixMillis: &completed}}
}
func (e *knownExecutor) Reconcile(commitcontract.AuthorizedAction) ports.ExecutionResult {
return ports.ExecutionResult{Known: true, Succeeded: true}
@@ -66,6 +73,9 @@ func TestExecutionIsDurablyBegunAndCannotBeBlindlyRepeated(t *testing.T) {
if result.Code != http.StatusOK || executor.calls != 1 || repository.attempts[0].State != commitcontract.ActionSucceeded {
t.Fatalf("status=%d calls=%d attempts=%#v", result.Code, executor.calls, repository.attempts)
}
if repository.receipt.ActionAttemptRef != repository.attempts[0].ID {
t.Fatalf("receipt attempt reference=%q want %q", repository.receipt.ActionAttemptRef, repository.attempts[0].ID)
}
repeated := httptest.NewRequest(http.MethodPost, "/v1/internal/authorized-actions/authorized-1/execute", strings.NewReader(`{"AttemptID":"attempt-2"}`))
repeated.Header.Set("Authorization", "Bearer secret")
+26 -5
View File
@@ -73,15 +73,18 @@ func main() {
if err != nil {
log.Fatal(err)
}
notificationSecret, err := readCommitSecretFile("NOTIFICATION_WRITE_TOKEN_FILE")
candidateContext, err := candidateReleaseContextFromRuntime()
if err != nil {
log.Fatal(err)
}
notificationSecretRef := os.Getenv("NOTIFICATION_WRITE_SECRET_REF")
if notificationSecretRef == "" {
log.Fatal("NOTIFICATION_WRITE_SECRET_REF is required")
notificationSecretRef, notificationSecret, err := notificationCredentialsFromRuntime(candidateContext != nil)
if err != nil {
log.Fatal(err)
}
dispatcher := operationDispatcher{continuityExecutor: continuityExecutor, continuityReconciler: continuityExecutor, bindings: creatorCore, secretRef: notificationSecretRef, secret: notificationSecret, client: creatorHTTPClient}
if candidateContext != nil {
creatorCore.serviceCredential = delegatedControlPlaneCredential
}
dispatcher := operationDispatcher{continuityExecutor: continuityExecutor, continuityReconciler: continuityExecutor, bindings: creatorCore, secretRef: notificationSecretRef, secret: notificationSecret, client: creatorHTTPClient, candidateContext: candidateContext}
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "commit-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
@@ -162,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)
}
}
@@ -37,3 +37,26 @@ func TestControlPlaneCredentialUsesDedicatedSecretRoot(t *testing.T) {
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")
}
}
+114
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
@@ -49,6 +50,60 @@ const (
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() {
@@ -147,6 +202,10 @@ func main() {
} 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)
@@ -1499,6 +1558,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 {
+6 -1
View File
@@ -19,7 +19,12 @@ func main() {
if err != nil {
log.Fatal(err)
}
server, err := newReceiverServer(envOr("RECEIPT_DIRECTORY", "/var/lib/notification-receiver"), writeToken, readToken, time.Now)
var server *receiverServer
if os.Getenv("SHADOW_RECEIVER_MODE") == "1" {
server, err = newShadowReceiverServer(envOr("RECEIPT_DIRECTORY", "/var/lib/notification-receiver"), writeToken, readToken, os.Getenv("CANDIDATE_RELEASE_SET_ID"), os.Getenv("CANDIDATE_TASK_ID"), time.Now)
} else {
server, err = newReceiverServer(envOr("RECEIPT_DIRECTORY", "/var/lib/notification-receiver"), writeToken, readToken, time.Now)
}
if err != nil {
log.Fatal(err)
}
+23 -1
View File
@@ -32,8 +32,11 @@ type receiverServer struct {
writeToken string
readToken string
now func() time.Time
shadow *shadowReceiverContext
}
type shadowReceiverContext struct{ releaseSetID, taskID string }
func newReceiverServer(directory, writeToken, readToken string, now func() time.Time) (*receiverServer, error) {
if directory == "" || strings.TrimSpace(writeToken) == "" || strings.TrimSpace(readToken) == "" ||
writeToken == readToken || now == nil {
@@ -45,6 +48,15 @@ func newReceiverServer(directory, writeToken, readToken string, now func() time.
return &receiverServer{directory: directory, writeToken: writeToken, readToken: readToken, now: now}, nil
}
func newShadowReceiverServer(directory, writeToken, readToken, releaseSetID, taskID string, now func() time.Time) (*receiverServer, error) {
server, err := newReceiverServer(directory, writeToken, readToken, now)
if err != nil || releaseSetID == "" || taskID == "" {
return nil, errors.New("invalid shadow receiver configuration")
}
server.shadow = &shadowReceiverContext{releaseSetID: releaseSetID, taskID: taskID}
return server, nil
}
func (s *receiverServer) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
@@ -62,6 +74,10 @@ func (s *receiverServer) put(w http.ResponseWriter, r *http.Request) {
http.Error(w, "receiver writer denied", http.StatusUnauthorized)
return
}
if s.shadow != nil && (r.Header.Get("X-Candidate-Release") != s.shadow.releaseSetID || r.Header.Get("X-Candidate-Task") != s.shadow.taskID) {
http.Error(w, "candidate shadow context denied", http.StatusForbidden)
return
}
intent := r.PathValue("intent")
if !safeIntent.MatchString(intent) {
http.Error(w, "invalid action intent", http.StatusBadRequest)
@@ -83,7 +99,13 @@ func (s *receiverServer) put(w http.ResponseWriter, r *http.Request) {
http.Error(w, "delivery digest mismatch", http.StatusConflict)
return
}
receipt := storedReceipt{Receipt: notificationexecutor.Receipt{ReceiptID: "receipt-" + intent, ActionIntentID: request.ActionIntentID, RequestDigest: digest, AcceptedAt: s.now().UnixMilli()}, Payload: request.Payload}
receipt := storedReceipt{Receipt: notificationexecutor.Receipt{ReceiptID: "receipt-" + intent, ActionIntentID: request.ActionIntentID, RequestDigest: digest, AcceptedAt: s.now().UnixMilli()}}
if s.shadow != nil {
receipt.DeliveryMode = "SHADOW"
receipt.ExternalDeliveryAttempted = false
} else {
receipt.Payload = request.Payload
}
created, existing, err := s.create(receipt)
if err != nil {
http.Error(w, "receipt unavailable", http.StatusInternalServerError)
+28
View File
@@ -120,3 +120,31 @@ func TestConcurrentDuplicateCreatesOneReceipt(t *testing.T) {
t.Fatalf("created=%d", created)
}
}
func TestShadowReceiverRequiresContextAndDoesNotPersistPayload(t *testing.T) {
server, err := newShadowReceiverServer(t.TempDir(), "shadow-write", "shadow-read", "assistant-slot-b", "GEN2-CAPABILITY-001", func() time.Time { return time.UnixMilli(100) })
if err != nil {
t.Fatal(err)
}
value := delivery(t, "shadow-intent", "must not persist")
body, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPut, "/delivery/v1/deliveries/shadow-intent", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer shadow-write")
req.Header.Set("X-Candidate-Release", "assistant-slot-b")
req.Header.Set("X-Candidate-Task", "GEN2-CAPABILITY-001")
response := httptest.NewRecorder()
server.routes().ServeHTTP(response, req)
if response.Code != http.StatusCreated || !bytes.Contains(response.Body.Bytes(), []byte(`"delivery_mode":"SHADOW"`)) || bytes.Contains(response.Body.Bytes(), []byte("must not persist")) {
t.Fatalf("shadow response status=%d body=%s", response.Code, response.Body.String())
}
req = httptest.NewRequest(http.MethodPut, "/delivery/v1/deliveries/shadow-intent", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer shadow-write")
response = httptest.NewRecorder()
server.routes().ServeHTTP(response, req)
if response.Code != http.StatusForbidden {
t.Fatalf("unscoped shadow request status=%d", response.Code)
}
}
+29 -10
View File
@@ -18,11 +18,14 @@ const GrantClassCreator GrantClass = "CREATOR"
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"
DelegatedOperationRevokeControlPlaneGrant DelegatedControlPlaneOperation = "REVOKE_DELEGATED_CONTROL_PLANE_GRANT"
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
@@ -41,6 +44,19 @@ type CognitiveBindingResource struct {
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
@@ -50,6 +66,7 @@ type DelegatedControlPlaneGrant struct {
TaskRevision int64
Operations []DelegatedControlPlaneOperation
Resource CognitiveBindingResource
CandidateResource CandidateCapabilityBindingResource
IssuedAtUnixMillis int64
ExpiresAtUnixMillis int64
ArchitectureRevision string
@@ -59,11 +76,12 @@ type DelegatedControlPlaneGrant struct {
}
type DelegatedControlPlaneRequest struct {
GrantID GrantID
TaskID string
TaskRevision int64
Operation DelegatedControlPlaneOperation
Resource CognitiveBindingResource
GrantID GrantID
TaskID string
TaskRevision int64
Operation DelegatedControlPlaneOperation
Resource CognitiveBindingResource
CandidateResource CandidateCapabilityBindingResource
}
type DelegatedControlPlaneAudit struct {
@@ -75,6 +93,7 @@ type DelegatedControlPlaneAudit struct {
TaskRevision int64
Operation DelegatedControlPlaneOperation
Resource CognitiveBindingResource
CandidateResource CandidateCapabilityBindingResource
Decision string
Outcome string
RecordedAtUnixMillis int64
+53
View File
@@ -12,10 +12,21 @@ type CapabilityKind string
type BindingState string
type BindingProtocol string
// CandidateBindingMode controls how a release-scoped binding may be used.
// It is deliberately separate from a production BindingState.
type CandidateBindingMode string
type CandidateBindingStatus string
const (
BindingActive BindingState = "ACTIVE"
BindingRetired BindingState = "RETIRED"
CandidateBindingModeCandidate CandidateBindingMode = "CANDIDATE"
CandidateBindingModeShadow CandidateBindingMode = "SHADOW"
CandidateBindingActive CandidateBindingStatus = "ACTIVE"
CandidateBindingRevoked CandidateBindingStatus = "REVOKED"
ProtocolIdempotentReceiptV1 BindingProtocol = "IDEMPOTENT_RECEIPT_V1"
CapabilityKindReadEmail CapabilityKind = "READ_EMAIL"
@@ -54,6 +65,48 @@ type Binding struct {
Health HealthState
}
// CandidateReleaseContext is supplied by a candidate runtime, not inferred
// from a host name or process-local state. It scopes dynamic capability
// resolution to one release slot and task.
type CandidateReleaseContext struct {
ReleaseSetID string
Slot string
TaskID string
TaskRevision int64
PrincipalID identity.PrincipalID
CapabilityID CapabilityID
ResourceScope string
Mode CandidateBindingMode
TargetKind string
TargetRef string
TargetDigest string
AuthorityGrantID string
}
// CandidateBinding is Capability-owned release-scoped state. TargetOrigin is
// intentionally distinct from Binding.HTTPSOrigin: a SHADOW target can be an
// internal candidate receiver and must never be promoted into the ACTIVE head.
type CandidateBinding struct {
ID CapabilityBindingID
Revision int64
BaseActiveBindingID CapabilityBindingID
BaseActiveBindingRevision int64
Context CandidateReleaseContext
TargetOrigin string
DeliveryPath string
ReceiptPath string
Protocol BindingProtocol
CreatedAtUnixMillis int64
ExpiresAtUnixMillis int64
Status CandidateBindingStatus
}
type CandidateBindingResolution struct {
Binding CandidateBinding
Mode CandidateBindingMode
Reason string
}
type HealthState string
const (
+8
View File
@@ -25,6 +25,14 @@ type ExecutionReceipt struct {
Ambiguity AmbiguityState
ReconciliationKey string
RawArtifactRef string
DeliveryMode string
ExternalDeliveryAttempted bool
ReleaseSetID string
TaskID string
TaskRevision int64
CapabilityBindingID string
CapabilityBindingRevision int64
TargetReference string
}
func (r ExecutionReceipt) Valid() bool {
@@ -0,0 +1,61 @@
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_audit DROP CONSTRAINT IF EXISTS delegated_control_plane_audit_operation_check;
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;
+1 -1
View File
@@ -1 +1 @@
{"task_file":"dev/tasks/GEN2-DEPLOY-004-candidate-commit-runtime-releaseset.json","task_id":"GEN2-DEPLOY-004","task_revision":2}
{"task_file":"dev/tasks/GEN2-CAPABILITY-001-release-scoped-candidate-shadow-bindings.json","task_id":"GEN2-CAPABILITY-001","task_revision":7}
@@ -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,11 +1,11 @@
{
"task_id": "GEN2-DEPLOY-004",
"task_revision": 2,
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Close the isolated candidate slot over all versioned Assistant runtime dependencies of its candidate components, extend the typed Supervisor ReleaseSet accordingly, and keep stable containers untouched so the delegated Authority path can be accepted without candidate-to-stable runtime shortcuts.",
"requirement_ids": ["REQ-GEN2-DEPLOY-004-DEPENDENCY-CLOSURE", "REQ-GEN2-DEPLOY-004-CANDIDATE-COMMIT", "REQ-GEN2-AUTHORITY-001-ISOLATED-COMMIT", "REQ-GEN2-DEPLOY-003-STABLE-ID-GUARD"],
"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": [
@@ -47,17 +47,17 @@
},
"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", "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 ReleaseSet evidence", "candidate Compose dependency wiring", "candidate deployment control script"]
"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 dependency closure start -> all component health/readiness/revision and dependency-target observation -> Supervisor canary -> promote or whole ReleaseSet rollback", "missing, unhealthy, mixed-revision, or stable-targeted candidate component -> candidate ReleaseSet unhealthy -> traffic stable without stable container recreation"],
"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 closure of creator-web, core-runtime, cognitive-worker, research-runtime, commit-runtime, continuity-runtime, agent-worker, agent-execution-bridge, work-graph-runtime, and task-verifier, each at one revision", "every candidate Assistant runtime URL targets a component in the same candidate ReleaseSet; stable Assistant runtime URLs abort before canary", "missing, unhealthy, or revision-mismatched required component prevents release READY", "candidate Core -> Commit -> Continuity and Creator -> Continuity/Agent Worker remain within candidate topology while stable topology is unchanged", "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 in LKG and candidate evidence", "Commit readiness proves database and Authority endpoint readiness without emitting a secret"],
"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"]
}
+1
View File
@@ -58,5 +58,6 @@ Bounded post-M5 remediation. Provides the technical protected-secret control-pla
|---|---|---|---|
| `GEN2-AUTHORITY-001` | `MILESTONE` | owner decision, existing Identity/Authority/Capability lifecycle | Bounded non-interactive service principal and task-scoped control-plane delegation; does not retire V1 or change Gen2 canonical ownership. |
| `GEN2-DEPLOY-004` | `MILESTONE` | GEN2-DEPLOY-003, GEN2-AUTHORITY-001 implementation | Extend the isolated candidate slot and typed Supervisor ReleaseSet with Commit runtime before delegated Authority acceptance; stable containers are never recreated by candidate operations. |
| `GEN2-CAPABILITY-001` | `MILESTONE` | owner architecture decision, GEN2-AUTHORITY-001 implementation | Capability-owned release-scoped candidate/shadow binding lifecycle for dynamic candidate dependency closure; stable ACTIVE bindings remain immutable. |
Между реперными точками выполняется только FAST-профиль конкретного packet-а.
@@ -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.
@@ -24,9 +24,25 @@ func ValidateDelegatedControlPlaneGrant(nowUnixMillis int64, binding identity.Pr
}
func RequestMatchesDelegatedControlPlaneGrant(grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest) bool {
return grant.TaskID != "" && grant.TaskID == request.TaskID && grant.TaskRevision > 0 && grant.TaskRevision == request.TaskRevision &&
request.GrantID == grant.ID && request.Operation != "" && slices.Contains(grant.Operations, request.Operation) &&
grant.Resource.BindingID != "" && grant.Resource.ProviderInstanceID != "" && grant.Resource.ModelDescriptorID != "" && grant.Resource == request.Resource
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 {
@@ -41,3 +41,17 @@ func TestDelegatedGrantCannotSelfRevokeOrBroaden(t *testing.T) {
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)
}
}
+3
View File
@@ -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
+11 -4
View File
@@ -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
}
+1
View File
@@ -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.