Skip to content

Latest commit

 

History

History
444 lines (333 loc) · 15.3 KB

File metadata and controls

444 lines (333 loc) · 15.3 KB

Architecture

This document covers the system design, request lifecycle, security model, database schema, and API reference.

System Components

┌─────────────────────────────────────────────────────────┐
│                        VPS                              │
│                                                         │
│  ┌──────────────┐                                       │
│  │  AI Agent /   │                                      │
│  │  User shell   │                                      │
│  │               │                                      │
│  │  rm -rf /var  │                                      │
│  └──────┬───────┘                                       │
│         │ execv                                          │
│         ▼                                               │
│  ┌──────────────┐                                       │
│  │   safe-rm    │  Python script at /usr/local/bin/rm   │
│  │              │                                       │
│  │  1. Parse args                                       │
│  │  2. Check session                                    │
│  │  3. Assess risk                                      │
│  │  4. POST to API (if risky)                           │
│  │  5. Poll for status                                  │
│  │  6. exec real rm (if approved)                       │
│  └──────────────┘                                       │
└─────────────────────────────────────────────────────────┘
            │ HTTPS
            ▼
┌─────────────────────────────────────────────────────────┐
│                   Approval Server                       │
│                                                         │
│  ┌──────────────┐     ┌────────────────┐                │
│  │  Express.js  │     │   SQLite DBs   │                │
│  │              │     │                │                │
│  │  /api/delete-│────►│ delete_requests│                │
│  │   requests   │     │                │                │
│  │              │     │ claude_events  │                │
│  │  /api/claude-│────►│                │                │
│  │   events     │     │ claude_auto_   │                │
│  │              │     │  approve       │                │
│  └──────┬───────┘     └────────────────┘                │
│         │ POST (fire-and-forget)                        │
└─────────┼───────────────────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────┐
│                        n8n                              │
│                                                         │
│  ┌──────────────┐     ┌────────────────┐                │
│  │   Webhook    │────►│  Send Email    │                │
│  │   Trigger    │     │  (SMTP/Gmail)  │                │
│  └──────────────┘     └────────────────┘                │
└─────────────────────────────────────────────────────────┘
          │
          ▼
      User's Inbox
    [Approve] [Deny]
          │
          │ Click (GET request with token)
          ▼
      Approval Server
      updates status

Request Lifecycle: Delete Approval

1. Command interception

When rm is invoked, the shell resolves /usr/local/bin/rm (the safe-rm symlink) before /bin/rm because /usr/local/bin is first in $PATH.

safe-rm separates its own flags (--dry-run, --config, etc.) from the arguments intended for rm, then:

  1. If no arguments are given, it passes through to the real rm immediately.
  2. If the session is not guarded (based on IP filtering), it passes through.
  3. It assesses risk on the remaining arguments.

2. Risk assessment

The assess_risk function checks:

  • Hard-blocked paths: /, ~, ., .. -- immediately blocked with exit code 1, no API call.
  • Flags: -r/-R/--recursive and -f/--force are risk factors.
  • Glob expansion: Patterns like *.txt are expanded and each result is checked.
  • Protected paths: Each resolved path is checked against the configured protected path list.
  • Safe pattern exemption: If all resolved paths match a safe pattern (e.g., /tmp/build-*), the command is allowed through.
  • File count: For recursive deletes, the total file count is tallied (capped at 1000). If it exceeds FILE_THRESHOLD, it is a risk factor.

If no risk factors remain after safe pattern filtering, the command passes through to the real rm.

3. Approval request

The client constructs a JSON payload:

{
  "request_id": "del_1700000000_12345",
  "command": "rm -rf /var/www/data",
  "risk_reasons": ["recursive deletion (-r/-R)", "protected path: /var/www"],
  "affected_paths": ["/var/www/data"],
  "file_count": 47,
  "user": "deploy",
  "hostname": "prod-01",
  "cwd": "/home/deploy",
  "timestamp": 1700000000
}

The payload is signed with HMAC-SHA256 using the shared secret. The signature is sent in the X-SafeRM-Signature header. The server verifies the signature before processing.

4. Server processing

The server:

  1. Verifies the HMAC signature (timing-safe comparison).
  2. Generates two random 32-byte tokens: one for approve, one for deny.
  3. Stores the request in SQLite with a TTL (default: 10 minutes).
  4. Constructs approve/deny URLs containing the request ID and token.
  5. Fires a webhook POST to n8n with the request details and URLs.
  6. Returns 201 Created with the request ID.

5. Email notification

n8n receives the webhook, formats an HTML email with the command details and action buttons (Approve, Deny, Stop Session), and sends it to the configured admin address.

6. User action

The user clicks a link in the email. The server:

  1. Looks up the request by ID.
  2. Verifies the token (timing-safe comparison against the stored token).
  3. Checks that the token has not been used and the request has not expired.
  4. Updates the status to approved or denied and marks the token as used.
  5. Returns an HTML confirmation page.

7. Client resolution

The client polls GET /api/delete-requests/{id}/status every POLL_INTERVAL seconds. When the status changes from pending:

  • approved: The client calls os.execv to execute the real /bin/rm with the original arguments.
  • denied: The client exits with code 1.
  • expired: The client exits with code 1.
  • stopped: The client prints a stop message and exits with code 130 (simulating SIGINT).

If POLL_TIMEOUT is reached with no response, the client exits with code 1.


Security Model

HMAC-SHA256 request signing

Every request from the client to the server includes an HMAC-SHA256 signature computed over the JSON-serialized payload (keys sorted, no extra whitespace). The server recomputes the signature and compares using crypto.timingSafeEqual. This ensures:

  • Requests cannot be forged without the shared secret.
  • Payloads cannot be tampered with in transit (integrity).

One-time-use tokens

Each approval request generates two cryptographically random tokens (32 bytes each). The approve token and deny token are stored in the database and embedded in the email URLs. Once a token is used, the used_token flag is set and the link cannot be reused.

Timing-safe comparison

All token comparisons use crypto.timingSafeEqual to prevent timing side-channel attacks. If the token lengths differ or decoding fails, the comparison returns false.

Request expiry

Pending requests expire after APPROVAL_TIMEOUT_SECONDS (default: 600 seconds). The server checks expiry on every status poll and on approve/deny link access. Expired pending requests are cleaned up from the database periodically (every 60 seconds, with a 1-hour grace period).

Hard-blocked paths

The paths /, ~, ., and .. are unconditionally blocked on the client side. No API request is made. The client exits with code 1 immediately.

XSS prevention

All user-supplied data rendered in HTML response pages is escaped via a dedicated escapeHtml function that replaces &, <, >, ", and '.

Rate limiting

The server applies a per-IP rate limit (default: 100 requests/minute) using express-rate-limit.

TLS

The server should be deployed behind a TLS-terminating reverse proxy (nginx, Caddy). The example nginx config enforces HTTPS with TLS 1.2+.


Database Schema

The server uses two SQLite databases stored in DATA_DIR.

delete_approvals.db

Table: delete_requests

Column Type Description
id TEXT PK Request ID (e.g., del_1700000000_12345)
command TEXT Full rm command string
risk_reasons TEXT JSON array of risk reason strings
affected_paths TEXT JSON array of resolved absolute paths (max 50)
file_count INTEGER Number of affected files
user TEXT Unix username
hostname TEXT Server hostname
cwd TEXT Working directory at time of command
status TEXT pending, approved, denied
created_at INTEGER Unix timestamp
expires_at INTEGER Unix timestamp
approved_by TEXT Who approved/denied (currently always admin)
approved_at INTEGER Unix timestamp of resolution
approve_token TEXT 64-char hex token for approval
deny_token TEXT 64-char hex token for denial
used_token INTEGER 0 or 1 -- whether any token has been used

claude_events.db

Table: claude_events

Column Type Description
id TEXT PK Event ID
event_type TEXT Event type (e.g., tool_approval)
session_id TEXT Claude Code session identifier
tool_name TEXT Name of the tool being called
tool_input TEXT Tool input (JSON string)
status TEXT pending, approved, denied, responded, expired
approve_token TEXT 64-char hex token
deny_token TEXT 64-char hex token
response_token TEXT 64-char hex token for custom text responses
response_text TEXT Custom response text (if responded)
auto_approve_until INTEGER Unix timestamp if auto-approve was used
created_at INTEGER Unix timestamp
expires_at INTEGER Unix timestamp
resolved_at INTEGER Unix timestamp of resolution
resolved_by TEXT email or auto_approve

Table: claude_auto_approve

Column Type Description
session_id TEXT PK Claude Code session identifier
until INTEGER Unix timestamp when auto-approve expires
created_at INTEGER Unix timestamp

Both databases use WAL journal mode for concurrent read/write performance.


API Reference

Health Check

GET /health

Response 200:

{ "status": "ok", "uptime": 12345.67 }

Delete Approval System

Create a delete request

POST /api/delete-requests

Headers:

  • X-SafeRM-Signature: HMAC-SHA256 signature of the JSON body

Body:

{
  "request_id": "del_1700000000_12345",
  "command": "rm -rf /var/www/data",
  "risk_reasons": ["recursive deletion (-r/-R)"],
  "affected_paths": ["/var/www/data"],
  "file_count": 47,
  "user": "deploy",
  "hostname": "prod-01",
  "cwd": "/home/deploy",
  "timestamp": 1700000000
}

Response 201:

{
  "success": true,
  "request_id": "del_1700000000_12345",
  "approval_url": "https://safe-rm.example.com/api/delete-requests/approve/del_1700000000_12345/abc123...",
  "expires_at": 1700000600
}

Poll request status

GET /api/delete-requests/:id/status

Response 200:

{ "status": "pending", "approved_by": null }

Status values: pending, approved, denied, expired.

Approve a request (email link)

GET /api/delete-requests/approve/:id/:token

Returns an HTML confirmation page. The token must match the stored approve token. One-time use.

Deny a request (email link)

GET /api/delete-requests/deny/:id/:token

Returns an HTML confirmation page. The token must match the stored deny token. One-time use.

List requests (admin)

GET /api/delete-requests?status=pending&limit=50

Response 200:

{
  "success": true,
  "requests": [{ "id": "...", "command": "...", "status": "pending", ... }]
}

Claude Code Hook System

Create a Claude event

POST /api/claude-events

Headers:

  • X-Claude-Signature: HMAC-SHA256 signature of the JSON body

Body:

{
  "event_id": "evt_abc123",
  "event_type": "tool_approval",
  "session_id": "session-xyz",
  "tool_name": "Bash",
  "tool_input": "rm -rf /tmp/build",
  "timestamp": "2024-01-01T00:00:00Z"
}

Response 201:

{ "success": true, "event_id": "evt_abc123", "expires_at": 1700000600 }

If the session has an active auto-approve window:

Response 200:

{ "success": true, "event_id": "evt_abc123", "status": "auto_approved", "auto_approved": true }

Poll event status

GET /api/claude-events/:id/status

Response 200:

{ "status": "pending", "response_text": null, "resolved_by": null }

Status values: pending, approved, denied, responded, expired.

Approve an event (email link)

GET /api/claude-events/approve/:id/:token

Deny an event (email link)

GET /api/claude-events/deny/:id/:token

Send a custom response (form)

GET /api/claude-events/respond/:id/:token

Renders an HTML form. Submitting the form sends a POST:

POST /api/claude-events/respond/:id/:token
Content-Type: application/x-www-form-urlencoded

response=Your+message+here

Sets status to responded with the custom text in response_text.

Enable auto-approve

GET /api/claude-events/auto-approve/:id/:token/:minutes
  • Approves the current event.
  • Sets auto-approve for the session for :minutes minutes (1--60, clamped).
  • Subsequent events from the same session are auto-approved without email.

List events (admin)

GET /api/claude-events?status=pending&session_id=xyz&limit=50

Response 200:

{
  "success": true,
  "events": [{ "id": "...", "tool_name": "...", "status": "pending", ... }]
}