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 returnsdevice_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. Thedevice_keyis 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]):
- Version probe:
GET /api/policy/versionwithAuthorization: Bearer <device_key>; a change in version/profile/capture/managed-repos triggers a pull. - Pull:
GET /api/policy/resolve→{yaml, signature}. - Verify: a detached Ed25519 signature checked with
openssl pkeyutl -verifyagainst the bundled trust rootkeys/public.pub. The private key is never in this repo. - Fail-closed: an unsigned/tampered/unverifiable policy is ignored and the last verified policy keeps applying — a compromised control plane cannot inject rules.
- 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. - 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
NOTICEand inprismor 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:
- A stale source-sync still reported hooks in
enforcemode, but its livehook-dispatchpath allowed every dangerous command in the block matrix. - An unrelated installed
prismorpackage 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-dispatchpath with a synthetic dangerous command - treat
statusalone 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/403 ⇒ mark_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.