Skip to main content

Security & Permissions

Penguin includes a comprehensive permission and policy engine that controls what operations the AI agent can perform. This system provides granular control over file system access, process execution, network operations, and more.

Overview​

The security system operates on three core principles:

  1. Defense in Depth: Multiple layers of checks (mode → policy → path validation)
  2. Least Privilege: Agents start with minimal permissions and can be granted more
  3. Transparent Auditing: All permission decisions are logged for debugging and compliance

Security Modes​

Penguin supports three security modes that provide different levels of access:

read_only Mode​

The most restrictive mode. The agent can only read files and cannot modify the file system.

Allowed operations:

  • filesystem.read - Read file contents
  • filesystem.list - List directory contents
  • memory.* - All memory operations

Blocked operations:

  • All write, delete, and execute operations
  • Git operations (except read-only ones)
  • Network requests

workspace Mode (Default)​

A balanced mode that restricts file operations to the workspace and project directories.

Allowed operations:

  • All read operations
  • Write/delete within workspace and project roots
  • Process execution (with approval for dangerous commands)
  • Git operations within project

Blocked operations:

  • File operations outside workspace/project boundaries
  • System directory access (/etc, /usr, ~/.ssh, etc.)
  • Force push and destructive git operations (require approval)

full Mode​

Minimal restrictions for trusted environments. Use with caution.

Allowed operations:

  • Most operations allowed by default
  • System directories still protected

Operations requiring approval:

  • git.force - Force push
  • process.execute - For potentially dangerous commands

Configuration​

Basic Configuration​

Add security settings to your config.yml:

security:
# Security mode: read_only, workspace, or full
mode: workspace

# Enable/disable permission checks entirely
enabled: true

# Additional allowed paths (beyond workspace/project)
allowed_paths:
- /path/to/shared/resources
- ~/Documents/reference

# Explicitly denied paths (overrides allowed)
denied_paths:
- ~/.ssh
- ~/.aws
- /etc
- /usr

# Operations that always require user approval
require_approval:
- filesystem.delete
- git.push
- git.force

Environment Variables​

Security can be controlled via environment variables:

# Disable all permission checks (YOLO mode - use with caution!)
PENGUIN_YOLO=true

Note: security.mode itself is set from the YAML security: block or changed at runtime via the API / RuntimeConfig — there is no PENGUIN_SECURITY_MODE environment variable.

Runtime Configuration​

Security settings can be changed at runtime via the Web API or RuntimeConfig (set_security_mode, set_security_enabled):

# Via API
curl -X PATCH http://127.0.0.1:9000/api/v1/security/config \
-H "Content-Type: application/json" \
-d '{"mode": "read_only"}'

# Toggle YOLO mode (disables permission checks)
curl -X POST "http://127.0.0.1:9000/api/v1/security/yolo?enable=true"

# Read current security config
curl http://127.0.0.1:9000/api/v1/security/config

The /config slash command persists configuration keys (e.g. /config set security.mode read_only), but there is no /config runtime subcommand — security changes take effect immediately via the API endpoints above, or after a restart for YAML changes.

Operation Taxonomy​

The permission system uses a standardized operation taxonomy:

CategoryOperationsDescription
filesystemread, write, delete, list, create_dirFile system operations
processexecute, background, killProcess management
networkrequest, download, uploadNetwork operations
gitread, write, commit, push, forceGit operations
memoryread, write, delete, searchMemory system operations

Tool to Operation Mapping​

Each tool is mapped to one or more operations:

TOOL_OPERATION_MAP = {
"read_file": [Operation.FILESYSTEM_READ],
"write_file": [Operation.FILESYSTEM_WRITE],
"bash": [Operation.PROCESS_EXECUTE],
"git_push": [Operation.GIT_PUSH],
# ... etc
}

Approval Flow​

When an operation returns ASK, the user must approve it before execution.

Web Interface​

In the web interface, approval requests appear as interactive prompts:

{
"event": "approval_required",
"data": {
"request_id": "apr_abc123",
"tool_name": "bash",
"operation": "process.execute",
"resource": "rm -rf ./build",
"reason": "Destructive command requires approval"
}
}

API Approval​

Approve or deny requests programmatically:

# Approve a request
curl -X POST http://127.0.0.1:9000/api/v1/approvals/apr_abc123/approve \
-H "Content-Type: application/json" \
-d '{"scope": "once"}'

# Deny a request
curl -X POST http://127.0.0.1:9000/api/v1/approvals/apr_abc123/deny

# Pre-approve an operation pattern
curl -X POST http://127.0.0.1:9000/api/v1/approvals/pre-approve \
-H "Content-Type: application/json" \
-d '{
"operation": "filesystem.delete",
"pattern": "./build/*",
"session_id": "sess_xyz"
}'

Approval Scopes​

When approving a request, you can specify the scope:

  • once - Approve this single request only
  • session - Approve similar operations for the current session
  • pattern - Approve operations matching a resource pattern

Multi-Agent Permissions​

When using multiple agents, each agent can have its own permission configuration.

Agent Permission Configuration​

Define permissions in your agent personas:

# In config.yml
agents:
code-reviewer:
persona: "Code Review Expert"
permissions:
mode: read_only
operations:
- filesystem.read
- memory.read
allowed_paths:
- ./src
- ./tests

deployment-agent:
persona: "Deployment Specialist"
permissions:
mode: workspace
operations:
- filesystem.read
- filesystem.write
- process.execute
- git.push
require_approval:
- git.force

Permission Inheritance​

Sub-agents inherit permissions from their parent, with refinement:

Parent (workspace mode, all operations)
└── Child (can only have equal or more restrictive permissions)
- Can reduce to read_only mode
- Can remove operations from allowed list
- Cannot add operations parent doesn't have
- Cannot access paths parent can't access

Programmatic Agent Permissions​

from penguin.multi import MultiAgentCoordinator

coordinator = MultiAgentCoordinator()

# Spawn agent with restricted permissions
await coordinator.spawn_agent(
agent_id="restricted-helper",
persona="helper",
permissions={
"mode": "read_only",
"allowed_paths": ["./docs"],
"operations": ["filesystem.read", "memory.read"]
},
parent_agent_id="main-agent" # Inherits and refines parent permissions
)

Audit Logging​

All permission checks are logged for debugging and compliance.

Audit Configuration​

security:
audit:
enabled: true
log_file: ".penguin/permission_audit.log"

# Per-category verbosity
categories:
filesystem: all # Log everything
process: ask_and_deny # Log ASK and DENY only
network: deny_only # Log only denials
git: ask_and_deny
memory: off # No logging

# Memory buffer for API queries
max_memory_entries: 1000

# Include full context (may contain sensitive data)
include_context: false

Verbosity Levels​

  • off - No logging for this category
  • deny_only - Only log DENY results
  • ask_and_deny - Log ASK and DENY results
  • all - Log all permission checks (ALLOW, ASK, DENY)

CLI Commands​

# View current permission settings
penguin permissions list

# View recent audit log entries
penguin permissions audit -n 50

# Filter by result
penguin permissions audit --result deny

# View audit statistics
penguin permissions summary

API Endpoints​

# Get recent audit entries
curl "http://127.0.0.1:9000/api/v1/security/audit?limit=100&result=deny"

# Get audit statistics
curl http://127.0.0.1:9000/api/v1/security/audit/stats

Response:

{
"total": 1250,
"by_result": {
"allow": 1180,
"ask": 45,
"deny": 25
},
"by_category": {
"filesystem": 890,
"process": 200,
"git": 160
}
}

Custom Policies​

You can implement custom policies by extending the PolicyEngine base class:

from penguin.security import PolicyEngine, Operation, PermissionResult

class CustomSecurityPolicy(PolicyEngine):
"""Custom policy that restricts operations during business hours."""

def __init__(self):
super().__init__(name="business-hours-policy", priority=50)

def check_operation(
self,
operation: Operation,
resource: str,
context: dict
) -> tuple[PermissionResult, str]:
import datetime

hour = datetime.datetime.now().hour

# Restrict destructive operations outside business hours
if operation in [Operation.GIT_PUSH, Operation.FILESYSTEM_DELETE]:
if hour < 9 or hour > 17:
return (
PermissionResult.DENY,
"Destructive operations not allowed outside business hours (9 AM - 5 PM)"
)

# Defer to other policies
return (PermissionResult.ALLOW, "Within business hours")

Register your custom policy:

from penguin.security import PermissionEnforcer

enforcer = PermissionEnforcer(mode="workspace")
enforcer.add_policy(CustomSecurityPolicy())

Path Security​

The permission system includes robust path validation:

Path Traversal Detection​

Prevents ../ attacks and symbolic link escapes:

from penguin.security import validate_path_security

# This will raise PathTraversalError
validate_path_security(
path="../../../etc/passwd",
boundaries=["/home/user/workspace"]
)

# This will raise SymlinkEscapeError if symlink points outside boundaries
validate_path_security(
path="./link-to-secrets",
boundaries=["/home/user/workspace"],
check_symlinks=True
)

Sensitive Path Protection​

These paths are always protected regardless of mode:

  • ~/.ssh - SSH keys
  • ~/.aws - AWS credentials
  • ~/.gnupg - GPG keys
  • /etc - System configuration
  • /usr - System binaries
  • Environment files containing secrets

Best Practices​

Development Environment​

security:
mode: workspace
enabled: true
audit:
enabled: true
categories:
filesystem: all # Full visibility during development

Production Environment​

security:
mode: read_only # or workspace with strict boundaries
enabled: true
denied_paths:
- ~/.ssh
- ~/.aws
- /etc
- /var
require_approval:
- filesystem.delete
- process.execute
- git.push
audit:
enabled: true
log_file: /var/log/penguin/permissions.log
categories:
filesystem: ask_and_deny
process: all
network: all

CI/CD Pipelines​

security:
mode: workspace
enabled: true
allowed_paths:
- ./ # Current directory only
require_approval: [] # No interactive approval in CI
audit:
enabled: true
categories:
filesystem: deny_only
process: deny_only

Troubleshooting​

Common Issues​

Operation unexpectedly denied:

  1. Check audit log: penguin permissions audit --result deny
  2. Verify security mode: penguin permissions list
  3. Check path boundaries in config

Approval flow not working:

  1. Ensure WebSocket connection is active
  2. Check for approval_required events in browser console
  3. Verify session ID matches

Agent permissions not applying:

  1. Check agent is registered: GET /api/v1/agents
  2. Verify permissions block in config
  3. Check parent agent permissions (inheritance rules)

Debug Logging​

Enable verbose permission logging:

security:
audit:
enabled: true
categories:
filesystem: all
process: all
network: all
git: all
memory: all
include_context: true # Warning: may log sensitive data

Then check logs:

tail -f .penguin/permission_audit.log | jq .

Web Server Hardening Notes​

Provider Credential Precedence​

Penguin now prefers operator-managed environment variables for provider credentials. Legacy plaintext JSON credential files remain a compatibility fallback only.

Precedence order:

  1. Environment variables (recommended for CI, containers, servers, and headless usage)
  2. Legacy local JSON credential store (deprecated fallback)
  3. Future local-only secret backends such as OS keyring integrations may be added later

Important notes:

  • Environment variables are the recommended default because they work consistently across platforms and deployment targets.
  • The legacy JSON provider credential store is still readable for compatibility, but Penguin emits warnings when it is used.
  • Plaintext credential persistence should not be treated as the primary security posture for production deployments.

GitHub Webhook Replay Defense​

Penguin's GitHub webhook endpoint now rejects duplicate X-GitHub-Delivery IDs using a process-local in-memory TTL cache.

When HTTP auth is enabled, the webhook route must also be intentionally exposed (for example via PENGUIN_PUBLIC_ENDPOINTS=/api/v1/integrations/github/webhook) or otherwise fronted by an authenticated relay, because GitHub will not send Penguin API keys.

This is effective for a single-process / single-instance deployment. It is not sufficient for multi-instance deployments because delivery IDs are not shared across processes or hosts.

For horizontally scaled deployments, move replay tracking to shared state such as Redis or another low-latency shared cache.