Skip to content

feat: resource limits and rate limiting (#68)#80

Open
Deepak-negi11 wants to merge 6 commits into
mofa-org:mainfrom
Deepak-negi11:feat/resource-limits-68
Open

feat: resource limits and rate limiting (#68)#80
Deepak-negi11 wants to merge 6 commits into
mofa-org:mainfrom
Deepak-negi11:feat/resource-limits-68

Conversation

@Deepak-negi11

Copy link
Copy Markdown
Contributor

Description
This Pull Request resolves Issue #68 by implementing comprehensive Resource Consumption and Rate Limiting controls within the sandboxed execution environment. These features are designed to prevent systemic abuse scenarios, secure system memory execution, and ensure fair tier-based allocation across active users.

Fixes #68

Type of change
New feature (non-breaking change which adds functionality)
Security patch/enhancement
Core Implementation Details
The following architectural updates have been implemented to enforce execution safety:

ResourceLimiter
Struct: Built in
core/src/sandbox/resource.rs
representing the primary engine for dynamically reading usages and executing limits on commands, file operations, web requests, and subagent spawns.
Output Truncation: Applied dynamic threshold truncation preventing massive output crashes. Outputs over byte limits seamlessly clamp, appending a [TRUNCATED: ...] tail sequence.
Robust Concurrency Hooks (
ResourceSlot
): Built robust RAII patterns to track active processes. Dropped execution locks cleanly decrement current active usages even during panic/crash conditions.
Per-Role Injection Architecture: Enabled configuring limits tiered per-role (Guest, Member, Admin, etc.). Roles are dynamically evaluated and bridged through
AgentLoop
's native RBAC manager and funneled directly into
ToolRegistry
.
Observability: Interspersed tracing::warn! and tracing::debug! metrics to broadcast limit overshoots seamlessly into identical server hook logs.
Acceptance Criteria Verified
Rate limits are reliably enforced per user tracking.
Oversized output strings successfully slice/truncate at designated thresholds.
Concurrent operations intelligently throttle above capacity limits.
Threshold boundaries efficiently shift based on dynamically extracted User context roles.
Throws explicit and readable RateLimitError or ResourceError on violation.
Added internal Rust tests for Role-based logic enforcement.

@Deepak-negi11 Deepak-negi11 force-pushed the feat/resource-limits-68 branch from 4fb2e85 to eb314e7 Compare March 10, 2026 07:07
- Fix seconds_until_reset() underflow bug with saturating_sub
- Remove dead GlobalUsage struct (never read/written)
- Replace lock().unwrap() with unwrap_or_else(poisoned) for robustness
- Map read_file to Operation::FileRead (was incorrectly FileOp)
- Make acquire_slot() sync (no awaits inside)
- Remove duplicate role_limits field from Config
- Clean up unused imports
- Add 4 new tests: truncation, concurrency/Drop, FileOp/WebRequest, underflow

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a sandbox “resource limiter” subsystem and wires it into tool execution so the agent can enforce per-user/per-role rate limits, concurrency caps, and output truncation to mitigate abuse scenarios (Issue #68).

Changes:

  • Introduces sandbox module with ResourceLimiter + configuration types (SandboxConfig, ResourceLimitsConfig, RoleLimitConfig) and tests for limiter behavior.
  • Integrates rate-limit/concurrency checks and output truncation into ToolRegistryExecutor execution path.
  • Plumbs sandbox config + RBAC-derived role/user context through AgentLoop (and updates CLI tool executor construction).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
core/src/tools/registry.rs Adds per-tool operation mapping and enforces limiter checks around tool execution + truncation.
core/src/sandbox/resource.rs Implements core limiter logic (rate limits, concurrency slots, truncation) and unit tests.
core/src/sandbox/config.rs Adds config schema + builder that converts config into a ResourceLimiter.
core/src/sandbox/mod.rs Exposes sandbox config/resource modules and re-exports.
core/src/lib.rs Registers new sandbox module in the core crate.
core/src/config.rs Extends Config with optional sandbox section.
core/src/agent/loop_.rs Builds resource limiter from config and passes user/role into tool executor.
cli/src/main.rs Updates ToolRegistryExecutor::new call sites for the new constructor signature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread core/src/agent/loop_.rs Outdated
Comment on lines +112 to +118
let rbac_manager = config.get_rbac_config().ok().flatten().map(|rc| {
Arc::new(crate::rbac::manager::RbacManager::new(
rc,
config.workspace_path(),
dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/")),
))
});
Comment thread core/src/tools/registry.rs Outdated
Comment on lines +218 to +223
limiter.increment_usage(&user, &op);
Some(
limiter
.acquire_slot(&user, self.role.as_ref(), op.clone())
.map_err(|e| mofa_sdk::llm::LLMError::Other(e.to_string()))?,
)
fn map_tool_to_operation(name: &str) -> Option<crate::sandbox::resource::Operation> {
match name {
"exec" => Some(crate::sandbox::resource::Operation::Command),
"read_file" => Some(crate::sandbox::resource::Operation::FileRead),
Comment on lines +162 to +194
match operation {
Operation::Command => {
let limit = limits
.and_then(|l| l.commands_per_minute)
.unwrap_or(self.limits.max_commands_per_minute);
if usage.commands_this_minute >= limit {
tracing::warn!(user_id = %user.0, role = ?role.map(|r| r.as_str()), limit, current = usage.commands_this_minute, "Command rate limit exceeded");
return Err(RateLimitError::CommandLimitExceeded {
limit,
reset_in: usage.seconds_until_reset(),
});
}
}
Operation::FileOp => {
let limit = limits
.and_then(|l| l.file_ops_per_minute)
.unwrap_or(self.limits.max_file_ops_per_minute);
if usage.file_ops_this_minute >= limit {
tracing::warn!(user_id = %user.0, role = ?role.map(|r| r.as_str()), limit, current = usage.file_ops_this_minute, "FileOp rate limit exceeded");
return Err(RateLimitError::FileOpLimitExceeded);
}
}
Operation::WebRequest => {
let limit = limits
.and_then(|l| l.web_requests_per_minute)
.unwrap_or(self.limits.max_web_requests_per_minute);
if usage.web_requests_this_minute >= limit {
tracing::warn!(user_id = %user.0, role = ?role.map(|r| r.as_str()), limit, current = usage.web_requests_this_minute, "WebRequest rate limit exceeded");
return Err(RateLimitError::WebRequestLimitExceeded);
}
}
_ => {}
}
Comment on lines +15 to +39
impl SandboxConfig {
pub fn build_limiter(&self) -> Option<super::resource::ResourceLimiter> {
self.resource_limits
.as_ref()
.map(|rc| super::resource::ResourceLimiter {
limits: super::resource::ResourceLimits {
command_timeout_seconds: rc.timeouts.command_seconds,
file_operation_timeout_seconds: rc.timeouts.file_operation_seconds,
web_request_timeout_seconds: rc.timeouts.web_request_seconds,

max_command_output_bytes: rc.sizes.max_command_output_mb * 1024 * 1024,
max_file_read_bytes: rc.sizes.max_file_read_mb * 1024 * 1024,
max_web_response_bytes: rc.sizes.max_web_response_mb * 1024 * 1024,

max_commands_per_minute: rc.rates.commands_per_minute,
max_file_ops_per_minute: rc.rates.file_ops_per_minute,
max_web_requests_per_minute: rc.rates.web_requests_per_minute,

max_concurrent_commands: rc.concurrency.max_concurrent_commands,
max_concurrent_subagents: rc.concurrency.max_concurrent_subagents,
role_limits: self.role_limits.clone().unwrap_or_default(),
},
usage: super::resource::ResourceUsageTracker::new(),
})
}
Comment thread core/src/sandbox/resource.rs Outdated
Comment on lines +287 to +303
if let Ok(mut lock) = self.usage_tracker.lock() {
if let Some(usage) = lock.get_mut(&self.user) {
match self.operation {
Operation::Command => {
if usage.active_commands > 0 {
usage.active_commands -= 1;
tracing::debug!(user_id = %self.user.0, active = usage.active_commands, "Released Command resource slot");
}
}
Operation::SpawnSubagent => {
if usage.active_subagents > 0 {
usage.active_subagents -= 1;
tracing::debug!(user_id = %self.user.0, active = usage.active_subagents, "Released SpawnSubagent resource slot");
}
}
_ => {}
}
Comment thread core/src/sandbox/config.rs Outdated
Comment on lines +25 to +27
max_command_output_bytes: rc.sizes.max_command_output_mb * 1024 * 1024,
max_file_read_bytes: rc.sizes.max_file_read_mb * 1024 * 1024,
max_web_response_bytes: rc.sizes.max_web_response_mb * 1024 * 1024,
Comment thread core/src/sandbox/resource.rs Outdated
Comment on lines +151 to +156
let mut user_usage_lock = self.usage.user_usage.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let usage = user_usage_lock
.entry(user.clone())
.or_default();
usage.reset_if_minute_elapsed();
Comment on lines +221 to +235
pub fn truncate_output<'a>(&self, output: &'a [u8], operation: &Operation) -> Cow<'a, [u8]> {
let max_size = match operation {
Operation::Command => self.limits.max_command_output_bytes,
Operation::FileRead => self.limits.max_file_read_bytes,
Operation::WebRequest => self.limits.max_web_response_bytes,
_ => usize::MAX,
};
if output.len() <= max_size {
Cow::Borrowed(output)
} else {
let truncated = &output[..max_size];
let message = format!("\n\n[TRUNCATED: Output exceeded {} bytes]", max_size);
Cow::Owned([truncated, message.as_bytes()].concat())
}
}
Comment thread core/src/agent/loop_.rs Outdated
Comment on lines +113 to +116
Arc::new(crate::rbac::manager::RbacManager::new(
rc,
config.workspace_path(),
dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/")),

@Nixxx19 Nixxx19 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the work here. the direction is good, but i found a few blocking issues before merge.

blocking findings

1) read_file is not actually rate limited

  • in toolregistryexecutor, read_file is mapped to operation::fileread.
  • in resourcelimiter, rate checks and counters are implemented for operation::fileop, not fileread.
  • result: repeated read_file calls can bypass file operation throttling.

2) role based limits may be incorrect at runtime

  • in agentloop, role resolution for tool execution calls:
    • get_role_from_discord(&msg.sender_id, &[])
    • get_role_from_dingtalk(&msg.sender_id, &[])
    • get_role_from_feishu(&msg.sender_id, &[])
  • empty role arrays mean channel role context is not passed, so role based limits can fall back to defaults and not reflect actual user roles.

3) timeout limits appear configured but not enforced

  • command_timeout_seconds, file_operation_timeout_seconds, and web_request_timeout_seconds are added in config and stored in resourcelimits.
  • i could not find enforcement paths that apply these values during tool execution.
  • this creates a config surface that looks active but does not currently protect runtime behavior.

recommendation

please address these before merge:

  1. align fileread with rate checking and usage counters, or map read_file to fileop.
  2. pass real channel role metadata into role resolution in the agent loop.
  3. wire timeout fields into actual execution paths for command, file ops, and web requests.

once these are fixed, this should be in much better shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Resource Limits and Rate Limiting

3 participants