From 0e4af7f9bc354f36d1f74097b3a71190c3f95de6 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sun, 18 Oct 2026 01:03:21 -0400 Subject: [PATCH] feat(protos): require a delegation permission before an agent may dispatch to another Signed-off-by: Yordis Prieto --- .../agents/agents/v1alpha1/dependencies.proto | 5 +- .../state/v1alpha1/state.proto | 32 ++++++++++++ .../clear_delegation_permission.proto | 16 ++++++ .../delegation_permission_cleared.proto | 11 +++++ .../delegation_permission_initialized.proto | 20 ++++++++ .../v1alpha1/delegation_permission_set.proto | 12 +++++ .../v1alpha1/events.proto | 20 ++++++++ .../v1alpha1/faults.proto | 49 +++++++++++++++++++ .../initialize_delegation_permission.proto | 28 +++++++++++ .../v1alpha1/permission_policy.proto | 47 ++++++++++++++++++ .../v1alpha1/set_delegation_permission.proto | 19 +++++++ .../policy/subjects/v1alpha1/rule_ref.proto | 1 + 12 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 proto/trogonai/policy/delegation_permissions/state/v1alpha1/state.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/clear_delegation_permission.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_cleared.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_initialized.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_set.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/events.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/faults.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/initialize_delegation_permission.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto create mode 100644 proto/trogonai/policy/delegation_permissions/v1alpha1/set_delegation_permission.proto diff --git a/proto/trogonai/agents/agents/v1alpha1/dependencies.proto b/proto/trogonai/agents/agents/v1alpha1/dependencies.proto index e601a46a02..f4f48268fb 100644 --- a/proto/trogonai/agents/agents/v1alpha1/dependencies.proto +++ b/proto/trogonai/agents/agents/v1alpha1/dependencies.proto @@ -132,7 +132,10 @@ message DelegateDeclaration { ]; // Exactly one known arm is required. Selectors match each Agent's latest // minted revision's configuration-owned labels. Display names and annotations - // never participate. Session admission pins the selected revision. + // never participate. Session admission pins the selected revision. Resolution + // runs over the Session's hierarchy scope first; the resolved Agent is then + // checked against the dispatching Agent's delegation permissions, which may + // refuse it but never substitute another (ADR#0071). oneof selection { option (buf.validate.oneof).required = true; diff --git a/proto/trogonai/policy/delegation_permissions/state/v1alpha1/state.proto b/proto/trogonai/policy/delegation_permissions/state/v1alpha1/state.proto new file mode 100644 index 0000000000..7d954434fd --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/state/v1alpha1/state.proto @@ -0,0 +1,32 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.state.v1alpha1; + +import "buf/validate/validate.proto"; +import "google/protobuf/empty.proto"; +import "trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Policy fold only. Replay rejects unknown events, a subject change, repeated +// genesis, and mutations before initialization. Clear retains initialized state. +// Provisioning origin is copied from genesis and never cleared or replaced. +message State { + oneof state { + option (buf.validate.oneof).required = true; + + google.protobuf.Empty missing = 1; + InitializedDelegationPermission initialized = 2; + } +} + +message InitializedDelegationPermission { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + // Absence means cleared, which is deny rather than a permit of nothing in + // particular. Restoring targets consults the same history. + trogonai.policy.delegation_permissions.v1alpha1.DelegationPermissionPolicy policy = 2; + string provisioning_id = 3 [(buf.validate.field).cel = { + id: "initialized_delegation_permission.provisioning_id_nonblank" + message: "a supplied provisioning_id must contain a non-whitespace character" + expression: "this.trim() != ''" + }]; +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/clear_delegation_permission.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/clear_delegation_permission.proto new file mode 100644 index 0000000000..9c818ac285 --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/clear_delegation_permission.proto @@ -0,0 +1,16 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Clears only this subject's permit, which returns it to deny unless an +// ancestor node still permits the target. The initialized identity remains. +// Dispatch requires live authority and a nonempty Decider command_id plus the +// original positive expected_revision; retries preserve both. An already cleared +// permission succeeds without an event after that concurrency check. For an +// Agent subject, authoritative registry lifecycle must be ACTIVE or ARCHIVED, +// so requested provisioning setup cannot disappear before activation. +message ClearDelegationPermission { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_cleared.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_cleared.proto new file mode 100644 index 0000000000..aa0676b811 --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_cleared.proto @@ -0,0 +1,11 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Removes the permission, never the identity. An already cleared history emits +// no second clear. +message DelegationPermissionCleared { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_initialized.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_initialized.proto new file mode 100644 index 0000000000..e64dedba66 --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_initialized.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Genesis fixes the subject for this history. The admitted origin and exact +// initial policy remain immutable receipt evidence after replacements and +// clears, independent of the current policy projection. +message DelegationPermissionInitialized { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + DelegationPermissionPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; + string provisioning_id = 3 [(buf.validate.field).cel = { + id: "delegation_permission_initialized.provisioning_id_nonblank" + message: "a supplied provisioning_id must contain a non-whitespace character" + expression: "this.trim() != ''" + }]; +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_set.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_set.proto new file mode 100644 index 0000000000..23a77b9bf6 --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_set.proto @@ -0,0 +1,12 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Replaces the permission in force without replacing the history's identity. +message DelegationPermissionSet { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + DelegationPermissionPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/events.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/events.proto new file mode 100644 index 0000000000..fb8feb4e46 --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/events.proto @@ -0,0 +1,20 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_cleared.proto"; +import "trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_initialized.proto"; +import "trogonai/policy/delegation_permissions/v1alpha1/delegation_permission_set.proto"; + +// An unset or unknown event rejects replay; skipping a permission change could +// admit a delegation the current permission forbids, or refuse one it allows. +message DelegationPermissionEvent { + oneof event { + option (buf.validate.oneof).required = true; + + DelegationPermissionInitialized delegation_permission_initialized = 1; + DelegationPermissionSet delegation_permission_set = 2; + DelegationPermissionCleared delegation_permission_cleared = 3; + } +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/faults.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/faults.proto new file mode 100644 index 0000000000..d907501924 --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/faults.proto @@ -0,0 +1,49 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the policy.delegation_permissions module's +// DeciderService refusals. These messages are never encoded. Administration +// authorization and delegation dispatch admission keep their own failure +// contracts; this fold only owns permission mutations. +message DelegationPermissionCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.delegation_permissions" + reason: "DELEGATION_PERMISSION_COMMAND_INVALID" + message: "The command violates the delegation permission policy contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message DelegationPermissionNotInitializedError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.delegation_permissions" + reason: "DELEGATION_PERMISSION_NOT_INITIALIZED" + message: "The delegation permission history must be initialized before this command." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message DelegationPermissionAlreadyInitializedError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.delegation_permissions" + reason: "DELEGATION_PERMISSION_ALREADY_INITIALIZED" + message: "The delegation permission history has already been initialized." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message DelegationPermissionSubjectMismatchError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.delegation_permissions" + reason: "DELEGATION_PERMISSION_SUBJECT_MISMATCH" + message: "The command names a different subject from this delegation permission history." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/initialize_delegation_permission.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/initialize_delegation_permission.proto new file mode 100644 index 0000000000..798e85d15b --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/initialize_delegation_permission.proto @@ -0,0 +1,28 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Exists for the same reason as budget initialization: with absence meaning +// deny, an Agent provisioned with a REQUIRED delegate declaration would fail its +// first Session unless the provisioning operation can install its permission. +// provisioning_id is valid only for an Agent subject. Dispatch verifies the +// subject exists and live administration authority. During PROVISIONING only the +// owning operation may initialize, and dispatch verifies registry origin rather +// than trusting caller-supplied provenance; PROVISIONING_CANCELLED and unknown +// lifecycle reject. NoStream creation requires a nonempty Decider command_id and +// no expected_revision; retries preserve the id. An already-initialized +// rejection is not success; recovery verifies the durable genesis receipt's +// subject, provisioning origin, and exact policy. +message InitializeDelegationPermission { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + DelegationPermissionPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; + string provisioning_id = 3 [(buf.validate.field).cel = { + id: "initialize_delegation_permission.provisioning_id_nonblank" + message: "a supplied provisioning_id must contain a non-whitespace character" + expression: "this.trim() != ''" + }]; +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto new file mode 100644 index 0000000000..dc0862e070 --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto @@ -0,0 +1,47 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogon/hierarchy/v1alpha1/node.proto"; +import "trogonai/agents/agents/v1alpha1/identity.proto"; + +// One thing a dispatching Agent may delegate to: an exact Agent, or every Agent +// placed at or below a node. Both are identities, never selectors, so a relabel +// cannot widen a permission. +message DelegationTarget { + oneof target { + option (buf.validate.oneof).required = true; + + trogonai.agents.agents.v1alpha1.AgentId agent_id = 1; + trogon.hierarchy.v1alpha1.NodeId agents_under = 2; + } +} + +// A permission widens; it is the grant that lets a Session of the subject +// dispatch a delegation to a target Agent. Absence is deny: an Agent with no +// permission on itself or any ancestor node may not delegate at all, and a +// DelegateDeclaration in its configuration grants nothing (ADR#0025, ADR#0063 +// Decision 5). +// +// Permits accumulate downward: the effective set for an Agent is the union of +// its own permission and every ancestor node's. A dispatch is admitted when the +// resolved target Agent is named exactly or is placed at or below a permitted +// node. Permits do not follow the delegation lineage: each dispatch is checked +// against the dispatching Agent's own chain, so a permit to reach one Agent +// never lets that Agent reach a third. +// +// The selector resolves first over the Session's hierarchy scope, ambiguity +// fails there, and the permission check then runs on the single resolved Agent. +// Policy may deny a resolution but never changes which Agent it resolves to. +// +// An empty policy is invalid; clear the permission to withdraw it. +message DelegationPermissionPolicy { + option (buf.validate.message).cel = { + id: "delegation_permission_policy.targets_unique" + message: "each delegation target may appear at most once" + expression: "this.permitted.map(t, has(t.agent_id) ? 'agent:' + t.agent_id.value : 'node:' + t.agents_under.value).unique()" + }; + + repeated DelegationTarget permitted = 1 [(buf.validate.field).repeated.min_items = 1]; +} diff --git a/proto/trogonai/policy/delegation_permissions/v1alpha1/set_delegation_permission.proto b/proto/trogonai/policy/delegation_permissions/v1alpha1/set_delegation_permission.proto new file mode 100644 index 0000000000..53b9b3a0da --- /dev/null +++ b/proto/trogonai/policy/delegation_permissions/v1alpha1/set_delegation_permission.proto @@ -0,0 +1,19 @@ +edition = "2024"; + +package trogonai.policy.delegation_permissions.v1alpha1; + +import "trogonai/policy/delegation_permissions/v1alpha1/permission_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Replaces an initialized subject's permission, including after a clear. +// Dispatch requires live administration authority and a nonempty Decider +// command_id plus original positive expected_revision. Retries preserve both; a +// refreshed revision is a new intent. Equal targets succeed without an event +// after the concurrency check; reordering targets alone is not a change. For an +// Agent subject, authoritative registry lifecycle must be ACTIVE or ARCHIVED, so +// requested provisioning setup cannot be replaced before activation. Genesis +// provisioning origin survives every replacement. +message SetDelegationPermission { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + DelegationPermissionPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/subjects/v1alpha1/rule_ref.proto b/proto/trogonai/policy/subjects/v1alpha1/rule_ref.proto index 405ab29d34..463a802beb 100644 --- a/proto/trogonai/policy/subjects/v1alpha1/rule_ref.proto +++ b/proto/trogonai/policy/subjects/v1alpha1/rule_ref.proto @@ -16,6 +16,7 @@ enum PolicyRuleKind { POLICY_RULE_KIND_AGENT_RATE_LIMIT = 2; POLICY_RULE_KIND_HOLD = 3; POLICY_RULE_KIND_MODEL_CEILING = 4; + POLICY_RULE_KIND_DELEGATION_PERMISSION = 5; } message PolicyRuleRef {