Skip to main content
Loom Core docs

Overview

Enterprise Security Features

Loom Core provides five enterprise security features configurable in the daemon config file (~/.config/loom/config.yaml). All features are disabled by default and activate independently.

For shipped/in-progress status, see docs/IMPLEMENTATION_STATUS.md.

Overview

FeatureConfig KeyPurpose
RBACrbacRestrict tool access per agent role
Rate Limitingrbac.rate_limitsCap per-agent/per-tool call volume at gateway
Audit TrailauditAppend-only JSONL log of all tool calls
Cost TrackingcostUsage attribution by agent, server, and tool
OAuth 2.1http.oauth + http.auth.type: oauth2Standards-based authorization for remote access

RBAC (Role-Based Access Control)

RBAC restricts which tools each agent can invoke. The proxy evaluates access before forwarding tool calls.

Configuration

rbac:
  enabled: true
  default_policy: deny          # "allow" or "deny" when no role matches
  global_deny:
    - "server_mgmt__*"          # Organization-wide hard block (all agents/roles)
  rate_limits:
    - agent_id: "codex-prod"
      server: "github"
      tool: "list_*"
      requests_per_minute: 120
  roles:
    developer:
      allow:
        - "git__*"              # All git tools
        - "github__*"           # All GitHub tools
        - "codebase_memory__*"  # Code search
      deny:
        - "k8s__k8s_apply"     # Block destructive K8s ops
    readonly:
      allow:
        - "*__list_*"          # Any list operation
        - "*__get_*"           # Any get operation
        - "*__search_*"        # Any search operation
  bindings:
    - agent_id: "claude-code"
      role: developer
    - agent_type: "codex"       # All Codex agents
      role: readonly
    - agent_id: "*"             # Wildcard fallback
      role: readonly

Binding Resolution

Bindings resolve in priority order:

  1. Exact agent_id match
  2. agent_type match
  3. Wildcard agent_id: "*"

Access Decision

Tools are qualified as server__tool (e.g., git__git_status). The enforcer evaluates:

  1. Check global_deny patterns first (always deny, before role lookup)
  2. Resolve agent's role via bindings
  3. Check role deny patterns (deny wins)
  4. Check role allow patterns (or default_policy when no binding matches)
  5. If the call is allowed, apply the first matching rate_limits rule (if configured)

Pattern matching uses glob syntax (path.Match): * matches any sequence within a segment.

Audit Trail

The audit logger writes a structured JSONL entry for every tool call through the proxy.

Configuration

audit:
  enabled: true
  log_path: /var/log/loom/audit.jsonl   # Default: ~/.config/loom/audit.jsonl

Entry Format

Each line is a JSON object:

{
  "timestamp": "2026-02-16T14:30:00Z",
  "agent_id": "claude-code",
  "agent_type": "claude-code",
  "server": "git",
  "tool": "git_status",
  "duration_ms": 45,
  "status": "success",
  "target": "local",
  "cached": false
}
FieldTypeDescription
timestampstringISO 8601 UTC timestamp
agent_idstringAgent identifier
agent_typestringAgent platform (claude-code, codex, gemini)
serverstringMCP server name
toolstringTool name
duration_msintCall duration in milliseconds
statusstringsuccess, error, or denied
errorstringError message (if status is error)
targetstringlocal or hub
cachedboolWhether response came from cache

SIEM Integration

The JSONL format integrates with standard log ingestion pipelines:

# Stream to Loki via promtail
tail -f ~/.config/loom/audit.jsonl | promtail --stdin

# Query with jq
cat ~/.config/loom/audit.jsonl | jq 'select(.status == "denied")'

# Count calls per agent
cat ~/.config/loom/audit.jsonl | jq -s 'group_by(.agent_id) | map({agent: .[0].agent_id, count: length})'

Cost Tracking

The cost tracker aggregates tool call usage by agent, server, and tool. It tracks call counts, error rates, byte volumes, and durations.

Configuration

cost:
  enabled: true

Usage Records

Each tool call produces a UsageRecord with:

FieldDescription
agent_idCalling agent
serverMCP server
toolTool name
duration_msCall duration
request_bytesRequest payload size
response_bytesResponse payload size
statussuccess, error, denied, or cached

Snapshot Method

Query aggregated usage via daemon JSON-RPC method loom/cost-stats (for example, from a proxy client or internal HUD/bridge integration).

This is not a standalone REST endpoint path.

Response structure:

{
  "timestamp": "2026-02-16T14:30:00Z",
  "by_agent": [
    {
      "agent_id": "claude-code",
      "call_count": 142,
      "error_count": 3,
      "total_duration_ms": 45200,
      "total_request_bytes": 28400,
      "total_response_bytes": 156000
    }
  ],
  "by_server": [
    {
      "server": "git",
      "call_count": 89,
      "error_count": 1
    }
  ],
  "totals": {
    "call_count": 284,
    "error_count": 5,
    "denied_count": 2,
    "cached_count": 41
  }
}

OAuth 2.1 Authorization Server

The built-in OAuth 2.1 server enables standards-based authorization for remote MCP access. It implements PKCE (S256), dynamic client registration, and token revocation.

Configuration

Enable OAuth endpoints under http.oauth and set HTTP auth mode to oauth2:

http:
  auth:
    type: oauth2
  oauth:
    enabled: true
    issuer: https://loom.example.com     # Default: derived from --http-addr
    token_ttl_minutes: 60                # Access token lifetime (default: 60)
    auth_code_ttl_seconds: 600           # Authorization code lifetime (default: 600)
    allow_dynamic_registration: true     # RFC 7591 dynamic registration (default: true)

Endpoints

PathMethodStandardDescription
/.well-known/oauth-authorization-serverGETRFC 8414Authorization server metadata
/.well-known/oauth-protected-resourceGETRFC 9728Protected resource metadata
/oauth2/registerPOSTRFC 7591Dynamic client registration
/oauth2/authorizeGETRFC 6749Authorization endpoint (auto-approves)
/oauth2/tokenPOSTRFC 6749Token exchange (PKCE S256 required)
/oauth2/revokePOSTRFC 7009Token revocation

PKCE Flow

# 1. Register a client
curl -X POST https://loom.example.com/oauth2/register \
  -H "Content-Type: application/json" \
  -d '{"redirect_uris": ["http://localhost:9999/callback"], "client_name": "my-agent"}'
# Returns: client_id, client_secret

# 2. Generate PKCE verifier and challenge
VERIFIER=$(openssl rand -base64 32 | tr -d '=+/' | head -c 43)
CHALLENGE=$(echo -n "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_')

# 3. Request authorization code
curl -G "https://loom.example.com/oauth2/authorize" \
  --data-urlencode "response_type=code" \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "redirect_uri=http://localhost:9999/callback" \
  --data-urlencode "code_challenge=$CHALLENGE" \
  --data-urlencode "code_challenge_method=S256" \
  --data-urlencode "scope=mcp"
# Redirects to: http://localhost:9999/callback?code=AUTH_CODE

# 4. Exchange code for token
curl -X POST https://loom.example.com/oauth2/token \
  -d "grant_type=authorization_code" \
  -d "code=$AUTH_CODE" \
  -d "client_id=$CLIENT_ID" \
  -d "redirect_uri=http://localhost:9999/callback" \
  -d "code_verifier=$VERIFIER"
# Returns: access_token, token_type, expires_in

# 5. Use token for MCP calls
curl -X POST https://loom.example.com/mcp \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Token Revocation

curl -X POST https://loom.example.com/oauth2/revoke \
  -d "token=$ACCESS_TOKEN"

Full Configuration Example

All enterprise features enabled:

# ~/.config/loom/config.yaml
http:
  session_timeout_minutes: 30
  tls_cert_file: /etc/loom/cert.pem
  tls_key_file: /etc/loom/key.pem
  auth:
    type: oauth2
  oauth:
    enabled: true
    token_ttl_minutes: 60
    allow_dynamic_registration: true

rbac:
  enabled: true
  default_policy: deny
  global_deny:
    - "server_mgmt__*"
    - "k8s__k8s_apply"
  rate_limits:
    - agent_type: "codex"
      server: "github"
      tool: "list_*"
      requests_per_minute: 120
    - agent_id: "admin-agent"
      server: "*"
      tool: "*"
      requests_per_minute: 600
  roles:
    full:
      allow: ["*"]
    readonly:
      allow: ["*__list_*", "*__get_*", "*__search_*"]
    developer:
      allow: ["git__*", "github__*", "codebase_memory__*"]
      deny: ["k8s__k8s_apply", "docker__docker_exec"]
  bindings:
    - agent_id: "admin-agent"
      role: full
    - agent_type: "claude-code"
      role: developer
    - agent_id: "*"
      role: readonly

audit:
  enabled: true
  log_path: /var/log/loom/audit.jsonl

cost:
  enabled: true