Compare commits

...
Author SHA1 Message Date
Alex Emmet
3faa1e5712
Quay, splits, QoL 2026-08-23 22:38:12 +02:00
Alex Emmet
1d408f0734 Merge origin/main 2026-08-12 17:37:43 +02:00
11 changed files with 1120 additions and 101 deletions

1
Cargo.lock generated
View file

@ -1724,6 +1724,7 @@ dependencies = [
"async-trait",
"base64 0.21.7",
"chrono",
"ed25519-dalek",
"hmac 0.12.1",
"mtp",
"regex",

View file

@ -15,6 +15,7 @@ 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"

219
src/compute.rs Normal file
View file

@ -0,0 +1,219 @@
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> {
self.0.checked_add(other.0).map(Self)
}
pub fn checked_sub(self, other: Self) -> Option<Self> {
self.0.checked_sub(other.0).map(Self)
}
}
impl From<Units> for u32 {
fn from(units: Units) -> Self {
units.0
}
}
impl TryFrom<u64> for Units {
type Error = std::num::TryFromIntError;
fn try_from(value: u64) -> Result<Self, Self::Error> {
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<u64>,
pub gpu_memory_free_mb: Option<u64>,
pub system_memory_total_mb: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuayHardwareSnapshot {
pub logical_cpu_count: Option<u32>,
pub system_memory_total_mb: Option<u64>,
pub scratch_total_bytes: Option<u64>,
pub scratch_free_bytes: Option<u64>,
}
/// 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<u64>,
pub min_gpu_memory_mb: Option<u64>,
}
#[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)
);
}
}

View file

@ -1,6 +1,25 @@
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 {

View file

@ -2,6 +2,7 @@ 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 {
@ -16,6 +17,40 @@ 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<u64>,
pub min_gpu_memory_mb: Option<u64>,
}
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<u64>,
min_gpu_memory_mb: Option<u64>,
) -> Self {
Self {
compute_units,
min_memory_mb,
min_gpu_memory_mb,
}
}
}
#[derive(Debug, Clone)]
pub struct KrillDescriptor {
pub id: KrillId,
@ -24,6 +59,7 @@ pub struct KrillDescriptor {
pub model_type: ModelType,
pub model_path: Option<String>,
pub config: KrillConfig,
pub runtime_profile: KrillRuntimeProfile,
pub version: String,
pub created_at: DateTime<Utc>,
}
@ -37,6 +73,7 @@ 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(),
}
@ -52,6 +89,7 @@ impl KrillDescriptor {
model_type,
model_path: None,
config: KrillConfig::default(),
runtime_profile: KrillRuntimeProfile::default(),
version: "0.1.0".to_string(),
created_at: Utc::now(),
}
@ -72,6 +110,11 @@ 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
@ -143,4 +186,9 @@ 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);
}
}

View file

@ -5,6 +5,7 @@ pub use uuid::Uuid;
pub mod agents;
pub mod ai_response;
pub mod compute;
pub mod conclusion;
pub mod coral;
pub mod data_retention;
@ -14,6 +15,7 @@ pub mod krill;
pub mod mutations;
pub mod principal;
pub mod project;
pub mod rollback;
pub mod sandbox;
pub mod sync;
pub mod task;
@ -24,6 +26,10 @@ 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,
};
@ -33,29 +39,35 @@ pub use data_retention::{
};
pub use enums::*;
pub use errors::{AuditOutcome, AuditRecord, FieldError, PublicError, ShoalError, ValidationError};
pub use krill::{KrillConfig, KrillDescriptor, KrillId, PodProtocolVersion};
pub use krill::{KrillConfig, KrillDescriptor, KrillId, KrillRuntimeProfile, PodProtocolVersion};
pub use mutations::{
merged_affected_files, normalize_repository_path, FileMutation, PathValidationError,
};
pub use principal::{
AuthError, AuthenticatedPrincipal, AuthorizationService, DispatchAuthError,
DiscussionCredentialClaims, DiscussionExecutionPrincipal, ExecutionPrincipal, GlobalRole,
ModelGatewayClaims, ModelGatewayPrincipal, PodPrincipal, ProjectAction, ProjectRole,
RequestContext, ReservoirConnectionContext, ReservoirPrincipal, ReservoirServiceRole,
TokenValidationResult,
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,
};
pub use project::{Project, ProjectFile, ProjectId, ProjectSettings};
pub use project::{Project, ProjectFile, ProjectId, ProjectSettings, ValidationCommand};
pub use rollback::RollbackReference;
pub use sync::{ChangeSetDescriptor, ChangeSetFile, FileOperationKind};
pub use task::{Task, TaskResult};
pub use todo::{Dependency, ToDo};
pub use 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,
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,
};
#[cfg(test)]

View file

@ -190,6 +190,14 @@ 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
@ -198,6 +206,16 @@ 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.
@ -373,6 +391,20 @@ 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 {
@ -385,12 +417,241 @@ 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<Utc>,
}
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<Uuid>,
pub operations: Vec<String>,
pub issued_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub credential_id: Uuid,
}
impl ServiceCredentialClaims {
pub fn allows(&self, operation: &str, project_id: Option<Uuid>, now: DateTime<Utc>) -> 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<String, ServiceCredentialError> {
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<QuayExecutionClaims, ServiceCredentialError> {
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<String, ServiceCredentialError> {
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<ServiceCredentialClaims, ServiceCredentialError> {
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<String, ServiceCredentialError> {
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<T: for<'de> Deserialize<'de>>(
credential: &str,
verifying_key: &[u8; 32],
) -> Result<T, ServiceCredentialError> {
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 {
@ -1520,4 +1781,83 @@ 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
);
}
}

View file

@ -2,6 +2,8 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::enums::CompletionPolicy;
pub type ProjectId = Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -112,10 +114,25 @@ pub struct ProjectSettings {
pub run_command: Option<String>,
/// Conventional subject/body template used for generated git commits.
pub git_commit_template: Option<String>,
/// 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<ValidationCommand>,
#[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 {
@ -123,6 +140,9 @@ 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!({}),
}
}
@ -147,11 +167,63 @@ 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<ValidationCommand>) -> 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)]
@ -200,4 +272,49 @@ 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");
}
}

50
src/rollback.rs Normal file
View file

@ -0,0 +1,50 @@
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<Utc>,
}
impl RollbackReference {
pub fn is_expired(&self, now: DateTime<Utc>) -> 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()));
}
}

View file

@ -1,4 +1,48 @@
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<String>,
pub result_sha256: Option<String>,
}
/// 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<String>,
pub candidate_version: String,
pub files: Vec<ChangeSetFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConflictResolution {
@ -31,6 +75,43 @@ 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,

View file

@ -2,7 +2,7 @@ use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::principal::ProjectAction;
use crate::{principal::ProjectAction, Units};
/// Authoritative execution ownership for every tool.
/// Each tool declares exactly one execution location. Connector-krill uses this
@ -10,19 +10,78 @@ use crate::principal::ProjectAction;
#[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::Local
Self::Unknown
}
}
#[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")]
@ -154,17 +213,11 @@ 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()));
}
}
@ -247,10 +300,7 @@ 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
@ -389,12 +439,16 @@ pub struct ToolDefinition {
1+ = only visible to agents with max_depth >= this value. */
#[serde(default)]
pub required_depth: u32,
/* 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). */
/* Authoritative execution ownership. Project workspace tools declare Quay;
orchestration tools declare Reef; connector-global tools declare Local. */
#[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 {
@ -420,6 +474,8 @@ impl Default for ToolDefinition {
context_visibility: ContextVisibility::default(),
required_depth: 0,
execution_location: ToolExecutionLocation::default(),
sandbox_access: SandboxAccess::default(),
compute_units: Units::ZERO,
}
}
}
@ -974,6 +1030,8 @@ 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) => {
@ -994,6 +1052,8 @@ macro_rules! tool {
context_visibility: ContextVisibility::default(),
required_depth: 0,
execution_location: ToolExecutionLocation::default(),
sandbox_access: SandboxAccess::default(),
compute_units: Units::ZERO,
}
};
}
@ -1198,34 +1258,6 @@ 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"];
@ -1235,14 +1267,6 @@ 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<ToolDefinition> {
let mut tools = vec![
@ -2243,6 +2267,33 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
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
@ -2270,7 +2321,6 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
// 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),
@ -2286,6 +2336,16 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
.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()) {
@ -2372,11 +2432,9 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
}
}
/* 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)
/* Assign execution ownership and the independent workspace/capacity
metadata for every canonical tool. */
let quay_tools: std::collections::HashSet<&str> = [
"read_file",
"file_read",
"write_file",
@ -2386,25 +2444,27 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
"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",
@ -2446,13 +2506,34 @@ pub fn tool_definitions() -> Vec<ToolDefinition> {
.collect();
for tool in tools.iter_mut() {
if reef_tools.contains(tool.id.as_str()) {
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()) {
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
}
@ -3334,10 +3415,11 @@ Done with tools.
fn every_tool_has_declared_execution_location() {
let tools = tool_definitions();
for tool in &tools {
// Every tool must have either Reef or Local — the enum guarantees this,
// but we verify the field is present and meaningful.
// Unknown is reserved for incomplete/deserialized definitions and
// must never be present in the canonical registry.
assert!(
tool.execution_location == ToolExecutionLocation::Reef
|| tool.execution_location == ToolExecutionLocation::Quay
|| tool.execution_location == ToolExecutionLocation::Local,
"tool '{}' has unclassified execution_location",
tool.id
@ -3346,18 +3428,18 @@ Done with tools.
}
#[test]
fn reef_owned_mutation_tools_are_classified() {
fn quay_owned_workspace_tools_are_classified() {
let tools = tool_definitions();
let reef_tools: Vec<&str> = tools
let quay_tools: Vec<&str> = tools
.iter()
.filter(|t| t.execution_location == ToolExecutionLocation::Reef)
.filter(|t| t.execution_location == ToolExecutionLocation::Quay)
.map(|t| t.id.as_str())
.collect();
// Privileged mutation tools must be Reef-owned
// Workspace mutation tools must be Quay-owned.
for name in &["write_file", "edit_file", "bash", "delete_workspace"] {
assert!(
reef_tools.contains(name),
"privileged tool '{}' must be Reef-owned",
quay_tools.contains(name),
"workspace tool '{}' must be Quay-owned",
name
);
}
@ -3377,6 +3459,33 @@ 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();
@ -3385,6 +3494,7 @@ 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
@ -3437,6 +3547,36 @@ 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();
@ -3446,14 +3586,8 @@ 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);
@ -3476,10 +3610,7 @@ 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);