diff --git a/proto/trogonai/policy/model_ceilings/state/v1alpha1/state.proto b/proto/trogonai/policy/model_ceilings/state/v1alpha1/state.proto new file mode 100644 index 000000000..95137cc0b --- /dev/null +++ b/proto/trogonai/policy/model_ceilings/state/v1alpha1/state.proto @@ -0,0 +1,25 @@ +edition = "2024"; + +package trogonai.policy.model_ceilings.state.v1alpha1; + +import "buf/validate/validate.proto"; +import "google/protobuf/empty.proto"; +import "trogonai/policy/model_ceilings/v1alpha1/ceiling_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Policy fold only. Replay rejects unknown events and a subject change. Clear +// retains the history. +message State { + oneof state { + option (buf.validate.oneof).required = true; + + google.protobuf.Empty missing = 1; + ModelCeilingHistory history = 2; + } +} + +message ModelCeilingHistory { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + // Absence means cleared, not a zero cap. + trogonai.policy.model_ceilings.v1alpha1.ModelCeilingPolicy policy = 2; +} diff --git a/proto/trogonai/policy/model_ceilings/v1alpha1/ceiling_policy.proto b/proto/trogonai/policy/model_ceilings/v1alpha1/ceiling_policy.proto new file mode 100644 index 000000000..41226a355 --- /dev/null +++ b/proto/trogonai/policy/model_ceilings/v1alpha1/ceiling_policy.proto @@ -0,0 +1,41 @@ +edition = "2024"; + +package trogonai.policy.model_ceilings.v1alpha1; + +import "buf/validate/validate.proto"; + +// Exact match on the catalog model identifier of ModelVersionRef, any version of +// it. A version arm is an additive change for when a ceiling needs to pin +// releases. Names, families, and patterns are not arms because a late-bound name +// would widen a ceiling without a policy change. +message ModelMatcher { + oneof matcher { + option (buf.validate.oneof).required = true; + + string model_id = 1 [(buf.validate.field).string.min_len = 1]; + } +} + +// A ceiling bounds which catalog models the subject's Sessions may select, +// including auxiliary roles. A Session's every model selection must match some +// matcher. An empty ceiling is invalid: use ClearModelCeiling to remove the +// ceiling and a hold to stop work. +// +// Absence means no ceiling from this subject, never a zero cap, and never +// bypasses connection-level model authorization under ADR#0032: the +// connection's allowed models authorize, and a ceiling only narrows. Policy +// never substitutes a model; a selection outside the ceiling fails admission +// (ADR#0032 Decision 6). +// +// Ceilings follow the delegation lineage downward exactly like holds and +// budgets, so an Agent limited to one model cannot reach another by delegating. +// Evaluation is a conjunction over every ceiling on the chain. +message ModelCeilingPolicy { + option (buf.validate.message).cel = { + id: "model_ceiling_policy.models_unique" + message: "each model identifier may appear at most once" + expression: "this.within.map(m, m.model_id).unique()" + }; + + repeated ModelMatcher within = 1 [(buf.validate.field).repeated.min_items = 1]; +} diff --git a/proto/trogonai/policy/model_ceilings/v1alpha1/clear_model_ceiling.proto b/proto/trogonai/policy/model_ceilings/v1alpha1/clear_model_ceiling.proto new file mode 100644 index 000000000..305bbb20f --- /dev/null +++ b/proto/trogonai/policy/model_ceilings/v1alpha1/clear_model_ceiling.proto @@ -0,0 +1,14 @@ +edition = "2024"; + +package trogonai.policy.model_ceilings.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Removes only this subject's ceiling; ceilings elsewhere on a Session's chain +// still apply. Clearing a missing history rejects with ModelCeilingNotSet. An +// already cleared history succeeds without an event after the concurrency +// check. Dispatch requires live administration authority and a nonempty Decider +// command_id plus the original positive expected_revision; retries preserve both. +message ClearModelCeiling { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/model_ceilings/v1alpha1/events.proto b/proto/trogonai/policy/model_ceilings/v1alpha1/events.proto new file mode 100644 index 000000000..a1c5fed06 --- /dev/null +++ b/proto/trogonai/policy/model_ceilings/v1alpha1/events.proto @@ -0,0 +1,18 @@ +edition = "2024"; + +package trogonai.policy.model_ceilings.v1alpha1; + +import "buf/validate/validate.proto"; +import "trogonai/policy/model_ceilings/v1alpha1/model_ceiling_cleared.proto"; +import "trogonai/policy/model_ceilings/v1alpha1/model_ceiling_set.proto"; + +// An unset or unknown event rejects replay; skipping a ceiling change could +// admit a model selection the current ceiling forbids, or refuse one it allows. +message ModelCeilingEvent { + oneof event { + option (buf.validate.oneof).required = true; + + ModelCeilingSet model_ceiling_set = 1; + ModelCeilingCleared model_ceiling_cleared = 2; + } +} diff --git a/proto/trogonai/policy/model_ceilings/v1alpha1/faults.proto b/proto/trogonai/policy/model_ceilings/v1alpha1/faults.proto new file mode 100644 index 000000000..0d47ab881 --- /dev/null +++ b/proto/trogonai/policy/model_ceilings/v1alpha1/faults.proto @@ -0,0 +1,39 @@ +edition = "2024"; + +package trogonai.policy.model_ceilings.v1alpha1; + +import "trogon/error/v1alpha1/options.proto"; + +// Schema-only templates for the policy.model_ceilings module's DeciderService +// refusals. These messages are never encoded. Administration authorization and +// model selection admission keep their own failure contracts; this fold only +// owns ceiling mutations. +message ModelCeilingCommandInvalidError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.model_ceilings" + reason: "MODEL_CEILING_COMMAND_INVALID" + message: "The command violates the model ceiling policy contract." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message ModelCeilingNotSetError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.model_ceilings" + reason: "MODEL_CEILING_NOT_SET" + message: "No model ceiling history exists for this subject." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} + +message ModelCeilingSubjectMismatchError { + option (trogon.error.v1alpha1.message).template = { + domain: "policy.model_ceilings" + reason: "MODEL_CEILING_SUBJECT_MISMATCH" + message: "The command names a different subject from this model ceiling history." + code: FAILED_PRECONDITION + visibility: VISIBILITY_PUBLIC + }; +} diff --git a/proto/trogonai/policy/model_ceilings/v1alpha1/model_ceiling_cleared.proto b/proto/trogonai/policy/model_ceilings/v1alpha1/model_ceiling_cleared.proto new file mode 100644 index 000000000..67e6b05a0 --- /dev/null +++ b/proto/trogonai/policy/model_ceilings/v1alpha1/model_ceiling_cleared.proto @@ -0,0 +1,11 @@ +edition = "2024"; + +package trogonai.policy.model_ceilings.v1alpha1; + +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Removes the subject's ceiling and retains its history. A later set reuses the +// same stream. +message ModelCeilingCleared { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/model_ceilings/v1alpha1/model_ceiling_set.proto b/proto/trogonai/policy/model_ceilings/v1alpha1/model_ceiling_set.proto new file mode 100644 index 000000000..c20f26e38 --- /dev/null +++ b/proto/trogonai/policy/model_ceilings/v1alpha1/model_ceiling_set.proto @@ -0,0 +1,12 @@ +edition = "2024"; + +package trogonai.policy.model_ceilings.v1alpha1; + +import "trogonai/policy/model_ceilings/v1alpha1/ceiling_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Records the ceiling in force from this event, replacing any earlier one. +message ModelCeilingSet { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + ModelCeilingPolicy policy = 2 [features.field_presence = LEGACY_REQUIRED]; +} diff --git a/proto/trogonai/policy/model_ceilings/v1alpha1/set_model_ceiling.proto b/proto/trogonai/policy/model_ceilings/v1alpha1/set_model_ceiling.proto new file mode 100644 index 000000000..d5d9da2b5 --- /dev/null +++ b/proto/trogonai/policy/model_ceilings/v1alpha1/set_model_ceiling.proto @@ -0,0 +1,18 @@ +edition = "2024"; + +package trogonai.policy.model_ceilings.v1alpha1; + +import "trogonai/policy/model_ceilings/v1alpha1/ceiling_policy.proto"; +import "trogonai/policy/subjects/v1alpha1/subject.proto"; + +// Sets or replaces the subject's ceiling. There is no Initialize command and no +// provisioning participation, because absence needs nothing set up. On a missing +// history it creates the stream: NoStream requires a nonempty Decider command_id +// and no expected_revision. Later commands carry the original positive +// expected_revision; retries preserve both. Dispatch verifies the subject exists +// and live administration authority. Equal matchers succeed without an event +// after the concurrency check; reordering matchers alone is not a change. +message SetModelCeiling { + trogonai.policy.subjects.v1alpha1.PolicySubject subject = 1 [features.field_presence = LEGACY_REQUIRED]; + ModelCeilingPolicy 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 09777b0ca..405ab29d3 100644 --- a/proto/trogonai/policy/subjects/v1alpha1/rule_ref.proto +++ b/proto/trogonai/policy/subjects/v1alpha1/rule_ref.proto @@ -15,6 +15,7 @@ enum PolicyRuleKind { POLICY_RULE_KIND_AGENT_BUDGET = 1; POLICY_RULE_KIND_AGENT_RATE_LIMIT = 2; POLICY_RULE_KIND_HOLD = 3; + POLICY_RULE_KIND_MODEL_CEILING = 4; } message PolicyRuleRef {