| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| pub mod catalog; | |
| pub mod history; | |
| pub mod title; | |
| use futures_util::future::BoxFuture; | |
| use serde::{Deserialize, Serialize}; | |
| use std::{collections::BTreeSet, sync::Arc}; | |
| /// A bounded configuration projection. Revision is for optimistic control; | |
| /// reading this document cannot mint execution or filesystem authority. | |
| #[derive(Clone, Debug, Serialize, Deserialize)] | |
| #[serde(rename_all = "camelCase", deny_unknown_fields)] | |
| pub struct View { | |
| pub session_id: String, | |
| pub revision: u64, | |
| pub name: String, | |
| pub boundary_revision: u64, | |
| pub workspace: maka_agent_facts::execution::WorkspaceProjection, | |
| pub target: crate::execution::Target, | |
| pub sandbox_mode: maka_agent_facts::execution::SandboxMode, | |
| pub approval_policy: maka_agent_facts::execution::ApprovalPolicy, | |
| pub collaboration_mode: maka_agent_facts::execution::CollaborationMode, | |
| pub behavior: maka_agent_facts::execution::BehaviorId, | |
| pub bound_tools: Option<BTreeSet<String>>, | |
| #[serde(default, skip_serializing_if = "Option::is_none")] | |
| pub tool_profile: Option<maka_agent_facts::execution::ToolProfile>, | |
| } | |
| /// Session behavior prepares its scoped capabilities. It cannot modify an | |
| /// already-frozen model request or replace Host execution authority. | |
| pub struct Request { | |
| /// The candidate configuration, including any per-execution behavior override. | |
| /// This is an observation, not permission to access or execute the Session. | |
| pub session: View, | |
| pub cancellation: tokio_util::sync::CancellationToken, | |
| } | |
| pub trait Behavior: Send + Sync { | |
| fn prepare(&self, request: Request) -> BoxFuture<'_, Result<Preparation, String>>; | |
| } | |
| /// Explicit delegation of canonical native message admission to the Host. | |
| /// This is registration metadata; preparing a behavior cannot grant input access. | |
| #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| #[serde(rename_all = "snake_case")] | |
| pub enum NativeInputPolicy { | |
| #[default] | |
| Denied, | |
| NativeUserMessages, | |
| } | |
| pub struct SessionBehavior { | |
| pub behavior: Arc<dyn Behavior>, | |
| pub native_input: NativeInputPolicy, | |
| } | |
| impl SessionBehavior { | |
| pub fn new(behavior: Arc<dyn Behavior>) -> Self { | |
| Self { | |
| behavior, | |
| native_input: NativeInputPolicy::Denied, | |
| } | |
| } | |
| pub fn with_native_input(mut self, policy: NativeInputPolicy) -> Self { | |
| self.native_input = policy; | |
| self | |
| } | |
| } | |
| #[derive(Default, Serialize, Deserialize)] | |
| #[serde(default, rename_all = "camelCase", deny_unknown_fields)] | |
| pub struct Preparation { | |
| pub instructions: String, | |
| pub native_tools: maka_agent_facts::execution::NativeToolSet, | |
| pub required_clients: Option<ClientTools>, | |
| pub tool_ceiling: Option<BTreeSet<String>>, | |
| /// A captured domain version, checked and pinned during durable admission. | |
| #[serde(skip)] | |
| pub basis: Option<maka_capabilities::revision::Basis>, | |
| } | |
| /// Required tools must share one Session-bound provider. Optional tools are | |
| /// exposed only when available; neither list can grant a Client capability. | |
| #[derive(Serialize, Deserialize)] | |
| #[serde(rename_all = "camelCase", deny_unknown_fields)] | |
| pub struct ClientTools { | |
| pub required: Vec<String>, | |
| #[serde(default)] | |
| pub optional: Vec<String>, | |
| #[serde(default)] | |
| pub private: BTreeSet<String>, | |
| } | |
| impl Preparation { | |
| pub fn validate(&self) -> Result<(), crate::Error> { | |
| if let Some(clients) = &self.required_clients { | |
| if clients.required.is_empty() { | |
| return Err(crate::Error::Invalid( | |
| "Invalid required Client tool selection".into(), | |
| )); | |
| } | |
| for name in clients.required.iter().chain(&clients.optional) { | |
| crate::name(name)?; | |
| } | |
| if clients | |
| .private | |
| .iter() | |
| .any(|name| !clients.required.contains(name) && !clients.optional.contains(name)) | |
| { | |
| return Err(crate::Error::Invalid( | |
| "Private Client tools must be bound by the behavior".into(), | |
| )); | |
| } | |
| } | |
| if let Some(names) = &self.tool_ceiling { | |
| for name in names { | |
| crate::name(name)?; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| #[test] | |
| fn large_tool_selections_preserve_client_binding_validation() { | |
| let names: Vec<_> = (0..160).map(|i| format!("tool_{i}")).collect(); | |
| let mut preparation = Preparation { | |
| tool_ceiling: Some(names.iter().cloned().collect()), | |
| required_clients: Some(ClientTools { | |
| required: names, | |
| optional: vec![], | |
| private: BTreeSet::from(["tool_159".into()]), | |
| }), | |
| ..Default::default() | |
| }; | |
| preparation.validate().unwrap(); | |
| preparation | |
| .required_clients | |
| .as_mut() | |
| .unwrap() | |
| .private | |
| .insert("unbound".into()); | |
| assert!(preparation.validate().is_err()); | |
| } | |
| } |