feat: resource limits and rate limiting (#68)#80
Open
Deepak-negi11 wants to merge 6 commits into
Open
Conversation
4fb2e85 to
eb314e7
Compare
… variable, missing constructor)
- 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
There was a problem hiding this comment.
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
sandboxmodule withResourceLimiter+ configuration types (SandboxConfig,ResourceLimitsConfig,RoleLimitConfig) and tests for limiter behavior. - Integrates rate-limit/concurrency checks and output truncation into
ToolRegistryExecutorexecution 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 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 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 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 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 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 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("/")), |
8 tasks
Nixxx19
requested changes
Mar 19, 2026
Nixxx19
left a comment
Collaborator
There was a problem hiding this comment.
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_fileis mapped tooperation::fileread. - in
resourcelimiter, rate checks and counters are implemented foroperation::fileop, notfileread. - result: repeated
read_filecalls 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, andweb_request_timeout_secondsare added in config and stored inresourcelimits.- 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:
- align
filereadwith rate checking and usage counters, or mapread_filetofileop. - pass real channel role metadata into role resolution in the agent loop.
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.