Connecting a Self-Hosted Runtime to the Prismor Platform

Prismor is open-core. The runtime ("Warden"), the framework adapters, and the YAML rule format in this repo are open and auditable. The control plane (prismor-web — the org dashboard, the policy-signing private key, and the premium feed) is proprietary. This document describes the open client side of that link: how an enrolled Warden install talks to the control plane, what it trusts, and exactly what does and does not leave the machine.

See also: docs/sdk-integration.md (the runtime, not the SDK, is what connects).

Everything here is opt-in. On a plain install, warden/enterprise/* is the client side and a guarded no-op: is_enrolled() returns False and every control-plane path short-circuits. Local protection — the 63 default rules in warden/default_policy.yaml — is always on, enrolled or not.

Connection lifecycle at a glance

  Developer machine (Warden, this repo)      Prismor control plane (prismor-web, proprietary)
  ─────────────────────────────────────      ───────────────────────────────────────────────
  prismor enroll <token>  ──POST /api/devices/enroll──▶  validate one-time token
   identity.enroll()           {token,label,platform}     mint revocable device_key
   save_identity() 0600  ◀──{device_id,org_id,user_id,──  record device row
   ~/.prismor/identity.json        device_key,org_name}

  hook-dispatch (hot path)
   remote_policy.check_and_refresh()
     ──GET /api/policy/version?applied=N──────────────▶  {version,profileId,fullCapture,...}
     if changed: GET /api/policy/resolve ─────────────▶  SIGN policy with PRIVATE key
   verify_and_load() ◀────────{yaml, signature}─────────  (private key NOT in this repo)
     openssl verify vs keys/public.pub ── fail ⇒ ignore, keep last good

   evaluate_tool_call() ⇒ finding
   _dispatch_prismor() → build_record() (redact) → assert_redacted()
     ──POST /api/telemetry/ingest──{org_id,device_id,events[]}──▶ store
   heartbeat.maybe_flush()  ──POST /api/telemetry/ingest (count only)──▶

  on 401/403 from any call ⇒ mark_revoked() ⇒ 1h backoff; last good policy stays

The endpoints above are what the client calls; the server implementation lives in the proprietary control plane.

1. Enroll (token → device key)

prismor enroll <TOKEN>
prismor enroll --token <TOKEN> --label "ci-runner-3" --api-base https://prismor.dev
  • Token: a one-time enrollment token from the dashboard (Admin → Devices → Enroll), exchanged exactly once.
  • What happens: identity.enroll() POSTs {token, label, platform, warden_version} to /api/devices/enroll; the server returns device_id, org_id, user_id, device_key, org_name.
  • Where the key is stored: save_identity() writes ~/.prismor/identity.json (override via $PRISMOR_HOME) at 0600, dir 0700. The device_key is a long-lived, revocable bearer credential for telemetry + signed-policy pull; it cannot push or sign anything, and it is separate from the scan API key and from cloaked secrets so revoking a laptop never breaks CI scans.
  • After enroll, policy is fetched immediately so admin policy applies on the first tool call.

Base URL: $PRISMOR_API_BASE (default https://prismor.dev), persisted per-device as api_base so self-hosted/staging deployments repoint without a rebuild.

2. Signed remote policy: verify-before-apply, fail-closed

On the hot path, remote_policy.check_and_refresh() (debounced, clamped to [5s, 600s]):

  1. Version probe: GET /api/policy/version with Authorization: Bearer <device_key>; a change in version/profile/capture/managed-repos triggers a pull.
  2. Pull: GET /api/policy/resolve{yaml, signature}.
  3. Verify: a detached Ed25519 signature checked with openssl pkeyutl -verify against the bundled trust root keys/public.pub. The private key is never in this repo.
  4. Fail-closed: an unsigned/tampered/unverifiable policy is ignored and the last verified policy keeps applying — a compromised control plane cannot inject rules.
  5. Tighten-only floor: the engine enforces _NON_OVERRIDABLE_RULE_IDS, so even a valid signed policy can never disable the destructive-command / secret-exfil protections or turn Warden off.
  6. Offline-safe: unreachable control plane ⇒ cached policy persists.

3. Redacted telemetry (what leaves the box)

Telemetry flows only for org-managed workspaces (workspace_scope.py). Personal repos emit nothing. The prismor sink (warden/sinks.py) builds records via telemetry.build_record() and runs assert_redacted() before upload — a fail-closed guard.

  • Redacted mode (default): metadata + enums + hashes only — severity, category, rule_id, event type, agent, verdict, a static rule title (paths/hosts/URLs/secrets stripped), a 16-char evidence_hash, tool_name, managed-repo context, policy_scope, device_id, session_id, subject (principal ids). Never sent: commands, stdout/stderr, file paths, URLs, file contents, prompts, responses, payloads, matched evidence.
  • Full capture (admin opt-in, per-org): additionally ships evidence/content, still scrubbed through the cloaking secret patterns so registered secrets never leave. A flip is surfaced to the developer via a stderr NOTICE and in prismor enroll-status (capture: FULL).

Upload: POST /api/telemetry/ingest. An offline spool gives at-least-once delivery, written after redaction. Short hot-path timeout (~6s) ⇒ a slow control plane spools, never blocks a tool call. The destination is pinned to the enrolled api_base — a local policy cannot redirect the device-key-bearing upload elsewhere.

Operational note: trust the active runtime, not the label

In field testing on July 6, 2026, two deployment mistakes produced a dangerous false sense of safety:

  1. A stale source-sync still reported hooks in enforce mode, but its live hook-dispatch path allowed every dangerous command in the block matrix.
  2. An unrelated installed prismor package on the host shadowed the checked-out open-source runtime, so a repo-local entrypoint could import the wrong top-level package entirely.

The rule for operators is simple:

  • verify the import path of the runtime you are benchmarking
  • drive the real hook-dispatch path with a synthetic dangerous command
  • treat status alone as advisory, not proof of enforcement

The current Prismor checkout now ships a real top-level prismor/__init__.py so repo-local entrypoints are harder to shadow, but runtime-path verification is still part of the deployment checklist. See docs/RUNTIME_ENFORCEMENT_TESTING.md for the exact commands and the measured before/after benchmark.

4. Heartbeat

heartbeat counts inspected tool calls locally and uploads one agent_activity record per ≤60s carrying only a count + agent/session enums — volume, not detail — gated to managed workspaces. Powers the "tool calls inspected" KPI.

5. Revocation + backoff

Any control-plane 401/403mark_revoked(); for 1h, control-plane calls short-circuit (no hammering). Local protection is unaffected — the last good signed policy keeps enforcing. prismor logout wipes identity, cached policy, spool, heartbeat, and scope map.

Trust model

  • Public-key verify only. The device holds no signing key; it only verifies org policy against keys/public.pub. The signing key lives in the control plane.
  • No secret leaves. Cloaked secrets and the scan API key are never sent; even full capture scrubs secret-shaped values. The device key is scoped, revocable, stored 0600.
  • Admin CAN see (managed repos only): redacted findings, per-user subject ids, repo identifiers, tool-call volume; with opt-in, scrubbed raw content.
  • Admin CANNOT see: personal-repo activity, raw secrets, or — without opt-in — any commands, paths, URLs, prompts, or file contents; and cannot silently disable the local security floor.

Known follow-ups for this path: auth on the enroll UX, and subject handling.