This guide explains how to create and test agent profiles for PuzzlePod.
Agent profiles define per-agent access control, resource limits, and
behavioral monitoring configuration used by the puzzled governance daemon
to set up kernel-enforced sandboxes.
Profile files are YAML documents stored in the profiles directory
(default: /etc/puzzled/profiles/). The filename (without the .yaml
extension) is used as the profile name when creating branches:
puzzlectl branch create --profile my-agent --base-path /home/user/projectEvery profile has the following top-level sections:
name: my-agent
description: >
Human-readable description of what this profile is for.
fail_mode: fail-closed # optional, default: fail-closed
filesystem:
read_allowlist: []
write_allowlist: []
denylist: []
exec_allowlist: []
resource_limits:
memory_bytes: 268435456
cpu_shares: 100
io_weight: 100
max_pids: 32
storage_quota_mb: 512
inode_quota: 5000
network:
mode: Blocked
allowed_domains: []
behavioral:
max_deletions: 50
max_reads_per_minute: 500
credential_access_alert: truename-- Must match the filename (e.g.,my-agent.yamlmust havename: my-agent).filesystem-- At leastread_allowlistanddenylistmust be present.exec_allowlist-- List of permitted executables.resource_limits-- All six sub-fields are required.network-- Bothmodeandallowed_domainsare required.behavioral-- All three sub-fields are required.
description-- Recommended but not required.fail_mode-- Defaults tofail-closedif omitted.
Profiles can inherit from a parent profile using the extends field:
# Partial example — fields not shown here are required in the full profile.
# Omitted Vec fields (e.g., read_allowlist) inherit from the parent when empty.
# Omitted scalar fields use serde defaults, NOT the parent's values (see warning below).
name: my-custom-agent
description: Custom agent extending standard
extends: standard
filesystem:
write_allowlist:
- /workspace
# read_allowlist, denylist inherited from standard (empty lists inherit)
resource_limits:
memory_bytes: 1073741824
cpu_shares: 100
io_weight: 100
max_pids: 128
storage_quota_mb: 1024
inode_quota: 10000Merge rules:
- Vec fields (exec_allowlist, exec_denylist, capabilities, all filesystem lists): if the child's list is empty, the parent's list is inherited; if non-empty, the child's list replaces the parent's entirely
- Scalar/struct fields (resource_limits, network, behavioral, fail_mode, seccomp_mode, etc.): always use the child's values
- credentials: child's if present, else parent's
Security warning: Scalar fields like
fail_mode,seccomp_mode,allow_symlinks, andallow_exec_overlaydo NOT inherit from the parent. When omitted in a child profile, they receive serde defaults (e.g.,SeccompMode::Permissive), which may be less restrictive than the parent. Always explicitly set security-relevant scalar fields in child profiles.
Constraints:
- Maximum inheritance depth: 3 levels (e.g., grandchild extends child extends parent)
- Circular inheritance is detected and rejected
- The parent profile must exist in the same profiles directory
- The merged profile is validated after resolution
Filesystem access is enforced by Landlock LSM, an in-kernel mechanism that is irrevocable once applied. Rules are evaluated on every file access with less than 1 microsecond overhead.
Paths the agent can read. These are recursive -- granting read access to
/usr/share permits reading any file under that directory tree.
filesystem:
read_allowlist:
- /usr/share
- /usr/lib
- /usr/lib64
- /usr/includePaths the agent can write to directly (outside the OverlayFS branch). In most profiles this should be empty, because all writes are captured by the OverlayFS upper layer and reviewed at commit time.
filesystem:
write_allowlist: []Only set write paths when the agent needs to write to locations outside
the branch (e.g., /tmp for temporary files in the privileged profile).
Paths that are always denied, regardless of allowlist rules. Denylists take precedence over allowlists. Use this for sensitive system files:
filesystem:
denylist:
- /etc/shadow
- /etc/gshadow
- /etc/ssh
- /root/.ssh
- /homeBest practice: Always deny /etc/shadow, /etc/ssh, and
/root/.ssh at minimum.
Every execve() call is intercepted by seccomp USER_NOTIF and validated
by puzzled against this list before the exec is permitted. Glob patterns
are supported.
exec_allowlist:
- /usr/bin/python3
- /usr/bin/gcc
- /usr/bin/make
- /usr/bin/git
- /usr/bin/curlFor profiles that need broad exec access, use glob patterns:
exec_allowlist:
- /usr/bin/*
- /usr/sbin/*Security note: Broad glob patterns reduce containment. Prefer explicit lists of required executables.
Network access is controlled by network namespace isolation and nftables rules. Four modes are available:
No network access. The agent runs in an isolated network namespace with no external interfaces. This is the most restrictive and recommended default for untrusted agents.
network:
mode: Blocked
allowed_domains: []Network access is permitted only to explicitly listed domains. DNS
resolution is performed by puzzled, and nftables rules are configured
for the resolved IP addresses. Connection attempts to unlisted
destinations are blocked by seccomp USER_NOTIF interception of
connect().
network:
mode: Gated
allowed_domains:
- pypi.org
- files.pythonhosted.org
- github.com
- api.github.com
- crates.ioAll network access is permitted but every connection attempt is logged to the audit trail. Use this for trusted agents where you need visibility but not enforcement.
network:
mode: Monitored
allowed_domains: [] # ignored in Monitored modeAll network access is permitted without logging. Use only for fully trusted agents in controlled environments.
network:
mode: Unrestricted
allowed_domains: [] # ignored in Unrestricted modeResource limits are enforced by cgroups v2 and XFS project quotas. These are hard limits -- the kernel enforces them regardless of the agent's behavior.
resource_limits:
memory_bytes: 536870912 # 512 MiB
cpu_shares: 100 # relative weight (1-10000)
io_weight: 100 # relative weight (1-10000)
max_pids: 64 # maximum processes
storage_quota_mb: 1024 # OverlayFS upper layer storage limit
inode_quota: 10000 # OverlayFS upper layer file count limit| Field | Enforcement | Description |
|---|---|---|
memory_bytes |
cgroup v2 memory.max |
Hard memory limit. OOM killer terminates agent on exceed. |
cpu_shares |
cgroup v2 cpu.weight |
Relative CPU share (1-10000). Higher = more CPU time. |
io_weight |
cgroup v2 io.weight |
Relative I/O priority (1-10000). Higher = more I/O bandwidth. |
max_pids |
cgroup v2 pids.max |
Maximum process count. Fork bomb protection. |
storage_quota_mb |
XFS project quota | Maximum storage for branch filesystem in megabytes. |
inode_quota |
XFS project quota | Maximum file count for branch filesystem. |
| Workload | Memory | PIDs | Storage | Inodes |
|---|---|---|---|---|
| Simple script | 256 MiB | 16 | 256 MB | 1,000 |
| Development agent | 512 MiB | 64 | 1 GB | 10,000 |
| Build agent | 2 GiB | 256 | 4 GB | 100,000 |
| Edge device | 128 MiB | 8 | 64 MB | 500 |
Behavioral monitoring uses fanotify to observe file access patterns in
real time with zero agent overhead (asynchronous monitoring). When a
trigger fires, puzzled emits a BehavioralTrigger D-Bus signal and
records an audit event.
behavioral:
max_deletions: 50 # mass deletion detection
max_reads_per_minute: 1000 # data exfiltration detection
credential_access_alert: true # credential sniffing detectionMaximum number of file deletions before triggering an alert. Protects against a hallucinating agent that attempts to delete large numbers of files. Set lower for safety-critical deployments.
Maximum file read operations per minute. Detects data exfiltration patterns where an agent rapidly reads many files. Reads within the agent's own branch do not count toward this limit.
When true, generates an alert when the agent attempts to access files
matching known credential patterns (SSH keys, AWS credentials, GPG
keyrings, .env files), even if the access is denied by Landlock. This
provides early warning of potentially malicious agent behavior.
The fail_mode field controls behavior when the governance daemon
cannot evaluate policy (daemon crash, resource exhaustion, timeout).
| Mode | Behavior | Use Case |
|---|---|---|
fail-closed |
Deny the operation, roll back | Default. Production servers. |
fail-silent |
Hold last known safe state | Edge devices with intermittent connectivity. |
fail-operational |
Continue with reduced capabilities | High-availability environments. |
fail-safe-state |
Controlled stop, return to safe state | Safety-critical (IEC 61508, ISO 26262). |
fail_mode: fail-closedImportant: For safety-critical deployments (vehicles, robots, drones,
industrial controllers), always use fail-safe-state and ensure a
certified safety controller sits between the agent and physical actuators.
Use puzzlectl profile init to generate a new profile YAML:
# Generate a new profile interactively
puzzlectl profile init --out /etc/puzzled/profiles/my-agent.yaml
# Generate non-interactively with inheritance
puzzlectl profile init --non-interactive --name my-agent --extends standardBefore deploying a profile, validate it against the JSON schema:
puzzlectl profile validate /etc/puzzled/profiles/my-agent.yamlThis checks:
- YAML syntax is valid
- All required fields are present
- Field values are within allowed ranges
namematches filename- No unknown fields
Test a profile against simulated agent behavior:
puzzlectl profile test my-agent --simulate read-write-testThe --simulate flag accepts a scenario name or path to a scenario file.
The command reports which operations would be allowed or denied under the
profile's rules:
Simulating profile 'my-agent' against scenario 'read-write-test':
READ /usr/share/doc/README -> ALLOWED (read_allowlist match)
READ /etc/shadow -> DENIED (denylist match)
WRITE /tmp/output.txt -> DENIED (not in write_allowlist)
EXEC /usr/bin/python3 -> ALLOWED (exec_allowlist match)
EXEC /usr/bin/rm -> DENIED (not in exec_allowlist)
NET connect pypi.org:443 -> DENIED (network mode: Blocked)
Result: 3 allowed, 3 denied
Create a test branch with the profile and run a test workload:
# Create a branch with the new profile
puzzlectl branch create --profile my-agent --base-path /tmp/test-project \
--command '["python3", "-c", "print(\"hello\")"]'
# Inspect the branch to verify sandbox configuration
puzzlectl branch list
puzzlectl branch inspect <branch-id>
# Check the diff
puzzlectl branch diff <branch-id>
# Roll back (discard) the test branch
puzzlectl branch rollback <branch-id>An agent that reads source code and produces review comments:
name: code-reviewer
description: Read-only access to source code for automated review.
filesystem:
read_allowlist:
- /usr/share
- /usr/lib
- /usr/lib64
write_allowlist: []
denylist:
- /etc/shadow
- /etc/ssh
- /root
- /home
exec_allowlist:
- /usr/bin/python3
- /usr/bin/git
- /usr/bin/grep
- /usr/bin/cat
resource_limits:
memory_bytes: 268435456
cpu_shares: 50
io_weight: 50
max_pids: 16
storage_quota_mb: 128
inode_quota: 1000
network:
mode: Blocked
allowed_domains: []
behavioral:
max_deletions: 0
max_reads_per_minute: 500
credential_access_alert: trueAn agent that builds and tests code:
name: ci-runner
description: Build and test agent with network access to package registries.
filesystem:
read_allowlist:
- /usr/share
- /usr/lib
- /usr/lib64
- /usr/include
- /usr/bin
write_allowlist: []
denylist:
- /etc/shadow
- /etc/ssh
- /root/.ssh
exec_allowlist:
- /usr/bin/python3
- /usr/bin/gcc
- /usr/bin/g++
- /usr/bin/make
- /usr/bin/cmake
- /usr/bin/cargo
- /usr/bin/rustc
- /usr/bin/npm
- /usr/bin/node
- /usr/bin/git
- /usr/bin/curl
resource_limits:
memory_bytes: 2147483648
cpu_shares: 200
io_weight: 200
max_pids: 256
storage_quota_mb: 4096
inode_quota: 100000
network:
mode: Gated
allowed_domains:
- pypi.org
- files.pythonhosted.org
- registry.npmjs.org
- crates.io
- static.crates.io
- github.com
behavioral:
max_deletions: 100
max_reads_per_minute: 2000
credential_access_alert: trueA minimal agent for resource-constrained edge devices:
name: edge-minimal
description: Minimal profile for edge devices with 4GB RAM.
fail_mode: fail-safe-state
filesystem:
read_allowlist:
- /usr/share
- /usr/lib
write_allowlist: []
denylist:
- /etc
- /root
- /home
- /boot
exec_allowlist:
- /usr/bin/python3
resource_limits:
memory_bytes: 134217728
cpu_shares: 25
io_weight: 25
max_pids: 8
storage_quota_mb: 64
inode_quota: 500
network:
mode: Blocked
allowed_domains: []
behavioral:
max_deletions: 5
max_reads_per_minute: 50
credential_access_alert: trueEnsure you are using the exact field names documented above. Common mistakes:
read_allowinstead ofread_allowlistmemoryinstead ofmemory_bytescpuinstead ofcpu_shares
Check that the paths in read_allowlist are correct and that no parent
path is in the denylist. Denylists take precedence over allowlists.
Verify the executable path is in exec_allowlist. Use the full absolute
path. Check that the binary exists at that path on the system (not a
symlink to a different location).
Ensure the exact domain is listed in allowed_domains. Subdomains are
not automatically included -- github.com does not grant access to
api.github.com. List each domain explicitly.
Profiles can declare enforcement requirements that puzzled verifies at
branch creation time. If the host does not meet the requirements, branch
creation fails with an error rather than silently degrading.
enforcement_requirements:
landlock_abi: 4 # Minimum Landlock ABI version
seccomp_user_notif: true # Require seccomp USER_NOTIF support
bpf_lsm: true # Require BPF LSM
selinux: true # Require SELinux in enforcing mode
xfs_quotas: true # Require XFS project quotas
fanotify_fid: true # Require fanotify FAN_REPORT_FID supportAll fields are optional. Omitted fields are not checked. This is useful for safety-critical profiles that must not run with degraded enforcement.
Profiles operate independently of trust tiers. A profile defines the maximum access an agent can have; trust tiers may restrict access further in the future. Currently:
- Trust tier transitions emit D-Bus signals and update JWT-SVID claims
- Dynamic Landlock/seccomp tightening based on trust tier is planned
- Operators can subscribe to
TrustTransitionsignals and manually switch an agent to a more restrictive profile on demotion
puzzled(8)-- Governance daemonpuzzlectl(1)-- CLI management toolpuzzled.conf(5)-- Daemon configurationpuzzlepod-profile(5)-- Profile YAML format reference (man page)docs/security-guide.md-- Trust scoring and attestation chain detailsdocs/admin-guide.md-- Trust management and workload identity