Skip to content

fix: resolve deferred review items from PRs #883, #848, #788 - #915

Merged
ilblackdragon merged 1 commit into
stagingfrom
fix/deferred-review-items
Mar 11, 2026
Merged

ilblackdragon merged 1 commit into
stagingfrom
fix/deferred-review-items

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Summary

Addresses three deferred implementation items that were explicitly flagged as follow-up work during recent code reviews:

  • SIGHUP lock across .await (#883 comment): Refactored restart_with_addr into merged_router_clone() + install_listener() so the async TcpListener::bind happens outside the mutex. Lock is now only held for brief sync state swaps.
  • Recursion depth limit for check_strings (#848 comment): Added MAX_DEPTH = 32 cap on recursive JSON traversal in the safety validator to prevent stack overflow on pathological tool parameters.
  • Named error type for add_tokens (#788 comment): Replaced Result<(), String> with TokenBudgetExceeded { used, limit } for type-safe budget enforcement.

A fourth item (generic unsupported parameter stripping from PR #809) was investigated and found to already be addressed in the current codebase — strip_unsupported_completion_params() handles all three parameter types.

Test plan

  • cargo clippy -- -D warnings — zero warnings
  • cargo test --lib webhook_server — restart + rollback tests pass with new API
  • cargo test --lib validator — new depth-limit regression tests pass
  • cargo test --lib test_add_tokens — budget enforcement tests pass with new error type

🤖 Generated with Claude Code

Address three deferred implementation items flagged during code review:

1. SIGHUP lock held across .await (#883): Split restart_with_addr into
   merged_router_clone() + install_listener() so the async TcpListener
   bind happens outside the mutex, eliminating lock contention risk.

2. Recursion depth limit for check_strings (#848): Cap JSON traversal
   at 32 levels to prevent stack overflow on pathological tool params.

3. Named error type for add_tokens (#788): Replace Result<(), String>
   with TokenBudgetExceeded { used, limit } for type-safe budget errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 11, 2026 03:26
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions github-actions Bot added scope: safety Prompt injection defense scope: worker Container worker size: L 200-499 changed lines risk: high Safety, secrets, auth, or critical infrastructure contributor: core 20+ merged PRs labels Mar 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses deferred review follow-ups across the worker token-budget path, safety validator robustness, and webhook server restart behavior (notably eliminating a mutex held across an async bind during SIGHUP reload).

Changes:

  • Replace JobContext::add_tokens() error return from String to a typed TokenBudgetExceeded { used, limit }.
  • Add a recursion depth cap to Validator::validate_tool_params() and regression tests around the cap.
  • Refactor webhook server restart to a two-phase approach (bind outside lock; swap state under lock) via merged_router_clone() + install_listener().

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/worker/job.rs Updates token budget enforcement call sites and tests to handle the new error type.
src/context/state.rs Introduces TokenBudgetExceeded error type and updates add_tokens() signature accordingly.
src/context/mod.rs Re-exports TokenBudgetExceeded from the context module.
src/safety/validator.rs Adds depth-limited traversal for tool-param string validation plus new tests.
src/main.rs Refactors SIGHUP webhook listener restart to bind outside the mutex and swap under lock.
src/channels/webhook_server.rs Adds router cloning + listener installation API to support lock-minimized restarts; updates restart/rollback tests.

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

Comment thread src/worker/job.rs
.await?
{
self.worker.mark_failed(&msg).await?;
self.worker.mark_failed(&err.to_string()).await?;

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

mark_failed(&err.to_string()).await borrows a temporary String across an .await, which won’t compile (temporary is dropped at the end of the statement while the future may hold the &str). Bind the message to a local String first (or change mark_failed to take an owned String/impl Into<String>).

Suggested change
self.worker.mark_failed(&err.to_string()).await?;
let msg = err.to_string();
self.worker.mark_failed(&msg).await?;

Copilot uses AI. Check for mistakes.
Comment thread src/worker/job.rs
Comment on lines 1798 to 1801
worker
.mark_failed(&budget_result.unwrap_err())
.mark_failed(&budget_result.unwrap_err().to_string())
.await
.unwrap();

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

This test calls mark_failed(&budget_result.unwrap_err().to_string()).await, which borrows a temporary String across an .await and will not compile. Store the .to_string() into a local variable (or adjust mark_failed to take an owned string) before awaiting.

Copilot uses AI. Check for mistakes.
Comment thread src/safety/validator.rs
Comment on lines 207 to +211
validator: &Validator,
result: &mut ValidationResult,
depth: usize,
) {
if depth > MAX_DEPTH {

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

The new depth cap silently skips validating any strings beyond MAX_DEPTH. Since validate_tool_params is a safety boundary (null-byte / forbidden-pattern checks), this creates an easy bypass by nesting a malicious string deeper than the limit. Instead of returning Ok, treat “too deep” as a validation error (or switch to an iterative traversal that can enforce a depth limit without recursion).

Suggested change
validator: &Validator,
result: &mut ValidationResult,
depth: usize,
) {
if depth > MAX_DEPTH {
validator: & Validator,
result: &mut ValidationResult,
depth: usize,
) {
if depth > MAX_DEPTH {
// Treat exceeding the maximum depth as a validation failure,
// to avoid silently skipping checks on deeply nested strings.
result.is_valid = false;
result.warnings.push(format!(
"Validation aborted at path '{}' due to exceeding maximum depth of {}",
path,
MAX_DEPTH
));

Copilot uses AI. Check for mistakes.
Comment thread src/safety/validator.rs
Comment on lines +446 to +451
// The "evil payload" is beyond the depth limit so it should NOT be
// detected — the traversal stops before reaching it.
assert!(
result.is_valid,
"Strings beyond depth limit should be silently skipped, got errors: {:?}",
result.errors

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

test_tool_params_depth_limit_prevents_stack_overflow asserts that forbidden content beyond MAX_DEPTH is not detected and validation remains is_valid. If the depth cap is meant as a DoS/stack-safety measure, the safer behavior is to fail validation when the nesting is too deep (or at least emit an error/warning), rather than silently accepting unvalidated content. Update this test to match the safer behavior.

Suggested change
// The "evil payload" is beyond the depth limit so it should NOT be
// detected — the traversal stops before reaching it.
assert!(
result.is_valid,
"Strings beyond depth limit should be silently skipped, got errors: {:?}",
result.errors
// The "evil payload" is beyond the depth limit. To avoid silently
// accepting unvalidated content, exceeding the depth cap should
// cause validation to fail instead of being treated as valid.
assert!(
!result.is_valid,
"Inputs that exceed the maximum nesting depth should not be considered valid"

Copilot uses AI. Check for mistakes.
Comment on lines +309 to +320
// Try to restart on an invalid address (port 1 typically requires elevated privileges)
let invalid_addr: SocketAddr = "127.0.0.1:1".parse().unwrap();

// Attempt restart (should fail)
let result = server.restart_with_addr(invalid_addr).await;
assert!(result.is_err(), "Restart with invalid address should fail");
// Attempt bind (should fail); server state is untouched because we
// never call install_listener on failure.
let app = server
.merged_router_clone()
.expect("Router should exist after start()");
let result = tokio::net::TcpListener::bind(invalid_addr).await;
assert!(result.is_err(), "Bind to privileged port should fail");
// `app` is dropped — server state unchanged (rollback by construction)
drop(app);

Copilot AI Mar 11, 2026

Copy link

Choose a reason for hiding this comment

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

This test assumes binding to 127.0.0.1:1 will fail due to privilege requirements, which can be false in CI/containerized environments (often running as root) and make the test flaky. Use a deterministic bind failure instead (e.g., keep a StdTcpListener open on an ephemeral port, then attempt to bind tokio::net::TcpListener to the same addr to reliably hit AddrInUse).

Copilot uses AI. Check for mistakes.

@zmanian zmanian 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.

Review: fix: resolve deferred review items from PRs #883, #848, #788

CI is green across all clippy targets and tests. All three deferred items are addressed correctly. Approving.

1. SIGHUP lock across .await (#883) -- webhook_server.rs + main.rs

The refactor is well-designed. The old restart_with_addr held the mutex across an async TcpListener::bind, which could block other lock waiters. The new two-phase approach:

  • Phase 1: merged_router_clone() + TcpListener::bind() happen outside the lock
  • Phase 2: install_listener() does only sync bookkeeping under the lock
  • Phase 3: Old listener shutdown happens outside the lock

This eliminates the lock-across-await issue cleanly. The rollback-by-construction approach (if bind fails, install_listener is never called, so server state is untouched) is simpler and more correct than the old try/restore pattern.

One minor observation: install_listener is now pub and takes arbitrary TcpListener + Router -- callers must ensure the listener matches the router's expectations. This is fine for the current internal usage but worth noting if the API surface expands.

2. Recursion depth limit for check_strings (#848) -- validator.rs

MAX_DEPTH = 32 is a reasonable cap. The implementation correctly passes depth + 1 on both array and object recursion, and silently stops traversal beyond the limit. The two regression tests cover both sides: deep nesting beyond the limit (payload not detected) and shallow nesting within the limit (payload detected).

One thing to consider (not blocking): silently skipping validation on deeply nested content means an attacker could hide injection payloads at depth 33+. A tracing::warn! when the depth limit is hit would make this observable in production. However, 32 levels of JSON nesting in tool parameters is already pathological, and any real tool would reject such input for other reasons.

3. Named error type for add_tokens (#788) -- context/state.rs + worker/job.rs

Clean replacement of Result<(), String> with Result<(), TokenBudgetExceeded>. The thiserror derive provides the same Display message as the old format string. The pub fields (used, limit) enable callers to make programmatic decisions based on the budget state.

The call site in worker/job.rs correctly uses .to_string() on the error when passing to mark_failed(), preserving behavior. The update_context generic return type (F: FnOnce(&mut JobContext) -> R) means R = Result<(), TokenBudgetExceeded>, which nests correctly.

Note: there's already a TokenBudgetExceeded variant in SkillRegistryError (skills/registry.rs) with different fields. The naming collision is acceptable since they're in different modules and represent different budget types (LLM tokens vs skill prompt tokens).

Tests

All three changes include regression tests:

  • Restart + rollback tests updated for two-phase API
  • test_tool_params_depth_limit_prevents_stack_overflow + test_tool_params_within_depth_limit_still_validated
  • Existing test_add_tokens_enforces_budget exercises the new error type

LGTM.

@ilblackdragon
ilblackdragon merged commit 8f51342 into staging Mar 11, 2026
13 checks passed
@ilblackdragon
ilblackdragon deleted the fix/deferred-review-items branch March 11, 2026 07:12
@github-actions github-actions Bot mentioned this pull request Mar 11, 2026
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
…earai#788 (nearai#915)

Address three deferred implementation items flagged during code review:

1. SIGHUP lock held across .await (nearai#883): Split restart_with_addr into
   merged_router_clone() + install_listener() so the async TcpListener
   bind happens outside the mutex, eliminating lock contention risk.

2. Recursion depth limit for check_strings (nearai#848): Cap JSON traversal
   at 32 levels to prevent stack overflow on pathological tool params.

3. Named error type for add_tokens (nearai#788): Replace Result<(), String>
   with TokenBudgetExceeded { used, limit } for type-safe budget errors.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
drchirag1991 pushed a commit to drchirag1991/ironclaw that referenced this pull request Apr 8, 2026
…earai#788 (nearai#915)

Address three deferred implementation items flagged during code review:

1. SIGHUP lock held across .await (nearai#883): Split restart_with_addr into
   merged_router_clone() + install_listener() so the async TcpListener
   bind happens outside the mutex, eliminating lock contention risk.

2. Recursion depth limit for check_strings (nearai#848): Cap JSON traversal
   at 32 levels to prevent stack overflow on pathological tool params.

3. Named error type for add_tokens (nearai#788): Replace Result<(), String>
   with TokenBudgetExceeded { used, limit } for type-safe budget errors.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: high Safety, secrets, auth, or critical infrastructure scope: safety Prompt injection defense scope: worker Container worker size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants