Skip to content

feat: add CPU limits per agent via cgroups v2#289

Merged
khaliqgant merged 1 commit intomainfrom
claude/fix-npm-stall-issue-e5yzc
Jan 24, 2026
Merged

feat: add CPU limits per agent via cgroups v2#289
khaliqgant merged 1 commit intomainfrom
claude/fix-npm-stall-issue-e5yzc

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

@khaliqgant khaliqgant commented Jan 24, 2026

Implement per-agent CPU isolation using Linux cgroups v2 to prevent one
agent (e.g., running npm install or build) from starving other agents
of CPU resources.

Features:

  • CgroupManager utility in @agent-relay/resiliency package
  • Auto-detects cgroups v2 availability and gracefully degrades
  • Per-agent CPU limits with configurable percentage
  • Automatic cleanup on agent exit

Configuration:

  • cpuLimitPercent option in RelayPtyOrchestratorConfig
  • AGENT_CPU_LIMIT env var to set default in cloud (default: 100%)
  • Only enabled when WORKSPACE_ID is set (cloud environment)

Files:

  • packages/resiliency/src/cgroup-manager.ts: New cgroup manager
  • packages/resiliency/src/index.ts: Export cgroup manager
  • packages/wrapper/src/relay-pty-orchestrator.ts: Integrate cgroups
  • packages/bridge/src/spawner.ts: Pass cpuLimitPercent config

Example cgroup structure:
/sys/fs/cgroup/agent-relay/{agentName}/
cpu.max: "100000 100000" (100% of one core)
cgroup.procs: {pid}


Open with Devin

Implement per-agent CPU isolation using Linux cgroups v2 to prevent one
agent (e.g., running npm install or build) from starving other agents
of CPU resources.

Features:
- CgroupManager utility in @agent-relay/resiliency package
- Auto-detects cgroups v2 availability and gracefully degrades
- Per-agent CPU limits with configurable percentage
- Automatic cleanup on agent exit

Configuration:
- cpuLimitPercent option in RelayPtyOrchestratorConfig
- AGENT_CPU_LIMIT env var to set default in cloud (default: 100%)
- Only enabled when WORKSPACE_ID is set (cloud environment)

Files:
- packages/resiliency/src/cgroup-manager.ts: New cgroup manager
- packages/resiliency/src/index.ts: Export cgroup manager
- packages/wrapper/src/relay-pty-orchestrator.ts: Integrate cgroups
- packages/bridge/src/spawner.ts: Pass cpuLimitPercent config

Example cgroup structure:
  /sys/fs/cgroup/agent-relay/{agentName}/
    cpu.max: "100000 100000" (100% of one core)
    cgroup.procs: {pid}
@my-senior-dev-pr-review
Copy link
Copy Markdown

🤖 My Senior Dev — Analysis Complete

👤 For @khaliqgant

📁 Expert in src (662 edits) • ⚡ 142nd PR this month

View your contributor analytics →


📊 4 files reviewed • 5 need attention

⚠️ Needs Attention:

  • packages/resiliency/src/cgroup-manager.ts — Critical resource management code that interacts with the system directly, must ensure robust error handling and validation.
  • packages/bridge/src/spawner.ts — Introduction of CPU limits directly alters the spawning mechanics for agents, necessitating deeper scrutiny of edge cases and validation.

📚 Relevant Past Discussions:

  • ⚠️ Unresolved discussions on spawner.ts — 3 unresolved comments on this file from past PRs
  • ⚠️ Unresolved discussions on relay-pty-orchestrator.ts — 10 unresolved comments on this file from past PRs

🚀 Open Interactive Review →

The full interface unlocks features not available in GitHub:

  • 💬 AI Chat — Ask questions on any file, get context-aware answers
  • 🔍 Smart Hovers — See symbol definitions and usage without leaving the diff
  • 📚 Code Archeology — Understand how files evolved over time (/archeology)
  • 🎯 Learning Insights — See how this PR compares to similar changes

💬 Chat here: @my-senior-dev explain this change — or try @chaos-monkey @security-auditor @optimizer @skeptic @junior-dev

📖 View all 12 personas & slash commands

You can interact with me by mentioning @my-senior-dev in any comment:

In PR comments or on any line of code:

  • Ask questions about the code or PR
  • Request explanations of specific changes
  • Get suggestions for improvements

Slash commands:

  • /help — Show all available commands
  • /archeology — See the history and evolution of changed files
  • /profile — Performance analysis and suggestions
  • /expertise — Find who knows this code best
  • /personas — List all available AI personas

AI Personas (mention to get their perspective):

Persona Focus
@chaos-monkey 🐵 Edge cases & failure scenarios
@skeptic 🤨 Challenge assumptions
@optimizer Performance & efficiency
@security-auditor 🔒 Security vulnerabilities
@accessibility-advocate Inclusive design
@junior-dev 🌱 Simple explanations
@tech-debt-collector 💳 Code quality & shortcuts
@ux-champion 🎨 User experience
@devops-engineer 🚀 Deployment & scaling
@documentation-nazi 📚 Documentation gaps
@legacy-whisperer 🏛️ Working with existing code
@test-driven-purist Testing & TDD

For the best experience, view this PR on myseniordev.com — includes AI chat, file annotations, and interactive reviews.

const subtreeControlPath = join(this.cgroupBase, 'cgroup.subtree_control');
if (existsSync(subtreeControlPath)) {
try {
writeFileSync(subtreeControlPath, '+cpu');

Check failure

Code scanning / CodeQL

Potential file system race condition High

The file may have changed since it
was checked
.
this.agentCgroups.set(agentName, info);

this.emit('cgroup-created', { agentName, path: cgroupPath, cpuPercent });
console.info(`[cgroup-manager] Created cgroup for ${agentName} with ${cpuPercent}% CPU limit`);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

General approach: sanitize user-controlled values before logging them. For plain-text logs, especially where log entries are line oriented, we should remove newline characters (\n, \r) and optionally other control characters from the agent name before interpolating it into log messages. We can also ensure that error messages constructed from user input do not contain newlines.

Best concrete fix here: in CgroupManager.createAgentCgroup (packages/resiliency/src/cgroup-manager.ts), introduce a small sanitization step for agentName that strips newline and carriage-return characters for logging purposes, and then use this sanitized value in the console.info success log and the console.warn error log. We should not change the actual agentName used for filesystem paths or map keys, to avoid altering behavior; instead, only the logged representation is sanitized. A simple helper variable safeAgentName defined inside the function is enough and does not require new imports.

More specifically:

  • In createAgentCgroup, after validating agentName and before logging, define const safeAgentName = agentName.replace(/[\r\n]/g, '');.
  • Use safeAgentName instead of agentName in:
    • the success log: console.info(`[cgroup-manager] Created cgroup for ${safeAgentName} with ${cpuPercent}% CPU limit`);
    • the error construction/logging: new Error(\Failed to create cgroup for ${safeAgentName}: ${err.message}`);`.
      This keeps all functional behavior identical (cgroup names, map keys, events) but prevents an attacker from injecting line breaks into the log output via the agent name.
Suggested changeset 1
packages/resiliency/src/cgroup-manager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/resiliency/src/cgroup-manager.ts b/packages/resiliency/src/cgroup-manager.ts
--- a/packages/resiliency/src/cgroup-manager.ts
+++ b/packages/resiliency/src/cgroup-manager.ts
@@ -203,6 +203,9 @@
 
     const cgroupPath = join(this.cgroupBase, agentName);
 
+    // Sanitize agent name for logging to prevent log injection via newlines
+    const safeAgentName = agentName.replace(/[\r\n]/g, '');
+
     try {
       // Create cgroup directory
       if (!existsSync(cgroupPath)) {
@@ -231,11 +234,11 @@
       this.agentCgroups.set(agentName, info);
 
       this.emit('cgroup-created', { agentName, path: cgroupPath, cpuPercent });
-      console.info(`[cgroup-manager] Created cgroup for ${agentName} with ${cpuPercent}% CPU limit`);
+      console.info(`[cgroup-manager] Created cgroup for ${safeAgentName} with ${cpuPercent}% CPU limit`);
 
       return true;
     } catch (err: any) {
-      const error = new Error(`Failed to create cgroup for ${agentName}: ${err.message}`);
+      const error = new Error(`Failed to create cgroup for ${safeAgentName}: ${err.message}`);
       this.emit('error', error);
       console.warn(`[cgroup-manager] ${error.message}`);
       return false;
EOF
@@ -203,6 +203,9 @@

const cgroupPath = join(this.cgroupBase, agentName);

// Sanitize agent name for logging to prevent log injection via newlines
const safeAgentName = agentName.replace(/[\r\n]/g, '');

try {
// Create cgroup directory
if (!existsSync(cgroupPath)) {
@@ -231,11 +234,11 @@
this.agentCgroups.set(agentName, info);

this.emit('cgroup-created', { agentName, path: cgroupPath, cpuPercent });
console.info(`[cgroup-manager] Created cgroup for ${agentName} with ${cpuPercent}% CPU limit`);
console.info(`[cgroup-manager] Created cgroup for ${safeAgentName} with ${cpuPercent}% CPU limit`);

return true;
} catch (err: any) {
const error = new Error(`Failed to create cgroup for ${agentName}: ${err.message}`);
const error = new Error(`Failed to create cgroup for ${safeAgentName}: ${err.message}`);
this.emit('error', error);
console.warn(`[cgroup-manager] ${error.message}`);
return false;
Copilot is powered by AI and may make mistakes. Always verify output.
} catch (err: any) {
const error = new Error(`Failed to create cgroup for ${agentName}: ${err.message}`);
this.emit('error', error);
console.warn(`[cgroup-manager] ${error.message}`);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

General approach: ensure that any user-controlled data that ends up in log output is sanitized so it cannot inject newlines or control characters into the logs. For this specific path, that means ensuring that agentName (derived from HTTP name) is normalized to a safe form early, and/or sanitizing it at the point where it is included in log messages. The simplest robust fix is to define a small helper that strips \r, \n, and other control characters from strings before logging them, and apply that to agentName and any derived error messages used with console.*.

Best fix with minimal behavior change:

  1. In packages/resiliency/src/cgroup-manager.ts, introduce a small internal helper function, e.g. sanitizeForLog, which:
    • Accepts a string.
    • Replaces all \r and \n with a safe character (or removes them), and optionally strips other control characters.
  2. Use this helper where user-controlled content is logged:
    • In the success log in createAgentCgroup, sanitize agentName when embedding it into the log message.
    • In the error path, instead of logging error.message directly (which contains the unsanitized agentName), build a log message that uses a sanitized agentName and a sanitized version of err.message. Keep the Error object as-is for programmatic handling via this.emit('error', error) so external behavior is unchanged except for the console text.
  3. Keep all existing logic and return values intact; only adjust what is passed to console.warn / console.info.

No other files need modification: by sanitizing in cgroup-manager.ts at the sink, we cover all alert variants tied to this flow, regardless of how agentName is constructed upstream.


Suggested changeset 1
packages/resiliency/src/cgroup-manager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/resiliency/src/cgroup-manager.ts b/packages/resiliency/src/cgroup-manager.ts
--- a/packages/resiliency/src/cgroup-manager.ts
+++ b/packages/resiliency/src/cgroup-manager.ts
@@ -29,6 +29,12 @@
 import { join } from 'node:path';
 import { EventEmitter } from 'node:events';
 
+// Sanitize potentially user-controlled values before logging to avoid log injection
+function sanitizeForLog(value: string): string {
+  // Remove CR/LF and other ASCII control characters
+  return value.replace(/[\r\n\x00-\x1F\x7F]/g, '');
+}
+
 /**
  * CPU limit configuration for an agent
  */
@@ -231,13 +237,18 @@
       this.agentCgroups.set(agentName, info);
 
       this.emit('cgroup-created', { agentName, path: cgroupPath, cpuPercent });
-      console.info(`[cgroup-manager] Created cgroup for ${agentName} with ${cpuPercent}% CPU limit`);
+      const safeAgentName = sanitizeForLog(agentName);
+      console.info(`[cgroup-manager] Created cgroup for ${safeAgentName} with ${cpuPercent}% CPU limit`);
 
       return true;
     } catch (err: any) {
+      const safeAgentName = sanitizeForLog(agentName);
+      const safeErrMessage = err && typeof err.message === 'string' ? sanitizeForLog(err.message) : '';
       const error = new Error(`Failed to create cgroup for ${agentName}: ${err.message}`);
       this.emit('error', error);
-      console.warn(`[cgroup-manager] ${error.message}`);
+      console.warn(
+        `[cgroup-manager] Failed to create cgroup for ${safeAgentName}: ${safeErrMessage}`,
+      );
       return false;
     }
   }
EOF
@@ -29,6 +29,12 @@
import { join } from 'node:path';
import { EventEmitter } from 'node:events';

// Sanitize potentially user-controlled values before logging to avoid log injection
function sanitizeForLog(value: string): string {
// Remove CR/LF and other ASCII control characters
return value.replace(/[\r\n\x00-\x1F\x7F]/g, '');
}

/**
* CPU limit configuration for an agent
*/
@@ -231,13 +237,18 @@
this.agentCgroups.set(agentName, info);

this.emit('cgroup-created', { agentName, path: cgroupPath, cpuPercent });
console.info(`[cgroup-manager] Created cgroup for ${agentName} with ${cpuPercent}% CPU limit`);
const safeAgentName = sanitizeForLog(agentName);
console.info(`[cgroup-manager] Created cgroup for ${safeAgentName} with ${cpuPercent}% CPU limit`);

return true;
} catch (err: any) {
const safeAgentName = sanitizeForLog(agentName);
const safeErrMessage = err && typeof err.message === 'string' ? sanitizeForLog(err.message) : '';
const error = new Error(`Failed to create cgroup for ${agentName}: ${err.message}`);
this.emit('error', error);
console.warn(`[cgroup-manager] ${error.message}`);
console.warn(
`[cgroup-manager] Failed to create cgroup for ${safeAgentName}: ${safeErrMessage}`,
);
return false;
}
}
Copilot is powered by AI and may make mistakes. Always verify output.

const info = this.agentCgroups.get(agentName);
if (!info) {
console.warn(`[cgroup-manager] No cgroup found for agent ${agentName}`);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

General approach: sanitize user-controlled values before including them in log messages. For plain-text logs, the main concern is stripping newline (\n) and carriage return (\r) characters (and optionally other control characters) from attacker-controlled strings. To avoid widespread code churn and keep behavior unchanged, we can add a small helper function in cgroup-manager.ts that produces a safe, log-friendly version of the agent name, and then use it consistently in all console.* and emitted event payloads that contain agentName. This will address the current alert and any similar variants arising from logging agentName from tainted sources.

Best concrete fix in this file:

  1. Define a sanitizeLogValue helper near the top of packages/resiliency/src/cgroup-manager.ts. This function will:

    • Convert the input to a string (defensive).
    • Remove \r and \n characters using String.prototype.replace with a regex like /[\r\n]+/g.
    • Optionally trim the result (not required, but harmless).
      This matches the pattern recommended in the background description.
  2. Use this helper for all log statements and error messages that interpolate agentName (or values derived solely from it) inside CgroupManager:

    • In createAgentCgroup:
      • For the validation error message Invalid agent name: ${agentName}.
      • For the success log: Created cgroup for ${agentName}....
      • For the error construction and warning: Failed to create cgroup for ${agentName}: ....
    • In addProcess:
      • For the “No cgroup found for agent …” warning (the one flagged by CodeQL).
      • For the “Added process … to cgroup …” info log.
      • For the “Failed to add process … to cgroup …” warning.
    • For internal emit events we can leave the raw agentName (since they are in-process objects, not logs). However, if you want to be extra defensive, you can emit both raw and sanitized names; for minimal behavior changes, I will leave the event payloads unchanged and only sanitize the string interpolation used for logging.
  3. Keep functionality otherwise identical: the cgroup directories, map keys, and file paths will still use the original (already path-validated) agentName. Only the text that is written to logs will be sanitized. This ensures no change to runtime behavior except for removing control characters from logged agent names.

No additional imports are needed; we only add a local function.


Suggested changeset 1
packages/resiliency/src/cgroup-manager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/resiliency/src/cgroup-manager.ts b/packages/resiliency/src/cgroup-manager.ts
--- a/packages/resiliency/src/cgroup-manager.ts
+++ b/packages/resiliency/src/cgroup-manager.ts
@@ -30,6 +30,14 @@
 import { EventEmitter } from 'node:events';
 
 /**
+ * Sanitize values before logging to prevent log injection.
+ * Strips newline characters so user-controlled values can't forge log entries.
+ */
+function sanitizeLogValue(value: unknown): string {
+  return String(value).replace(/[\r\n]+/g, '');
+}
+
+/**
  * CPU limit configuration for an agent
  */
 export interface CpuLimitConfig {
@@ -198,7 +206,7 @@
 
     // Validate agent name (no path traversal)
     if (agentName.includes('/') || agentName.includes('..')) {
-      throw new Error(`Invalid agent name: ${agentName}`);
+      throw new Error(`Invalid agent name: ${sanitizeLogValue(agentName)}`);
     }
 
     const cgroupPath = join(this.cgroupBase, agentName);
@@ -231,11 +239,14 @@
       this.agentCgroups.set(agentName, info);
 
       this.emit('cgroup-created', { agentName, path: cgroupPath, cpuPercent });
-      console.info(`[cgroup-manager] Created cgroup for ${agentName} with ${cpuPercent}% CPU limit`);
+      console.info(
+        `[cgroup-manager] Created cgroup for ${sanitizeLogValue(agentName)} with ${cpuPercent}% CPU limit`,
+      );
 
       return true;
     } catch (err: any) {
-      const error = new Error(`Failed to create cgroup for ${agentName}: ${err.message}`);
+      const safeAgentName = sanitizeLogValue(agentName);
+      const error = new Error(`Failed to create cgroup for ${safeAgentName}: ${err.message}`);
       this.emit('error', error);
       console.warn(`[cgroup-manager] ${error.message}`);
       return false;
@@ -256,7 +264,9 @@
 
     const info = this.agentCgroups.get(agentName);
     if (!info) {
-      console.warn(`[cgroup-manager] No cgroup found for agent ${agentName}`);
+      console.warn(
+        `[cgroup-manager] No cgroup found for agent ${sanitizeLogValue(agentName)}`,
+      );
       return false;
     }
 
@@ -266,11 +276,15 @@
       info.pids.push(pid);
 
       this.emit('process-added', { agentName, pid });
-      console.info(`[cgroup-manager] Added process ${pid} to cgroup ${agentName}`);
+      console.info(
+        `[cgroup-manager] Added process ${pid} to cgroup ${sanitizeLogValue(agentName)}`,
+      );
 
       return true;
     } catch (err: any) {
-      console.warn(`[cgroup-manager] Failed to add process ${pid} to cgroup ${agentName}: ${err.message}`);
+      console.warn(
+        `[cgroup-manager] Failed to add process ${pid} to cgroup ${sanitizeLogValue(agentName)}: ${err.message}`,
+      );
       return false;
     }
   }
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
info.pids.push(pid);

this.emit('process-added', { agentName, pid });
console.info(`[cgroup-manager] Added process ${pid} to cgroup ${agentName}`);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

In general, to prevent log injection when logging user-controlled strings, sanitize them before interpolation: for plain-text logs, at least strip or replace newline (\n) and carriage-return (\r) characters, and optionally other non-printable characters. This ensures a user cannot forge additional log lines or alter log structure. It’s also good practice to mark/log user input with quotes or other delimiters, which this code already partially does in some messages.

For this specific case, the single best fix is to sanitize agentName right before it is used in log messages inside CgroupManager. Because agentName might be used elsewhere (e.g., filesystem paths) where newlines would already be invalid or handled by the OS, we should not change the actual value used for cgroup management; instead we should derive a sanitized version solely for logging. We can implement a small helper function within cgroup-manager.ts to normalize user-controlled strings for logs by removing \r and \n, then use that helper where agentName is logged in createAgentCgroup and addProcess (and other relevant log lines referencing agentName in this file). This approach avoids changing behavior of the cgroup logic and addresses all current and future log-injection issues involving agentName in this module, without requiring new dependencies or changes to other files.

Concretely:

  • Add a simple sanitizeForLog function to packages/resiliency/src/cgroup-manager.ts that takes string | number and returns a string with \r and \n removed (and safely handles non-string inputs).
  • In createAgentCgroup, change the console.info and error construction to use sanitizeForLog(agentName) instead of agentName directly.
  • In addProcess, change the console.warn and console.info lines that reference agentName (and optionally pid, though pid is numeric and not user-controlled) to use the sanitized value for agentName.
  • Keep all other logic, including event emission payloads and cgroup path handling, unchanged so that functionality remains the same.
Suggested changeset 1
packages/resiliency/src/cgroup-manager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/resiliency/src/cgroup-manager.ts b/packages/resiliency/src/cgroup-manager.ts
--- a/packages/resiliency/src/cgroup-manager.ts
+++ b/packages/resiliency/src/cgroup-manager.ts
@@ -30,6 +30,17 @@
 import { EventEmitter } from 'node:events';
 
 /**
+ * Sanitize potentially user-controlled values before logging to prevent log injection.
+ * Currently strips newline and carriage-return characters from strings.
+ */
+function sanitizeForLog(value: unknown): string {
+  if (typeof value !== 'string') {
+    return String(value);
+  }
+  return value.replace(/[\r\n]/g, '');
+}
+
+/**
  * CPU limit configuration for an agent
  */
 export interface CpuLimitConfig {
@@ -198,7 +209,7 @@
 
     // Validate agent name (no path traversal)
     if (agentName.includes('/') || agentName.includes('..')) {
-      throw new Error(`Invalid agent name: ${agentName}`);
+      throw new Error(`Invalid agent name: ${sanitizeForLog(agentName)}`);
     }
 
     const cgroupPath = join(this.cgroupBase, agentName);
@@ -231,11 +242,14 @@
       this.agentCgroups.set(agentName, info);
 
       this.emit('cgroup-created', { agentName, path: cgroupPath, cpuPercent });
-      console.info(`[cgroup-manager] Created cgroup for ${agentName} with ${cpuPercent}% CPU limit`);
+      console.info(
+        `[cgroup-manager] Created cgroup for ${sanitizeForLog(agentName)} with ${cpuPercent}% CPU limit`,
+      );
 
       return true;
     } catch (err: any) {
-      const error = new Error(`Failed to create cgroup for ${agentName}: ${err.message}`);
+      const safeAgentName = sanitizeForLog(agentName);
+      const error = new Error(`Failed to create cgroup for ${safeAgentName}: ${err.message}`);
       this.emit('error', error);
       console.warn(`[cgroup-manager] ${error.message}`);
       return false;
@@ -256,7 +267,9 @@
 
     const info = this.agentCgroups.get(agentName);
     if (!info) {
-      console.warn(`[cgroup-manager] No cgroup found for agent ${agentName}`);
+      console.warn(
+        `[cgroup-manager] No cgroup found for agent ${sanitizeForLog(agentName)}`,
+      );
       return false;
     }
 
@@ -266,11 +279,17 @@
       info.pids.push(pid);
 
       this.emit('process-added', { agentName, pid });
-      console.info(`[cgroup-manager] Added process ${pid} to cgroup ${agentName}`);
+      console.info(
+        `[cgroup-manager] Added process ${pid} to cgroup ${sanitizeForLog(agentName)}`,
+      );
 
       return true;
     } catch (err: any) {
-      console.warn(`[cgroup-manager] Failed to add process ${pid} to cgroup ${agentName}: ${err.message}`);
+      console.warn(
+        `[cgroup-manager] Failed to add process ${pid} to cgroup ${sanitizeForLog(
+          agentName,
+        )}: ${err.message}`,
+      );
       return false;
     }
   }
EOF
Copilot is powered by AI and may make mistakes. Always verify output.

return true;
} catch (err: any) {
console.warn(`[cgroup-manager] Failed to add process ${pid} to cgroup ${agentName}: ${err.message}`);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

In general, to fix log injection where user-controlled strings are interpolated into logs, you should sanitize or encode those strings before logging them. For plain-text logs, the key step is to strip or normalize newline and carriage return characters (and optionally other control characters), and ensure logged user input is clearly delimited.

For this codebase, the single best minimal fix is to sanitize agentName right at the point where it is logged inside CgroupManager. That ensures that, regardless of where agentName originally came from (dashboard HTTP request, daemon, or other sources), any log messages emitted by CgroupManager cannot contain hostile newlines or control characters. We can accomplish this by introducing a small helper inside cgroup-manager.ts to clean log values (e.g., remove \r and \n, and optionally replace other control characters with safe placeholders), and using it when interpolating agentName into log strings.

Concretely:

  • Define a private helper function in cgroup-manager.ts, e.g. sanitizeLogValue(value: string): string, that:
    • Converts the input to string.
    • Removes \r and \n (and optionally other control characters like \t or anything in \x00-\x1F and \x7F) using String.prototype.replace and a regular expression.
  • Use this helper when logging agentName in:
    • the catch block of createAgentCgroup (line 238–241 region),
    • the “no cgroup found” warning in addProcess,
    • the success and failure logs in addProcess,
    • the success and failure logs in removeAgentCgroup.
  • Keep behavior of the rest of the system unchanged: we only modify log text, not identifiers used for cgroup paths or keys in maps.

No imports are needed: sanitization can be implemented with plain TypeScript/JavaScript.


Suggested changeset 1
packages/resiliency/src/cgroup-manager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/resiliency/src/cgroup-manager.ts b/packages/resiliency/src/cgroup-manager.ts
--- a/packages/resiliency/src/cgroup-manager.ts
+++ b/packages/resiliency/src/cgroup-manager.ts
@@ -29,6 +29,11 @@
 import { join } from 'node:path';
 import { EventEmitter } from 'node:events';
 
+// Sanitize potentially user-controlled values before logging to prevent log injection
+function sanitizeLogValue(value: unknown): string {
+  return String(value).replace(/[\r\n]+/g, ' ');
+}
+
 /**
  * CPU limit configuration for an agent
  */
@@ -235,7 +240,8 @@
 
       return true;
     } catch (err: any) {
-      const error = new Error(`Failed to create cgroup for ${agentName}: ${err.message}`);
+      const safeAgentName = sanitizeLogValue(agentName);
+      const error = new Error(`Failed to create cgroup for ${safeAgentName}: ${err.message}`);
       this.emit('error', error);
       console.warn(`[cgroup-manager] ${error.message}`);
       return false;
@@ -255,8 +261,9 @@
     }
 
     const info = this.agentCgroups.get(agentName);
+    const safeAgentName = sanitizeLogValue(agentName);
     if (!info) {
-      console.warn(`[cgroup-manager] No cgroup found for agent ${agentName}`);
+      console.warn(`[cgroup-manager] No cgroup found for agent ${safeAgentName}`);
       return false;
     }
 
@@ -266,11 +272,13 @@
       info.pids.push(pid);
 
       this.emit('process-added', { agentName, pid });
-      console.info(`[cgroup-manager] Added process ${pid} to cgroup ${agentName}`);
+      console.info(`[cgroup-manager] Added process ${pid} to cgroup ${safeAgentName}`);
 
       return true;
     } catch (err: any) {
-      console.warn(`[cgroup-manager] Failed to add process ${pid} to cgroup ${agentName}: ${err.message}`);
+      console.warn(
+        `[cgroup-manager] Failed to add process ${pid} to cgroup ${safeAgentName}: ${err.message}`,
+      );
       return false;
     }
   }
@@ -291,6 +296,8 @@
       return false;
     }
 
+    const safeAgentName = sanitizeLogValue(agentName);
+
     try {
       // Move processes to parent cgroup first (required before removal)
       const parentProcs = join(this.cgroupBase, 'cgroup.procs');
@@ -307,11 +314,13 @@
       this.agentCgroups.delete(agentName);
 
       this.emit('cgroup-removed', { agentName });
-      console.info(`[cgroup-manager] Removed cgroup for ${agentName}`);
+      console.info(`[cgroup-manager] Removed cgroup for ${safeAgentName}`);
 
       return true;
     } catch (err: any) {
-      console.warn(`[cgroup-manager] Failed to remove cgroup ${agentName}: ${err.message}`);
+      console.warn(
+        `[cgroup-manager] Failed to remove cgroup ${safeAgentName}: ${err.message}`,
+      );
       return false;
     }
   }
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional flags.

Open in Devin Review

@khaliqgant khaliqgant merged commit e0193b5 into main Jan 24, 2026
24 of 26 checks passed
@khaliqgant khaliqgant deleted the claude/fix-npm-stall-issue-e5yzc branch January 24, 2026 14:27
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.

3 participants