Compare commits

...
Author SHA1 Message Date
m d1710907ec docs(deploy): record candidate dependency closure gap
CI / test (push) Failing after 34s
2026-08-31 14:13:40 +03:00
m 2f085858db chore(governance): add GEN2-DEPLOY-004 release set task
CI / test (push) Failing after 36s
2026-08-31 13:42:48 +03:00
m a8be998cee docs(authority): record candidate topology gap
CI / test (push) Failing after 32s
2026-08-31 13:28:15 +03:00
m f42a94ee7a fix(authority): use isolated service credential mount
CI / test (push) Failing after 36s
2026-08-31 13:26:50 +03:00
m 35666c9923 merge: GEN2-SECRETS-001 into delegated authority
# Conflicts:
#	deploy/docker-compose.prod.yml
#	dev/ACTIVE_TASK.json
#	docs/CREDENTIALS_AND_SECRETS.md
2026-08-31 13:26:28 +03:00
m da83248ac9 merge: GEN2-SECRETS-001 production evidence
CI / test (push) Failing after 39s
2026-08-31 13:22:28 +03:00
m db139e6800 docs(secrets): record provisioning evidence
CI / test (push) Failing after 1m14s
2026-08-31 13:22:23 +03:00
m b4335d6735 merge: GEN2-SECRETS-001 rotation wrapper fix
CI / test (push) Failing after 37s
2026-08-31 13:21:27 +03:00
m 44541b1ba3 fix(secrets): pass bounded rotation reason
CI / test (push) Failing after 39s
2026-08-31 13:21:23 +03:00
m 9c42b18850 merge: GEN2-SECRETS-001 local credential provisioning
CI / test (push) Failing after 43s
2026-08-31 13:18:10 +03:00
m 1c05619899 feat(authority): add delegated control-plane grants
CI / test (push) Failing after 37s
2026-08-31 11:33:48 +03:00
24 changed files with 1076 additions and 10 deletions
+26
View File
@@ -3,6 +3,7 @@ package postgres
import (
"context"
"fmt"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"github.com/jackc/pgx/v5/pgxpool"
@@ -27,3 +28,28 @@ func (r *AuthorityRepository) LoadGrant(ctx context.Context, id authority.GrantI
}
return grant, scope, nil
}
func (r *AuthorityRepository) LoadDelegatedControlPlaneGrant(ctx context.Context, id authority.GrantID) (authority.DelegatedControlPlaneGrant, error) {
var grant authority.DelegatedControlPlaneGrant
err := r.pool.QueryRow(ctx, `SELECT grant_id,service_actor_id,service_principal_id,issuer_principal_id,task_id,task_revision,operations,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)
if err != nil {
return authority.DelegatedControlPlaneGrant{}, fmt.Errorf("load delegated control-plane grant: %w", err)
}
return grant, nil
}
func (r *AuthorityRepository) RecordDelegatedControlPlaneAudit(ctx context.Context, audit authority.DelegatedControlPlaneAudit) error {
_, err := r.pool.Exec(ctx, `INSERT INTO authority.delegated_control_plane_audit (audit_id,grant_id,service_actor_id,service_principal_id,task_id,task_revision,operation,binding_id,provider_instance_id,model_descriptor_id,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)
if err != nil {
return fmt.Errorf("record delegated control-plane audit: %w", err)
}
return nil
}
func (r *AuthorityRepository) RevokeDelegatedControlPlaneGrant(ctx context.Context, id authority.GrantID, issuer string) (bool, error) {
result, err := r.pool.Exec(ctx, `UPDATE authority.delegated_control_plane_grants SET status='REVOKED',revision=revision+1,revoked_at_unix_millis=$3 WHERE grant_id=$1 AND issuer_principal_id=$2 AND status='ACTIVE'`, id, issuer, time.Now().UnixMilli())
if err != nil {
return false, fmt.Errorf("revoke delegated control-plane grant: %w", err)
}
return result.RowsAffected() == 1, nil
}
@@ -0,0 +1,62 @@
# ADR-0021 — Delegated Non-Interactive Control-Plane Authority
Status: Accepted
Date: 2026-08-31
Accepted under: owner decision for GEN2-AUTHORITY-001
## Context
The existing Creator session endpoints correctly require a human principal for
governed Cognitive CANARY and ACTIVE transitions. A bounded background task
cannot use those endpoints without either a browser session, impersonation, or
a direct Capability write. All three alternatives are invalid.
## Decision
1. Identity remains the sole owner of Actors, Principals and their bindings. A
service principal is a separately identified Actor/Principal binding; it is
neither a Human Principal nor an impersonation of one.
2. Authority remains the sole owner of delegated task grants, validation,
revocation and safe action audit. This extends `AuthorityGrant`; it does not
introduce a robot-authority owner or a second authorization registry.
3. A delegated grant is typed and narrow: service principal, task id and
revision, operation capability, exact Cognitive binding resource/revision,
issuer, issue/expiry times, grant revision and lifecycle status. It is
default-deny and cannot authorize grant creation, extension, policy change,
or administration.
4. A control-plane request is authorized in the existing `commit-runtime`, the
runtime topology location of Authority. It must pass two independent checks: service credential
authentication and a current delegated-grant authorization for its exact
task/operation/resource. Credential possession never expands authority.
5. Long-lived credential material remains only in the existing protected secret
backend and is addressed by a `CredentialRef` plus bootstrap file reference.
The runtime never logs, persists, or returns its token. Rotation changes the
protected artifact; revocation of a grant blocks new actions immediately.
6. Capability remains the only owner of Cognitive binding/evaluation lifecycle.
The delegated path invokes the existing owner validation and promotion
builders; it cannot synthesize an evaluation or mark a binding active.
7. Control-plane actions receive durable Authority audit records containing
identity, task, operation, resource, decision and safe result reference.
Secret values, request payloads, user memory and hidden reasoning are never
stored in that audit.
8. External mutations continue to use Commit/Executor/Receipt/Outcome where
the existing action is an external effect. Capability's owner-local binding
lifecycle remains an owner-local governed state transition, not a shortcut
around an external executor.
## Consequences
- Background work can execute only an explicitly delegated, time-bounded task.
- Restart reconstructs authorization from Identity/Authority durable state; no
process-local permission cache is authoritative.
- Each later task must receive a new delegated grant. A grant for
GEN2-COGNITIVE-003 cannot be reused for SELF development or code promotion.
- The first implementation deliberately covers only the Cognitive lifecycle
operations required by GEN2-COGNITIVE-003. Broader control-plane actions
need their own task-scoped authority and acceptance evidence.
## Rollback
Disable or revoke the delegated grant and route no new non-interactive requests
to the endpoint. Existing Authority audit and Capability history remain
append-only evidence; no binding state is rolled back by deleting history.
@@ -0,0 +1,170 @@
package main
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/identity"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
authoritydomain "git.cin.su/m/Assistant/modules/authority/domain"
"git.cin.su/m/Assistant/platform/ids"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
const (
delegatedControlPlaneActor identity.ActorID = "domovoy-control-plane-actor"
delegatedControlPlanePrincipal identity.PrincipalID = "domovoy-control-plane-principal"
delegatedControlPlaneTokenFile = "control_plane_service_token"
)
type delegatedControlPlaneAuthoritySource interface {
LoadDelegatedControlPlaneGrant(context.Context, authority.GrantID) (authority.DelegatedControlPlaneGrant, error)
RecordDelegatedControlPlaneAudit(context.Context, authority.DelegatedControlPlaneAudit) error
RevokeDelegatedControlPlaneGrant(context.Context, authority.GrantID, string) (bool, error)
}
type delegatedControlPlaneCore interface {
Session(context.Context, string) (identity.CreatorSession, error)
Binding(context.Context, identity.ActorID, identity.PrincipalID) (identity.PrincipalBinding, error)
}
type delegatedControlPlaneHandler struct {
authority delegatedControlPlaneAuthoritySource
core delegatedControlPlaneCore
credential func() (string, error)
now func() time.Time
newID func() (ids.ID, error)
}
func registerDelegatedControlPlaneRoutes(mux *http.ServeMux, handler delegatedControlPlaneHandler) {
mux.HandleFunc("POST /v1/internal/control-plane/authorize", handler.authorize)
mux.HandleFunc("POST /v1/internal/control-plane/completions", handler.complete)
mux.HandleFunc("POST /v1/creator/control-plane/grants/{grant}/revoke", handler.revoke)
}
func (h delegatedControlPlaneHandler) authorize(w http.ResponseWriter, r *http.Request) {
if !h.serviceAuthenticated(r) {
http.Error(w, "service principal denied", http.StatusUnauthorized)
return
}
var request authority.DelegatedControlPlaneRequest
decoder := json.NewDecoder(io.LimitReader(r.Body, 8<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid delegated control-plane request", http.StatusBadRequest)
return
}
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), request.GrantID)
if err != nil {
http.Error(w, "delegated authority denied", http.StatusForbidden)
return
}
binding, err := h.core.Binding(r.Context(), delegatedControlPlaneActor, delegatedControlPlanePrincipal)
if err != nil || authoritydomain.ValidateDelegatedControlPlaneGrant(h.now().UnixMilli(), binding, grant, request) != nil {
_ = h.audit(r.Context(), grant, request, "DENY", "FAILED")
http.Error(w, "delegated authority denied", http.StatusForbidden)
return
}
if err := h.audit(r.Context(), grant, request, "ALLOW", "PENDING"); err != nil {
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h delegatedControlPlaneHandler) complete(w http.ResponseWriter, r *http.Request) {
if !h.serviceAuthenticated(r) {
http.Error(w, "service principal denied", http.StatusUnauthorized)
return
}
var value authority.DelegatedControlPlaneCompletion
decoder := json.NewDecoder(io.LimitReader(r.Body, 8<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || (value.Outcome != "COMPLETED" && value.Outcome != "FAILED") {
http.Error(w, "invalid delegated completion", http.StatusBadRequest)
return
}
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), value.Request.GrantID)
if err != nil {
http.Error(w, "delegated authority unavailable", http.StatusServiceUnavailable)
return
}
if !authoritydomain.RequestMatchesDelegatedControlPlaneGrant(grant, value.Request) {
http.Error(w, "delegated completion denied", http.StatusForbidden)
return
}
if err := h.audit(r.Context(), grant, value.Request, "ALLOW", value.Outcome); err != nil {
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h delegatedControlPlaneHandler) revoke(w http.ResponseWriter, r *http.Request) {
session, err := h.core.Session(r.Context(), r.Header.Get("Authorization"))
if err != nil {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return
}
grant, err := h.authority.LoadDelegatedControlPlaneGrant(r.Context(), authority.GrantID(r.PathValue("grant")))
if err != nil || !authoritydomain.CanIssuerRevokeDelegatedGrant(session.PrincipalID, grant) {
http.Error(w, "delegated grant revoke denied", http.StatusForbidden)
return
}
ok, err := h.authority.RevokeDelegatedControlPlaneGrant(r.Context(), grant.ID, string(session.PrincipalID))
if err != nil || !ok {
http.Error(w, "delegated grant revoke denied", http.StatusConflict)
return
}
request := authority.DelegatedControlPlaneRequest{GrantID: grant.ID, TaskID: grant.TaskID, TaskRevision: grant.TaskRevision, Operation: authority.DelegatedOperationRevokeControlPlaneGrant, Resource: grant.Resource}
if err := h.audit(r.Context(), grant, request, "REVOKE", "REVOKED"); err != nil {
http.Error(w, "delegated audit unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h delegatedControlPlaneHandler) serviceAuthenticated(r *http.Request) bool {
if h.credential == nil {
return false
}
expected, err := h.credential()
presented := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
return err == nil && expected != "" && presented != "" && subtle.ConstantTimeCompare([]byte(expected), []byte(presented)) == 1
}
func (h delegatedControlPlaneHandler) audit(ctx context.Context, grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest, decision, outcome string) error {
if h.newID == nil {
return errors.New("delegated audit id unavailable")
}
id, err := h.newID()
if err != nil {
return err
}
return h.authority.RecordDelegatedControlPlaneAudit(ctx, authority.DelegatedControlPlaneAudit{AuditID: id, GrantID: grant.ID, ServiceActorID: delegatedControlPlaneActor, ServicePrincipalID: delegatedControlPlanePrincipal, TaskID: request.TaskID, TaskRevision: request.TaskRevision, Operation: request.Operation, Resource: request.Resource, Decision: decision, Outcome: outcome, RecordedAtUnixMillis: h.now().UnixMilli()})
}
func delegatedControlPlaneCredential() (string, error) {
ref := os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF")
if ref == "" {
return "", errors.New("control-plane service credential ref is required")
}
root := os.Getenv("CONTROL_PLANE_SERVICE_SECRET_ROOT")
if root == "" {
root = "/run/control-plane-secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumer: secretcontracts.RuntimeConsumerCommit, Path: filepath.Join(root, delegatedControlPlaneTokenFile)})
}
@@ -0,0 +1,120 @@
package main
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type delegatedAuthorityFake struct {
grant authority.DelegatedControlPlaneGrant
audits []authority.DelegatedControlPlaneAudit
revoked bool
}
func TestDelegatedAuthorityCredentialRotationRejectsOldToken(t *testing.T) {
root := t.TempDir()
tokenPath := filepath.Join(root, "control_plane_service_token")
if err := os.WriteFile(tokenPath, []byte("old-token\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF", "credential://domovoy/control-plane/service-token/v1")
t.Setenv("CONTROL_PLANE_SERVICE_SECRET_ROOT", root)
h := delegatedControlPlaneHandler{credential: delegatedControlPlaneCredential}
oldRequest := httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", nil)
oldRequest.Header.Set("Authorization", "Bearer old-token")
if !h.serviceAuthenticated(oldRequest) {
t.Fatal("active credential was rejected before rotation")
}
if err := os.WriteFile(tokenPath, []byte("new-token\n"), 0o600); err != nil {
t.Fatal(err)
}
if h.serviceAuthenticated(oldRequest) {
t.Fatal("old credential remained valid after rotation")
}
newRequest := httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", nil)
newRequest.Header.Set("Authorization", "Bearer new-token")
if !h.serviceAuthenticated(newRequest) {
t.Fatal("new credential was rejected after rotation")
}
}
func (f *delegatedAuthorityFake) LoadDelegatedControlPlaneGrant(context.Context, authority.GrantID) (authority.DelegatedControlPlaneGrant, error) {
if f.grant.ID == "" {
return authority.DelegatedControlPlaneGrant{}, errors.New("missing")
}
return f.grant, nil
}
func (f *delegatedAuthorityFake) RecordDelegatedControlPlaneAudit(_ context.Context, audit authority.DelegatedControlPlaneAudit) error {
f.audits = append(f.audits, audit)
return nil
}
func (f *delegatedAuthorityFake) RevokeDelegatedControlPlaneGrant(context.Context, authority.GrantID, string) (bool, error) {
f.revoked = true
f.grant.Status = authority.DelegatedGrantRevoked
return true, nil
}
type delegatedCoreFake struct{}
func (delegatedCoreFake) Binding(context.Context, identity.ActorID, identity.PrincipalID) (identity.PrincipalBinding, error) {
return identity.PrincipalBinding{ActorID: delegatedControlPlaneActor, PrincipalID: delegatedControlPlanePrincipal, Active: true}, nil
}
func (delegatedCoreFake) Session(context.Context, string) (identity.CreatorSession, error) {
return identity.CreatorSession{PrincipalID: "owner"}, nil
}
func TestDelegatedAuthorityRejectsWrongResourceAndSurvivesRestart(t *testing.T) {
resource := authority.CognitiveBindingResource{BindingID: "binding", ProviderInstanceID: "provider", ModelDescriptorID: "model"}
fake := &delegatedAuthorityFake{grant: authority.DelegatedControlPlaneGrant{ID: "grant", ServiceActorID: delegatedControlPlaneActor, ServicePrincipalID: delegatedControlPlanePrincipal, IssuerPrincipalID: "owner", TaskID: "GEN2-COGNITIVE-003", TaskRevision: 4, Operations: []authority.DelegatedControlPlaneOperation{authority.DelegatedOperationCanaryCognitiveBinding}, Resource: resource, IssuedAtUnixMillis: 1, ExpiresAtUnixMillis: 100, ArchitectureRevision: "ADR-0021", Nonce: "nonce", Revision: 1, Status: authority.DelegatedGrantActive}}
h := delegatedControlPlaneHandler{authority: fake, core: delegatedCoreFake{}, credential: func() (string, error) { return "service-token", nil }, now: func() time.Time { return time.UnixMilli(10) }, newID: func() (ids.ID, error) { return "audit", nil }}
mux := http.NewServeMux()
registerDelegatedControlPlaneRoutes(mux, h)
valid := `{"GrantID":"grant","TaskID":"GEN2-COGNITIVE-003","TaskRevision":4,"Operation":"CANARY_COGNITIVE_BINDING","Resource":{"BindingID":"binding","ProviderInstanceID":"provider","ModelDescriptorID":"model"}}`
request := httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", strings.NewReader(valid))
request.Header.Set("Authorization", "Bearer service-token")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusNoContent {
t.Fatalf("valid delegation status=%d", response.Code)
}
wrong := strings.Replace(valid, `"binding"`, `"other"`, 1)
request = httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", strings.NewReader(wrong))
request.Header.Set("Authorization", "Bearer service-token")
response = httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("wrong resource status=%d", response.Code)
}
// A fresh handler reads the same durable grant source; no process-local allow survives restart.
request = httptest.NewRequest(http.MethodPost, "/v1/creator/control-plane/grants/grant/revoke", nil)
request.Header.Set("Authorization", "Bearer creator-session")
response = httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusNoContent || !fake.revoked {
t.Fatalf("revoke status=%d revoked=%v", response.Code, fake.revoked)
}
restarted := delegatedControlPlaneHandler{authority: fake, core: delegatedCoreFake{}, credential: h.credential, now: h.now, newID: h.newID}
mux = http.NewServeMux()
registerDelegatedControlPlaneRoutes(mux, restarted)
request = httptest.NewRequest(http.MethodPost, "/v1/internal/control-plane/authorize", strings.NewReader(valid))
request.Header.Set("Authorization", "Bearer service-token")
response = httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("revoked grant accepted after restart: %d", response.Code)
}
if len(fake.audits) < 3 {
t.Fatalf("audit trail missing: %#v", fake.audits)
}
}
+3
View File
@@ -100,6 +100,9 @@ func main() {
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
executor: dispatcher, newID: ids.New, now: time.Now,
}, executorToken)
registerDelegatedControlPlaneRoutes(mux, delegatedControlPlaneHandler{
authority: authorityRepository, core: creatorCore, credential: delegatedControlPlaneCredential, now: time.Now, newID: ids.New,
})
mux.HandleFunc("POST /v1/authorized-actions", func(w http.ResponseWriter, r *http.Request) {
var request authorizationRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
@@ -0,0 +1,76 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/authority"
)
var ErrControlPlaneUnavailable = errors.New("delegated control-plane authority unavailable")
type controlPlaneAuthority struct {
baseURL string
client *http.Client
credential func() (string, error)
}
func (a controlPlaneAuthority) authorize(ctx context.Context, request authority.DelegatedControlPlaneRequest) error {
return a.post(ctx, "/v1/internal/control-plane/authorize", request)
}
func (a controlPlaneAuthority) completed(ctx context.Context, request authority.DelegatedControlPlaneRequest, success bool) {
outcome := "FAILED"
if success {
outcome = "COMPLETED"
}
_ = a.post(ctx, "/v1/internal/control-plane/completions", authority.DelegatedControlPlaneCompletion{Request: request, Outcome: outcome})
}
func (a controlPlaneAuthority) revoke(ctx context.Context, creatorAuthorization string, grantID authority.GrantID) error {
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(a.baseURL, "/")+"/v1/creator/control-plane/grants/"+string(grantID)+"/revoke", nil)
if err != nil {
return err
}
request.Header.Set("Authorization", creatorAuthorization)
response, err := a.client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusNoContent {
return fmt.Errorf("delegated grant revoke status %d", response.StatusCode)
}
return nil
}
func (a controlPlaneAuthority) post(ctx context.Context, path string, value any) error {
credential, err := a.credential()
if err != nil || credential == "" {
return ErrControlPlaneUnavailable
}
body, err := json.Marshal(value)
if err != nil {
return err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(a.baseURL, "/")+path, bytes.NewReader(body))
if err != nil {
return err
}
request.Header.Set("Authorization", "Bearer "+credential)
request.Header.Set("Content-Type", "application/json")
response, err := a.client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("delegated control-plane status %d", response.StatusCode)
}
return nil
}
@@ -0,0 +1,39 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"git.cin.su/m/Assistant/contracts/authority"
)
func TestControlPlaneClientSendsOnlyServiceCredentialToAuthorityRuntime(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/internal/control-plane/authorize" || r.Header.Get("Authorization") != "Bearer service-token" {
t.Fatalf("unexpected delegated request: %s %q", r.URL.Path, r.Header.Get("Authorization"))
}
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
client := controlPlaneAuthority{baseURL: server.URL, client: server.Client(), credential: func() (string, error) { return "service-token", nil }}
request := authority.DelegatedControlPlaneRequest{GrantID: "grant", TaskID: "task", TaskRevision: 1, Operation: authority.DelegatedOperationCanaryCognitiveBinding, Resource: authority.CognitiveBindingResource{BindingID: "binding", ProviderInstanceID: "provider", ModelDescriptorID: "model"}}
if err := client.authorize(context.Background(), request); err != nil {
t.Fatalf("authorize: %v", err)
}
}
func TestControlPlaneCredentialUsesDedicatedSecretRoot(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, controlPlaneServiceTokenFile), []byte("test-machine-token\n"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF", "credential://domovoy/control-plane/service-token/v1")
t.Setenv("CONTROL_PLANE_SERVICE_SECRET_ROOT", root)
if got, err := readControlPlaneServiceCredential(); err != nil || got != "test-machine-token" {
t.Fatalf("control-plane credential resolution failed: %v", err)
}
}
+147 -7
View File
@@ -31,6 +31,7 @@ import (
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
@@ -39,12 +40,13 @@ import (
)
const (
creatorPasswordPath = "/run/secrets/creator_password"
creatorBootstrapTokenPath = "/run/secrets/creator_bootstrap_token"
creatorSessionKeyPath = "/run/secrets/creator_session_signing_key"
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
selfDBPasswordPath = "/run/secrets/self_db_password"
creatorPasswordPath = "/run/secrets/creator_password"
creatorBootstrapTokenPath = "/run/secrets/creator_bootstrap_token"
creatorSessionKeyPath = "/run/secrets/creator_session_signing_key"
epistemicsDBPasswordPath = "/run/secrets/epistemics_db_password"
interactionDBPasswordPath = "/run/secrets/interaction_db_password"
selfDBPasswordPath = "/run/secrets/self_db_password"
controlPlaneServiceTokenFile = "control_plane_service_token"
)
const maxCreatorCredentialsBytes = 4 << 10
@@ -137,6 +139,14 @@ func main() {
} else if selfUser == "" || os.Getenv("SELF_POSTGRES_CREDENTIAL_REF") == "" {
log.Printf("SELF learning repository is not configured; user responses remain unaffected")
}
var delegatedAuthority *controlPlaneAuthority
if os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF") != "" {
delegatedAuthority = &controlPlaneAuthority{baseURL: envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"), client: &http.Client{Timeout: 5 * time.Second}, credential: func() (string, error) {
return readControlPlaneServiceCredential()
}}
} else {
log.Printf("delegated control-plane authority is not configured; existing human control remains available")
}
reminderClient, err := newContinuityReminderClient(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), &http.Client{Timeout: 5 * time.Second})
if err != nil {
log.Fatal(err)
@@ -1669,6 +1679,119 @@ func main() {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/control-plane/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
if delegatedAuthority == nil {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
return
}
var value struct {
Delegation authority.DelegatedControlPlaneRequest `json:"delegation"`
Promotion capability.CognitiveCanaryPromotionRequest `json:"promotion"`
}
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.Delegation.Operation != authority.DelegatedOperationCanaryCognitiveBinding {
http.Error(w, "invalid delegated canary request", http.StatusBadRequest)
return
}
current, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), value.Promotion.BindingID, value.Promotion.ExpectedBindingRevision)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
resource := authority.CognitiveBindingResource{BindingID: string(current.Binding.ID), ProviderInstanceID: string(current.Provider.ID), ModelDescriptorID: string(current.Model.ID)}
if value.Delegation.Resource != resource {
http.Error(w, "delegated resource denied", http.StatusForbidden)
return
}
err = delegatedAuthority.authorize(r.Context(), value.Delegation)
if err != nil {
if errors.Is(err, ErrControlPlaneUnavailable) {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
} else {
http.Error(w, "delegated control-plane authority denied", http.StatusForbidden)
}
return
}
value.Promotion.RequestedByPrincipalID = current.Binding.OwnerPrincipalID
value.Promotion.CapabilityID = current.Binding.CapabilityID
promotion, err := capabilitydomain.BuildCanaryPromotion(value.Promotion, current)
if err == nil {
err = capabilityRepository.SaveCognitiveCanaryPromotion(r.Context(), promotion)
}
delegatedAuthority.completed(r.Context(), value.Delegation, err == nil)
if err != nil {
http.Error(w, "canary promotion precondition failed", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/control-plane/cognitive-activate", func(w http.ResponseWriter, r *http.Request) {
if delegatedAuthority == nil {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
return
}
var value struct {
Delegation authority.DelegatedControlPlaneRequest `json:"delegation"`
Promotion capability.CognitiveActivationPromotionRequest `json:"promotion"`
}
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.Delegation.Operation != authority.DelegatedOperationActivateCognitiveBinding {
http.Error(w, "invalid delegated activation request", http.StatusBadRequest)
return
}
current, err := capabilityRepository.LoadCognitiveCandidate(r.Context(), value.Promotion.BindingID, value.Promotion.ExpectedBindingRevision)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
resource := authority.CognitiveBindingResource{BindingID: string(current.Binding.ID), ProviderInstanceID: string(current.Provider.ID), ModelDescriptorID: string(current.Model.ID)}
if value.Delegation.Resource != resource {
http.Error(w, "delegated resource denied", http.StatusForbidden)
return
}
err = delegatedAuthority.authorize(r.Context(), value.Delegation)
if err != nil {
if errors.Is(err, ErrControlPlaneUnavailable) {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
} else {
http.Error(w, "delegated control-plane authority denied", http.StatusForbidden)
}
return
}
value.Promotion.RequestedByPrincipalID = current.Binding.OwnerPrincipalID
value.Promotion.CapabilityID = current.Binding.CapabilityID
promotion, err := capabilitydomain.BuildActivationPromotion(value.Promotion, current)
if err == nil {
err = capabilityRepository.SaveCognitiveActivationPromotion(r.Context(), promotion)
}
delegatedAuthority.completed(r.Context(), value.Delegation, err == nil)
if err != nil {
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/creator/control-plane/grants/{grant}/revoke", func(w http.ResponseWriter, r *http.Request) {
if delegatedAuthority == nil {
http.Error(w, "delegated control-plane authority unavailable", http.StatusServiceUnavailable)
return
}
_, ok := authenticateCreator(w, r)
if !ok {
return
}
if err := delegatedAuthority.revoke(r.Context(), r.Header.Get("Authorization"), authority.GrantID(r.PathValue("grant"))); err != nil {
http.Error(w, "delegated grant revoke denied", http.StatusForbidden)
return
}
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("POST /v1/cognitive-registry/performance-evidence", func(w http.ResponseWriter, r *http.Request) {
var value capability.PerformanceEvidence
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
@@ -1963,6 +2086,23 @@ func normalizeSecretFile(name string, contents []byte) (string, error) {
}
func readCredentialRefSecret(name, refName, fileName string) (string, error) {
return readCredentialRefSecretPurpose(name, refName, fileName, secretcontracts.SecretPurposeDatabasePassword)
}
func readControlPlaneServiceCredential() (string, error) {
ref := os.Getenv("CONTROL_PLANE_SERVICE_CREDENTIAL_REF")
if ref == "" {
return "", errors.New("CONTROL_PLANE_SERVICE_CREDENTIAL_REF is required")
}
root := envOr("CONTROL_PLANE_SERVICE_SECRET_ROOT", "/run/control-plane-secrets")
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeServiceAuthentication, Consumer: secretcontracts.RuntimeConsumerCore, Path: filepath.Join(root, controlPlaneServiceTokenFile)})
}
func readCredentialRefSecretPurpose(name, refName, fileName string, purpose secretcontracts.SecretPurpose) (string, error) {
if _, directSet := os.LookupEnv(name); directSet {
return "", fmt.Errorf("%s direct environment secret is forbidden; use the standard secret file", name)
}
@@ -1980,7 +2120,7 @@ func readCredentialRefSecret(name, refName, fileName string) (string, error) {
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{
CredentialRef: secretcontracts.CredentialRef(ref),
Purpose: secretcontracts.SecretPurposeDatabasePassword,
Purpose: purpose,
Consumer: secretcontracts.RuntimeConsumerCore,
Path: fileName,
})
+14
View File
@@ -0,0 +1,14 @@
package authority
import "git.cin.su/m/Assistant/platform/ids"
// DelegatedControlPlaneAuthorization is the safe, payload-free decision that
// the Authority runtime returns to an already bounded caller.
type DelegatedControlPlaneAuthorization struct {
AuditID ids.ID
}
type DelegatedControlPlaneCompletion struct {
Request DelegatedControlPlaneRequest
Outcome string
}
+67
View File
@@ -13,6 +13,73 @@ type GrantClass string
const GrantClassCreator GrantClass = "CREATOR"
// DelegatedControlPlaneOperation is deliberately a closed, semantic operation
// set. A service credential cannot turn into arbitrary control-plane access.
type DelegatedControlPlaneOperation string
const (
DelegatedOperationQualifyCognitiveBinding DelegatedControlPlaneOperation = "QUALIFY_COGNITIVE_BINDING"
DelegatedOperationEvaluateCognitiveBinding DelegatedControlPlaneOperation = "EVALUATE_COGNITIVE_BINDING"
DelegatedOperationCanaryCognitiveBinding DelegatedControlPlaneOperation = "CANARY_COGNITIVE_BINDING"
DelegatedOperationActivateCognitiveBinding DelegatedControlPlaneOperation = "ACTIVATE_VALIDATED_COGNITIVE_BINDING"
DelegatedOperationRevokeControlPlaneGrant DelegatedControlPlaneOperation = "REVOKE_DELEGATED_CONTROL_PLANE_GRANT"
)
type DelegatedGrantStatus string
const (
DelegatedGrantActive DelegatedGrantStatus = "ACTIVE"
DelegatedGrantRevoked DelegatedGrantStatus = "REVOKED"
)
// CognitiveBindingResource identifies the one governed Capability lifecycle
// resource to which a task delegation applies. It is intentionally not a
// general-purpose resource selector.
type CognitiveBindingResource struct {
BindingID string
ProviderInstanceID string
ModelDescriptorID string
}
type DelegatedControlPlaneGrant struct {
ID GrantID
ServiceActorID identity.ActorID
ServicePrincipalID identity.PrincipalID
IssuerPrincipalID identity.PrincipalID
TaskID string
TaskRevision int64
Operations []DelegatedControlPlaneOperation
Resource CognitiveBindingResource
IssuedAtUnixMillis int64
ExpiresAtUnixMillis int64
ArchitectureRevision string
Nonce string
Revision AuthorityRevision
Status DelegatedGrantStatus
}
type DelegatedControlPlaneRequest struct {
GrantID GrantID
TaskID string
TaskRevision int64
Operation DelegatedControlPlaneOperation
Resource CognitiveBindingResource
}
type DelegatedControlPlaneAudit struct {
AuditID ids.ID
GrantID GrantID
ServiceActorID identity.ActorID
ServicePrincipalID identity.PrincipalID
TaskID string
TaskRevision int64
Operation DelegatedControlPlaneOperation
Resource CognitiveBindingResource
Decision string
Outcome string
RecordedAtUnixMillis int64
}
type Scope struct {
ID ScopeID
Global bool
@@ -0,0 +1,36 @@
BEGIN;
SET ROLE identity_role;
INSERT INTO identity.actors (actor_id, display_name) VALUES ('domovoy-control-plane-actor', 'Domovoy Control Plane') ON CONFLICT (actor_id) DO NOTHING;
INSERT INTO identity.principals (principal_id, display_name) VALUES ('domovoy-control-plane-principal', 'Domovoy Control Plane Principal') ON CONFLICT (principal_id) DO NOTHING;
INSERT INTO identity.principal_bindings (actor_id, principal_id, active) VALUES ('domovoy-control-plane-actor', 'domovoy-control-plane-principal', TRUE) ON CONFLICT (actor_id, principal_id) DO NOTHING;
RESET ROLE;
SET ROLE authority_role;
CREATE TABLE IF NOT EXISTS authority.delegated_control_plane_grants (
grant_id TEXT PRIMARY KEY, service_actor_id TEXT NOT NULL, service_principal_id TEXT NOT NULL, issuer_principal_id TEXT NOT NULL,
task_id TEXT NOT NULL, task_revision BIGINT NOT NULL, operations TEXT[] NOT NULL,
binding_id TEXT NOT NULL, provider_instance_id TEXT NOT NULL, model_descriptor_id TEXT NOT NULL,
issued_at_unix_millis BIGINT NOT NULL, expires_at_unix_millis BIGINT NOT NULL, architecture_revision TEXT NOT NULL, nonce TEXT NOT NULL,
revision BIGINT NOT NULL, status TEXT NOT NULL, revoked_at_unix_millis BIGINT NOT NULL DEFAULT 0,
CHECK (task_revision > 0), CHECK (cardinality(operations) > 0),
CHECK (operations <@ ARRAY['QUALIFY_COGNITIVE_BINDING','EVALUATE_COGNITIVE_BINDING','CANARY_COGNITIVE_BINDING','ACTIVATE_VALIDATED_COGNITIVE_BINDING']::TEXT[]),
CHECK (issued_at_unix_millis > 0), CHECK (expires_at_unix_millis > issued_at_unix_millis), CHECK (revision > 0),
CHECK (status IN ('ACTIVE','REVOKED')), CHECK (binding_id <> '' AND provider_instance_id <> '' AND model_descriptor_id <> ''),
CHECK (nonce <> '' AND architecture_revision <> '')
);
CREATE TABLE IF NOT EXISTS authority.delegated_control_plane_audit (
audit_id TEXT PRIMARY KEY, grant_id TEXT NOT NULL, service_actor_id TEXT NOT NULL, service_principal_id TEXT NOT NULL,
task_id TEXT NOT NULL, task_revision BIGINT NOT NULL, operation TEXT NOT NULL,
binding_id TEXT NOT NULL, provider_instance_id TEXT NOT NULL, model_descriptor_id TEXT NOT NULL,
decision TEXT NOT NULL, outcome TEXT NOT NULL, recorded_at_unix_millis BIGINT NOT NULL,
CHECK (task_revision > 0), CHECK (recorded_at_unix_millis > 0),
CHECK (operation IN ('QUALIFY_COGNITIVE_BINDING','EVALUATE_COGNITIVE_BINDING','CANARY_COGNITIVE_BINDING','ACTIVATE_VALIDATED_COGNITIVE_BINDING','REVOKE_DELEGATED_CONTROL_PLANE_GRANT')),
CHECK (decision IN ('ALLOW','DENY','REVOKE')), CHECK (outcome IN ('PENDING','COMPLETED','FAILED','REVOKED'))
);
CREATE INDEX IF NOT EXISTS delegated_control_plane_audit_grant_idx ON authority.delegated_control_plane_audit (grant_id, recorded_at_unix_millis);
INSERT INTO authority.delegated_control_plane_grants (grant_id,service_actor_id,service_principal_id,issuer_principal_id,task_id,task_revision,operations,binding_id,provider_instance_id,model_descriptor_id,issued_at_unix_millis,expires_at_unix_millis,architecture_revision,nonce,revision,status)
VALUES ('gen2-cognitive-003-control-plane-grant','domovoy-control-plane-actor','domovoy-control-plane-principal','domovoy-creator-principal','GEN2-COGNITIVE-003',4,ARRAY['QUALIFY_COGNITIVE_BINDING','EVALUATE_COGNITIVE_BINDING','CANARY_COGNITIVE_BINDING','ACTIVATE_VALIDATED_COGNITIVE_BINDING']::TEXT[],'binding-c1be4c9f85d8b4a19314f2b8','local-medium-primary','model-26d63a1102b49c5583ffca53',1788134400000,1789344000000,'ADR-0021','gen2-cognitive-003-local-medium-v1',1,'ACTIVE') ON CONFLICT (grant_id) DO NOTHING;
GRANT SELECT, INSERT, UPDATE ON authority.delegated_control_plane_grants, authority.delegated_control_plane_audit TO authority_role;
RESET ROLE;
COMMIT;
+3 -1
View File
@@ -23,7 +23,6 @@ services:
SECRET_BACKEND_ROOT: /run/secrets
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
- assistant_research_sources:/var/lib/research-sources:ro
networks:
shared:
@@ -54,6 +53,8 @@ services:
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
CAPABILITY_POSTGRES_USER: capability_role
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
CONTROL_PLANE_SERVICE_CREDENTIAL_REF: credential://domovoy/control-plane/service-token/v1
CONTROL_PLANE_SERVICE_SECRET_ROOT: /run/control-plane-secrets
SELF_POSTGRES_USER: self_role
SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1
EPISTEMICS_POSTGRES_USER: epistemics_role
@@ -70,6 +71,7 @@ services:
RESEARCH_RUNTIME_URL: http://research-runtime-candidate:8087
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
networks:
shared:
aliases: [core-runtime-candidate]
+6
View File
@@ -70,6 +70,8 @@ services:
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
CAPABILITY_POSTGRES_USER: capability_role
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
CONTROL_PLANE_SERVICE_CREDENTIAL_REF: credential://domovoy/control-plane/service-token/v1
CONTROL_PLANE_SERVICE_SECRET_ROOT: /run/control-plane-secrets
SELF_POSTGRES_USER: self_role
SELF_POSTGRES_CREDENTIAL_REF: credential://domovoy/self/db-password/v1
EPISTEMICS_POSTGRES_USER: epistemics_role
@@ -240,6 +242,9 @@ services:
POSTGRES_PASSWORD: ${COMMIT_DB_PASSWORD}
AUTHORITY_POSTGRES_USER: authority_role
AUTHORITY_POSTGRES_PASSWORD: ${AUTHORITY_DB_PASSWORD}
CONTROL_PLANE_SERVICE_CREDENTIAL_REF: credential://domovoy/control-plane/service-token/v1
CONTROL_PLANE_SERVICE_SECRET_ROOT: /run/control-plane-secrets
SECRET_BACKEND_ROOT: /run/secrets
CORE_RUNTIME_URL: http://core-runtime:8080
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN}
@@ -248,6 +253,7 @@ services:
# External mutation credentials belong here only via Portainer/Docker secrets/env injection.
depends_on: [core-runtime, continuity-runtime, notification-receiver]
volumes:
- assistant_core_secrets:/run/secrets:ro
- assistant_control_plane_service_secrets:/run/control-plane-secrets:ro
- assistant_notification_writer_secret:/run/notification-writer:ro
+1 -1
View File
@@ -32,7 +32,7 @@ if [[ "$ACTION" == "rotate" ]]; then
owner-requested-rotation|exposure-detected) ;;
*) echo "invalid rotation reason" >&2; exit 2 ;;
esac
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane rotate -reason "$reason"
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane -reason "$reason" rotate
else
ASSISTANT_SOURCE_REVISION="$revision" compose --profile secret-control-plane run --rm --no-deps secret-control-plane ensure
fi
@@ -23,6 +23,8 @@ capture="$tmp/capture"
PATH="$tmp/bin:$PATH" CAPTURE="$capture" REPO_DIR="$tmp/repo" ENV_FILE="$tmp/env" "$tmp/repo/deploy/provision-machine-credential.sh" ensure >/dev/null
grep -qx 'volume create assistant_control_plane_service_secrets' "$capture"
grep -q -- '--profile secret-control-plane run --rm --no-deps secret-control-plane ensure' "$capture"
PATH="$tmp/bin:$PATH" CAPTURE="$capture" REPO_DIR="$tmp/repo" ENV_FILE="$tmp/env" ROTATION_REASON=owner-requested-rotation "$tmp/repo/deploy/provision-machine-credential.sh" rotate >/dev/null
grep -q -- '--profile secret-control-plane run --rm --no-deps secret-control-plane -reason owner-requested-rotation rotate' "$capture"
if grep -Eqi '(token=|password=|secret=|authorization:)' "$capture"; then
echo "provisioning wrapper passed credential material" >&2
exit 1
+1 -1
View File
@@ -1 +1 @@
{"task_file":"dev/tasks/GEN2-SECRETS-001-local-machine-credential-provisioning.json","task_id":"GEN2-SECRETS-001","task_revision":2}
{"task_file":"dev/tasks/GEN2-DEPLOY-004-candidate-commit-runtime-releaseset.json","task_id":"GEN2-DEPLOY-004","task_revision":2}
@@ -0,0 +1,28 @@
{
"task_id": "GEN2-AUTHORITY-001",
"task_revision": 3,
"architecture_revision": "ARCH-GEN2-000",
"base_repo_revision": "gen2-integration",
"risk_tier": "E",
"intent": "Add a bounded, non-interactive service-principal and task-scoped delegated Authority path for existing governed Cognitive binding lifecycle operations without browser impersonation, a second Authority owner, or direct Capability mutation.",
"requirement_ids": ["REQ-GEN2-AUTHORITY-001-DELEGATED-CONTROL-PLANE", "REQ-GEN2-COGNITIVE-003-Q8-Q17"],
"allowed_change_scope": {
"modules": ["Identity service principal bootstrap", "Authority delegated grant validation and audit", "Core composition and typed control-plane endpoint", "existing Capability lifecycle invocation", "production secret-reference wiring", "targeted evidence"],
"paths": ["architecture/adrs/ADR-0021-delegated-noninteractive-control-plane-authority.md", "contracts/authority/types.go", "contracts/authority/control_plane.go", "contracts/authority/*_test.go", "contracts/secrets/types.go", "modules/authority/domain/**", "modules/authority/application/**", "modules/authority/ports/**", "adapters/postgres/authority_repository.go", "adapters/postgres/authority_repository_test.go", "cmd/core-runtime/main.go", "cmd/core-runtime/control_plane_authority.go", "cmd/core-runtime/control_plane_authority_test.go", "cmd/commit-runtime/main.go", "cmd/commit-runtime/control_plane_authority.go", "cmd/commit-runtime/control_plane_authority_test.go", "database/migrations/056_delegated_control_plane_authority.sql", "deploy/docker-compose.prod.yml", "deploy/docker-compose.candidate-slot.yml", "docs/CREDENTIALS_AND_SECRETS.md", "docs/evidence/**", "docs/PROJECT_STATUS.md", "development/GEN2_TASK_INDEX.md", "dev/ACTIVE_TASK.json", "dev/tasks/GEN2-AUTHORITY-001-delegated-noninteractive-control-plane-authority.json"]
},
"forbidden_change_scope": {
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "contracts/identity/**", "modules/identity/**", "adapters/postgres/identity_repository.go", "contracts/capability/**", "modules/capability/**", "adapters/postgres/capability_repository.go", "cmd/cognitive-worker/**", "database/bootstrap/**", "security/**", "go.mod", "go.sum", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 1, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["existing Identity Actor/PrincipalBinding", "existing Capability cognitive promotion requests/builders", "existing CredentialRef file resolver"], "may_change": ["typed Authority delegated-grant and audit contract", "typed secret purpose for service authentication"]},
"semantic_owner": "Identity remains the sole owner of service identity. Authority remains the sole owner of delegated grant, validation, revocation and audit. Capability remains the sole owner of cognitive binding/evaluation lifecycle.",
"canonical_state": ["service authentication is distinct from delegated authority", "delegation is task/capability/resource/time scoped and default-deny", "service principal cannot issue, extend, revoke or broaden its own authority", "Capability activation continues to require independent evaluation and owner-local lifecycle validation", "audit contains no secret or user payload"],
"state_transitions": ["service credential authentication -> delegated grant validation -> existing Capability CANARY/ACTIVE validation -> owner-local lifecycle persistence -> Authority audit", "grant ACTIVE -> REVOKED/EXPIRED denies new privileged actions while retaining audit history"],
"side_effects_and_privileges": ["Core receives only an Authority CredentialRef and service authentication SecretRef through the protected file backend", "the positive new_privileges and new_database_owner_roles values are devctl compatibility gates for deploy/database surfaces; no new role, broad administrator privilege, or ownership change is granted", "no external effect bypasses Commit/Executor where those owners already govern it"],
"persistence_and_migrations": ["additive owner-local Authority tables only; seed exactly one bounded service identity and GEN2-COGNITIVE-003 grant through owner roles", "historical audit is retained on rollback"],
"failure_retry_restart": ["authentication or validation failure is fail-closed", "grant state is read durably on each request", "no in-memory permission cache", "revocation prevents new actions immediately", "existing Capability revision preconditions prevent replay or duplicate promotion"],
"assumptions": ["the protected assistant_core_secrets control plane can materialize a non-empty control-plane service credential without exposing its value", "the current production schema has migrations through 055", "the owner decision authorizes a bounded GEN2-COGNITIVE-003 grant only, not future task grants"],
"acceptance_scenarios": ["valid grant permits only its declared Cognitive lifecycle operation and binding revision", "different binding, missing grant, expired grant and revoked grant are denied", "service principal cannot create or extend grants", "restart uses durable grant state", "audit is recorded without payload/secret", "browser session is not required", "existing independent Capability evaluation remains a mandatory promotion precondition"],
"required_tests": ["targeted go test", "targeted go vet", "migration/compose validation", "preflight", "check-diff", "independent", "architecture/security MILESTONE", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,63 @@
{
"task_id": "GEN2-DEPLOY-004",
"task_revision": 2,
"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"],
"allowed_change_scope": {
"modules": ["deployment control", "runtime supervisor", "candidate Assistant runtime dependency closure", "inherited delegated Authority prerequisite only"],
"paths": [
"deploy/docker-compose.candidate-slot.yml",
"deploy/run-production-canary.sh",
"runtime/supervisor/**",
"cmd/supervisor/**",
"tests/deployment/**",
"docs/evidence/**",
"docs/DEPLOYMENT.md",
"docs/PROJECT_STATUS.md",
"development/GEN2_TASK_INDEX.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/GEN2-DEPLOY-004-candidate-commit-runtime-releaseset.json",
"architecture/adrs/ADR-0021-delegated-noninteractive-control-plane-authority.md",
"contracts/authority/types.go",
"contracts/authority/control_plane.go",
"contracts/authority/*_test.go",
"contracts/secrets/types.go",
"modules/authority/domain/**",
"modules/authority/application/**",
"modules/authority/ports/**",
"adapters/postgres/authority_repository.go",
"adapters/postgres/authority_repository_test.go",
"cmd/core-runtime/main.go",
"cmd/core-runtime/control_plane_authority.go",
"cmd/core-runtime/control_plane_authority_test.go",
"cmd/commit-runtime/main.go",
"cmd/commit-runtime/control_plane_authority.go",
"cmd/commit-runtime/control_plane_authority_test.go",
"database/migrations/056_delegated_control_plane_authority.sql",
"deploy/docker-compose.prod.yml",
"docs/CREDENTIALS_AND_SECRETS.md",
"dev/tasks/GEN2-AUTHORITY-001-delegated-noninteractive-control-plane-authority.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "cmd/devctl/**", "contracts/identity/**", "modules/identity/**", "adapters/postgres/identity_repository.go", "contracts/capability/**", "modules/capability/**", "adapters/postgres/capability_repository.go", "cmd/cognitive-worker/**", "database/bootstrap/**", "security/**", "go.mod", "go.sum", "UI", "V1 retirement", "GEN2-EVOLVE-005", "Gen3/**"]
},
"allowed_architecture_delta": {"new_cross_module_dependencies": 1, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {
"consume": ["existing typed Supervisor ReleaseSet and ComponentRelease", "existing runtime health/readiness/revision endpoints", "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"]
},
"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"],
"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"],
"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"]
}
+7
View File
@@ -52,4 +52,11 @@ Bounded post-M5 remediation. Provides the technical protected-secret control-pla
- **M6 retirement:** `GEN2-029` — FULL before irreversible retirement.
- **M6 final:** `GEN2-030` — FULL final acceptance.
## Bounded post-M5 remediation
| Task | Gate | Depends on | Purpose |
|---|---|---|---|
| `GEN2-AUTHORITY-001` | `MILESTONE` | owner decision, existing Identity/Authority/Capability lifecycle | Bounded non-interactive service principal and task-scoped control-plane delegation; does not retire V1 or change Gen2 canonical ownership. |
| `GEN2-DEPLOY-004` | `MILESTONE` | GEN2-DEPLOY-003, GEN2-AUTHORITY-001 implementation | Extend the isolated candidate slot and typed Supervisor ReleaseSet with Commit runtime before delegated Authority acceptance; stable containers are never recreated by candidate operations. |
Между реперными точками выполняется только FAST-профиль конкретного packet-а.
@@ -0,0 +1,50 @@
# GEN2-AUTHORITY-001 — delegated control-plane deployment evidence
Date: 2026-08-31
The former missing-credential prerequisite is resolved by
`GEN2-SECRETS-001`. A local one-shot provisioner created the dedicated
protected artifact and verified idempotent ensure/rotation without reading or
emitting its value. Core and Commit are wired to resolve it from the dedicated
read-only mount, not the shared secret volume.
## Implemented and verified locally
The bounded implementation is present on branch
`gen2/authority-001-delegated-control-plane`. It keeps Identity as the service
identity owner, Authority as the durable grant/revocation/audit owner, and
Capability as the cognitive lifecycle owner. Core is an Authority-runtime
client; the Authority endpoint is served by the existing Commit runtime that
already owns the Authority repository in the V2 runtime topology.
On 2026-08-31 the following checks completed successfully without secret
values:
- targeted `go test` for Authority contracts/domain, secret resolver, Core,
Commit runtime, and the Authority PostgreSQL adapter;
- targeted `go vet` for the same packages;
- production and candidate Compose static validation;
- task preflight, diff scope, independent governance, and architecture checks;
- `git diff --check`.
## Current deployment-control gap
No delegated lifecycle operation has been attempted in production.
On 2026-08-31 the stable Core and Commit runtimes both reported release
revision `2ee5da81145cdad2da1c92c24a91145ad4af61a5`. The existing candidate
slot script declares exactly four components: Creator Web, Core, Cognitive
Worker, and Research Runtime. It has no candidate Commit runtime.
The Authority endpoint is intentionally served by Commit runtime. Therefore a
candidate Core cannot perform a real delegated Authority request against the
required matching candidate Commit runtime without either recreating stable
Commit (forbidden) or extending the candidate ReleaseSet/Supervisor contract
to contain Commit as an independently healthy and rollback-aware component.
This is `BLOCKED_BY_SPEC_GAP`, not a credential or implementation failure. A
new explicit packet/revision must authorize the minimum deployment paths:
`deploy/docker-compose.candidate-slot.yml`, `deploy/run-production-canary.sh`,
and the Supervisor release-set contract/tests if its fixed four-component
schema must change. The change must define Commit candidate health, revision
consistency, traffic/promotion behavior, and whole-ReleaseSet rollback.
@@ -0,0 +1,60 @@
# GEN2-DEPLOY-004 r2 — candidate dependency-closure finding
## Decision and graph evidence
The owner-approved r2 rule requires every versioned Assistant runtime reached
by normal candidate operation to be in the candidate ReleaseSet. Static
runtime configuration establishes this candidate closure:
```text
creator-web -> core-runtime, continuity-runtime, commit-runtime,
cognitive-worker, agent-worker
core-runtime -> cognitive-worker, research-runtime, continuity-runtime,
commit-runtime
commit-runtime -> core-runtime, continuity-runtime
continuity-runtime -> core-runtime, commit-runtime
agent-worker -> task-verifier, agent-execution-bridge
agent-execution-bridge -> cognitive-worker, work-graph-runtime, task-verifier
work-graph-runtime -> core-runtime
```
Therefore the static closure is: Creator Web, Core Runtime, Cognitive Worker,
Research Runtime, Commit Runtime, Continuity Runtime, Agent Worker, Agent
Execution Bridge, Work Graph Runtime, and Task Verifier.
PostgreSQL, protected secret volumes, the local model gateway/lifeboat, and
the configured web-search endpoint are classified as shared durable
infrastructure or shared external services. They do not contain an Assistant
semantic/runtime release revision.
## Blocking dynamic dependency
Commit's normal prospective-notification operation obtains the exact active
notification Capability binding from Core. The binding's immutable
`HTTPSOrigin`, delivery path, and revision select the receiver. This is not a
Compose environment URL and cannot be safely redirected by a container alias.
Consequently, with the existing active binding, a candidate Commit executing a
normal notification operation would call the stable notification receiver. A
candidate notification-receiver container alone does not solve the problem:
the candidate would still resolve the stable binding. Reusing the stable name
would create ambiguous network routing, and changing the active binding would
mutate Capability-owned canonical state.
No runtime, Capability state, migration, secret, or production container was
changed while establishing this finding.
## Required owner decision
Choose one governed boundary before DEPLOY-004 implementation can prove full
dependency isolation:
1. authorize a bounded Capability-owned candidate binding/shadow-binding
lifecycle, including its authority, rollback and selection semantics; or
2. explicitly classify the notification receiver as a shared external service
for this ReleaseSet, despite it being an Assistant-managed runtime.
The first option requires a new packet revision with the exact Capability
contract/owner paths. The second changes the release-boundary policy and must
be accepted explicitly. No direct binding update, stable alias reuse, or
candidate-to-stable shortcut is an acceptable substitute.
@@ -33,3 +33,21 @@ The following checks completed successfully before production execution:
No credential value, fingerprint, length, header, or plaintext environment
value was read, printed, copied, committed, or included in this evidence.
## Production control-plane evidence
The one-shot provisioner was run from a detached `gen2-integration` worktree
on `docker.cin.su`. It created the dedicated external volume and provisioned
the credential once. A repeat `ensure` reported that the already active
artifact was reused. A bounded owner-requested rotation then completed.
For creation, idempotent ensure, and rotation, the stable Core, Commit,
Creator Web, Cognitive Worker, and Research Runtime container identities were
recorded before and after the one-shot operation and were unchanged. The
verification checked only artifact and separate metadata/audit file presence;
it did not read their content.
At this point the credential is provisioned but not yet used by the stable
runtime. Consumer mount and service-authentication acceptance are completed
only when `GEN2-AUTHORITY-001` is merged and deployed as its own controlled
candidate ReleaseSet.
@@ -0,0 +1,34 @@
package domain
import (
"errors"
"slices"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/identity"
)
var ErrDelegatedAuthorityDenied = errors.New("delegated control-plane authority denied")
// ValidateDelegatedControlPlaneGrant proves only the Authority-owned decision.
// It does not replace the receiving semantic owner's lifecycle preconditions.
func ValidateDelegatedControlPlaneGrant(nowUnixMillis int64, binding identity.PrincipalBinding, grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest) error {
if nowUnixMillis < 1 || binding.ActorID == "" || binding.PrincipalID == "" || !binding.Active ||
grant.ID == "" || grant.Revision < 1 || grant.Status != authority.DelegatedGrantActive ||
grant.ServiceActorID != binding.ActorID || grant.ServicePrincipalID != binding.PrincipalID ||
grant.IssuerPrincipalID == "" || grant.ArchitectureRevision == "" || grant.Nonce == "" ||
grant.IssuedAtUnixMillis < 1 || grant.ExpiresAtUnixMillis <= grant.IssuedAtUnixMillis || nowUnixMillis < grant.IssuedAtUnixMillis || nowUnixMillis >= grant.ExpiresAtUnixMillis || !RequestMatchesDelegatedControlPlaneGrant(grant, request) {
return ErrDelegatedAuthorityDenied
}
return nil
}
func RequestMatchesDelegatedControlPlaneGrant(grant authority.DelegatedControlPlaneGrant, request authority.DelegatedControlPlaneRequest) bool {
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
}
func CanIssuerRevokeDelegatedGrant(issuer identity.PrincipalID, grant authority.DelegatedControlPlaneGrant) bool {
return issuer != "" && issuer == grant.IssuerPrincipalID && grant.Status == authority.DelegatedGrantActive
}
@@ -0,0 +1,43 @@
package domain
import (
"testing"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/identity"
)
func delegatedGrant() authority.DelegatedControlPlaneGrant {
return authority.DelegatedControlPlaneGrant{ID: "grant-1", ServiceActorID: "service-actor", ServicePrincipalID: "service-principal", IssuerPrincipalID: "owner", TaskID: "GEN2-COGNITIVE-003", TaskRevision: 4, Operations: []authority.DelegatedControlPlaneOperation{authority.DelegatedOperationCanaryCognitiveBinding}, Resource: authority.CognitiveBindingResource{BindingID: "binding-1", ProviderInstanceID: "provider-1", ModelDescriptorID: "model-1"}, IssuedAtUnixMillis: 10, ExpiresAtUnixMillis: 100, ArchitectureRevision: "ADR-0021", Nonce: "nonce", Revision: 1, Status: authority.DelegatedGrantActive}
}
func TestDelegatedControlPlaneGrantIsExactAndTimeBound(t *testing.T) {
grant := delegatedGrant()
binding := identity.PrincipalBinding{ActorID: "service-actor", PrincipalID: "service-principal", Active: true}
request := authority.DelegatedControlPlaneRequest{GrantID: grant.ID, TaskID: grant.TaskID, TaskRevision: grant.TaskRevision, Operation: authority.DelegatedOperationCanaryCognitiveBinding, Resource: grant.Resource}
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err != nil {
t.Fatalf("valid delegation denied: %v", err)
}
request.Resource.BindingID = "other"
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err == nil {
t.Fatal("different resource was allowed")
}
request.Resource = grant.Resource
if err := ValidateDelegatedControlPlaneGrant(100, binding, grant, request); err == nil {
t.Fatal("expired grant was allowed")
}
grant.Status = authority.DelegatedGrantRevoked
if err := ValidateDelegatedControlPlaneGrant(20, binding, grant, request); err == nil {
t.Fatal("revoked grant was allowed")
}
}
func TestDelegatedGrantCannotSelfRevokeOrBroaden(t *testing.T) {
grant := delegatedGrant()
if CanIssuerRevokeDelegatedGrant("service-principal", grant) {
t.Fatal("service principal may not revoke its own grant")
}
if !CanIssuerRevokeDelegatedGrant("owner", grant) {
t.Fatal("issuer could not revoke active grant")
}
}