Observe Snapshots
Observe plugins never touch Praxrr’s live domain objects. At each wired
observe point the host builds a small, JSON-safe snapshot by copying
only allow-listed fields, then hands that snapshot across the plugin
seam. This page defines the snapshot value type, the two layers that
protect it, and the exact shape the sync.previewComputed.observe point
would expose.
PluginJsonValue
Section titled “PluginJsonValue”PluginJsonValue is the only shape allowed to cross the plugin
seam. It is recursive and structured-clone / JSON-safe by construction —
no functions, class instances, Dates, or cyclic references survive.
type PluginJsonValue = | null | boolean | number | string | readonly PluginJsonValue[] | { readonly [key: string]: PluginJsonValue };Every snapshot the host produces, and every value a plugin returns, must conform to this type.
Two layers of protection
Section titled “Two layers of protection”Two independent mechanisms guard the seam, applied in this order.
-
Allow-list projection (
buildCapabilityInput) — the primary, structural guarantee. This runs first. It copies only the allow-listed top-level fields that are actually present on the source into a fresh, JSON-safe snapshot. Because the projection copies field by field, plugins never receive live domain objects, DB rows, config, environment values, or any credential-bearing value — those fields simply are not on any allow-list, so they are never copied. The projection returnsnullwhen the capability has no allow-list, the source is not a plain object, or none of the allow-listed fields are present. -
scrubPluginBoundary(==redactSecrets) — defense-in-depth only. After projection, the host runs a key-suffix heuristic over the snapshot. It replaces only string values whose key ends in a secret-shaped suffix —api_key,token,secret,password/passwd(includingpassword_hash),credential, orauthorization— with[REDACTED].
The sync.previewComputed snapshot
Section titled “The sync.previewComputed snapshot”The sync.previewComputed.observe point is wired: it fires in the API
sync-preview create handler after the preview is built and before any
apply. A plugin that declares read:sync-preview would receive
(once the Phase-2 runtime lands) exactly the read:sync-preview
projection over the internal GeneratePreviewResult — that is, exactly
these four fields:
{ "arrType": "radarr", "instanceId": 3, "summary": { "totalChanges": 4, "creates": 2, "updates": 2, "deletes": 0 }, "sections": [ { "kind": "customFormats", "label": "Custom Formats", "changes": [ { "op": "create", "name": "HDR10+" }, { "op": "update", "name": "Dolby Vision" } ] } ]}Every other GeneratePreviewResult field is dropped because it is
not on the allow-list:
instanceNamestatuscreatedAtMssectionOutcomesqualityProfilesdelayProfilesmediaManagementmetadataProfiles
Capability field allow-lists
Section titled “Capability field allow-lists”CAPABILITY_FIELD_ALLOWLIST maps each capability to the top-level fields
its projection may copy. Capabilities marked provisional back
extension points that are declared but not yet wired.
| Capability | Allow-listed top-level fields | Status |
|---|---|---|
read:resolved-profile |
arrType, id, name, qualities, customFormats |
wired |
read:sync-preview |
arrType, instanceId, summary, sections |
wired |
read:custom-format |
formatId, name, specifications |
provisional (unwired) |
read:config-validation |
valid, issues, entity |
provisional (unwired) |
See /plugins/capabilities/ for the full capability descriptors and /plugins/extension-points/ for which points each capability may attach to.
Dispatch guarantees
Section titled “Dispatch guarantees”Every per-plugin dispatch is bounded by a finite AbortSignal timeout,
OBSERVE_DISPATCH_TIMEOUT_MS = 5000 (5000 ms). Observe dispatch is
strictly fire-and-observe: a plugin throw, rejection, or timeout never
propagates and can never alter host output or block an apply. Both wire
sites are guarded by if (config.pluginsEnabled) with an inner
try/catch.
In Phase-1, because no runtime is present, every dispatch settles as
runtime-unavailable and is swallowed (logged at debug:
Plugin runtime unavailable; observe dispatch skipped). Any other throw
is logged at warn. No snapshot is ever actually delivered today — the
projection and scrub logic are exercised only once the Phase-2 runtime
lands.