diff --git a/Cargo.lock b/Cargo.lock index 291bc05..5bda36e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1724,7 +1724,6 @@ dependencies = [ "async-trait", "base64 0.21.7", "chrono", - "ed25519-dalek", "hmac 0.12.1", "mtp", "regex", diff --git a/Cargo.toml b/Cargo.toml index d2ce2c0..bcee5e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ mtp = { git = "https://git@git.methanium.net/methanium/mtp.git", features = [ ] } tokio = "1" async-trait = "0.1" -ed25519-dalek = "2" url = "2" regex = "1" hmac = "0.12" diff --git a/src/compute.rs b/src/compute.rs deleted file mode 100644 index 1104203..0000000 --- a/src/compute.rs +++ /dev/null @@ -1,219 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Administrator-defined weighted compute admission cost. -/// -/// Units are deliberately not tied to a physical quantity such as CPU cores -/// or GPU count. They are a relative scheduling weight configured for a -/// worker and its workloads. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -#[serde(transparent)] -pub struct Units(pub u32); - -impl Units { - pub const ZERO: Self = Self(0); - - pub const fn new(value: u32) -> Self { - Self(value) - } - - pub const fn get(self) -> u32 { - self.0 - } - - /// Tokio's weighted semaphore API accepts a `u32` permit count. Keeping - /// this conversion explicit prevents callers from silently truncating a - /// wider configuration value before admission. - pub const fn semaphore_permits(self) -> u32 { - self.0 - } - - pub fn checked_add(self, other: Self) -> Option { - self.0.checked_add(other.0).map(Self) - } - - pub fn checked_sub(self, other: Self) -> Option { - self.0.checked_sub(other.0).map(Self) - } -} - -impl From for u32 { - fn from(units: Units) -> Self { - units.0 - } -} - -impl TryFrom for Units { - type Error = std::num::TryFromIntError; - - fn try_from(value: u64) -> Result { - u32::try_from(value).map(Self) - } -} - -/// A worker's current weighted capacity and work-slot snapshot. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct CapacitySnapshot { - pub total_units: Units, - pub available_units: Units, - pub active_work: u32, - pub max_work: u32, -} - -impl CapacitySnapshot { - pub const fn new(total_units: Units, max_work: u32) -> Self { - Self { - total_units, - available_units: total_units, - active_work: 0, - max_work, - } - } - - /// Returns whether both independent admission guards have room for work. - pub fn can_admit(&self, required_units: Units) -> bool { - self.available_units >= required_units && self.active_work < self.max_work - } - - /// Capacity snapshots are external observations, so reject impossible - /// values before they reach scheduling code. - pub fn validate(&self) -> Result<(), CapacitySnapshotError> { - if self.available_units > self.total_units { - return Err(CapacitySnapshotError::AvailableExceedsTotal); - } - if self.active_work > self.max_work { - return Err(CapacitySnapshotError::ActiveWorkExceedsMaximum); - } - Ok(()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] -pub enum CapacitySnapshotError { - #[error("available Units exceed total Units")] - AvailableExceedsTotal, - #[error("active work exceeds the configured maximum")] - ActiveWorkExceedsMaximum, -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct PodHardwareSnapshot { - pub gpu_count: u32, - pub gpu_memory_total_mb: Option, - pub gpu_memory_free_mb: Option, - pub system_memory_total_mb: Option, -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct QuayHardwareSnapshot { - pub logical_cpu_count: Option, - pub system_memory_total_mb: Option, - pub scratch_total_bytes: Option, - pub scratch_free_bytes: Option, -} - -/// Hard scheduling requirements. This is separate from task execution -/// containment limits such as timeout and memory rlimits. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct ComputeRequirement { - pub units: Units, - pub min_memory_mb: Option, - pub min_gpu_memory_mb: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct PodCapacityConfig { - pub total_units: Units, - pub max_agent_sessions: u32, -} - -impl Default for PodCapacityConfig { - fn default() -> Self { - Self { - total_units: Units::ZERO, - max_agent_sessions: 0, - } - } -} - -impl PodCapacityConfig { - pub const fn new(total_units: Units, max_agent_sessions: u32) -> Self { - Self { - total_units, - max_agent_sessions, - } - } - - pub const fn snapshot(self) -> CapacitySnapshot { - CapacitySnapshot::new(self.total_units, self.max_agent_sessions) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct QuayCapacityConfig { - pub total_units: Units, - pub max_executions: u32, -} - -impl Default for QuayCapacityConfig { - fn default() -> Self { - Self { - total_units: Units::ZERO, - max_executions: 0, - } - } -} - -impl QuayCapacityConfig { - pub const fn new(total_units: Units, max_executions: u32) -> Self { - Self { - total_units, - max_executions, - } - } - - pub const fn snapshot(self) -> CapacitySnapshot { - CapacitySnapshot::new(self.total_units, self.max_executions) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn units_are_transparent_and_checked() { - let units = Units::new(7); - let encoded = serde_json::to_value(units).expect("Units should serialize"); - assert_eq!(encoded, serde_json::json!(7)); - assert_eq!(Units::try_from(7_u64), Ok(units)); - assert!(Units::try_from(u64::from(u32::MAX) + 1).is_err()); - assert_eq!(units.checked_add(Units::new(2)), Some(Units::new(9))); - assert_eq!(Units::new(2).checked_sub(units), None); - } - - #[test] - fn capacity_uses_units_and_work_as_separate_guards() { - let mut snapshot = CapacitySnapshot::new(Units::new(4), 2); - assert!(snapshot.can_admit(Units::new(4))); - snapshot.active_work = 1; - snapshot.available_units = Units::new(0); - assert!(!snapshot.can_admit(Units::new(1))); - snapshot.available_units = Units::new(4); - snapshot.active_work = 2; - assert!(!snapshot.can_admit(Units::ZERO)); - } - - #[test] - fn capacity_snapshot_rejects_inconsistent_observations() { - let snapshot = CapacitySnapshot { - total_units: Units::new(1), - available_units: Units::new(2), - active_work: 0, - max_work: 1, - }; - assert_eq!( - snapshot.validate(), - Err(CapacitySnapshotError::AvailableExceedsTotal) - ); - } -} diff --git a/src/enums.rs b/src/enums.rs index d5b0e7b..48ddcf9 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,25 +1,6 @@ use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum InfrastructureKind { - Pod, - Quay, - Reservoir, -} - -impl fmt::Display for InfrastructureKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let value = match self { - Self::Pod => "pod", - Self::Quay => "quay", - Self::Reservoir => "reservoir", - }; - f.write_str(value) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ToDoMode { diff --git a/src/krill.rs b/src/krill.rs index 0e3988e..9fefb7f 100644 --- a/src/krill.rs +++ b/src/krill.rs @@ -2,7 +2,6 @@ use chrono::{DateTime, Utc}; use uuid::Uuid; use crate::enums::ModelType; -use crate::Units; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum PodProtocolVersion { @@ -17,40 +16,6 @@ impl Default for PodProtocolVersion { pub type KrillId = Uuid; -/// Trusted scheduling metadata for one Krill runtime. -/// -/// This profile is administrator/registry data. It is intentionally separate -/// from `KrillConfig::settings`, which may contain model-specific runtime -/// options and is not a scheduling authority. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct KrillRuntimeProfile { - pub compute_units: Units, - pub min_memory_mb: Option, - pub min_gpu_memory_mb: Option, -} - -impl KrillRuntimeProfile { - pub const fn remote() -> Self { - Self { - compute_units: Units::ZERO, - min_memory_mb: None, - min_gpu_memory_mb: None, - } - } - - pub const fn local( - compute_units: Units, - min_memory_mb: Option, - min_gpu_memory_mb: Option, - ) -> Self { - Self { - compute_units, - min_memory_mb, - min_gpu_memory_mb, - } - } -} - #[derive(Debug, Clone)] pub struct KrillDescriptor { pub id: KrillId, @@ -59,7 +24,6 @@ pub struct KrillDescriptor { pub model_type: ModelType, pub model_path: Option, pub config: KrillConfig, - pub runtime_profile: KrillRuntimeProfile, pub version: String, pub created_at: DateTime, } @@ -73,7 +37,6 @@ impl Default for KrillDescriptor { model_type: ModelType::Llm, model_path: None, config: KrillConfig::default(), - runtime_profile: KrillRuntimeProfile::default(), version: "0.1.0".to_string(), created_at: Utc::now(), } @@ -89,7 +52,6 @@ impl KrillDescriptor { model_type, model_path: None, config: KrillConfig::default(), - runtime_profile: KrillRuntimeProfile::default(), version: "0.1.0".to_string(), created_at: Utc::now(), } @@ -110,11 +72,6 @@ impl KrillDescriptor { self } - pub fn with_runtime_profile(mut self, runtime_profile: KrillRuntimeProfile) -> Self { - self.runtime_profile = runtime_profile; - self - } - pub fn with_version(mut self, version: String) -> Self { self.version = version; self @@ -186,9 +143,4 @@ mod tests { assert_eq!(config.get("temperature").unwrap().as_f64(), Some(0.7)); assert_eq!(config.get("max_tokens").unwrap().as_u64(), Some(2048)); } - - #[test] - fn remote_profile_does_not_consume_pod_units() { - assert_eq!(KrillRuntimeProfile::remote().compute_units, Units::ZERO); - } } diff --git a/src/lib.rs b/src/lib.rs index 1092dd6..0e14a5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ pub use uuid::Uuid; pub mod agents; pub mod ai_response; -pub mod compute; pub mod conclusion; pub mod coral; pub mod data_retention; @@ -15,7 +14,6 @@ pub mod krill; pub mod mutations; pub mod principal; pub mod project; -pub mod rollback; pub mod sandbox; pub mod sync; pub mod task; @@ -26,10 +24,6 @@ pub use agents::{ AgentPolicy, AgentPromptConfig, AgentType, ApprovalMode, PlannedAction, SimulationResult, }; pub use ai_response::{Conversation, Message}; -pub use compute::{ - CapacitySnapshot, CapacitySnapshotError, ComputeRequirement, PodCapacityConfig, - PodHardwareSnapshot, QuayCapacityConfig, QuayHardwareSnapshot, Units, -}; pub use conclusion::{ Artifact, ArtifactType, ConclusionCard, ConclusionMetrics, ConclusionTrigger, }; @@ -39,35 +33,29 @@ pub use data_retention::{ }; pub use enums::*; pub use errors::{AuditOutcome, AuditRecord, FieldError, PublicError, ShoalError, ValidationError}; -pub use krill::{KrillConfig, KrillDescriptor, KrillId, KrillRuntimeProfile, PodProtocolVersion}; +pub use krill::{KrillConfig, KrillDescriptor, KrillId, PodProtocolVersion}; pub use mutations::{ merged_affected_files, normalize_repository_path, FileMutation, PathValidationError, }; pub use principal::{ - sign_quay_execution_credential, sign_service_credential, verify_quay_execution_credential, - verify_service_credential, AuthError, AuthenticatedPrincipal, AuthorizationService, - DiscussionCredentialClaims, DiscussionExecutionPrincipal, DispatchAuthError, - ExecutionPrincipal, GlobalRole, ModelGatewayClaims, ModelGatewayPrincipal, PodPrincipal, - ProjectAction, ProjectRole, QuayExecutionClaims, RequestContext, ReservoirConnectionContext, - ReservoirPrincipal, ReservoirServiceRole, ServiceCredentialClaims, ServiceCredentialError, - TokenValidationResult, QUAY_EXECUTION_AUDIENCE, + AuthError, AuthenticatedPrincipal, AuthorizationService, DispatchAuthError, + DiscussionCredentialClaims, DiscussionExecutionPrincipal, ExecutionPrincipal, GlobalRole, + ModelGatewayClaims, ModelGatewayPrincipal, PodPrincipal, ProjectAction, ProjectRole, + RequestContext, ReservoirConnectionContext, ReservoirPrincipal, ReservoirServiceRole, + TokenValidationResult, }; -pub use project::{Project, ProjectFile, ProjectId, ProjectSettings, ValidationCommand}; -pub use rollback::RollbackReference; -pub use sync::{ChangeSetDescriptor, ChangeSetFile, FileOperationKind}; +pub use project::{Project, ProjectFile, ProjectId, ProjectSettings}; pub use task::{Task, TaskResult}; pub use todo::{Dependency, ToDo}; pub use tools::{ - apply_verified_scope, compute_tools_hash, format_tool_error, format_tool_result, - format_tools_json, is_global_tool, normalize_tool_call, parse_tool_call_blocks, - parse_tool_call_stream, resolve_effective_project, tool_definitions, tool_ids_for_agent_type, - validate_tool_definition, validate_tool_definitions, ApprovalRequirement, CompositionStep, - ContextVisibility, EffectiveProject, ExecutionContext, MarketplaceSearchResults, - ParsedToolCall, SandboxAccess, TestAction, ToolComposition, ToolDefinition, - ToolDefinitionError, ToolDependency, ToolDocumentation, ToolExample, ToolExecutionLocation, - ToolExecutor, ToolListing, ToolParser, ToolPlugin, ToolRegistry, ToolState, ToolTest, - ToolTestResult, ToolTestSuite, ToolTestSuiteResult, ToolVersion, VerifiedToolScope, - GLOBAL_TOOLS, + compute_tools_hash, format_tool_error, format_tool_result, format_tools_json, is_global_tool, + is_reef_proxy_tool, parse_tool_call_blocks, parse_tool_call_stream, tool_definitions, + tool_ids_for_agent_type, apply_verified_scope, normalize_tool_call, resolve_effective_project, + ApprovalRequirement, CompositionStep, ContextVisibility, EffectiveProject, ExecutionContext, + MarketplaceSearchResults, ParsedToolCall, TestAction, ToolComposition, ToolDefinition, + ToolDependency, ToolDocumentation, ToolExample, ToolExecutor, ToolListing, ToolParser, + ToolPlugin, ToolRegistry, ToolState, ToolTest, ToolTestResult, ToolTestSuite, + ToolTestSuiteResult, ToolVersion, VerifiedToolScope, GLOBAL_TOOLS, REEF_PROXY_TOOLS, }; #[cfg(test)] diff --git a/src/principal.rs b/src/principal.rs index 9444eca..60064e5 100644 --- a/src/principal.rs +++ b/src/principal.rs @@ -190,14 +190,6 @@ pub enum ReservoirPrincipal { service_id: Uuid, role: ReservoirServiceRole, }, - /// A short-lived Reef-issued service capability. Unlike the legacy - /// `Service` variant this principal carries the operation and project - /// scope that Reservoir must enforce for every request. - ScopedService { - service_id: Uuid, - role: ReservoirServiceRole, - claims: ServiceCredentialClaims, - }, } /// Explicit infrastructure roles accepted by Reservoir. Adding a new service @@ -206,16 +198,6 @@ pub enum ReservoirPrincipal { #[serde(rename_all = "snake_case")] pub enum ReservoirServiceRole { ReefStorage, - QuayWorkspace, -} - -impl ReservoirServiceRole { - pub const fn as_str(self) -> &'static str { - match self { - Self::ReefStorage => "reef_storage", - Self::QuayWorkspace => "quay_workspace", - } - } } /// Authoritative result returned when Reef validates a user session token. @@ -391,20 +373,6 @@ pub enum CredentialError { AttemptInactive, /// The requested tool is not in the credential's allow-list. ToolNotAllowed, - /// The credential was issued for another service or control-plane. - AudienceMismatch, - /// The credential was issued by an unexpected authority. - IssuerMismatch, - /// The credential does not grant the requested operation or project. - ScopeDenied, - /// A Quay authorization does not match the durable execution request. - ExecutionMismatch, - /// A Quay authorization does not match the leased workspace. - WorkspaceMismatch, - /// A Quay authorization does not match the trusted tool metadata. - ToolMismatch, - /// A Quay authorization does not match the trusted Unit cost. - UnitsMismatch, } impl std::fmt::Display for CredentialError { @@ -417,241 +385,12 @@ impl std::fmt::Display for CredentialError { Self::PodMismatch => write!(f, "credential not valid for this pod"), Self::AttemptInactive => write!(f, "attempt inactive"), Self::ToolNotAllowed => write!(f, "tool not in credential allow-list"), - Self::AudienceMismatch => write!(f, "credential audience mismatch"), - Self::IssuerMismatch => write!(f, "credential issuer mismatch"), - Self::ScopeDenied => write!(f, "credential scope denied"), - Self::ExecutionMismatch => write!(f, "credential execution mismatch"), - Self::WorkspaceMismatch => write!(f, "credential workspace mismatch"), - Self::ToolMismatch => write!(f, "credential tool mismatch"), - Self::UnitsMismatch => write!(f, "credential Unit requirement mismatch"), } } } impl std::error::Error for CredentialError {} -/// The audience used by Reef-issued execution authorizations delivered to a -/// Quay. Keeping this value in the shared contract prevents a token minted -/// for one service from being accepted by another worker. -pub const QUAY_EXECUTION_AUDIENCE: &str = "quay"; - -/// Claims for exactly one authorized Quay execution. -/// -/// These claims are deliberately narrower than the Pod execution credential. -/// Reef performs user/agent authorization first, then mints this capability -/// for the selected Quay. The Quay must not derive any of these values from -/// model-supplied arguments. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct QuayExecutionClaims { - pub execution_id: Uuid, - pub workspace_lease_id: Uuid, - pub project_id: Uuid, - pub quay_id: Uuid, - pub workspace_id: Uuid, - pub tool_id: String, - pub compute_units: crate::Units, - pub credential_id: Uuid, - pub expires_at: DateTime, -} - -impl QuayExecutionClaims { - /// Validate the parts of a request that are known by the Quay at - /// execution time. The signature, audience, issuer, and expiry are - /// checked by `verify_quay_execution_credential`. - pub fn validate_request( - &self, - execution_id: Uuid, - workspace_lease_id: Uuid, - project_id: Uuid, - quay_id: Uuid, - workspace_id: Uuid, - tool_id: &str, - compute_units: crate::Units, - ) -> Result<(), CredentialError> { - if self.execution_id != execution_id { - return Err(CredentialError::ExecutionMismatch); - } - if self.workspace_lease_id != workspace_lease_id || self.workspace_id != workspace_id { - return Err(CredentialError::WorkspaceMismatch); - } - if self.project_id != project_id || self.quay_id != quay_id { - return Err(CredentialError::ScopeDenied); - } - if self.tool_id != tool_id { - return Err(CredentialError::ToolMismatch); - } - if self.compute_units != compute_units { - return Err(CredentialError::UnitsMismatch); - } - Ok(()) - } -} - -/// A Reef-to-service capability. `operations` contains canonical operation -/// names, never free-form request text. `project_id == None` is reserved for -/// explicitly project-independent service operations. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ServiceCredentialClaims { - pub issuer: String, - pub audience: String, - pub service_role: String, - pub project_id: Option, - pub operations: Vec, - pub issued_at: DateTime, - pub expires_at: DateTime, - pub credential_id: Uuid, -} - -impl ServiceCredentialClaims { - pub fn allows(&self, operation: &str, project_id: Option, now: DateTime) -> bool { - now >= self.issued_at - && now <= self.expires_at - && self.operations.iter().any(|allowed| allowed == operation) - && (self.project_id.is_none() || self.project_id == project_id) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum ServiceCredentialError { - #[error("service credential payload could not be serialized")] - Serialization, - #[error("service credential is malformed")] - Malformed, - #[error("service credential signature is invalid")] - InvalidSignature, - #[error("service credential is expired")] - Expired, - #[error("service credential issuer mismatch")] - IssuerMismatch, - #[error("service credential audience mismatch")] - AudienceMismatch, - #[error("service credential scope denied")] - ScopeDenied, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct QuayExecutionEnvelope { - issuer: String, - audience: String, - claims: QuayExecutionClaims, - #[serde(default)] - signature: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ServiceCredentialEnvelope { - claims: ServiceCredentialClaims, - signature: String, -} - -/// Sign a Quay execution capability with Reef's control-plane Ed25519 key. -/// The returned value is an opaque `base64url(payload).base64url(signature)` -/// token suitable for transport in an MTP response. -pub fn sign_quay_execution_credential( - claims: &QuayExecutionClaims, - issuer: &str, - signing_key: &[u8; 32], -) -> Result { - let payload = serde_json::json!({ - "issuer": issuer, - "audience": QUAY_EXECUTION_AUDIENCE, - "claims": claims, - }); - sign_json_payload(&payload, signing_key) -} - -/// Verify a Quay execution capability and its service audience/issuer. -pub fn verify_quay_execution_credential( - credential: &str, - verifying_key: &[u8; 32], - expected_issuer: &str, -) -> Result { - let payload: QuayExecutionEnvelope = verify_json_payload(credential, verifying_key)?; - if payload.issuer != expected_issuer { - return Err(ServiceCredentialError::IssuerMismatch); - } - if payload.audience != QUAY_EXECUTION_AUDIENCE { - return Err(ServiceCredentialError::AudienceMismatch); - } - if Utc::now() > payload.claims.expires_at { - return Err(ServiceCredentialError::Expired); - } - Ok(payload.claims) -} - -/// Sign a scoped Reef service credential for Reservoir or another service. -pub fn sign_service_credential( - claims: &ServiceCredentialClaims, - signing_key: &[u8; 32], -) -> Result { - let envelope = ServiceCredentialEnvelope { - claims: claims.clone(), - signature: String::new(), - }; - let payload = - serde_json::to_value(envelope).map_err(|_| ServiceCredentialError::Serialization)?; - sign_json_payload(&payload, signing_key) -} - -/// Verify a scoped service credential and its intended service audience. -pub fn verify_service_credential( - credential: &str, - verifying_key: &[u8; 32], - expected_issuer: &str, - expected_audience: &str, -) -> Result { - let envelope: ServiceCredentialEnvelope = verify_json_payload(credential, verifying_key)?; - if envelope.claims.issuer != expected_issuer { - return Err(ServiceCredentialError::IssuerMismatch); - } - if envelope.claims.audience != expected_audience { - return Err(ServiceCredentialError::AudienceMismatch); - } - if Utc::now() > envelope.claims.expires_at { - return Err(ServiceCredentialError::Expired); - } - Ok(envelope.claims) -} - -fn sign_json_payload( - value: &serde_json::Value, - signing_key: &[u8; 32], -) -> Result { - use base64::Engine; - use ed25519_dalek::{Signer, SigningKey}; - - let bytes = serde_json::to_vec(value).map_err(|_| ServiceCredentialError::Serialization)?; - let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes); - let signature = SigningKey::from_bytes(signing_key).sign(payload.as_bytes()); - let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature.to_bytes()); - Ok(format!("{payload}.{signature}")) -} - -fn verify_json_payload Deserialize<'de>>( - credential: &str, - verifying_key: &[u8; 32], -) -> Result { - use base64::Engine; - use ed25519_dalek::{Signature, Verifier, VerifyingKey}; - - let (payload, encoded_signature) = credential - .split_once('.') - .ok_or(ServiceCredentialError::Malformed)?; - let signature_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(encoded_signature) - .map_err(|_| ServiceCredentialError::Malformed)?; - let signature = - Signature::from_slice(&signature_bytes).map_err(|_| ServiceCredentialError::Malformed)?; - let key = - VerifyingKey::from_bytes(verifying_key).map_err(|_| ServiceCredentialError::Malformed)?; - key.verify(payload.as_bytes(), &signature) - .map_err(|_| ServiceCredentialError::InvalidSignature)?; - let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(payload) - .map_err(|_| ServiceCredentialError::Malformed)?; - serde_json::from_slice(&bytes).map_err(|_| ServiceCredentialError::Malformed) -} - /// The verifiable claims inside a signed execution credential. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CredentialClaims { @@ -1781,83 +1520,4 @@ mod tests { let claims = verify_model_gateway_signature(&credential, &secret).unwrap(); assert!(claims.allowed_models.is_empty()); } - - #[test] - fn quay_execution_credential_is_narrow_and_signed() { - let key = [7_u8; 32]; - let verifying_key = ed25519_dalek::SigningKey::from_bytes(&key) - .verifying_key() - .to_bytes(); - let claims = QuayExecutionClaims { - execution_id: Uuid::new_v4(), - workspace_lease_id: Uuid::new_v4(), - project_id: Uuid::new_v4(), - quay_id: Uuid::new_v4(), - workspace_id: Uuid::new_v4(), - tool_id: "bash".to_string(), - compute_units: crate::Units::new(2), - credential_id: Uuid::new_v4(), - expires_at: Utc::now() + chrono::Duration::minutes(5), - }; - let credential = sign_quay_execution_credential(&claims, "reef-test", &key) - .expect("Quay credential should sign"); - let verified = verify_quay_execution_credential(&credential, &verifying_key, "reef-test") - .expect("Quay credential should verify"); - assert_eq!(verified, claims); - claims - .validate_request( - claims.execution_id, - claims.workspace_lease_id, - claims.project_id, - claims.quay_id, - claims.workspace_id, - "bash", - crate::Units::new(2), - ) - .expect("matching request should be accepted"); - assert_eq!( - claims.validate_request( - claims.execution_id, - claims.workspace_lease_id, - claims.project_id, - claims.quay_id, - claims.workspace_id, - "read_file", - crate::Units::new(2), - ), - Err(CredentialError::ToolMismatch) - ); - } - - #[test] - fn service_credential_enforces_audience_and_operation_scope() { - let key = [9_u8; 32]; - let verifying_key = ed25519_dalek::SigningKey::from_bytes(&key) - .verifying_key() - .to_bytes(); - let project_id = Uuid::new_v4(); - let claims = ServiceCredentialClaims { - issuer: "reef-test".to_string(), - audience: "reservoir".to_string(), - service_role: "quay_workspace".to_string(), - project_id: Some(project_id), - operations: vec!["get_project_head".to_string()], - issued_at: Utc::now(), - expires_at: Utc::now() + chrono::Duration::minutes(5), - credential_id: Uuid::new_v4(), - }; - let credential = - sign_service_credential(&claims, &key).expect("service credential should sign"); - let verified = - verify_service_credential(&credential, &verifying_key, "reef-test", "reservoir") - .expect("service credential should verify"); - assert!(verified.allows("get_project_head", Some(project_id), Utc::now())); - assert!(!verified.allows("create_candidate", Some(project_id), Utc::now())); - assert!(!verified.allows("get_project_head", Some(Uuid::new_v4()), Utc::now())); - assert_eq!( - verify_service_credential(&credential, &verifying_key, "reef-test", "other-service",) - .unwrap_err(), - ServiceCredentialError::AudienceMismatch - ); - } } diff --git a/src/project.rs b/src/project.rs index 83b009d..bcb6c6b 100644 --- a/src/project.rs +++ b/src/project.rs @@ -2,8 +2,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::enums::CompletionPolicy; - pub type ProjectId = Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -114,25 +112,10 @@ pub struct ProjectSettings { pub run_command: Option, /// Conventional subject/body template used for generated git commits. pub git_commit_template: Option, - /// Publication policy for candidate changesets. - #[serde(default)] - pub completion_policy: CompletionPolicy, - /// Whether a candidate must run the configured deterministic checks. - #[serde(default)] - pub validation_required: bool, - /// Commands run against the exact candidate version before publication. - #[serde(default)] - pub validation_commands: Vec, #[serde(flatten)] pub extra: serde_json::Value, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ValidationCommand { - pub name: String, - pub command: String, -} - impl ProjectSettings { pub fn new() -> Self { Self { @@ -140,9 +123,6 @@ impl ProjectSettings { build_command: None, run_command: None, git_commit_template: None, - completion_policy: CompletionPolicy::default(), - validation_required: false, - validation_commands: Vec::new(), extra: serde_json::json!({}), } } @@ -167,63 +147,11 @@ impl ProjectSettings { self } - pub fn with_completion_policy(mut self, policy: CompletionPolicy) -> Self { - self.completion_policy = policy; - self - } - - pub fn with_validation(mut self, required: bool, commands: Vec) -> Self { - self.validation_required = required; - self.validation_commands = commands; - self - } - pub fn set_extra(&mut self, key: &str, value: serde_json::Value) { - if !self.extra.is_object() { - self.extra = serde_json::json!({}); - } if let Some(obj) = self.extra.as_object_mut() { obj.insert(key.to_string(), value); } } - - pub fn review_state_for(&self, has_mutations: bool) -> &'static str { - if !has_mutations { - return "not_required"; - } - match self.completion_policy { - CompletionPolicy::AutoApply | CompletionPolicy::DocumentationOnly => "not_required", - CompletionPolicy::ApprovalRequired | CompletionPolicy::DryRun => "pending", - } - } - - pub fn validation_state(&self, has_mutations: bool) -> &'static str { - if !has_mutations || !self.validation_required { - return "not_required"; - } - if self.validation_commands.is_empty() - || self - .validation_commands - .iter() - .any(|command| command.name.trim().is_empty() || command.command.trim().is_empty()) - { - "configuration_error" - } else { - "pending" - } - } - - pub fn is_publishable( - &self, - review_state: &str, - validation_state: &str, - publication_state: &str, - ) -> bool { - self.completion_policy != CompletionPolicy::DryRun - && matches!(review_state, "approved" | "not_required") - && matches!(validation_state, "passed" | "not_required") - && matches!(publication_state, "candidate" | "ready") - } } #[cfg(test)] @@ -272,49 +200,4 @@ mod tests { assert_eq!(settings.build_command, Some("cargo build".to_string())); assert_eq!(settings.run_command, Some("cargo run".to_string())); } - - #[test] - fn test_project_publication_settings_roundtrip() { - let settings = ProjectSettings::new() - .with_completion_policy(CompletionPolicy::AutoApply) - .with_validation( - true, - vec![ValidationCommand { - name: "build".to_string(), - command: "cargo check".to_string(), - }], - ); - let encoded = serde_json::to_string(&settings).expect("settings serialize"); - let decoded: ProjectSettings = - serde_json::from_str(&encoded).expect("settings deserialize"); - assert_eq!(decoded.completion_policy, CompletionPolicy::AutoApply); - assert!(decoded.validation_required); - assert_eq!(decoded.validation_commands[0].command, "cargo check"); - } - - #[test] - fn required_validation_without_commands_is_configuration_error() { - let settings = ProjectSettings::new().with_validation(true, Vec::new()); - assert_eq!(settings.validation_state(true), "configuration_error"); - assert!(!settings.is_publishable("not_required", "configuration_error", "candidate")); - } - - #[test] - fn dry_run_never_becomes_publishable() { - let settings = ProjectSettings::new().with_completion_policy(CompletionPolicy::DryRun); - assert_eq!(settings.review_state_for(true), "pending"); - assert!(!settings.is_publishable("approved", "not_required", "candidate")); - } - - #[test] - fn blank_required_validation_command_is_configuration_error() { - let settings = ProjectSettings::new().with_validation( - true, - vec![ValidationCommand { - name: "check".to_string(), - command: " ".to_string(), - }], - ); - assert_eq!(settings.validation_state(true), "configuration_error"); - } } diff --git a/src/rollback.rs b/src/rollback.rs deleted file mode 100644 index dbbbe42..0000000 --- a/src/rollback.rs +++ /dev/null @@ -1,50 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// Opaque reference to a pre-mutation snapshot owned by the workspace -/// executor. Reef stores this reference as workflow metadata; it must never -/// contain a host path or the snapshot bytes. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct RollbackReference { - pub reference_id: Uuid, - pub execution_id: Uuid, - pub workspace_lease_id: Uuid, - pub workspace_id: Uuid, - pub project_id: Uuid, - pub expires_at: DateTime, -} - -impl RollbackReference { - pub fn is_expired(&self, now: DateTime) -> bool { - self.expires_at <= now - } - - pub fn validates_execution(&self, execution_id: Uuid) -> bool { - self.execution_id == execution_id && self.reference_id != Uuid::nil() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn reference() -> RollbackReference { - RollbackReference { - reference_id: Uuid::new_v4(), - execution_id: Uuid::new_v4(), - workspace_lease_id: Uuid::new_v4(), - workspace_id: Uuid::new_v4(), - project_id: Uuid::new_v4(), - expires_at: Utc::now() + chrono::Duration::minutes(1), - } - } - - #[test] - fn reference_is_opaque_and_execution_bound() { - let reference = reference(); - assert!(reference.validates_execution(reference.execution_id)); - assert!(!reference.validates_execution(Uuid::new_v4())); - assert!(!reference.is_expired(Utc::now())); - } -} diff --git a/src/sync.rs b/src/sync.rs index a8845e4..2083fea 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,48 +1,4 @@ use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// The operation applied to one repository-relative regular file. -/// -/// This is deliberately shared by Pod, Reservoir, Reef, and Dock. Git object -/// IDs identify stored objects; the hashes on this protocol model identify -/// file contents and are always SHA-256. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum FileOperationKind { - Create, - Modify, - Delete, -} - -impl FileOperationKind { - pub fn as_str(self) -> &'static str { - match self { - Self::Create => "create", - Self::Modify => "modify", - Self::Delete => "delete", - } - } -} - -/// One file in an immutable candidate changeset. Content is fetched from -/// Reservoir at the candidate version instead of being embedded here. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ChangeSetFile { - pub path: String, - pub operation: FileOperationKind, - pub base_sha256: Option, - pub result_sha256: Option, -} - -/// Metadata shared by candidate registration, review, and synchronization. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ChangeSetDescriptor { - pub id: Uuid, - pub project_id: Uuid, - pub base_version: Option, - pub candidate_version: String, - pub files: Vec, -} #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ConflictResolution { @@ -75,43 +31,6 @@ impl ResolutionStrategy { } } -#[cfg(test)] -mod changeset_tests { - use super::*; - - #[test] - fn changeset_file_uses_wire_operation_names() { - let file = ChangeSetFile { - path: "src/lib.rs".to_string(), - operation: FileOperationKind::Modify, - base_sha256: Some("base".to_string()), - result_sha256: Some("result".to_string()), - }; - let json = serde_json::to_value(file).expect("changeset file serializes"); - assert_eq!(json["operation"], "modify"); - } - - #[test] - fn changeset_descriptor_round_trips() { - let descriptor = ChangeSetDescriptor { - id: Uuid::new_v4(), - project_id: Uuid::new_v4(), - base_version: None, - candidate_version: "candidate".to_string(), - files: vec![ChangeSetFile { - path: "new.txt".to_string(), - operation: FileOperationKind::Create, - base_sha256: None, - result_sha256: Some("sha256".to_string()), - }], - }; - let encoded = serde_json::to_string(&descriptor).expect("descriptor serializes"); - let decoded: ChangeSetDescriptor = - serde_json::from_str(&encoded).expect("descriptor deserializes"); - assert_eq!(decoded, descriptor); - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileConflict { pub file: String, diff --git a/src/tools.rs b/src/tools.rs index 720e780..0fee8a7 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{principal::ProjectAction, Units}; +use crate::principal::ProjectAction; /// Authoritative execution ownership for every tool. /// Each tool declares exactly one execution location. Connector-krill uses this @@ -10,78 +10,19 @@ use crate::{principal::ProjectAction, Units}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ToolExecutionLocation { - /// The tool has not been resolved against the canonical registry. - /// This is intentionally the deserialization default: callers must reject - /// it instead of silently executing a legacy or incomplete definition. - Unknown, /// Tool executes exclusively on Reef (the authorization boundary). /// Connector must not locally execute after Reef rejection/failure. Reef, - /// Tool executes on Quay inside the leased project workspace. - Quay, /// Tool executes locally on the connector (e.g. web_search, web_fetch). Local, } impl Default for ToolExecutionLocation { fn default() -> Self { - Self::Unknown + Self::Local } } -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum ToolDefinitionError { - #[error("tool '{tool_id}' has an unknown execution location")] - UnknownExecutionLocation { tool_id: String }, - #[error("sandbox tool '{tool_id}' must run on Quay")] - SandboxToolMustRunOnQuay { tool_id: String }, - #[error("Quay tool '{tool_id}' must declare sandbox access")] - QuayToolRequiresSandbox { tool_id: String }, -} - -/// Validate the independent ownership and workspace axes of one canonical -/// tool definition. Callers should run this over the complete registry at -/// startup and fail closed if any definition is invalid. -pub fn validate_tool_definition(tool: &ToolDefinition) -> Result<(), ToolDefinitionError> { - if tool.execution_location == ToolExecutionLocation::Unknown { - return Err(ToolDefinitionError::UnknownExecutionLocation { - tool_id: tool.id.clone(), - }); - } - - if tool.sandbox_access != SandboxAccess::None - && tool.execution_location != ToolExecutionLocation::Quay - { - return Err(ToolDefinitionError::SandboxToolMustRunOnQuay { - tool_id: tool.id.clone(), - }); - } - - if tool.execution_location == ToolExecutionLocation::Quay - && tool.sandbox_access == SandboxAccess::None - { - return Err(ToolDefinitionError::QuayToolRequiresSandbox { - tool_id: tool.id.clone(), - }); - } - - Ok(()) -} - -pub fn validate_tool_definitions(tools: &[ToolDefinition]) -> Result<(), ToolDefinitionError> { - tools.iter().try_for_each(validate_tool_definition) -} - -/* Whether a tool needs a leased Quay workspace, and whether it may mutate it. */ -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "snake_case")] -pub enum SandboxAccess { - #[default] - None, - ReadOnly, - ReadWrite, -} - /* Context in which a tool should be available */ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -213,11 +154,17 @@ pub fn apply_verified_scope( } if let Some(todo_id) = scope.todo_id { - args.insert("todo_id".into(), serde_json::json!(todo_id.to_string())); + args.insert( + "todo_id".into(), + serde_json::json!(todo_id.to_string()), + ); } if let Some(pod_id) = scope.pod_id { - args.insert("pod_id".into(), serde_json::json!(pod_id.to_string())); + args.insert( + "pod_id".into(), + serde_json::json!(pod_id.to_string()), + ); } } @@ -300,7 +247,10 @@ pub enum EffectiveProject { /// No project source supplied — valid for non-project-scoped tools. None, /// Two or more distinct project UUIDs were supplied and conflict. - Conflicting { first: Uuid, second: Uuid }, + Conflicting { + first: Uuid, + second: Uuid, + }, } /// Resolve a single effective project from the three main sources available @@ -439,16 +389,12 @@ pub struct ToolDefinition { 1+ = only visible to agents with max_depth >= this value. */ #[serde(default)] pub required_depth: u32, - /* Authoritative execution ownership. Project workspace tools declare Quay; - orchestration tools declare Reef; connector-global tools declare Local. */ + /* Authoritative execution ownership. Every privileged or project-changing + tool declares Reef; tools that execute only locally declare Local. + Connector-krill must never locally execute a Reef-owned tool after + any Reef failure (rejection, auth failure, timeout, connection error). */ #[serde(default)] pub execution_location: ToolExecutionLocation, - /// Whether this tool requires and may mutate a Quay workspace. - #[serde(default)] - pub sandbox_access: SandboxAccess, - /// Trusted Quay admission weight for the canonical operation. - #[serde(default)] - pub compute_units: Units, } const fn default_timeout_ms() -> u64 { @@ -474,8 +420,6 @@ impl Default for ToolDefinition { context_visibility: ContextVisibility::default(), required_depth: 0, execution_location: ToolExecutionLocation::default(), - sandbox_access: SandboxAccess::default(), - compute_units: Units::ZERO, } } } @@ -1030,8 +974,6 @@ macro_rules! tool { context_visibility: ContextVisibility::default(), required_depth: 0, execution_location: ToolExecutionLocation::default(), - sandbox_access: SandboxAccess::default(), - compute_units: Units::ZERO, } }; ($name:expr, $desc:expr, $cat:expr, $dangerous:expr, $approval:expr, $scoped:expr, $params:expr, $timeout:expr) => { @@ -1052,8 +994,6 @@ macro_rules! tool { context_visibility: ContextVisibility::default(), required_depth: 0, execution_location: ToolExecutionLocation::default(), - sandbox_access: SandboxAccess::default(), - compute_units: Units::ZERO, } }; } @@ -1258,6 +1198,34 @@ pub const AGENT_ONLY_TOOLS: &[&str] = &[ "kanban_remove_task", ]; +/* Tools that proxy to the Reef API when a `REEF_URL` / `REEF_API_URL` is +configured. These tools have both a local implementation and a Reef-backed +implementation (via Reservoir). When Reef is available the Reef path is +preferred so that file operations go through the Reservoir-backed sandbox; +when Reef is unreachable the local fallback executes instead. + +This is the Krill → Reef proxy set, not the full set of Reef-only tools. +Tools like kanban_*, document_*, and strategic items are always executed on +Reef via MTP and are NOT in this list — they have no local implementation. + +DEPRECATED: Use `ToolDefinition::execution_location` instead. This list is +retained only for backward compatibility and will be removed in a future release. */ +#[deprecated(note = "Use ToolDefinition::execution_location instead")] +pub const REEF_PROXY_TOOLS: &[&str] = &[ + "read_file", + "file_read", + "write_file", + "file_write", + "edit_file", + "list_directory", + "search_files", + "grep", + "bash", + "execute", + "delete_workspace", + "list_files", +]; + /* Tools that operate without a project scope (global). These are always executed locally on the Krill and never need project_id injection. */ pub const GLOBAL_TOOLS: &[&str] = &["web_search", "web_fetch", "web_api"]; @@ -1267,6 +1235,14 @@ pub fn is_global_tool(name: &str) -> bool { GLOBAL_TOOLS.contains(&name) } +/* Returns true if the tool name should proxy to Reef when Reef is available. + +DEPRECATED: Use `ToolDefinition::execution_location` instead. */ +#[deprecated(note = "Use ToolDefinition::execution_location instead")] +pub fn is_reef_proxy_tool(name: &str) -> bool { + REEF_PROXY_TOOLS.contains(&name) +} + /* Returns all built-in tool definitions across all categories. */ pub fn tool_definitions() -> Vec { let mut tools = vec![ @@ -2267,33 +2243,6 @@ pub fn tool_definitions() -> Vec { per-tool metadata. A project-scoped tool without an action causes authorization to fail closed. */ let action_map: std::collections::HashMap<&str, ProjectAction> = [ - // Project filesystem, execution, git and build tools. These must not - // inherit the non-project Read fallback: the project membership check - // is part of their security boundary. - ("read_file", ProjectAction::Read), - ("file_read", ProjectAction::Read), - ("list_directory", ProjectAction::Read), - ("search_files", ProjectAction::Read), - ("grep", ProjectAction::Read), - ("list_files", ProjectAction::Read), - ("git_status", ProjectAction::Read), - ("git_diff", ProjectAction::Read), - ("git_log", ProjectAction::Read), - ("git_branch", ProjectAction::Read), - ("workspace_info", ProjectAction::Read), - ("write_file", ProjectAction::ModifyFiles), - ("file_write", ProjectAction::ModifyFiles), - ("edit_file", ProjectAction::ModifyFiles), - ("bash", ProjectAction::ModifyFiles), - ("execute", ProjectAction::ModifyFiles), - ("delete_workspace", ProjectAction::ModifyFiles), - ("cargo_check", ProjectAction::ModifyFiles), - ("npm_build", ProjectAction::ModifyFiles), - ("python_check", ProjectAction::ModifyFiles), - ("test_runner", ProjectAction::ModifyFiles), - ("create_workspace", ProjectAction::ModifyFiles), - ("create_venv", ProjectAction::ModifyFiles), - ("install_dependencies", ProjectAction::ModifyFiles), // Kanban read ("kanban_list_board", ProjectAction::Read), // Kanban todo mutations @@ -2321,6 +2270,7 @@ pub fn tool_definitions() -> Vec { // Documentation write ("store_file_doc", ProjectAction::ModifyFiles), // File tools + ("list_files", ProjectAction::Read), // Strategic tools (read-only analysis) ("propose_strategic_item", ProjectAction::CreateTodo), ("split_task", ProjectAction::ModifyFiles), @@ -2336,16 +2286,6 @@ pub fn tool_definitions() -> Vec { .cloned() .collect(); - // Keep the scope decision beside the action decision. A tool with a - // project action is necessarily project-scoped; this prevents a newly - // added filesystem/build tool from accidentally being authorized as a - // global read operation. - for tool in tools.iter_mut() { - if action_map.contains_key(tool.id.as_str()) { - tool.project_scoped = true; - } - } - for tool in tools.iter_mut() { if tool.project_scoped { if let Some(&action) = action_map.get(tool.id.as_str()) { @@ -2432,9 +2372,11 @@ pub fn tool_definitions() -> Vec { } } - /* Assign execution ownership and the independent workspace/capacity - metadata for every canonical tool. */ - let quay_tools: std::collections::HashSet<&str> = [ + /* Assign execution_location for every tool. + Reef-owned tools must never fall back to local execution. This metadata + replaces the old REEF_PROXY_TOOLS list and name-prefix routing. */ + let reef_tools: std::collections::HashSet<&str> = [ + // Filesystem (Reef-backed via Reservoir) "read_file", "file_read", "write_file", @@ -2444,27 +2386,25 @@ pub fn tool_definitions() -> Vec { "search_files", "grep", "list_files", + // Execution (Reef-backed) "bash", "execute", + "delete_workspace", + // Git (Reef-backed) "git_status", "git_diff", "git_log", "git_branch", + // Build (Reef-backed) "cargo_check", "npm_build", "python_check", "test_runner", + // Workspace (Reef-backed) "create_workspace", - "delete_workspace", "create_venv", "install_dependencies", "workspace_info", - ] - .iter() - .copied() - .collect(); - - let reef_tools: std::collections::HashSet<&str> = [ // Kanban (always Reef — no valid local fallback) "kanban_list_board", "kanban_create_todo", @@ -2506,34 +2446,13 @@ pub fn tool_definitions() -> Vec { .collect(); for tool in tools.iter_mut() { - if quay_tools.contains(tool.id.as_str()) { - tool.execution_location = ToolExecutionLocation::Quay; - tool.sandbox_access = match tool.id.as_str() { - "read_file" | "file_read" | "list_directory" | "search_files" | "grep" - | "list_files" | "git_status" | "git_diff" | "git_log" | "git_branch" - | "workspace_info" => SandboxAccess::ReadOnly, - _ => SandboxAccess::ReadWrite, - }; - // The cost is trusted registry metadata, never a model argument. - tool.compute_units = match tool.id.as_str() { - "cargo_check" | "npm_build" | "python_check" | "test_runner" => Units::new(2), - _ => Units::new(1), - }; - } else if reef_tools.contains(tool.id.as_str()) { + if reef_tools.contains(tool.id.as_str()) { tool.execution_location = ToolExecutionLocation::Reef; - tool.sandbox_access = SandboxAccess::None; - tool.compute_units = Units::ZERO; } else { tool.execution_location = ToolExecutionLocation::Local; - tool.sandbox_access = SandboxAccess::None; - tool.compute_units = Units::ZERO; } } - if let Err(error) = validate_tool_definitions(&tools) { - panic!("invalid canonical tool definition: {error}"); - } - tools } @@ -3415,11 +3334,10 @@ Done with tools. fn every_tool_has_declared_execution_location() { let tools = tool_definitions(); for tool in &tools { - // Unknown is reserved for incomplete/deserialized definitions and - // must never be present in the canonical registry. + // Every tool must have either Reef or Local — the enum guarantees this, + // but we verify the field is present and meaningful. assert!( tool.execution_location == ToolExecutionLocation::Reef - || tool.execution_location == ToolExecutionLocation::Quay || tool.execution_location == ToolExecutionLocation::Local, "tool '{}' has unclassified execution_location", tool.id @@ -3428,18 +3346,18 @@ Done with tools. } #[test] - fn quay_owned_workspace_tools_are_classified() { + fn reef_owned_mutation_tools_are_classified() { let tools = tool_definitions(); - let quay_tools: Vec<&str> = tools + let reef_tools: Vec<&str> = tools .iter() - .filter(|t| t.execution_location == ToolExecutionLocation::Quay) + .filter(|t| t.execution_location == ToolExecutionLocation::Reef) .map(|t| t.id.as_str()) .collect(); - // Workspace mutation tools must be Quay-owned. + // Privileged mutation tools must be Reef-owned for name in &["write_file", "edit_file", "bash", "delete_workspace"] { assert!( - quay_tools.contains(name), - "workspace tool '{}' must be Quay-owned", + reef_tools.contains(name), + "privileged tool '{}' must be Reef-owned", name ); } @@ -3459,33 +3377,6 @@ Done with tools. } } - #[test] - fn project_execution_tools_have_explicit_actions() { - let tools = tool_definitions(); - for name in [ - "read_file", - "write_file", - "edit_file", - "bash", - "git_status", - "cargo_check", - "npm_build", - "python_check", - "test_runner", - "create_workspace", - "create_venv", - "install_dependencies", - "workspace_info", - ] { - let tool = tools.iter().find(|tool| tool.id == name).unwrap(); - assert!(tool.project_scoped, "{name} must be project-scoped"); - assert!( - tool.required_project_action.is_some(), - "{name} must declare a project action" - ); - } - } - #[test] fn no_tool_is_both_reef_and_local() { let tools = tool_definitions(); @@ -3494,7 +3385,6 @@ Done with tools. // the field is consistently set. assert!( tool.execution_location == ToolExecutionLocation::Reef - || tool.execution_location == ToolExecutionLocation::Quay || tool.execution_location == ToolExecutionLocation::Local, "tool '{}' has invalid execution_location", tool.id @@ -3547,36 +3437,6 @@ Done with tools. } } - #[test] - fn canonical_tool_sandbox_metadata_is_valid() { - let tools = tool_definitions(); - validate_tool_definitions(&tools).expect("canonical tool metadata must be valid"); - - for tool in tools { - if tool.execution_location == ToolExecutionLocation::Quay { - assert_ne!(tool.sandbox_access, SandboxAccess::None); - assert!(tool.compute_units > Units::ZERO); - } else { - assert_eq!(tool.sandbox_access, SandboxAccess::None); - assert_eq!(tool.compute_units, Units::ZERO); - } - } - } - - #[test] - fn sandbox_validation_rejects_wrong_owner() { - let tool = ToolDefinition { - id: "unsafe_reef_tool".to_string(), - execution_location: ToolExecutionLocation::Reef, - sandbox_access: SandboxAccess::ReadOnly, - ..Default::default() - }; - assert!(matches!( - validate_tool_definition(&tool), - Err(ToolDefinitionError::SandboxToolMustRunOnQuay { .. }) - )); - } - #[test] fn apply_verified_scope_normalizes_agent_run_id() { let run_id = Uuid::new_v4(); @@ -3586,8 +3446,14 @@ Done with tools. }; let mut args = serde_json::Map::new(); // Client supplies a conflicting agent_run_id - args.insert("agent_run_id".into(), serde_json::json!("spoofed-value")); - args.insert("run_id".into(), serde_json::json!("spoofed-run")); + args.insert( + "agent_run_id".into(), + serde_json::json!("spoofed-value"), + ); + args.insert( + "run_id".into(), + serde_json::json!("spoofed-run"), + ); apply_verified_scope(&mut args, &scope); @@ -3610,7 +3476,10 @@ Done with tools. ..Default::default() }; let mut args = serde_json::Map::new(); - args.insert("project_id".into(), serde_json::json!("spoofed-project")); + args.insert( + "project_id".into(), + serde_json::json!("spoofed-project"), + ); apply_verified_scope(&mut args, &scope);