clavitor/clavis/clavis-vault/SPEC-editions.md

224 lines
6.8 KiB
Markdown

# Clavitor Edition System Specification
## Overview
Clavitor Vault has two editions with **build-time separation** — not runtime flags, not config files, not license keys. The edition is determined at compile time via Go build tags.
| Edition | Build Tag | Target User |
|---------|-----------|-------------|
| **Community** | (none, default) | Self-hosted individuals, privacy-first users |
| **Commercial** | `-tags commercial` | Managed service, enterprise, multi-POP deployments |
## Architecture
```
edition/
├── edition.go # Interface + shared types (always compiled)
├── community.go # Community implementation (go:build !commercial)
└── commercial.go # Commercial implementation (go:build commercial)
```
The `edition.Current` variable is set at `init()` time based on build tags. Application code never checks build tags directly — it uses `edition.Current.Name()` or calls methods on the interface.
## Build Commands
```bash
# Community Edition (default) - Elastic 2, self-hosted, no external calls
go build -o clavitor ./cmd/clavitor/
# Commercial Edition - managed service, telemetry, replication
go build -tags commercial -o clavitor ./cmd/clavitor/
```
## The Edition Interface
```go
type Edition interface {
// Name returns "community" or "commercial"
Name() string
// AlertOperator sends critical operational alerts
// Community: logs to stderr
// Commercial: POSTs to telemetry endpoint + logs
AlertOperator(ctx context.Context, alertType, message string, details map[string]any)
// IsTelemetryEnabled returns true if data goes to central servers
IsTelemetryEnabled() bool
// NotFoundHandler handles 404/405 routes
// Community: simple 404 (fast, clean shutdown)
// Commercial: tarpit (anti-scanner, interruptible on shutdown)
NotFoundHandler() http.HandlerFunc
}
```
## Feature Matrix
| Feature | Community | Commercial |
|---------|-----------|------------|
| **License** | Elastic License 2.0 | Commercial |
| **Telemetry** | None (unless user manually configures) | Enabled by default to clavitor.ai |
| **Operator Alerts** | Local logs only | POSTs to `/v1/alerts` endpoint |
| **Replication** | ❌ Not available | ✅ Real-time sync to backup POPs |
| **Tarpit** | ❌ Simple 404 | ✅ 30s anti-scanner drain |
| **Central Management** | ❌ None | ✅ Multi-POP dashboard |
| **SCIM/SIEM** | ❌ No | ✅ Yes |
## Commercial-Only Features (Detailed)
### 1. Real-Time Replication to Backup POPs
**File:** `edition/replication.go` (only compiled with `-tags commercial`)
Background worker that:
- Polls `EntryListUnreplicated()` for dirty entries
- POSTs encrypted entry blobs to backup POP (Calgary/Zurich)
- Automatic retry with exponential backoff
- Batching (up to 100 entries per request)
- Conflict resolution: last-write-wins
**Community:** No replication code exists in binary. `ReplicatedAt` field in DB is never used.
### 2. Tarpit (Anti-Scanner Defense)
**File:** `edition/commercial.go``NotFoundHandler()`
For 404/405 responses:
- Holds connection for 30 seconds
- Drips one byte per second
- Caps at 1000 concurrent tarpit connections
- **Interruptible:** Checks `edition.ShutdownContext` each iteration
**Community:** Returns HTTP 404 immediately. Clean, fast shutdown.
### 3. Centralized Telemetry & Alerting
**Files:** `edition/commercial.go`
- Periodic metrics POST to `TelemetryHost`
- Operator alerts POST to `/v1/alerts`
- Configured via `CommercialConfig` struct
**Community:** `AlertOperator()` logs locally only. No network calls.
## Code Patterns
### Checking Edition
```go
// DON'T do this:
// if edition.Current.Name() == "commercial" { ... }
// DO this - use interface methods:
edition.Current.AlertOperator(ctx, "error", "message", details)
```
The interface abstracts the behavior. Don't branch on the name unless absolutely necessary (e.g., UI hints).
### Adding Commercial-Only Features
1. **Define behavior in interface** (`edition/edition.go`):
```go
type Edition interface {
// ... existing methods ...
NewFeature() SomeType
}
```
2. **Implement in community** (`edition/community.go`):
```go
func (e *communityEdition) NewFeature() SomeType {
// Return no-op or simple implementation
return nil
}
```
3. **Implement in commercial** (`edition/commercial.go`):
```go
func (e *commercialEdition) NewFeature() SomeType {
// Full implementation
return &commercialFeature{...}
}
```
4. **Use in application code**:
```go
feature := edition.Current.NewFeature()
if feature != nil {
feature.DoSomething()
}
```
## Graceful Shutdown
Both editions support graceful shutdown via `edition.ShutdownContext`:
```go
// main.go sets this on startup
edition.ShutdownContext = shutdownCtx
// Commercial tarpit checks this:
select {
case <-edition.ShutdownContext.Done():
return // Exit immediately on SIGTERM
case <-time.After(time.Second):
// Continue dripping
}
```
**Community:** Shutdown is instant (no long-running handlers).
**Commercial:** Tarpit connections exit within 1 second of SIGTERM.
## Testing
```bash
# Test both editions
go test ./edition/...
go test -tags commercial ./edition/...
# Build verification
go build ./cmd/clavitor/ # Community
go build -tags commercial ./cmd/clavitor/ # Commercial
# Verify feature presence
strings /tmp/clavitor-community | grep -i tarpit # Should be empty
strings /tmp/clavitor-commercial | grep -i tarpit # Should find references
```
## Important Rules
1. **Never remove build tags** — they enforce the dual-license model
2. **Always use `edition.Current`** — no build tag checks in app code
3. **Community is default**`go build` without tags must work
4. **Commercial config is optional** — commercial binary runs without config (just degraded)
5. **No runtime switching** — edition is baked into binary, not changeable
## FAQ for Agents
**Q: Why not use a config flag for community vs commercial?**
A: Build-time separation ensures:
- Community users cannot accidentally enable commercial features
- Commercial code (telemetry, replication) doesn't exist in Elastic 2 binary
- Clean mental model: you get what you build
**Q: Can a community user upgrade to commercial without rebuilding?**
A: No. They must rebuild with `-tags commercial` and configure `CommercialConfig`.
**Q: What happens if commercial code is called without config?**
A: Graceful degradation:
- Telemetry disabled (no host configured)
- Replication disabled (no config file)
- Alerts logged locally only
- Tarpit still works (needs no config)
**Q: How do I know which edition is running?**
A: Check logs at startup:
```
Starting Clavitor Vault v2.0.45 - Community Edition
# or
Starting Clavitor Vault v2.0.45 - Commercial Edition
```
Or call `edition.Current.Name()` programmatically.