# ADR-078: Paperclip AI Platform Isolation — Separate DB Role, Zero lm360 Access, Independent Backups

## Status

**Accepted** (2026-06-27)

## Status History

```yaml
- 2026-06-27:
    status: Accepted
    decision_maker: hkl
    rationale: Paperclip installed with data isolation confirmed; policy documented to enforce separation in future integrations
```

## Context

Paperclip is an AI agent orchestration platform (distinct product, separate from 360lm) installed on the same VPS (srv1111289.hstgr.cloud) as 360lm PWAs. It runs in its own Docker container (`paperclip`) and database role (`paperclip` in the shared PostgreSQL instance).

### Current Infrastructure

- **Container:** `paperclip` (image: `paperclip:local` from `/opt/paperclip`), port 3100
- **Network:** `root_default` (same Docker network as 360lm services)
- **PostgreSQL role:** `paperclip` (no superuser, no createdb, no replication)
- **Database:** `paperclip` (separate from `lm360` and `lm360_prod`)
- **Data volume:** `paperclip_data` (independent)
- **Subdomain:** `https://paperclip.srv1111289.hstgr.cloud` (Traefik labels in `/opt/paperclip/docker-compose.yml`)
- **Authentication:** Traefik basic auth (Harishlal / password in compose labels) + Paperclip's own private-mode setup

### Isolation Attempt and Findings

Early isolation attempts attempted to REVOKE database-level CONNECT on lm360 and lm360_prod from the paperclip role. PostgreSQL's PUBLIC default grants at pg_hba level override individual revokes. Real isolation enforced at schema/table level: the `paperclip` role can technically connect but sees/reads nothing (permission denied for all schemas in lm360).

### Operational Characteristics

- **Internal backups:** Paperclip runs its own auto-backup every 60 minutes to `/paperclip/instances/default/data/backups` (inside the volume)
- **External backups:** Separate cron job (`/usr/local/bin/paperclip-backup.sh`) at 3 AM daily; stores SQL dump + files tarball in `/backup/` with 14-day retention
- **API key sharing:** Currently shares `ANTHROPIC_API_KEY` with OpenClaw and PWA proxies (operational simplicity; no dedicated key yet)
- **First-run:** Requires UI onboarding (board user creation, first company) before agents are fully operational

### Why Paperclip Is Separate from 360lm

Paperclip is a general-purpose AI agent platform that may be used for non-360lm tasks (internal consulting, personal automation, experimental workflows). 360lm PWAs and their data are vertical business applications with regulated access control (PIN auth, role-based grants). Mixing the two would:

1. Risk accidental leakage of sensitive 360lm data to Paperclip projects
2. Blur responsibility boundaries for backup/restore and disaster recovery
3. Make it hard to decommission or repurpose Paperclip without affecting 360lm

## Decision

**Paperclip is isolated from 360lm data by architectural design. This isolation is enforced via separate PostgreSQL role, zero cross-schema grants, and independent backup schedules.**

1. The `paperclip` PostgreSQL role has ZERO access to any `lm360` or `lm360_prod` schemas or tables
2. Paperclip runs its own 60-minute internal backups; the external backup cron is independent of lm360's schedule
3. Shared `ANTHROPIC_API_KEY` is acceptable for operational simplicity now; if future usage patterns show conflicts (quota exhaustion, billing confusion), a dedicated key will be provisioned
4. When Paperclip needs 360lm data in the future, the correct pattern is an explicit API endpoint (proxy RPC or HTTP endpoint), not direct database access
5. Paperclip's Traefik routing and Docker container orchestration follow the same patterns as other 360lm services, but data never crosses the boundary

## Implementation Notes

### PostgreSQL Isolation

```sql
-- paperclip role: created without superuser, createdb, or replication privileges
CREATE ROLE paperclip WITH ENCRYPTED PASSWORD '<from-pgpass>' LOGIN;

-- Database ownership
CREATE DATABASE paperclip OWNER paperclip;

-- Explicit schema access denial for lm360 schemas
-- (schema-level REVOKE enforces isolation; database-level REVOKE is insufficient)
REVOKE USAGE ON SCHEMA public FROM paperclip;
REVOKE ALL ON ALL TABLES IN SCHEMA lm360 FROM paperclip;
REVOKE ALL ON ALL TABLES IN SCHEMA lm360_prod FROM paperclip;
-- ... per-schema, per-table as needed
```

**Important:** Isolation is enforced at schema/table level, not database level. Connection-time denial (database REVOKE) does not block the paperclip role from connecting; permission denial happens when it tries to query.

### Docker Container Setup

- Compose file: `/opt/paperclip/docker-compose.yml` (separate from `/root/360lm-web/docker-compose.yml`)
- Environment secrets: `/opt/paperclip/runtime/.env` (mode 600, root only)
- Start/stop: `docker compose -f /opt/paperclip/docker-compose.yml up -d` / `down`
- Restart single: `docker compose -f /opt/paperclip/docker-compose.yml restart`

### Backup Schedule

**Internal (Paperclip):** 60-minute intervals to `/paperclip/instances/default/data/backups` (volume-mounted)

**External (system cron):**
```bash
# /usr/local/bin/paperclip-backup.sh
# Scheduled: 0 3 * * * (3 AM daily)
# Outputs:
#   - /backup/paperclip-db-YYYY-MM-DD.sql.gz
#   - /backup/paperclip-files-YYYY-MM-DD.tar.gz
# Retention: 14 days
```

**Note:** Paperclip backups are independent of 360lm's backup schedule. No shared infrastructure.

### API Key Management

Currently, `ANTHROPIC_API_KEY` in `/opt/paperclip/runtime/.env` is shared with:
- OpenClaw (gateway service)
- Track proxy (event tracking)
- OCR proxy (vision inference)
- Dispatch AI proxy (address parsing)

**Future considerations:**
- If quota conflicts or billing ambiguity emerge, provision a dedicated key
- Document which service is using how much of the quota (if Anthropic's dashboard allows per-application segmentation)
- Rotate shared key on any compromise; restart affected services

### Traefik Labels (Routing)

Paperclip's `docker-compose.yml` includes:
```yaml
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.paperclip.rule=Host(`${WEB_DOMAIN}`)"  # checks subdomain separately
  - "traefik.http.routers.paperclip.entrypoints=websecure"
  - "traefik.http.routers.paperclip.tls.certresolver=mytlschallenge"
  - "traefik.http.services.paperclip.loadbalancer.server.port=3100"
  - "traefik.http.middlewares.paperclip-basicauth.basicauth.users=harishlal:..."  # password hash
  - "traefik.http.routers.paperclip.middlewares=paperclip-basicauth"
networks:
  - traefik-net  # external: root_default
```

## Alternatives Considered

### 1. Share the `lm360` database and schema with Paperclip (grants to Paperclip role)
**Rejected:**
- Violates data isolation principle for a general-purpose AI platform that may outlive 360lm
- Creates compliance risk if Paperclip is used for external consulting (client data leakage)
- Makes backup/restore strategy dependent on 360lm's schedule (operationally inflexible)
- Would require explicit schema design to distinguish Paperclip-only tables, adding complexity

### 2. Run Paperclip on a separate VPS
**Rejected:**
- Unnecessary infrastructure cost and operational overhead for a single-purpose tool on a startup budget
- Makes Paperclip updates and restarts independent, reducing coordination flexibility
- No security benefit (API key would still be in environment; network could still be compromised)
- Current single-VPS design is simpler and acceptable

### 3. Implement database-level isolation only (REVOKE CONNECT)
**Rejected:**
- PostgreSQL's PUBLIC grants at pg_hba level override individual REVOKE CONNECT
- The role can still connect; isolation happens at schema/table query time
- Appears isolated but is fragile (depends on all schemas and tables having explicit DENYs)
- Misleading: logs show "connected" but actual access is denied
- Real isolation: explicit schema/table REVOKE (current choice)

### 4. Dedicated API key for Paperclip now
**Rejected:**
- Premature optimization; current shared key works and simplifies rotation
- If future usage metrics show conflicts, separate key is easy to add
- Adds environment variable management complexity upfront

## Consequences

### Positive

1. **Data safety:** Paperclip cannot accidentally leak 360lm data (permission-denied at query time)
2. **Operational flexibility:** Paperclip can be updated, restarted, or decommissioned independently
3. **Backup independence:** Paperclip's schedule does not block or delay 360lm's production backup window
4. **Billing clarity (future):** Separate database usage can be queried independently
5. **Reuse:** Paperclip instance can be used for other projects or clients without touching 360lm

### Trade-Offs

1. **Data access (future):** If Paperclip needs 360lm data, explicit API endpoints must be built (no shortcut direct-DB queries)
2. **Complexity (minor):** Separate compose file, separate backups, separate env secrets require different tooling/runbooks
3. **Cost (minor):** Two database backups (lm360 + paperclip) instead of one, though retention is short (14 days for Paperclip)

### Risks and Mitigations

| Risk | Mitigation |
|---|---|
| PostgreSQL privilege inheritance or cascading grants breach isolation | Audit grants quarterly via `\dp` in psql; document any cross-schema grants as explicit exceptions with ADR justification |
| Paperclip's internal backup fails silently; external backup is the only copy | Monitor `/var/log/paperclip-backup.log` daily; set up log rotation and alert on backup script errors |
| Shared API key quota exhaustion impacts 360lm proxies | Monitor Anthropic API dashboard; if quota conflicts arise, split key and update env in both compose files |
| Paperclip accidental writes to lm360 (e.g., application bug) | Role has no INSERT/UPDATE/DELETE on lm360 tables, so permission error will be caught immediately; no silent corruption possible |

## Related Decisions

- **ADR-015 (Dev/Prod Two Stacks Same VPS):** Paperclip follows the same physical infrastructure model (single VPS, multiple services) but with stronger data isolation due to its general-purpose nature
- **ADR-073 (Traefik + Docker Labels):** Paperclip's routing uses the same Traefik pattern as all 360lm services
- **ADR-077 (OpenClaw Read-Only Access Pattern):** OpenClaw has read-only RPC grants to selected 360lm schemas (contrast: Paperclip has zero schema access)

## References

- **Memory:** `/root/.claude/projects/-var-www-360lm/memory/paperclip.md`
- **Memory:** `/root/.claude/projects/-var-www-360lm/memory/infra_vps.md`
- **Compose:** `/opt/paperclip/docker-compose.yml`
- **Backup script:** `/usr/local/bin/paperclip-backup.sh`
- **PostgreSQL role query:** `SELECT * FROM pg_roles WHERE rolname = 'paperclip';`
- **Paperclip installation docs:** `github.com/paperclipai/paperclip`

---

**Decision maker:** hkl  
**Changed via:** adr-kit (360lm)  
**Last reviewed:** 2026-06-27
