# LazyToken Enterprise — Administrator Guide

Everything an administrator needs to install LazyToken, enroll a developer fleet,
manage policy and access, apply a license, and read the dashboard. Commands and
endpoints below are the ones the product actually implements; see
[`server/openapi.yaml`](../server/openapi.yaml) for the full API surface and
[`docs/CONTRACT.md`](./CONTRACT.md) for the agent↔server wire protocol.

**Conventions in this guide**
- The binary is **`ltk`**; the product is **LazyToken**.
- API errors are always `{ "error": { "code", "message" } }`.
- All admin write actions are recorded in the audit log.

---

## 1. Architecture in one paragraph

LazyToken has three parts. The **agent** (`ltk`) runs on each developer machine,
filters coding-agent terminal output before it reaches the model, and reports
**numbers only** to your server. The **server** (Node/Fastify/PostgreSQL) ingests
those metrics, signs and serves policy, enforces licensing softly, and runs the
daily rollup and monthly ROI reports. The **dashboard** (React SPA, served by the
same server) is where you read ROI, teams, opportunities, security events and
fleet status. The server is **self-hosted** — you own the data; the vendor has no
tenant and no access.

---

## 2. Installation

Three supported paths, from simplest to strictest.

### 2.1 Docker Compose (MVP / pilots)

From the repo (`deploy/docker-compose.yml`):

```bash
cd deploy
cp .env.example .env
# Edit .env — at minimum:
#   POSTGRES_PASSWORD=<strong>
#   JWT_SECRET=$(openssl rand -hex 32)
#   LICENSE_PUBLIC_KEY=<base64 ed25519 pubkey>   # optional until you install a license
docker compose up -d
curl http://localhost:8080/healthz    # {"status":"ok"}
```

The compose stack is PostgreSQL 16 + the server (which also serves the dashboard
and runs the rollup job in-process). It publishes port `8080`.

### 2.2 Kubernetes / Helm (GA)

Chart at `deploy/helm`. Create secrets out-of-band, then install:

```bash
kubectl create namespace lazytoken
kubectl -n lazytoken create secret generic lazytoken-secrets \
  --from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
  --from-literal=SSO_ENCRYPTION_KEY="$(openssl rand -hex 32)"

helm install lazytoken ./deploy/helm -n lazytoken -f my-values.yaml
kubectl -n lazytoken rollout status deploy/lazytoken
```

The chart runs database migrations as a `pre-install`/`pre-upgrade` Helm hook Job
before the server starts, ships an optional bundled PostgreSQL, an HPA, an
Ingress, and a **NetworkPolicy** (`networkPolicy.enabled=true`) that technically
enforces the zero-egress posture. Review `deploy/helm/values.yaml` for all knobs.

### 2.3 Air-gapped

The strictest deployment — zero internet. Images are carried in as a tar, the
license is a signed file (no license server to phone home), and a NetworkPolicy
restricts server pods to DNS + in-cluster PostgreSQL. Full runbook:
[`deploy/AIR-GAPPED.md`](../deploy/AIR-GAPPED.md).

> **The server makes no unsolicited outbound calls.** The only optional egress
> paths are OIDC SSO (your IdP), and monthly-report delivery (SMTP / Slack) — both
> off by default, and only to endpoints you explicitly configure.

### Prerequisites & caveats (read before production)

- **`JWT_SECRET` is required in production** — the server refuses to boot without
  it.
- **PDF ROI reports** are rendered with Chromium via Playwright. If Chromium is
  not installed on the server image, `GET /v1/reports/monthly.pdf` returns `503`
  until you run `npx playwright install chromium`.
- **Agent installer signing is not yet turned on.** The packaging scripts under
  `deploy/packaging/` (MSI/pkg/deb/rpm) build the installers, but production
  requires EV code signing (Windows), Apple notarization (macOS), and GPG
  signatures (Linux). Treat signing as a release-engineering step to complete
  before a regulated rollout.
- **SSO is implemented but should be validated against your specific IdP** before
  go-live (see §7) — OIDC and SAML are wired, but each IdP has quirks.

---

## 3. First-admin bootstrap

You need exactly one admin to start; everyone else is created from the dashboard.

**Via environment (Docker/Helm):** set `BOOTSTRAP_ADMIN_EMAIL`,
`BOOTSTRAP_ADMIN_PASSWORD`, and `BOOTSTRAP_ORG_NAME`. On first boot, while no
users exist, the server creates the org + admin. It is a **no-op once any user
exists**.

**Via CLI (local/dev):**

```bash
cd server
npm run bootstrap:admin -- --email admin@acme.com --password '<strong>' --org "Acme"
```

Bootstrapping also creates the org's **Ed25519 policy-signing keypair** (the
private key never leaves the server) and a signed **default policy**.

Log in to get a 12-hour session token:

```bash
curl -sX POST http://localhost:8080/v1/auth/login \
  -H 'content-type: application/json' \
  -d '{"email":"admin@acme.com","password":"<strong>"}'
# -> { "token": "<jwt>", "user": {...} }
```

Use that token as `Authorization: Bearer <jwt>` for the admin API. `GET
/v1/auth/me` returns the current identity.

---

## 4. Licensing

Licenses are **offline, Ed25519-signed JSON** bound to `{org, seats, tier,
expiry}`. There is no license server and no activation call. Enforcement is
**soft**: over-seat or expired produces warnings in `GET /v1/license/status` plus
audit events — ingest and policy are never blocked. The one hard limit: **new**
device enrollments fail with `403 SEAT_LIMIT` when seats are exhausted;
already-enrolled devices keep working.

**Vendor side (issue a key):**

```bash
cd server
npm run gen:license -- keygen                                   # one-time; keep the private key offline
npm run gen:license -- create --key <priv> --org "Acme" \
  --seats 50 --tier enterprise --expiry 2027-06-30
npm run gen:license -- verify --pub <pub> --license <key>
```

**Customer side:** configure `LICENSE_PUBLIC_KEY` (base64 raw 32-byte pubkey) on
the server, then install the key once:

```
PUT /v1/admin/license   { "license_key": "<the signed key>" }
```

Check status any time: `GET /v1/license/status`.

---

## 5. Enrolling devices

### 5.1 Create an enroll token

Admin creates an org-scoped enrollment token (optionally with a max-uses limit
and expiry):

```
POST /v1/admin/enroll-tokens   { "max_uses": 200, "expires_at": "2026-12-31T00:00:00Z" }
# -> { "token": "ORG-XXXXXXXXXXXX", ... }
```

List / revoke via `GET /v1/admin/enroll-tokens` and
`DELETE /v1/admin/enroll-tokens/{id}`.

### 5.2 Enroll a machine

On the developer machine (or via MDM):

```bash
ltk enroll --server https://lt.company.internal --token ORG-XXXXXXXXXXXX
ltk status        # enrollment, server, active policy, reporting state
```

`ltk enroll` stores the device JWT + refresh token + pinned policy public key in
the **OS keychain** (service `lazytoken`) — never in files — and fetches the
initial signed policy. From then on the agent reports metrics asynchronously and
syncs policy on its own.

Useful agent commands:

| Command | What it does |
|---|---|
| `ltk enroll --server <url> --token <ORG-...>` | Enroll this device |
| `ltk status` | Show enrollment, active policy and reporting status |
| `ltk policy sync` | Fetch, verify (Ed25519) and apply the latest signed policy |
| `ltk report flush` | Collect new metrics and send buffered batches now |
| `ltk report status` | Show pending records, cursor and backoff state |
| `ltk update [--check]` | Update the agent from your server's channel — verifies sha256 + Ed25519 before an atomic self-replace |
| `ltk sdlc sync` | Fetch and apply signed org SDLC templates (CLAUDE.md/AGENTS.md standards) |

> **Self-update** pulls from your internal server only (`GET /v1/agent/latest?channel=stable|beta`,
> per CONTRACT §7): the download is verified against its published sha256 **and**
> an Ed25519 signature (the key pinned at enrollment) before an atomic replace
> that leaves the old binary intact on any failure — no root required. You can
> also roll out through the MDM/packaging channel (`deploy/mdm/`). Note: a
> dedicated open-source-licenses view (`ltk licenses`) is still a planned CLI
> addition (see [`05-LEGAL-COMPLIANCE.md`](../05-LEGAL-COMPLIANCE.md)).

### 5.3 Fleet rollout via MDM

Silent-install scaffolding lives under `deploy/mdm/`:
- **Intune** — `deploy/mdm/intune/Install-LazyToken.ps1`
- **Jamf** — `deploy/mdm/jamf/install-lazytoken.sh`
- **GPO** — `deploy/mdm/gpo/Deploy-LazyToken-GPO.md`

Package the agent with `deploy/packaging/` (WiX MSI, pkg, deb/rpm), pre-seed the
`--server` and `--token` values, and push through your MDM. Manage devices from
the dashboard Fleet screen or `GET /v1/admin/devices` (revoke with
`DELETE /v1/admin/devices/{id}`).

---

## 6. Teams, users and RBAC

Four roles: **admin**, **lead**, **viewer**, **finance**.

| Capability | admin | lead | finance | viewer |
|---|:--:|:--:|:--:|:--:|
| Read analytics dashboard | ✔ | ✔ (team) | ✔ | ✔ |
| Download ROI PDF | ✔ | ✔ | ✔ | — |
| Manage policies / assignments | ✔ | — | — | — |
| Manage teams / users / devices | ✔ | — | — | — |
| SSO / license / settings | ✔ | — | — | — |

Endpoints:
- **Teams** — `GET/POST /v1/admin/teams`, `PATCH/DELETE /v1/admin/teams/{id}`
- **Users** — `GET/POST /v1/admin/users`, `PATCH/DELETE /v1/admin/users/{id}`
  (assign role + team)
- **Devices** — `GET /v1/admin/devices`, `DELETE /v1/admin/devices/{id}` (revoke)

Every write here lands in the **audit log** (`GET /v1/admin/audit-log`), which is
append-only.

---

## 7. SSO (OIDC / SAML)

Configure per protocol; both are supported. Admin-only.

```
GET  /v1/admin/sso                 # list configured protocols (secrets masked)
PUT  /v1/admin/sso/oidc            # { enabled, config: { issuer, client_id, client_secret, ... } }
PUT  /v1/admin/sso/saml            # { enabled, config: { entryPoint, idpCert, ... } }
DELETE /v1/admin/sso/{protocol}
```

Login flows: `GET /v1/auth/oidc` → `…/oidc/callback`, and `GET /v1/auth/saml` →
`…/saml/acs`. The OIDC `client_secret` is **AES-256-GCM encrypted at rest** using
a key derived from `SSO_ENCRYPTION_KEY` (env only); secrets are never logged,
echoed, or written to the audit log. SSO login state is DB-backed so it works
across Helm replicas.

> **Caveat:** the OIDC/SAML code paths are implemented and unit-tested, but each
> IdP (Azure AD, Okta, Google Workspace, ADFS) has its own claim/cert quirks.
> Validate the full login round-trip against your actual IdP in a staging org
> before enabling it for all users. MFA is delegated to your IdP.

---

## 8. Policies

A policy is a signed JSON document the agent verifies before applying. Manage
via:

```
GET/POST /v1/admin/policies
GET/PUT/DELETE /v1/admin/policies/{id}
POST/DELETE /v1/admin/policies/{id}/assignments        # assign to team or device
```

Each edit **bumps the version and re-signs** with the org's private key; the
server serves the exact signed bytes and supports `If-None-Match`/`304`. Policy
body (see CONTRACT §3) controls:

- **reporting** — enabled, flush interval, whether to send `project_hash`.
- **filters** — `exclude_commands`, filter `level` (`strict`/`balanced`), `tee`.
- **privacy** — `anonymize_devices` (the "Dev #N" mode).
- **updates** — agent update `channel` (`stable`/`beta`).
- **firewall (Context Firewall / DLP)** — `enabled`, `sensitivity`, per-rule
  toggles (`aws-key`, `github-token`, `private-key`, …), custom org regex rules,
  and command **guardrails** (e.g. block `cat` on `**/.env*`).

Unassigned devices get the org **default** policy. Assignments let you target a
team (all its devices) or a single device. Because policies are signed, a
compromised server cannot push a malicious policy — the agent rejects an invalid
signature and keeps the last good one.

---

## 9. Settings (retention, token prices, report delivery)

```
GET /v1/admin/settings
PUT /v1/admin/settings
```

- **retention_days** — how long raw metrics are kept before the purge (default
  90; the rollup keeps the daily aggregates).
- **token_prices** — USD per **MTok**, keyed by `ai_agent` with a `"default"`
  fallback (e.g. `{ "default": 3, "copilot": 2 }`). This drives every USD figure
  on the dashboard and in the ROI report — set it to the org's real contract
  pricing.
- **reporting** — monthly ROI delivery config (`enabled`, `email_recipients`,
  `slack_webhook_url`). When absent or `enabled:false`, the server sends nothing
  (air-gapped-safe default).

> **Doc gap noted:** `PUT /v1/admin/settings` accepts a `reporting` object (see
> `server/src/routes/admin/org.ts`), but the `openapi.yaml` request schema for
> that endpoint currently lists only `retention_days` and `token_prices`. The
> behavior is real; the spec is slightly behind.

---

## 10. Scheduled ROI reports

The monthly ROI report summarizes savings and dollar value for a calendar month.

- **On demand:** `GET /v1/reports/monthly.pdf?month=YYYY-MM` (admin/finance/lead)
  — Chromium-rendered PDF. Returns `503` if Chromium isn't installed
  (`npx playwright install chromium`).
- **Scheduled delivery:** configure `reporting` in settings (email recipients
  and/or a Slack webhook). The server runs the report job at boot and every
  `REPORTS_INTERVAL_HOURS`, delivering the previous month's report and logging
  each send in `reports_log`. Email uses the `SMTP_*` env config; when SMTP is
  unset, email delivery is a logged no-op.
- **Manual job run:** `npm run job:reports`.

---

## 11. Reading the dashboard

| Screen | Backing endpoint | What it shows |
|---|---|---|
| **Executive** | `GET /v1/analytics/summary`, `…/trend` | Tokens saved, USD value, active/total devices, adoption %, 30/90-day trend |
| **Teams** | `GET /v1/analytics/by-team` | Savings and adoption per team; drill-down; anonymization mode |
| **Commands** | `GET /v1/analytics/by-command` | Which tools save the most |
| **Opportunities** | `GET /v1/analytics/opportunities` | High-volume, low-savings passthrough tools to tune next |
| **Security** | `GET /v1/analytics/security-events` | Context Firewall events by type over time (**type/tool/time only — never a value**) |
| **Fleet** | `GET /v1/admin/devices` | Enrolled devices, silent/stale devices, agent versions |
| **Policies / Settings** | admin endpoints above | Policy editing, retention, prices, SSO, license |

USD figures come from the `token_prices` table (§9). `summary.adoption_pct` is
active devices (seen in the last 7 days) against seats.

---

## 12. Operations quick reference

| Task | How |
|---|---|
| Health check | `GET /healthz` |
| Run the daily rollup manually | `npm run job:rollup` |
| Run the monthly report manually | `npm run job:reports` |
| Rate limits | Ingest is 60 req/hour/device (`INGEST_RATE_LIMIT_PER_HOUR`) |
| Retention purge | Runs with the rollup; window = `retention_days` |
| Audit trail | `GET /v1/admin/audit-log` (append-only) |
| Backups | Back up PostgreSQL; the org's policy private key lives in the `orgs` table — protect DB backups accordingly |

---

## 13. Related documents

- [QUICK-START.md](./QUICK-START.md) — 15-minute local setup.
- [DEMO.md](./DEMO.md) — synthetic-data sales demo.
- [SECURITY-WHITEPAPER.md](./SECURITY-WHITEPAPER.md) — trust model, allowlist,
  Context Firewall, air-gapped, security questionnaire.
- [ONBOARDING-CHECKLIST.md](./ONBOARDING-CHECKLIST.md) — the implementation
  playbook.
- [CONTRACT.md](./CONTRACT.md), [`server/openapi.yaml`](../server/openapi.yaml) —
  the wire protocol and full API.
