Resource Quota and cgroup v2 Implementation Design

Resource Quota and cgroup v2 Implementation Design

Document Version: 1.0 Date: 2026-01-31 Author: Security Architect Status: Draft - Ready for Review


Executive Summary

This document defines a comprehensive resource quota system for agentic-sandbox VMs to address the current security gaps:

GapRisk LevelImpact
No memory limitsHIGHVM can exhaust host memory, triggering OOM across system
No disk quotasHIGH1.9TB shared storage can be filled by single agent
No CPU throttlingMEDIUMRunaway process can starve other VMs
Unlimited PIDsHIGHFork bomb can crash VM and stress host
Unlimited file descriptorsMEDIUMFD exhaustion can cause service failures

The design provides defense-in-depth through three layers: 1. VM-level limits (libvirt/QEMU) - Hard memory and vCPU caps 2. cgroup v2 limits (systemd) - Process, memory, and I/O controls inside VM 3. Disk quotas (XFS project quotas) - Per-task storage limits on virtiofs mounts


1. Architecture Overview

+==============================================================================+
|                              HOST SYSTEM                                      |
|                                                                               |
|  +-------------------------------------------------------------------------+  |
|  |                    LAYER 1: libvirt Resource Limits                     |  |
|  |  - Hard memory ceiling per VM (no overcommit)                           |  |
|  |  - vCPU pinning and scheduling weight                                   |  |
|  |  - Block I/O throttling per VM                                          |  |
|  +-------------------------------------------------------------------------+  |
|                                     |                                         |
|  +-------------------------------------------------------------------------+  |
|  |                    LAYER 2: XFS Project Quotas                          |  |
|  |  - /srv/agentshare mounted with prjquota                                |  |
|  |  - Per-task project IDs with size limits                                |  |
|  |  - Quota enforcement at filesystem level                                |  |
|  +-------------------------------------------------------------------------+  |
|                                     |                                         |
+=====================================|=========================================+
                                      |
+=====================================|=========================================+
|                              VM GUEST                                         |
|                                     |                                         |
|  +-------------------------------------------------------------------------+  |
|  |                    LAYER 3: systemd cgroup v2 Limits                    |  |
|  |  - MemoryMax, MemoryHigh (soft limit with OOM protection)               |  |
|  |  - TasksMax (PID limit)                                                 |  |
|  |  - CPUWeight, CPUQuota                                                  |  |
|  |  - IOWeight, IOReadBandwidthMax, IOWriteBandwidthMax                    |  |
|  |  - LimitNOFILE (file descriptor limit)                                  |  |
|  +-------------------------------------------------------------------------+  |
+===============================================================================+

2. Resource Limit Specifications

2.1 Default Resource Profiles

ProfileMemoryvCPUDiskPIDsFDsI/O Weight
standard8GB450GB204865536500
minimal4GB220GB102432768250
heavy16GB8100GB4096131072750

2.2 Limit Rationale

Memory Limits

SettingValueJustification
`MemoryMax` (hard)7.5GB (94% of VM)Leave 500MB for kernel, systemd
`MemoryHigh` (soft)6GB (75% of VM)Trigger throttling before OOM
`MemorySwapMax`0Disable swap to prevent unpredictable latency

Why 7.5GB for 8GB VM?

  • Linux kernel requires ~100-200MB
  • systemd and essential services: ~100MB
  • QEMU guest agent: ~50MB
  • Buffer for cgroup accounting overhead

PID/Task Limits

SettingValueJustification
`TasksMax`2048Sufficient for Claude Code + build tools
Fork bomb threshold~1000Detected before hitting limit

Calculation:

  • Claude Code: ~50-100 processes (node, git, language servers)
  • Docker build: ~200-500 processes
  • Concurrent npm/cargo: ~100-300 processes
  • Safety margin: 2x expected peak

CPU Limits

SettingValueJustification
`CPUQuota`400% (4 cores)Match VM vCPU allocation
`CPUWeight`100 (default)Fair sharing with other services

**Why CPUQuota = vCPUs * 100%?**

  • Allows full utilization of allocated resources
  • Prevents stealing from host kernel or other VMs
  • libvirt already limits at hypervisor level

I/O Limits

SettingValueJustification
`IOWeight`500Balanced priority (100-10000 scale)
`IOReadBandwidthMax`500MB/sPrevent SSD saturation
`IOWriteBandwidthMax`200MB/sProtect shared storage write path

virtiofs I/O Considerations:

  • virtiofs writes go through host page cache
  • Large burst writes can pressure host memory
  • Rate limiting prevents cache thrashing

File Descriptor Limits

SettingValueJustification
`LimitNOFILE` (soft)65536Sufficient for most dev tools
`LimitNOFILE` (hard)65536Same as soft (no escalation)

Risk without limit:

  • Process can open unlimited FDs
  • Each FD consumes kernel memory
  • Can exhaust system-wide FD limit

3. Implementation Details

3.1 libvirt XML Enhancements

Update `define_vm()` in `/home/roctinam/dev/agentic-sandbox/images/qemu/provision-vm.sh`:

<domain type='kvm'>
  <name>$vm_name</name>
  <memory unit='MiB'>$memory_mb</memory>
  <currentMemory unit='MiB'>$memory_mb</currentMemory>
  <vcpu placement='static'>$cpus</vcpu>

  <!-- Memory tuning: No overcommit, locked pages -->
  <memtune>
    <hard_limit unit='MiB'>$((memory_mb + 256))</hard_limit>
    <soft_limit unit='MiB'>$memory_mb</soft_limit>
  </memtune>

  <!-- CPU tuning: Scheduling and shares -->
  <cputune>
    <shares>$((cpus * 1024))</shares>
    <period>100000</period>
    <quota>$((cpus * 100000))</quota>
  </cputune>

  <!-- Block I/O tuning -->
  <blkiotune>
    <weight>500</weight>
    <device>
      <path>/path/to/vda</path>
      <read_bytes_sec>524288000</read_bytes_sec>
      <write_bytes_sec>209715200</write_bytes_sec>
    </device>
  </blkiotune>

  <!-- ... rest of domain definition ... -->
</domain>

3.2 systemd Service with cgroup v2 Limits

Create `/home/roctinam/dev/agentic-sandbox/agent-rs/systemd/agent-client-hardened.service`:

[Unit]
Description=Agentic Sandbox Agent Client (Hardened)
Documentation=https://github.com/jmagly/agentic-sandbox
After=network-online.target qemu-guest-agent.service
Wants=network-online.target

[Service]
Type=simple
User=agent
Group=agent
WorkingDirectory=/home/agent

# Environment
EnvironmentFile=/etc/agentic-sandbox/agent.env

# Executable
ExecStart=/opt/agentic-sandbox/bin/agent-client

# Restart policy
Restart=always
RestartSec=5
StartLimitBurst=5
StartLimitIntervalSec=300

# ============================================================================
# RESOURCE LIMITS (cgroup v2)
# ============================================================================

# Memory limits - hard ceiling prevents OOM affecting other processes
MemoryMax=7680M
MemoryHigh=6144M
MemorySwapMax=0

# Task/PID limit - fork bomb defense
TasksMax=2048

# CPU limits - match VM allocation
CPUQuota=400%
CPUWeight=100

# I/O limits - prevent storage abuse
IOWeight=500
IOReadBandwidthMax=/dev/vda 500M
IOWriteBandwidthMax=/dev/vda 200M

# File descriptor limits
LimitNOFILE=65536

# Core dumps disabled (security)
LimitCORE=0

# ============================================================================
# SECURITY HARDENING
# ============================================================================

# Prevent privilege escalation
NoNewPrivileges=true

# Filesystem protection
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/agent /tmp /mnt/inbox /mnt/outbox /var/log
PrivateTmp=true
PrivateDevices=true

# Network restrictions (allow all for agent operations)
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK

# System call filtering (allow needed syscalls)
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
SystemCallErrorNumber=EPERM

# Misc hardening
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictSUIDSGID=true
MemoryDenyWriteExecute=false  # Required for JIT (Node.js, etc.)

# ============================================================================
# LOGGING
# ============================================================================

StandardOutput=journal
StandardError=journal
SyslogIdentifier=agent-client

# Audit resource limit events
AuditResourceLimits=true

[Install]
WantedBy=multi-user.target

3.3 XFS Project Quota Configuration

3.3.1 Host Setup Script

Create `/home/roctinam/dev/agentic-sandbox/scripts/setup-disk-quotas.sh`:

#!/bin/bash
# setup-disk-quotas.sh - Initialize XFS project quotas for agentshare
#
# Prerequisites:
#   - /srv/agentshare mounted on XFS filesystem
#   - Mount option 'prjquota' enabled
#
# Usage: sudo ./setup-disk-quotas.sh

set -euo pipefail

AGENTSHARE_ROOT="${AGENTSHARE_ROOT:-/srv/agentshare}"
PROJID_FILE="/etc/projid"
PROJECTS_FILE="/etc/projects"

# Project ID allocation
# 1000-1999: Reserved for system
# 2000-9999: Task inboxes (task-{uuid} -> project ID)
# 10000+: Future use
PROJECT_ID_START=2000
PROJECT_ID_MAX=9999

# Default quota per task
DEFAULT_QUOTA_GB=50
GLOBAL_QUOTA_GB=100

log_info() { echo "[INFO] $*"; }
log_error() { echo "[ERROR] $*" >&2; }

# Check XFS mount options
check_xfs_prjquota() {
    local mount_point="$1"

    if ! mount | grep -q "$mount_point.*xfs"; then
        log_error "$mount_point is not an XFS filesystem"
        echo "To create XFS with quotas:"
        echo "  mkfs.xfs /dev/sdX"
        echo "  mount -o prjquota /dev/sdX $mount_point"
        exit 1
    fi

    if ! mount | grep -q "$mount_point.*prjquota"; then
        log_error "$mount_point not mounted with prjquota option"
        echo "Add 'prjquota' to mount options in /etc/fstab:"
        echo "  /dev/sdX  $mount_point  xfs  defaults,prjquota  0  0"
        echo "Then remount: mount -o remount,prjquota $mount_point"
        exit 1
    fi

    log_info "XFS prjquota verified for $mount_point"
}

# Initialize quota files
init_quota_files() {
    touch "$PROJID_FILE" "$PROJECTS_FILE"
    chmod 644 "$PROJID_FILE" "$PROJECTS_FILE"
}

# Add project for global share
setup_global_quota() {
    local project_name="agentshare_global"
    local project_id=1001
    local path="$AGENTSHARE_ROOT/global"

    # Add to projid
    if ! grep -q "^${project_name}:" "$PROJID_FILE"; then
        echo "${project_name}:${project_id}" >> "$PROJID_FILE"
    fi

    # Add to projects
    if ! grep -q "^${project_id}:" "$PROJECTS_FILE"; then
        echo "${project_id}:${path}" >> "$PROJECTS_FILE"
    fi

    # Initialize project
    xfs_quota -x -c "project -s ${project_name}" "$AGENTSHARE_ROOT"

    # Set quota (soft = 90%, hard = 100%)
    local soft_kb=$((GLOBAL_QUOTA_GB * 1024 * 1024 * 90 / 100))
    local hard_kb=$((GLOBAL_QUOTA_GB * 1024 * 1024))
    xfs_quota -x -c "limit -p bsoft=${soft_kb}k bhard=${hard_kb}k ${project_name}" "$AGENTSHARE_ROOT"

    log_info "Global quota set: ${GLOBAL_QUOTA_GB}GB for $path"
}

# Create task-specific quota
create_task_quota() {
    local task_id="$1"
    local quota_gb="${2:-$DEFAULT_QUOTA_GB}"

    # Generate unique project ID from task UUID
    local hash=$(echo -n "$task_id" | md5sum | cut -c1-8)
    local project_id=$((16#$hash % (PROJECT_ID_MAX - PROJECT_ID_START) + PROJECT_ID_START))
    local project_name="task_${task_id:0:8}"

    local inbox_path="$AGENTSHARE_ROOT/tasks/${task_id}/inbox"
    local outbox_path="$AGENTSHARE_ROOT/tasks/${task_id}/outbox"

    # Create directories
    mkdir -p "$inbox_path" "$outbox_path"

    # Add to projid (if not exists)
    if ! grep -q "^${project_name}:" "$PROJID_FILE"; then
        echo "${project_name}:${project_id}" >> "$PROJID_FILE"
    fi

    # Add inbox to projects
    if ! grep -q "^${project_id}:${inbox_path}" "$PROJECTS_FILE"; then
        echo "${project_id}:${inbox_path}" >> "$PROJECTS_FILE"
    fi

    # Initialize project
    xfs_quota -x -c "project -s ${project_name}" "$AGENTSHARE_ROOT"

    # Set quota
    local soft_kb=$((quota_gb * 1024 * 1024 * 90 / 100))
    local hard_kb=$((quota_gb * 1024 * 1024))
    xfs_quota -x -c "limit -p bsoft=${soft_kb}k bhard=${hard_kb}k ${project_name}" "$AGENTSHARE_ROOT"

    echo "$project_id"
}

# Remove task quota
remove_task_quota() {
    local task_id="$1"
    local project_name="task_${task_id:0:8}"

    # Remove quota limit
    xfs_quota -x -c "limit -p bsoft=0 bhard=0 ${project_name}" "$AGENTSHARE_ROOT" 2>/dev/null || true

    # Remove from files
    sed -i "/^${project_name}:/d" "$PROJID_FILE" 2>/dev/null || true
    sed -i "/${task_id}/d" "$PROJECTS_FILE" 2>/dev/null || true
}

# Report quota usage
report_quotas() {
    echo "=== XFS Project Quota Report ==="
    xfs_quota -x -c "report -p -h" "$AGENTSHARE_ROOT"
}

# Main
main() {
    case "${1:-setup}" in
        setup)
            check_xfs_prjquota "$AGENTSHARE_ROOT"
            init_quota_files
            setup_global_quota
            report_quotas
            ;;
        create)
            create_task_quota "$2" "${3:-$DEFAULT_QUOTA_GB}"
            ;;
        remove)
            remove_task_quota "$2"
            ;;
        report)
            report_quotas
            ;;
        *)
            echo "Usage: $0 {setup|create <task-id> [quota-gb]|remove <task-id>|report}"
            exit 1
            ;;
    esac
}

main "$@"

3.3.2 Alternative: ext4 Quota (if XFS not available)

For ext4 filesystems, use user/group quotas mapped to task UIDs:

# /etc/fstab
/dev/sdX  /srv/agentshare  ext4  defaults,usrquota,grpquota  0  2

# Enable quotas
quotacheck -cug /srv/agentshare
quotaon /srv/agentshare

# Set per-user quota (map task to UID)
setquota -u $TASK_UID 0 50G 0 0 /srv/agentshare

3.4 Cloud-init Integration

Update cloud-init in `provision-vm.sh` to deploy hardened service:

write_files:
  # Resource limits configuration
  - path: /etc/agentic-sandbox/resource-limits.conf
    permissions: '0644'
    content: |
      # Agent resource limits (read by agent-client)
      MEMORY_LIMIT_MB=7680
      MEMORY_SOFT_MB=6144
      TASKS_MAX=2048
      CPU_QUOTA_PERCENT=400
      IO_READ_MBPS=500
      IO_WRITE_MBPS=200
      NOFILE_LIMIT=65536

  # Hardened systemd service
  - path: /etc/systemd/system/agentic-agent.service
    content: |
      [Unit]
      Description=Agentic Sandbox Agent Client (Hardened)
      After=network-online.target
      Wants=network-online.target

      [Service]
      Type=simple
      User=agent
      Group=agent
      EnvironmentFile=/etc/agentic-sandbox/agent.env
      ExecStart=/opt/agentic-sandbox/bin/agent-client

      # Resource limits
      MemoryMax=7680M
      MemoryHigh=6144M
      MemorySwapMax=0
      TasksMax=2048
      CPUQuota=400%
      IOWeight=500
      LimitNOFILE=65536
      LimitCORE=0

      # Security
      NoNewPrivileges=true
      ProtectSystem=strict
      ProtectHome=read-only
      ReadWritePaths=/home/agent /tmp /mnt/inbox /mnt/outbox
      PrivateTmp=true

      Restart=always
      RestartSec=5

      [Install]
      WantedBy=multi-user.target

runcmd:
  # Verify cgroup v2 is available
  - |
    if ! grep -q "cgroup2" /proc/filesystems; then
      echo "WARNING: cgroup v2 not available, resource limits may not work"
    fi

  # Apply sysctl limits
  - sysctl -w vm.max_map_count=262144
  - sysctl -w fs.file-max=500000
  - echo "vm.max_map_count=262144" >> /etc/sysctl.d/99-agent.conf
  - echo "fs.file-max=500000" >> /etc/sysctl.d/99-agent.conf

  # Configure systemd default limits
  - mkdir -p /etc/systemd/system.conf.d
  - |
    cat > /etc/systemd/system.conf.d/agent-limits.conf <<EOF
    [Manager]
    DefaultTasksMax=2048
    DefaultLimitNOFILE=65536
    EOF

  # Reload and start
  - systemctl daemon-reload
  - systemctl enable agentic-agent
  - systemctl start agentic-agent

4. Failure Mode Analysis

4.1 Memory Exhaustion

ScenarioTriggerSystem ResponseAgent Experience
Gradual growthAllocation crosses MemoryHighThrottling, reclaim pressureSlowdown, may trigger GC
Rapid allocationAllocation crosses MemoryMaxOOM killer terminates processProcess killed, agent restarts
Memory leakSlow growth over hoursEventually hits MemoryMaxSame as rapid allocation

Recommended Agent Behavior:

  • Monitor `/sys/fs/cgroup/memory.current`
  • Implement graceful degradation at 80% usage
  • Checkpoint long-running tasks periodically

4.2 PID Exhaustion (Fork Bomb)

ScenarioTriggerSystem ResponseAgent Experience
Fork bombMalicious/buggy code`fork()` returns EAGAINProcess cannot spawn children
Build tool explosioncargo build -j 256Excess workers fail to spawnBuild continues with fewer workers

Detection:

# From host, monitor task count
cat /sys/fs/cgroup/system.slice/*/pids.current

4.3 Disk Quota Exceeded

ScenarioTriggerSystem ResponseAgent Experience
Large clone`git clone huge-repo`Write returns EDQUOTClone fails with "No space left"
Build artifacts`cargo build --release`Writes fail at quotaBuild fails, cache unusable

Graceful Handling:

  • Agent should check available space before large operations
  • Report quota status in progress updates

4.4 I/O Throttling

ScenarioTriggerSystem ResponseAgent Experience
Large file copy`cp -r /mnt/global/dataset .`I/O limited to configured rateCopy takes longer
Concurrent writesMultiple processes writingI/O shared within limitAll processes slowed

Note: I/O throttling is "soft" - operations complete eventually, just slower.


5. Monitoring and Observability

5.1 Metrics to Expose

// management/src/metrics.rs additions
struct AgentResourceMetrics {
    // Memory
    memory_usage_bytes: Gauge,
    memory_limit_bytes: Gauge,
    memory_oom_kills: Counter,

    // CPU
    cpu_usage_seconds: Counter,
    cpu_throttled_seconds: Counter,

    // Tasks
    tasks_current: Gauge,
    tasks_limit: Gauge,

    // I/O
    io_read_bytes: Counter,
    io_write_bytes: Counter,

    // Disk
    disk_usage_bytes: Gauge,
    disk_quota_bytes: Gauge,
}

5.2 Health Check Enhancements

Update `/opt/agentic-sandbox/health/health-server.py`:

def collect_health(self):
    cgroup_path = "/sys/fs/cgroup"

    return {
        "status": "healthy",
        "resources": {
            "memory": {
                "current_mb": self._read_cgroup_bytes(f"{cgroup_path}/memory.current") / 1024 / 1024,
                "max_mb": self._read_cgroup_bytes(f"{cgroup_path}/memory.max") / 1024 / 1024,
                "usage_pct": self._memory_usage_percent(),
            },
            "tasks": {
                "current": self._read_cgroup_int(f"{cgroup_path}/pids.current"),
                "max": self._read_cgroup_int(f"{cgroup_path}/pids.max"),
            },
            "disk": {
                "inbox_used_mb": self._get_disk_usage("/mnt/inbox") / 1024 / 1024,
                "quota_mb": self._get_disk_quota("/mnt/inbox") / 1024 / 1024,
            }
        }
    }

5.3 Alert Thresholds

MetricWarningCriticalAction
`memory_usage_pct`80%90%Notify, prepare for OOM
`tasks_current / tasks_max`75%90%Warn about fork bomb risk
`disk_usage_pct`80%95%Warn, block new writes
`cpu_throttled_pct`50%80%Inform of performance impact

6. Testing Strategy

6.1 Unit Tests

// management/src/orchestrator/resource_limits_test.rs

#[test]
fn test_parse_memory_limit() {
    assert_eq!(parse_memory_limit("8G"), 8 * 1024 * 1024 * 1024);
    assert_eq!(parse_memory_limit("512M"), 512 * 1024 * 1024);
}

#[test]
fn test_quota_allocation() {
    let quota = TaskQuota::new("task-123", QuotaProfile::Standard);
    assert_eq!(quota.disk_gb, 50);
    assert_eq!(quota.pids, 2048);
}

6.2 Integration Tests

// management/tests/e2e_resource_limits.rs

#[test]
fn rust_vm_e2e_memory_pressure_is_contained() -> anyhow::Result<()> {
    // Verifies memory pressure is contained and the VM remains responsive.
    Ok(())
}

#[tokio::test]
async fn rust_vm_e2e_dispatch_resource_stress_hits_agent_limits() -> anyhow::Result<()> {
    // Verifies dispatch-backed PID and file-descriptor stress hit agent limits
    // without taking down the VM or management channel.
    Ok(())
}

#[test]
fn rust_vm_e2e_agentshare_quota_blocks_excess_write() -> anyhow::Result<()> {
    // Verifies agentshare quota enforcement when project quotas are available.
    Ok(())
}

#[tokio::test]
async fn rust_vm_e2e_dispatch_write_throughput_respects_io_limit() -> anyhow::Result<()> {
    // Verifies write throughput respects concrete cgroup io.max write limits.
    Ok(())
}

6.3 Chaos Tests

# scripts/chaos/test-resource-limits.sh

#!/bin/bash
# Chaos test: resource exhaustion scenarios

VM_NAME="${1:-agent-01}"
VM_IP=$(virsh domifaddr "$VM_NAME" | grep -oE '([0-9]+\.){3}[0-9]+')

echo "=== Chaos Test: Resource Limits ==="

# Test 1: Fork bomb (should be contained)
echo "[1/4] Testing fork bomb containment..."
# Direct runtime SSH is dev/break-glass only. Managed-profile SSH should route
# through the gateway access model in ADR-029.
timeout 30 ssh agent@$VM_IP "bash -c ':(){ :|:& };:'" 2>&1 || true
# Verify VM is still alive
if ssh -o ConnectTimeout=5 agent@$VM_IP "echo ok" | grep -q ok; then
    echo "  PASS: VM survived fork bomb"
else
    echo "  FAIL: VM became unresponsive"
fi

# Test 2: Memory exhaustion
echo "[2/4] Testing memory limit..."
ssh agent@$VM_IP "python3 -c 'import sys; x=[0]*sys.maxsize'" 2>&1 | head -5
if ssh agent@$VM_IP "echo ok" | grep -q ok; then
    echo "  PASS: VM survived memory exhaustion"
else
    echo "  FAIL: VM became unresponsive"
fi

# Test 3: Disk fill attempt
echo "[3/4] Testing disk quota..."
ssh agent@$VM_IP "dd if=/dev/zero of=/mnt/inbox/fill bs=1G count=100 2>&1" | tail -3
used=$(ssh agent@$VM_IP "du -sh /mnt/inbox | cut -f1")
echo "  Disk usage after fill attempt: $used"

# Test 4: FD exhaustion
echo "[4/4] Testing file descriptor limit..."
ssh agent@$VM_IP "python3 -c 'import os; fds=[os.open(\"/dev/null\", os.O_RDONLY) for _ in range(100000)]'" 2>&1 | tail -3
if ssh agent@$VM_IP "echo ok" | grep -q ok; then
    echo "  PASS: VM survived FD exhaustion"
else
    echo "  FAIL: VM became unresponsive"
fi

echo "=== Chaos test complete ==="

7. Migration Plan

Phase 1: libvirt Limits (Week 1)

1. Update `define_vm()` to include `<memtune>`, `<cputune>`, `<blkiotune>` 2. Add command-line options: `--mem-limit`, `--cpu-quota`, `--io-limit` 3. Test with existing VMs (verify no breaking changes) 4. Update documentation

Phase 2: systemd cgroup Limits (Week 2)

1. Create hardened systemd service file 2. Update cloud-init to deploy new service 3. Add health check resource reporting 4. Test all failure modes

Phase 3: Disk Quotas (Week 3)

1. Verify/create XFS filesystem for agentshare 2. Implement quota management scripts 3. Integrate with task provisioning 4. Add quota reporting to orchestrator

Phase 4: Monitoring (Week 4)

1. Add resource metrics to management server 2. Create alerting rules 3. Build dashboard visualizations 4. Document runbooks for quota events


8. Security Considerations

8.1 cgroup Escape Risks

RiskMitigation
cgroup namespace manipulationDisable user namespaces in VM
`/sys/fs/cgroup` write accessMount as read-only for agent user
CAP_SYS_ADMIN in containerVM boundary isolates from host cgroups

8.2 Quota Bypass Risks

RiskMitigation
Hardlinks to other projectsXFS project quotas count at inode level
Sparse file attacksAccount for allocated blocks, not file size
Symlink to unquoted pathvirtiofs mount isolation prevents escape

9. Configuration Reference

9.1 Environment Variables

VariableDefaultDescription
`AGENT_MEMORY_MAX_MB`7680Hard memory limit
`AGENT_MEMORY_HIGH_MB`6144Soft memory limit
`AGENT_TASKS_MAX`2048Maximum processes
`AGENT_CPU_QUOTA_PCT`400CPU quota (100 = 1 core)
`AGENT_IO_READ_MBPS`500Read bandwidth limit
`AGENT_IO_WRITE_MBPS`200Write bandwidth limit
`AGENT_DISK_QUOTA_GB`50Disk quota for inbox
`AGENT_NOFILE_LIMIT`65536File descriptor limit

9.2 Command-Line Options

./provision-vm.sh agent-01 \
  --profile agentic-dev \
  --cpus 4 \
  --memory 8G \
  --disk 50G \
  --mem-limit 7680M \    # NEW: cgroup memory max
  --tasks-max 2048 \     # NEW: PID limit
  --io-limit 200M \      # NEW: I/O write limit
  --agentshare \
  --start

10. Appendices

Appendix A: cgroup v2 Verification

# Check cgroup v2 is enabled
cat /sys/fs/cgroup/cgroup.controllers
# Should show: cpuset cpu io memory hugetlb pids rdma

# Check systemd is using cgroup v2
cat /proc/1/cgroup
# Should show single line: 0::/init.scope

# In VM, check agent service cgroup
systemctl status agentic-agent
# Shows cgroup path like: /system.slice/agentic-agent.service

Appendix B: XFS Quota Commands

# Initialize quotas
xfs_quota -x -c "project -s myproject" /srv/agentshare

# Set limits
xfs_quota -x -c "limit -p bsoft=45g bhard=50g myproject" /srv/agentshare

# Check usage
xfs_quota -x -c "report -p -h" /srv/agentshare

# Remove limits
xfs_quota -x -c "limit -p bsoft=0 bhard=0 myproject" /srv/agentshare

Appendix C: Troubleshooting

SymptomLikely CauseResolution
Service fails with "Failed to set up service cgroups"cgroup v2 not enabledBoot with `systemd.unified_cgroup_hierarchy=1`
MemoryMax ignoredKernel too oldRequires Linux 4.5+
IOBandwidth has no effectvirtio driver issueUse `IOWeight` instead
Quota shows wrong usageDelayed accountingRun `xfs_quota -x -c sync`

Document Approval

RoleNameDateStatus
AuthorSecurity Architect2026-01-31Complete
Technical Review-Pending-
Security Review-Pending-
Approval-Pending-