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
@@ -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.