@camstack/addon-provider-homematic 1.2.5 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +2393 -2144
  2. package/dist/addon.mjs +2393 -2144
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -7542,16 +7542,23 @@ var StorageLocationDeclarationSchema = object({
7542
7542
  * Which node root the seeded `<id>:default` instance is placed under on a
7543
7543
  * FRESH install:
7544
7544
  * - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
7545
- * the appData volume. Right for small/durable data (backups, logs, models).
7545
+ * the appData volume. Right for small/durable data (logs, models).
7546
7546
  * - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
7547
7547
  * env is set, else falls back to the data root. Right for bulky, hot media
7548
7548
  * (recordings, event media) that should stay off the appData disk.
7549
+ * - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
7550
+ * `/backups` in the image) so archives live on their own mount rather than
7551
+ * filling the appData disk. Falls back to the data root when unset.
7549
7552
  *
7550
7553
  * Only affects the seeded default's `basePath`; operators can repoint any
7551
7554
  * location afterwards, and a `defaultsTo` slot inherits its parent's root
7552
7555
  * regardless of this field. Absent (the common case) is treated as `'data'`.
7553
7556
  */
7554
- defaultRoot: _enum(["data", "media"]).optional()
7557
+ defaultRoot: _enum([
7558
+ "data",
7559
+ "media",
7560
+ "backup"
7561
+ ]).optional()
7555
7562
  });
7556
7563
  var DecoderStatsSchema = object({
7557
7564
  inputFps: number(),
@@ -9145,669 +9152,1307 @@ function shallowEqual(a, b) {
9145
9152
  return true;
9146
9153
  }
9147
9154
  /**
9148
- * Generic device-level status snapshot. Auto-registered by `BaseDevice`
9149
- * for every device, regardless of provider — the kernel needs a uniform
9150
- * cap-keyed slice for the basic device flags every consumer expects to
9151
- * read across processes (the `online` flag in particular). Driver-specific
9152
- * caps (`battery`, `doorbell`, …) carry their domain-specific state on
9153
- * their own slices.
9155
+ * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9156
+ * motion-zones, and the detection zones/lines editor all speak this one
9157
+ * language so a single drawing-plane editor and the providers stay
9158
+ * decoupled from each cap's storage.
9154
9159
  *
9155
- * Pattern is identical to `battery`: schema-bearing `runtimeState`,
9156
- * empty `methods`, single change event. Reads land at
9157
- * `runtimeState.getCapState('device-status')`; writes at
9158
- * `runtimeState.setCapState('device-status', …)`. Cross-process
9159
- * consumers reach the same data via the `device-state` cap router
9160
- * (`getCapSlice({deviceId, capName: 'device-status'})`).
9160
+ * All coordinates are normalized 0..1 of the camera frame (top-left
9161
+ * origin). Each cap composes the SUBSET of shape kinds it supports and
9162
+ * advertises it via `supportedShapes` in its `getOptions`.
9161
9163
  */
9162
- var DeviceStatusSchema = object({
9163
- /**
9164
- * Device-level liveness. Drivers flip via `markOnline(boolean)` on
9165
- * `BaseDevice`. Provider semantics vary — RTSP aggregates broker
9166
- * stream-health, Reolink reads firmware push events, ONVIF tracks
9167
- * ping responses. This cap intentionally does NOT prescribe which
9168
- * signal drives the flag.
9169
- */
9170
- online: boolean(),
9171
- /** Ms epoch of the last `online` transition. Lets consumers tell
9172
- * apart "just came online" from "still online". */
9173
- lastChangedAt: number()
9164
+ /** A normalized 0..1 point (top-left origin). */
9165
+ var MaskPointSchema = object({
9166
+ x: number(),
9167
+ y: number()
9168
+ });
9169
+ /** Axis-aligned rectangle (normalized 0..1). */
9170
+ var MaskRectShapeSchema = object({
9171
+ kind: literal("rect"),
9172
+ x: number(),
9173
+ y: number(),
9174
+ width: number(),
9175
+ height: number()
9176
+ });
9177
+ /** Free polygon — an ordered list of normalized vertices (≥3). */
9178
+ var MaskPolygonShapeSchema = object({
9179
+ kind: literal("polygon"),
9180
+ points: array(MaskPointSchema)
9181
+ });
9182
+ /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
9183
+ var MaskGridShapeSchema = object({
9184
+ kind: literal("grid"),
9185
+ gridWidth: number(),
9186
+ gridHeight: number(),
9187
+ cells: array(boolean())
9188
+ });
9189
+ discriminatedUnion("kind", [
9190
+ MaskRectShapeSchema,
9191
+ MaskPolygonShapeSchema,
9192
+ MaskGridShapeSchema,
9193
+ object({
9194
+ kind: literal("line"),
9195
+ points: array(MaskPointSchema)
9196
+ })
9197
+ ]);
9198
+ /** Every shape-kind discriminant, for `supportedShapes` advertisement. */
9199
+ var MaskShapeKindSchema = _enum([
9200
+ "rect",
9201
+ "polygon",
9202
+ "grid",
9203
+ "line"
9204
+ ]);
9205
+ /** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
9206
+ var MaskPolygonVerticesSchema = object({
9207
+ min: number(),
9208
+ max: number()
9209
+ });
9210
+ /** Grid dimensions when a cap supports 'grid'. */
9211
+ var MaskGridDimsSchema = object({
9212
+ width: number(),
9213
+ height: number()
9174
9214
  });
9175
- var deviceStatusCapability = {
9176
- name: "device-status",
9177
- scope: "device",
9178
- deviceNative: true,
9179
- mode: "singleton",
9180
- methods: {},
9181
- events: {
9182
- /** Emitted when `online` transitions. Mirrors the semantics of
9183
- * `battery.onStatusChanged`. */
9184
- onStatusChanged: { data: object({
9185
- deviceId: number(),
9186
- status: DeviceStatusSchema
9187
- }) } },
9188
- status: {
9189
- schema: DeviceStatusSchema,
9190
- kind: "push"
9191
- },
9192
- runtimeState: DeviceStatusSchema
9193
- };
9194
9215
  /**
9195
- * Per-device feature/identity probe slice. Holds the runtime-resolved
9196
- * truth about what a device CAN do — which the kernel uses to:
9197
- * 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
9198
- * based on what the firmware actually advertises).
9199
- * 2. Compute the public `features: DeviceFeature[]` array surfaced via
9200
- * `device-manager.listAll`.
9201
- * 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
9202
- * to register on the device's capability surface.
9216
+ * notification-rules — the Notification Center rule surface (P1 core).
9203
9217
  *
9204
- * Auto-registered by `BaseDevice` for every device. Drivers populate the
9205
- * slice from `onProbe()` (kernel calls it once after register, before
9206
- * accessory reconciliation). Consumers read via:
9207
- * `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
9218
+ * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
9219
+ * (operator decisions D-1/D-2/D-3 are binding):
9208
9220
  *
9209
- * `flags` is an open record so each driver carries its own keys without
9210
- * a centralized schema bottleneck — Reolink writes `hasPtz/hasIntercom`,
9211
- * Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
9221
+ * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
9222
+ * `notification-center` module), hooked on the durable persistence
9223
+ * moments (object-event insert, TrackCloser.closeExpired) with a
9224
+ * persisted outbox + retry — never the lossy telemetry bus (D8).
9225
+ * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
9226
+ * FIRST persisted detection matching the conditions (per-track dedup,
9227
+ * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
9228
+ * `delivery: 'track-end'` evaluates the finalized track record at close.
9229
+ * - DISPATCH stays behind `notification-output` (rules reference targets
9230
+ * by id; per-backend params are a passthrough blob capped by the
9231
+ * target kind's own caps/degrade engine).
9212
9232
  *
9213
- * Replaces the older driver-local `deviceCache.has*` blob: the per-device
9214
- * config is for operator-edited overrides + UI snapshots; runtime probe
9215
- * results belong in runtime-state where the kernel handles persistence,
9216
- * cross-process mirroring, and reactive updates.
9233
+ * P1 scope: admin-authored rules only (`createdBy` stamped from the
9234
+ * server-injected caller identity — the first `caller: 'required'`
9235
+ * adopter). The P1 condition subset is: devices, classes(+exclude),
9236
+ * minConfidence, admin zones (any/all + exclude), weekly schedule
9237
+ * windows, and the optional label/identity/plate matchers. User rules,
9238
+ * private zones, per-recipient fan-out and the wider condition table are
9239
+ * P2+ (see spec §7).
9240
+ *
9241
+ * All schemas here are the single source of truth — `NcRule` etc. are
9242
+ * `z.infer` exports; no duplicate interfaces (the advanced-notifier
9243
+ * schema/interface drift is explicitly not repeated).
9217
9244
  */
9218
- var FeatureProbeStatusSchema = object({
9245
+ /**
9246
+ * D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
9247
+ * The value maps 1:1 onto the evaluated record kind:
9248
+ * - `immediate` ↔ object-event persist (lowest-latency detection burst)
9249
+ * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
9250
+ * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
9251
+ * change of a LINKED device, one row per linked camera)
9252
+ * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
9253
+ * delivery / pick-up)
9254
+ *
9255
+ * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
9256
+ * `package-event` are pure trigger kinds (no urgency dimension). Extending
9257
+ * this one field keeps the schema additive — a rule still declares exactly
9258
+ * one trigger.
9259
+ */
9260
+ var NcDeliverySchema = _enum([
9261
+ "immediate",
9262
+ "track-end",
9263
+ "device-event",
9264
+ "package-event"
9265
+ ]);
9266
+ /** Weekly schedule — OR of windows; absence on the rule = always active. */
9267
+ var NcScheduleSchema = object({
9268
+ windows: array(object({
9269
+ /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
9270
+ days: array(number().int().min(0).max(6)).min(1),
9271
+ startMinute: number().int().min(0).max(1439),
9272
+ endMinute: number().int().min(0).max(1439)
9273
+ })).min(1),
9274
+ /** IANA timezone; default = hub host timezone. */
9275
+ timezone: string().optional(),
9276
+ /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
9277
+ invert: boolean().optional()
9278
+ });
9279
+ /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
9280
+ var NcPlateMatcherSchema = object({
9281
+ values: array(string().min(1)).min(1),
9282
+ /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
9283
+ maxDistance: number().int().min(0).max(3).default(1)
9284
+ });
9285
+ /**
9286
+ * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
9287
+ * occupancy edge for a device — optionally narrowed to a single admin
9288
+ * `zoneId` and/or object `className`. `op` selects the edge/threshold:
9289
+ * - `became-occupied` (default) — count crossed 0 → ≥ `count`
9290
+ * - `became-free` — count crossed ≥ `count` → below it
9291
+ * - `>=` / `<=` — count is at/over or at/under `count`
9292
+ * `sustainSeconds` requires the condition hold continuously that long
9293
+ * before firing (debounces flicker; 0 = fire on the first matching edge).
9294
+ * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
9295
+ * the condition never matches. Confirmed edge-state survives addon restarts
9296
+ * (declared SQLite collection, reseeded on boot).
9297
+ */
9298
+ var NcOccupancyConditionSchema = object({
9299
+ /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
9300
+ zoneId: string().optional(),
9301
+ /** Object class to count; absent = any class. */
9302
+ className: string().optional(),
9303
+ op: _enum([
9304
+ "became-occupied",
9305
+ "became-free",
9306
+ ">=",
9307
+ "<="
9308
+ ]).default("became-occupied"),
9309
+ count: number().int().min(0).default(1),
9310
+ sustainSeconds: number().int().min(0).max(3600).default(15)
9311
+ });
9312
+ /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9313
+ var NcZoneConditionSchema = object({
9314
+ ids: array(string().min(1)).min(1),
9315
+ /** Quantifier over `ids` — at least one / every one visited. */
9316
+ match: _enum(["any", "all"]).default("any")
9317
+ });
9318
+ /**
9319
+ * The P1 condition set — a flat AND of groups; absent group = pass;
9320
+ * membership lists are OR within the list (spec §2.3).
9321
+ */
9322
+ var NcConditionsSchema = object({
9323
+ /** Device scope — absent = all devices. */
9324
+ devices: array(number()).optional(),
9325
+ /** Detector class names (any overlap with the record's class set). */
9326
+ classes: array(string().min(1)).optional(),
9327
+ /** Veto classes — any overlap fails the rule. */
9328
+ classesExclude: array(string().min(1)).optional(),
9329
+ /** Minimum detection confidence 0–1 (fails when the record has none). */
9330
+ minConfidence: number().min(0).max(1).optional(),
9331
+ /** Admin zone membership over event `zones` / track `zonesVisited`. */
9332
+ zones: NcZoneConditionSchema.optional(),
9333
+ /** Veto zones — any hit fails the rule. */
9334
+ zonesExclude: array(string().min(1)).optional(),
9219
9335
  /**
9220
- * Driver-specific flag bag. Each driver picks its own key names — the
9221
- * cap deliberately does NOT enforce a closed enum here. Reolink keys:
9222
- * `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
9223
- * `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
9224
- * `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
9336
+ * Exact (case-insensitive) match on the record's collapsed `label`
9337
+ * (identity name / plate text / subclass).
9225
9338
  */
9226
- flags: record(string(), unknown()),
9339
+ labelEquals: array(string().min(1)).optional(),
9227
9340
  /**
9228
- * Coarse driver-classification — lets cross-process consumers tell apart
9229
- * cameras / battery-cams / NVRs without re-running the probe. `null`
9230
- * before the first probe completes.
9341
+ * Identity matcher. P1 boundary: matched against the record's collapsed
9342
+ * `label` (the identity display name propagated by the face pipeline) —
9343
+ * identity-ID matching rides in P2 when identity ids reach the record.
9231
9344
  */
9232
- deviceType: string().nullable(),
9233
- /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
9234
- model: string().nullable(),
9235
- /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
9236
- channelCount: number().nullable(),
9345
+ identities: array(string().min(1)).optional(),
9346
+ /** Fuzzy plate matcher against the record's `label` (plate text). */
9347
+ plates: NcPlateMatcherSchema.optional(),
9237
9348
  /**
9238
- * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
9239
- * completes — drivers' `getAccessoryChildren()` should treat zero as
9240
- * "probe not done yet, return empty" so accessories aren't spawned
9241
- * before the firmware is queried.
9349
+ * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
9350
+ * Same P1 boundary: matched against the record's collapsed `label` (the
9351
+ * identity display name). A record with NO label passes (nothing to
9352
+ * exclude), unlike the include variant which fails on an absent label.
9242
9353
  */
9243
- lastProbedAt: number(),
9354
+ identitiesExclude: array(string().min(1)).optional(),
9244
9355
  /**
9245
- * Framework convention: every runtime-state slice carries this for the
9246
- * createRuntimeStateBridge stale-check helper. We keep it in sync with
9247
- * `lastProbedAt` on every write.
9356
+ * Minimum server-computed key-event importance in [0,1] (`Track.importance`).
9357
+ * TRACK-END only: importance is scored at track close, so it does not exist
9358
+ * at immediate / object-event evaluation time (see catalog `appliesTo`). At
9359
+ * close the value is threaded via the close-time info (the `Track` clone is
9360
+ * captured before the DB row is updated, so it would otherwise read stale).
9361
+ * Fails when the record carries no importance (never guess quality — the
9362
+ * `minConfidence` precedent). MVP cut: a single scalar threshold.
9248
9363
  */
9249
- lastFetchedAt: number()
9364
+ minImportance: number().min(0).max(1).optional(),
9365
+ /**
9366
+ * Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
9367
+ * TRACK-END only: an `immediate` / object-event subject has no closed
9368
+ * lifespan, so a dwell condition never matches immediate delivery
9369
+ * (documented choice — the object-event record carries no `firstSeen`,
9370
+ * so dwell cannot be computed from what the subject actually carries).
9371
+ */
9372
+ minDwellSeconds: number().min(0).optional(),
9373
+ /**
9374
+ * Detection provenance filter. `any` (default / absent) matches every
9375
+ * source; otherwise the subject's source must equal it. Legacy records
9376
+ * with no stamped source are treated as `pipeline`. The union spans both
9377
+ * record kinds — object events carry `pipeline` | `onboard`, synthetic
9378
+ * tracks carry `sensor`.
9379
+ */
9380
+ source: _enum([
9381
+ "pipeline",
9382
+ "onboard",
9383
+ "sensor",
9384
+ "any"
9385
+ ]).optional(),
9386
+ /**
9387
+ * Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
9388
+ * detector `minConfidence` (that gates the object-detection score; this
9389
+ * gates the recognition/OCR match score). Fails when the subject carries
9390
+ * no label-match confidence (never guess). TRACK-END only: the confidence
9391
+ * lives on the recognition result and reaches the subject at track close.
9392
+ *
9393
+ * What it measures precisely (plumbed at track close — the closer threads
9394
+ * the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
9395
+ * `importance`): the BEST recognition match confidence observed for the
9396
+ * label the track carries at close — for a face, the peak cosine similarity
9397
+ * of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
9398
+ * for a plate, the peak OCR read score of the best-held plate
9399
+ * (`plateText.confidence`). When BOTH a face and a plate were recognized on
9400
+ * one track the higher of the two is used. A track that ended with no
9401
+ * confident identity/plate match carries no value, so the condition fails
9402
+ * closed for it (an un-recognized subject).
9403
+ */
9404
+ minLabelConfidence: number().min(0).max(1).optional(),
9405
+ /**
9406
+ * DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
9407
+ * e.g. a doorbell `press` / `press_long`) — matched case-insensitively
9408
+ * against the token carried on the device-event subject (extracted from the
9409
+ * event-emitter runtime slice's `lastEvent.eventType`). Fails when the
9410
+ * subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
9411
+ * eventType, so gate those with {@link sensorKinds} instead.
9412
+ */
9413
+ eventTypeTokens: array(string().min(1)).optional(),
9414
+ /**
9415
+ * DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
9416
+ * `contact`, `button`, `device-event`) — matched against the persisted
9417
+ * `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
9418
+ */
9419
+ sensorKinds: array(string().min(1)).optional(),
9420
+ /**
9421
+ * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
9422
+ * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
9423
+ * when the subject's phase does not match (a subject always carries a phase
9424
+ * on the package-event trigger).
9425
+ */
9426
+ packagePhase: _enum([
9427
+ "delivered",
9428
+ "picked-up",
9429
+ "both"
9430
+ ]).optional(),
9431
+ /**
9432
+ * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
9433
+ * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
9434
+ * listed polygon (ZoneEngine membership semantics). Evaluated only when
9435
+ * the subject carries a bbox; absent bbox ⇒ the condition FAILS.
9436
+ */
9437
+ customZones: array(MaskPolygonShapeSchema).optional(),
9438
+ /**
9439
+ * DEVICE-EVENT only. ZoneAnalytics occupancy edge — fires when a device's
9440
+ * (optionally zone/class-scoped) occupancy count crosses the configured
9441
+ * threshold and holds for `sustainSeconds`. Fail-closed on missing
9442
+ * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
9443
+ */
9444
+ occupancy: NcOccupancyConditionSchema.optional()
9445
+ });
9446
+ /** One delivery target: a `notification-output` Target ref + passthrough params. */
9447
+ var NcRuleTargetSchema = object({
9448
+ /** `notification-output` Target id. */
9449
+ targetId: string().min(1),
9450
+ /**
9451
+ * Per-backend passthrough. Recognized keys are mapped onto the canonical
9452
+ * Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
9453
+ * degrade engine drops what the backend can't render.
9454
+ */
9455
+ params: record(string(), unknown()).optional()
9250
9456
  });
9251
- var featureProbeCapability = {
9252
- name: "feature-probe",
9253
- scope: "device",
9254
- deviceNative: true,
9255
- mode: "singleton",
9256
- methods: {},
9257
- events: {
9258
- /** Fires whenever a fresh probe completes (kernel-driven `reprobe()`
9259
- * or driver-initiated re-detect after a state change). */
9260
- onProbeChanged: { data: object({
9261
- deviceId: number(),
9262
- status: FeatureProbeStatusSchema
9263
- }) } },
9264
- status: {
9265
- schema: FeatureProbeStatusSchema,
9266
- kind: "push"
9267
- },
9268
- runtimeState: FeatureProbeStatusSchema
9269
- };
9270
9457
  /**
9271
- * Multi-metric air-quality slice. Covers CO₂, total VOCs, particulate
9272
- * matter at PM2.5 / PM10, and a derived AQI index — all optional so
9273
- * a single-metric source populates only what it observes. Mirrors
9274
- * the HA `sensor` device_class set (`co2`, `volatile_organic_compounds`,
9275
- * `pm25`, `pm10`, `aqi`) collapsed into one cap because a typical
9276
- * air-quality node reports several of these together; modelling them
9277
- * as siblings keeps a single timestamp + one slice subscription.
9458
+ * Media attachment policy (P1 still-image subset).
9459
+ * - `best` — the best AVAILABLE subject image at dispatch time (D-3).
9460
+ * - `best-matching` — the media that explains WHY the rule fired: a rule
9461
+ * matched on identities attaches the subject's `faceCrop`, one matched on
9462
+ * plates attaches the `plateCrop`; a rule with no identity/plate condition
9463
+ * (or when the specific crop is missing) degrades to `best`, then
9464
+ * `keyFrame`, then no attachment — never delaying the send. The matched
9465
+ * condition summary is frozen on the outbox row at enqueue (like the rule
9466
+ * name), so the choice never drifts from the record that fired it.
9467
+ * - `keyFrame` — the clean scene frame (no subject box).
9468
+ * - `none` — no attachment.
9278
9469
  */
9279
- var AirQualitySensorStatusSchema = object({
9280
- /** Carbon dioxide concentration in ppm. */
9281
- co2Ppm: number().min(0).optional(),
9282
- /** Total volatile organic compounds in ppb. */
9283
- vocPpb: number().min(0).optional(),
9284
- /** Particulate matter ≤ 2.5 μm in µg/m³. */
9285
- pm25: number().min(0).optional(),
9286
- /** Particulate matter ≤ 10 μm in µg/m³. */
9287
- pm10: number().min(0).optional(),
9288
- /** Composite AQI value (typically 0..500). */
9289
- aqi: number().optional(),
9290
- /** Ms epoch when the slice was last updated. */
9291
- lastFetchedAt: number(),
9292
- /** Live display unit of the single metric this slice carries (e.g. HA
9293
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
9294
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
9295
- * per slice is unambiguous. */
9296
- unit: string().optional(),
9297
- /** Suggested decimal places for numeric display.
9298
- * Populated live from the upstream source when provided (e.g. HA
9299
- * `attributes.suggested_display_precision`). Falls back to
9300
- * auto-formatting when absent. */
9301
- precision: number().int().min(0).max(10).optional()
9470
+ var NcMediaPolicySchema = object({ attach: _enum([
9471
+ "best",
9472
+ "best-matching",
9473
+ "keyFrame",
9474
+ "none"
9475
+ ]).default("best") });
9476
+ /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9477
+ var NcThrottleSchema = object({
9478
+ cooldownSec: number().int().min(0).max(86400).default(60),
9479
+ /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
9480
+ scope: _enum(["rule", "rule-device"]).default("rule-device")
9481
+ });
9482
+ /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
9483
+ var NcRuleInputSchema = object({
9484
+ name: string().min(1).max(200),
9485
+ enabled: boolean().default(true),
9486
+ delivery: NcDeliverySchema,
9487
+ conditions: NcConditionsSchema.default({}),
9488
+ schedule: NcScheduleSchema.optional(),
9489
+ targets: array(NcRuleTargetSchema).min(1),
9490
+ media: NcMediaPolicySchema.default({ attach: "best" }),
9491
+ throttle: NcThrottleSchema.default({
9492
+ cooldownSec: 60,
9493
+ scope: "rule-device"
9494
+ }),
9495
+ /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
9496
+ template: object({
9497
+ title: string().max(500).optional(),
9498
+ body: string().max(2e3).optional()
9499
+ }).optional(),
9500
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
9501
+ priority: number().int().min(1).max(5).default(3),
9502
+ /**
9503
+ * Ownership/visibility key. Absent = admin/global rule (unchanged legacy
9504
+ * behaviour, visible to all, read-only in the viewer). Present = personal
9505
+ * rule owned by this userId. Server-stamped; never trusted from a client.
9506
+ */
9507
+ ownerUserId: string().optional()
9302
9508
  });
9303
- var airQualitySensorCapability = {
9304
- name: "air-quality-sensor",
9305
- scope: "device",
9306
- deviceNative: true,
9307
- mode: "singleton",
9308
- deviceTypes: [DeviceType.Sensor],
9309
- methods: {},
9310
- status: {
9311
- schema: AirQualitySensorStatusSchema,
9312
- kind: "push"
9313
- },
9314
- runtimeState: AirQualitySensorStatusSchema
9315
- };
9316
9509
  /**
9317
- * Alarm-panel cap. Models HA `alarm_control_panel.*` on
9318
- * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
9319
- * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
9320
- * arming / pending / triggered / disarming.
9321
- *
9322
- * Many panels require a PIN code on arm / disarm — the optional
9323
- * `code` field on the methods passes it through to the upstream
9324
- * service; it's NEVER persisted in the runtime slice or any event
9325
- * payload. The presence of a required code is signalled by
9326
- * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
9327
- * field without a slice fetch.
9328
- *
9329
- * `availableModes` mirrors HA's `supported_features`-derived arm
9330
- * mode list — the UI renders only the buttons the panel accepts.
9510
+ * Partial patch for `updateRule` — any subset of the input fields, plus the
9511
+ * persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
9512
+ * NOT a client-authored input field (it lives on the persisted rule, not the
9513
+ * input), so it is added here explicitly to let the store's per-target opt-out
9514
+ * toggle round-trip through the shared `update` path. Viewer opt-out mutations
9515
+ * still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
9516
+ * `updateRule` patch.
9331
9517
  */
9332
- var AlarmStateSchema = _enum([
9333
- "disarmed",
9334
- "armed_home",
9335
- "armed_away",
9336
- "armed_night",
9337
- "armed_vacation",
9338
- "armed_custom_bypass",
9339
- "arming",
9340
- "disarming",
9341
- "pending",
9342
- "triggered"
9343
- ]);
9344
- var AlarmArmModeSchema = _enum([
9345
- "home",
9346
- "away",
9347
- "night",
9348
- "vacation",
9349
- "custom_bypass"
9350
- ]);
9351
- var AlarmPanelStatusSchema = object({
9352
- /** Current lifecycle state. */
9353
- state: AlarmStateSchema,
9354
- /** Subset of arm modes the panel accepts. UI renders one button per
9355
- * mode in this list. */
9356
- availableModes: array(AlarmArmModeSchema),
9357
- /** Whether the panel requires a PIN on arm / disarm. Mirrors
9358
- * `DeviceFeature.AlarmPinRequired` for slice consumers. */
9359
- requiresCode: boolean(),
9360
- /** Ms epoch when the slice was last updated. */
9361
- lastChangedAt: number()
9362
- });
9363
- var alarmPanelCapability = {
9364
- name: "alarm-panel",
9365
- scope: "device",
9366
- deviceNative: true,
9367
- mode: "singleton",
9368
- deviceTypes: [DeviceType.AlarmPanel],
9369
- methods: {
9370
- arm: method(object({
9371
- deviceId: number().int().nonnegative(),
9372
- mode: AlarmArmModeSchema,
9373
- /** Optional PIN code. Required when `requiresCode === true`.
9374
- * Passed through to the upstream service; never persisted. */
9375
- code: string().min(1).optional()
9376
- }), _void(), {
9377
- kind: "mutation",
9378
- auth: "admin"
9379
- }),
9380
- disarm: method(object({
9381
- deviceId: number().int().nonnegative(),
9382
- code: string().min(1).optional()
9383
- }), _void(), {
9384
- kind: "mutation",
9385
- auth: "admin"
9386
- }),
9387
- /**
9388
- * Force the panel into the `triggered` state — used by HA
9389
- * automations to surface external sensor events through the panel
9390
- * (e.g. a Reolink camera intrusion event firing the security
9391
- * system). Provider rejects when the panel hardware doesn't
9392
- * support a software-initiated trigger.
9393
- */
9394
- trigger: method(object({ deviceId: number().int().nonnegative() }), _void(), {
9395
- kind: "mutation",
9396
- auth: "admin"
9397
- })
9398
- },
9399
- status: {
9400
- schema: AlarmPanelStatusSchema,
9401
- kind: "push"
9402
- },
9518
+ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
9519
+ /** A persisted rule. */
9520
+ var NcRuleSchema = NcRuleInputSchema.extend({
9521
+ id: string(),
9522
+ /** userId of the admin who created the rule (server-stamped caller). */
9523
+ createdBy: string(),
9524
+ createdAt: number(),
9525
+ updatedAt: number(),
9403
9526
  /**
9404
- * Runtime-state slice — mirrored by the kernel. UI panel reads the
9405
- * full slice; renders an arm button per `availableModes` entry and
9406
- * a PIN field iff `requiresCode === true`.
9527
+ * Per-target opt-out set. A targetId here is suppressed for THIS rule at
9528
+ * send time. Only a target's OWNER may add/remove its id (server-checked
9529
+ * in `nc.setRuleTargetEnabled`). Defaults to empty.
9407
9530
  */
9408
- runtimeState: AlarmPanelStatusSchema
9409
- };
9410
- /**
9411
- * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
9412
- * entries with `device_class: illuminance`.
9413
- */
9414
- var AmbientLightSensorStatusSchema = object({
9415
- /** Current illuminance in lux (lx). */
9416
- lux: number().min(0),
9417
- /** Ms epoch when the slice was last updated. */
9418
- lastFetchedAt: number(),
9419
- /** Live display unit from the upstream source (e.g. HA
9420
- * `attributes.unit_of_measurement`). The UI prefers this over the
9421
- * role's canonical unit. Absent → fall back to the canonical unit. */
9422
- unit: string().optional(),
9423
- /** Suggested decimal places for numeric display.
9424
- * Populated live from the upstream source when provided (e.g. HA
9425
- * `attributes.suggested_display_precision`). Falls back to
9426
- * auto-formatting when absent. */
9427
- precision: number().int().min(0).max(10).optional()
9531
+ disabledTargetIds: array(string()).default([])
9428
9532
  });
9429
- var ambientLightSensorCapability = {
9430
- name: "ambient-light-sensor",
9431
- scope: "device",
9432
- deviceNative: true,
9433
- mode: "singleton",
9434
- deviceTypes: [DeviceType.Sensor],
9435
- methods: {},
9436
- status: {
9437
- schema: AmbientLightSensorStatusSchema,
9438
- kind: "push"
9439
- },
9440
- runtimeState: AmbientLightSensorStatusSchema
9441
- };
9442
- /**
9443
- * Per-class audio metrics aggregated over a sliding window.
9444
- */
9445
- var AudioClassSummarySchema = object({
9446
- className: string(),
9447
- /** Number of windows (chunks) where this class was the top hit. */
9448
- hits: number().int().nonnegative(),
9449
- /** Mean score across those hits, clamped to [0,1]. */
9450
- avgScore: number().min(0).max(1),
9451
- /** Peak score in the window. */
9452
- peakScore: number().min(0).max(1)
9533
+ var NcTestResultSchema = object({
9534
+ recordId: string(),
9535
+ recordKind: _enum([
9536
+ "object-event",
9537
+ "track",
9538
+ "device-event",
9539
+ "package-event"
9540
+ ]),
9541
+ deviceId: number(),
9542
+ timestamp: number(),
9543
+ wouldFire: boolean(),
9544
+ /** Condition id that failed (first failing group), when `wouldFire` is false. */
9545
+ failedCondition: string().optional(),
9546
+ className: string().optional(),
9547
+ label: string().optional()
9548
+ });
9549
+ var NcConditionDescriptorSchema = object({
9550
+ /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
9551
+ id: string(),
9552
+ group: _enum([
9553
+ "scope",
9554
+ "class",
9555
+ "zones",
9556
+ "quality",
9557
+ "label",
9558
+ "schedule",
9559
+ "device",
9560
+ "package",
9561
+ "occupancy"
9562
+ ]),
9563
+ label: string(),
9564
+ /** Editor widget the UI renders — never hardcode per-condition forms. */
9565
+ valueType: _enum([
9566
+ "deviceIdList",
9567
+ "stringList",
9568
+ "number01",
9569
+ "number",
9570
+ "sourceSelect",
9571
+ "zoneSelection",
9572
+ "zoneIdList",
9573
+ "schedule",
9574
+ "plateMatcher",
9575
+ "packagePhase",
9576
+ "polygonDraw",
9577
+ "occupancy"
9578
+ ]),
9579
+ operator: _enum([
9580
+ "in",
9581
+ "notIn",
9582
+ "anyOf",
9583
+ "allOf",
9584
+ "gte",
9585
+ "fuzzyIn",
9586
+ "withinSchedule"
9587
+ ]),
9588
+ /** Which delivery kinds the condition applies to. */
9589
+ appliesTo: array(NcDeliverySchema),
9590
+ phase: string(),
9591
+ description: string().optional()
9453
9592
  });
9454
9593
  /**
9455
- * Per-camera audio metrics snapshot — emitted by the analytics frame
9456
- * handler on every `pipeline.audio-inference-result` event and
9457
- * mirrored into the `audio-metrics` device-state slice. Symmetric
9458
- * with `zone-analytics` snapshots for video — every consumer
9459
- * (admin UI panel, automations, alert rules) reads via the
9460
- * canonical `device.state.audioMetrics.value` reactive handle.
9594
+ * The delivery lifecycle status of a history row — a straight read of the
9595
+ * durable outbox row's own status (single source of truth):
9596
+ * - `pending` — enqueued, in-flight or retrying with backoff
9597
+ * - `sent` — delivered (terminal)
9598
+ * - `dead` — dead-lettered after exhausting retries / a permanent
9599
+ * backend rejection / a deleted target (terminal; carries
9600
+ * the failure `error`)
9461
9601
  *
9462
- * Aggregates are computed over a rolling `windowSec` window
9463
- * (default 60s). Past that window, classes drop out of `byClass`
9464
- * and the level history shifts forward.
9602
+ * P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
9603
+ * user dimension (quiet hours / snooze) and are additive when they land.
9465
9604
  */
9466
- var AudioMetricsSnapshotSchema = object({
9467
- /** Wall-clock timestamp (ms) of the most recent audio window. */
9468
- ts: number().int(),
9469
- /** Sliding-window length (seconds) used for aggregation. */
9470
- windowSec: number().int().positive(),
9471
- /** Latest level reading from the most recent window. */
9472
- level: object({
9473
- rms: number(),
9474
- dbfs: number()
9475
- }),
9476
- /** Peak dBFS observed across the rolling window. */
9477
- peakDbfs: number(),
9478
- /** Mean dBFS across the rolling window. */
9479
- avgDbfs: number(),
9480
- /** Most recent above-threshold classification, or null on silence. */
9481
- current: object({
9482
- className: string(),
9483
- score: number().min(0).max(1),
9484
- timestamp: number().int()
9485
- }).nullable(),
9486
- /** Per-class summary across the rolling window — keys are
9487
- * `macroClass` strings (e.g. `dog_bark`, `speech`, `glass_break`). */
9488
- byClass: array(AudioClassSummarySchema).readonly()
9605
+ var NcHistoryStatusSchema = _enum([
9606
+ "pending",
9607
+ "sent",
9608
+ "dead"
9609
+ ]);
9610
+ /** The evaluated record kind a history row descends from (one per trigger). */
9611
+ var NcHistoryRecordKindSchema = _enum([
9612
+ "object-event",
9613
+ "track-end",
9614
+ "device-event",
9615
+ "package-event"
9616
+ ]);
9617
+ /** Subject summary frozen on the row at fire time (survives rule/record edits). */
9618
+ var NcHistorySubjectSchema = object({
9619
+ className: string(),
9620
+ label: string().optional(),
9621
+ confidence: number().optional(),
9622
+ zones: array(string()),
9623
+ timestamp: number()
9489
9624
  });
9490
9625
  /**
9491
- * Audio-metrics history payload — a series of `AudioMetricsHistoryPoint`
9492
- * samples capped at `maxPoints` (default 1024). When the requested
9493
- * `windowSec / sampleEveryMs` would exceed the cap, the provider
9494
- * subsamples by bucketed averaging and reports the effective sample
9495
- * spacing on `effectiveSampleEveryMs` so the UI can label the x-axis.
9626
+ * One delivery-history row. This is a read-only VIEW over the durable
9627
+ * outbox row (single source of truth — the same row the drain loop drives;
9628
+ * NO second write path, so history can never drift from delivery state).
9629
+ * The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
9630
+ * `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
9631
+ * (fire) / `updatedAt` (last transition), `status` + `error` are the
9632
+ * lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
9633
+ * enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
9634
+ * P1 (admin scope only).
9496
9635
  */
9497
- var AudioMetricsHistorySchema = object({
9498
- points: array(object({
9499
- /** Wall-clock ms when this sample was recorded. */
9500
- ts: number().int(),
9501
- /** Instantaneous dBFS level at sample time. `null` for windows where
9502
- * the source had no level reading (rare; happens at decode startup). */
9503
- dbfs: number().nullable(),
9504
- /** Rolling-window peak dBFS at sample time. Same window the live
9505
- * snapshot reports. */
9506
- peakDbfs: number(),
9507
- /** Rolling-window mean dBFS at sample time. */
9508
- avgDbfs: number(),
9509
- /** Dominant above-threshold class at sample time, or null on silence. */
9510
- topClass: string().nullable(),
9511
- /** Score of the dominant class (`null` whenever `topClass` is null). */
9512
- topScore: number().min(0).max(1).nullable()
9513
- })).readonly(),
9514
- /** Actual ms between adjacent samples after any subsampling. */
9515
- effectiveSampleEveryMs: number().int().positive(),
9516
- /** Wall-clock window covered by `points` (`points[N-1].ts - points[0].ts`),
9517
- * or `0` when there's fewer than 2 samples. */
9518
- windowMsActual: number().int().nonnegative()
9636
+ var NcHistoryEntrySchema = object({
9637
+ /** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
9638
+ id: string(),
9639
+ ruleId: string(),
9640
+ /** Rule name frozen at fire time (outlives a later rename / delete). */
9641
+ ruleName: string(),
9642
+ /** The rule urgency/trigger that produced this delivery. */
9643
+ delivery: NcDeliverySchema,
9644
+ targetId: string(),
9645
+ deviceId: number(),
9646
+ recordKind: NcHistoryRecordKindSchema,
9647
+ /** Event / track ref of the evaluated record (§3.2 `eventRef`). */
9648
+ recordId: string(),
9649
+ /** Present for track-scoped deliveries (object-event / track-end). */
9650
+ trackId: string().optional(),
9651
+ status: NcHistoryStatusSchema,
9652
+ /** Delivery attempts made so far. */
9653
+ attempts: number().int(),
9654
+ /** Fire time (outbox enqueue). */
9655
+ createdAt: number(),
9656
+ /** Last transition time (terminal for sent / dead). */
9657
+ updatedAt: number(),
9658
+ /** Failure detail — present on a `dead` row. */
9659
+ error: string().optional(),
9660
+ subject: NcHistorySubjectSchema
9519
9661
  });
9520
9662
  /**
9521
- * Audio Metrics capability — sliding-window aggregates over the
9522
- * pipeline audio inference results. Hosted by `addon-pipeline-analytics`
9523
- * (same addon that owns `zone-analytics`); the runtime-state slice
9524
- * gives operators a live read on dB level + dominant classes without
9525
- * a custom event subscription.
9663
+ * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
9664
+ * AND; absent = unbounded on that axis. `since`/`until` bound the fire time
9665
+ * (`createdAt`, epoch ms, inclusive). `limit` is clamped to
9666
+ * {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
9526
9667
  */
9527
- var audioMetricsCapability = {
9528
- name: "audio-metrics",
9529
- scope: "device",
9530
- mode: "singleton",
9531
- deviceTypes: [DeviceType.Camera],
9532
- methods: {
9533
- /** Latest snapshot for this device. Null until the analytics
9534
- * pipeline has processed at least one audio window. */
9535
- getCurrentSnapshot: method(object({ deviceId: number() }), AudioMetricsSnapshotSchema.nullable()),
9536
- /**
9537
- * Time-series view of recent audio-metrics samples. The provider
9538
- * keeps an in-memory ring of ~1Hz samples (matching the slice-
9539
- * write rate) capped at `MAX_HISTORY_POINTS_KEPT` (provider-side).
9540
- * `windowSec` selects how far back to read; `sampleEveryMs`
9541
- * downsamples by bucketed averaging when finer than the kept
9542
- * granularity. Empty `points` array on freshly-booted providers
9543
- * with no audio yet — same convention as `getCurrentSnapshot`.
9544
- */
9545
- getHistory: method(object({
9546
- deviceId: number(),
9547
- /** History window in seconds. Default 300 (5 minutes).
9548
- * Provider clamps to its retention cap if larger. */
9549
- windowSec: number().int().positive().optional(),
9550
- /** Target sample interval in ms. Default 1000 (1 sample/second).
9551
- * Provider clamps to natural sample rate if smaller, and
9552
- * bucket-averages when bigger than the requested window
9553
- * would produce more than `maxPoints` samples. */
9554
- sampleEveryMs: number().int().positive().optional()
9555
- }), AudioMetricsHistorySchema)
9556
- },
9557
- /** Reactive runtime-state mirror — live `device.state.audioMetrics.value`. */
9558
- runtimeState: AudioMetricsSnapshotSchema
9559
- };
9668
+ var NcHistoryFilterSchema = object({
9669
+ ruleId: string().optional(),
9670
+ deviceId: number().optional(),
9671
+ status: NcHistoryStatusSchema.optional(),
9672
+ since: number().optional(),
9673
+ until: number().optional(),
9674
+ limit: number().int().min(1).max(500).default(100)
9675
+ });
9676
+ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), method(object({ ruleId: string() }), object({ rule: NcRuleSchema.nullable() }), { auth: "admin" }), method(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
9677
+ kind: "mutation",
9678
+ auth: "admin",
9679
+ caller: "required"
9680
+ }), method(object({
9681
+ ruleId: string(),
9682
+ patch: NcRulePatchSchema
9683
+ }), object({ rule: NcRuleSchema }), {
9684
+ kind: "mutation",
9685
+ auth: "admin",
9686
+ caller: "required"
9687
+ }), method(object({ ruleId: string() }), object({ success: literal(true) }), {
9688
+ kind: "mutation",
9689
+ auth: "admin"
9690
+ }), method(object({
9691
+ ruleId: string(),
9692
+ enabled: boolean()
9693
+ }), object({ success: literal(true) }), {
9694
+ kind: "mutation",
9695
+ auth: "admin"
9696
+ }), method(object({
9697
+ rule: NcRuleInputSchema,
9698
+ lookbackMinutes: number().int().min(1).max(1440).default(60)
9699
+ }), object({ results: array(NcTestResultSchema) }), {
9700
+ kind: "mutation",
9701
+ auth: "admin"
9702
+ }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
9560
9703
  /**
9561
- * Automation-control cap. Models HA `automation.*` entities on
9562
- * `DeviceType.Automation`. An automation is a trigger+condition+
9563
- * action rule that can be enabled / disabled and manually fired
9564
- * via the `trigger` method.
9704
+ * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9705
+ *
9706
+ * Spec: `docs/superpowers/specs/2026-07-24-nc-occupancy-timelapse-design.md`
9707
+ * §3.2/§3.3.
9708
+ *
9709
+ * Deliberately NOT a capability definition and NOT an `NcRule`:
9710
+ * - Every `NcDelivery` member is a *persisted-pipeline-record* trigger. A
9711
+ * timelapse fires on a SCHEDULE WINDOW BOUNDARY, evaluates no pipeline
9712
+ * record, and produces a video it assembled itself — so it rides no
9713
+ * delivery-enum member (the enum is frozen) and no cap method. This file is
9714
+ * a plain typed schema; it does NOT go through `npm run codegen`.
9715
+ * - It shares only the delivery leg (`notification-output.send`) and the
9716
+ * persistence/ownership patterns with the Notification Center, reusing
9717
+ * {@link NcScheduleSchema} (weekly windows, midnight-crossing, invertible)
9718
+ * and {@link NcRuleTargetSchema} (target ref + passthrough params).
9719
+ *
9720
+ * Ownership is SERVER-DERIVED. `ownerUserId` / `createdBy` / `createdAt` /
9721
+ * `updatedAt` / `id` / `lastGeneratedAt` live on the PERSISTED rule only —
9722
+ * {@link TimelapseRuleInputSchema} and {@link TimelapseRulePatchSchema} do not
9723
+ * carry them, so a forged client payload can never claim or re-own a rule
9724
+ * (Zod strips unknown keys). The store stamps them from the resolved caller.
9725
+ */
9726
+ /** `{{var}}` templating over camera/rule/time — same vocabulary as `NcRule`. */
9727
+ var TimelapseTemplateSchema = object({
9728
+ title: string().max(500).optional(),
9729
+ body: string().max(2e3).optional()
9730
+ });
9731
+ var NameField = string().min(1).max(200);
9732
+ var DeviceIdsField = array(number()).min(1);
9733
+ var CadenceSecField = number().int().min(2).max(3600);
9734
+ var FramerateField = number().int().min(1).max(60);
9735
+ var TargetsField = array(NcRuleTargetSchema).min(1);
9736
+ var PriorityField = number().int().min(1).max(5);
9737
+ /**
9738
+ * Client-supplied timelapse-rule fields. The server stamps id / createdBy /
9739
+ * createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
9740
+ * here (see the ownership note above).
9741
+ */
9742
+ var TimelapseRuleInputSchema = object({
9743
+ name: NameField,
9744
+ enabled: boolean().default(true),
9745
+ /** Cameras sampled by this rule — one scratch dir + one artifact per device. */
9746
+ deviceIds: DeviceIdsField,
9747
+ /**
9748
+ * Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
9749
+ * means "always active"): a timelapse is defined by its window boundaries —
9750
+ * open clears the scratch, close assembles and delivers.
9751
+ */
9752
+ schedule: NcScheduleSchema,
9753
+ /** Force-snapshot cadence inside the window, seconds (predecessor parity). */
9754
+ cadenceSec: CadenceSecField.default(15),
9755
+ /** Output frames per second of the assembled mp4 (predecessor parity). */
9756
+ framerate: FramerateField.default(10),
9757
+ /** `notification-output` targets the finished video/thumbnail is sent to. */
9758
+ targets: TargetsField,
9759
+ template: TimelapseTemplateSchema.optional(),
9760
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
9761
+ priority: PriorityField.default(3)
9762
+ });
9763
+ object({
9764
+ name: NameField.optional(),
9765
+ enabled: boolean().optional(),
9766
+ deviceIds: DeviceIdsField.optional(),
9767
+ schedule: NcScheduleSchema.optional(),
9768
+ cadenceSec: CadenceSecField.optional(),
9769
+ framerate: FramerateField.optional(),
9770
+ targets: TargetsField.optional(),
9771
+ template: TimelapseTemplateSchema.nullable().optional(),
9772
+ priority: PriorityField.optional()
9773
+ });
9774
+ TimelapseRuleInputSchema.extend({
9775
+ id: string(),
9776
+ /**
9777
+ * Ownership/visibility key. Absent = admin/global rule (visible to all).
9778
+ * Present = personal rule owned by this userId. Server-stamped from the
9779
+ * resolved caller; never trusted from a client payload.
9780
+ */
9781
+ ownerUserId: string().optional(),
9782
+ /**
9783
+ * Epoch-ms of the last successful generation — the 1-hour re-generation
9784
+ * guard's durable state (predecessor parity). Absent = never generated.
9785
+ */
9786
+ lastGeneratedAt: number().optional(),
9787
+ /** userId of the caller who created the rule (server-stamped). */
9788
+ createdBy: string(),
9789
+ createdAt: number(),
9790
+ updatedAt: number()
9791
+ });
9792
+ /**
9793
+ * Generic device-level status snapshot. Auto-registered by `BaseDevice`
9794
+ * for every device, regardless of provider — the kernel needs a uniform
9795
+ * cap-keyed slice for the basic device flags every consumer expects to
9796
+ * read across processes (the `online` flag in particular). Driver-specific
9797
+ * caps (`battery`, `doorbell`, …) carry their domain-specific state on
9798
+ * their own slices.
9565
9799
  *
9566
- * `trigger` accepts an optional `skipCondition` flag — when true,
9567
- * the automation's action block runs WITHOUT evaluating its
9568
- * condition block. Pair with `DeviceFeature.AutomationSkipCondition`
9569
- * to gate the UI checkbox for the manual-trigger dialog.
9800
+ * Pattern is identical to `battery`: schema-bearing `runtimeState`,
9801
+ * empty `methods`, single change event. Reads land at
9802
+ * `runtimeState.getCapState('device-status')`; writes at
9803
+ * `runtimeState.setCapState('device-status', …)`. Cross-process
9804
+ * consumers reach the same data via the `device-state` cap router
9805
+ * (`getCapSlice({deviceId, capName: 'device-status'})`).
9570
9806
  */
9571
- var AutomationControlStatusSchema = object({
9572
- /** Whether the automation is currently enabled. Disabled automations
9573
- * ignore their trigger block — manual `trigger` still works. */
9574
- enabled: boolean(),
9575
- /** Whether the automation is currently executing its action block. */
9576
- isRunning: boolean(),
9577
- /** Ms epoch of the last successful run. 0 when never run. */
9578
- lastTriggeredAt: number(),
9579
- /** Failure description from the last completed run. Null on success
9580
- * or when never run. */
9581
- lastError: string().nullable(),
9582
- /** Ms epoch when the slice was last updated. */
9807
+ var DeviceStatusSchema = object({
9808
+ /**
9809
+ * Device-level liveness. Drivers flip via `markOnline(boolean)` on
9810
+ * `BaseDevice`. Provider semantics vary — RTSP aggregates broker
9811
+ * stream-health, Reolink reads firmware push events, ONVIF tracks
9812
+ * ping responses. This cap intentionally does NOT prescribe which
9813
+ * signal drives the flag.
9814
+ */
9815
+ online: boolean(),
9816
+ /** Ms epoch of the last `online` transition. Lets consumers tell
9817
+ * apart "just came online" from "still online". */
9583
9818
  lastChangedAt: number()
9584
9819
  });
9585
- var automationControlCapability = {
9586
- name: "automation-control",
9820
+ var deviceStatusCapability = {
9821
+ name: "device-status",
9587
9822
  scope: "device",
9588
9823
  deviceNative: true,
9589
9824
  mode: "singleton",
9590
- deviceTypes: [DeviceType.Automation],
9591
- methods: {
9592
- enable: method(object({ deviceId: number().int().nonnegative() }), _void(), {
9593
- kind: "mutation",
9594
- auth: "admin"
9595
- }),
9596
- disable: method(object({ deviceId: number().int().nonnegative() }), _void(), {
9597
- kind: "mutation",
9598
- auth: "admin"
9599
- }),
9600
- trigger: method(object({
9601
- deviceId: number().int().nonnegative(),
9602
- /** When true, fires the action block while bypassing the
9603
- * automation's condition evaluation. Gated by
9604
- * `DeviceFeature.AutomationSkipCondition`. */
9605
- skipCondition: boolean().optional()
9606
- }), _void(), {
9607
- kind: "mutation",
9608
- auth: "admin"
9609
- })
9610
- },
9825
+ methods: {},
9826
+ events: {
9827
+ /** Emitted when `online` transitions. Mirrors the semantics of
9828
+ * `battery.onStatusChanged`. */
9829
+ onStatusChanged: { data: object({
9830
+ deviceId: number(),
9831
+ status: DeviceStatusSchema
9832
+ }) } },
9611
9833
  status: {
9612
- schema: AutomationControlStatusSchema,
9834
+ schema: DeviceStatusSchema,
9613
9835
  kind: "push"
9614
9836
  },
9615
- /**
9616
- * Runtime-state slice — mirrored by the kernel. UI automation tile
9617
- * reads `enabled` (toggle) + `isRunning` (spinner) + `lastError`
9618
- * (badge) directly.
9619
- */
9620
- runtimeState: AutomationControlStatusSchema
9837
+ runtimeState: DeviceStatusSchema
9621
9838
  };
9622
9839
  /**
9623
- * Battery status snapshot. Emitted by providers whose device is
9624
- * battery-operated (cameras with `DeviceFeature.BatteryOperated`,
9625
- * future sensor/button accessories). Consumers build their own "low
9626
- * battery" alerting on top — the cap deliberately does NOT enforce a
9627
- * threshold.
9840
+ * Per-device feature/identity probe slice. Holds the runtime-resolved
9841
+ * truth about what a device CAN do — which the kernel uses to:
9842
+ * 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
9843
+ * based on what the firmware actually advertises).
9844
+ * 2. Compute the public `features: DeviceFeature[]` array surfaced via
9845
+ * `device-manager.listAll`.
9846
+ * 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
9847
+ * to register on the device's capability surface.
9848
+ *
9849
+ * Auto-registered by `BaseDevice` for every device. Drivers populate the
9850
+ * slice from `onProbe()` (kernel calls it once after register, before
9851
+ * accessory reconciliation). Consumers read via:
9852
+ * `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
9853
+ *
9854
+ * `flags` is an open record so each driver carries its own keys without
9855
+ * a centralized schema bottleneck — Reolink writes `hasPtz/hasIntercom`,
9856
+ * Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
9857
+ *
9858
+ * Replaces the older driver-local `deviceCache.has*` blob: the per-device
9859
+ * config is for operator-edited overrides + UI snapshots; runtime probe
9860
+ * results belong in runtime-state where the kernel handles persistence,
9861
+ * cross-process mirroring, and reactive updates.
9628
9862
  */
9629
- var BatteryStatusSchema = object({
9630
- /** 0..100 inclusive. Firmware-reported. */
9631
- percentage: number().min(0).max(100),
9863
+ var FeatureProbeStatusSchema = object({
9632
9864
  /**
9633
- * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
9634
- * Reolink-specific for the Solar Panel 2 accessory (will become
9635
- * common on other battery cams). `'none'` means running on battery
9636
- * alone.
9865
+ * Driver-specific flag bag. Each driver picks its own key names — the
9866
+ * cap deliberately does NOT enforce a closed enum here. Reolink keys:
9867
+ * `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
9868
+ * `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
9869
+ * `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
9637
9870
  */
9638
- charging: _enum([
9639
- "dc",
9640
- "solar",
9641
- "none"
9642
- ]),
9871
+ flags: record(string(), unknown()),
9643
9872
  /**
9644
- * True when the camera firmware has gone into low-power mode. Battery
9645
- * providers MUST avoid polling during sleep — reading the battery
9646
- * wakes the camera up and drains charge.
9873
+ * Coarse driver-classification — lets cross-process consumers tell apart
9874
+ * cameras / battery-cams / NVRs without re-running the probe. `null`
9875
+ * before the first probe completes.
9647
9876
  */
9648
- sleeping: boolean(),
9649
- /** Ms epoch of the last observation. Lets consumers reason about freshness. */
9650
- lastUpdated: number(),
9877
+ deviceType: string().nullable(),
9878
+ /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
9879
+ model: string().nullable(),
9880
+ /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
9881
+ channelCount: number().nullable(),
9651
9882
  /**
9652
- * True when the source is a BINARY low-battery indicator (HA
9653
- * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
9654
- * charge level — `percentage` is then a coarse stand-in (100 = normal,
9655
- * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
9656
- * misleading exact percentage. Absent/false → genuine 0–100 % reading.
9883
+ * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
9884
+ * completes — drivers' `getAccessoryChildren()` should treat zero as
9885
+ * "probe not done yet, return empty" so accessories aren't spawned
9886
+ * before the firmware is queried.
9657
9887
  */
9658
- binary: boolean().optional()
9888
+ lastProbedAt: number(),
9889
+ /**
9890
+ * Framework convention: every runtime-state slice carries this for the
9891
+ * createRuntimeStateBridge stale-check helper. We keep it in sync with
9892
+ * `lastProbedAt` on every write.
9893
+ */
9894
+ lastFetchedAt: number()
9659
9895
  });
9660
- var batteryCapability = {
9661
- name: "battery",
9896
+ var featureProbeCapability = {
9897
+ name: "feature-probe",
9662
9898
  scope: "device",
9663
9899
  deviceNative: true,
9664
9900
  mode: "singleton",
9665
- deviceTypes: [
9666
- DeviceType.Camera,
9667
- DeviceType.Sensor,
9668
- DeviceType.Button,
9669
- DeviceType.Switch
9670
- ],
9671
- methods: {
9672
- /**
9673
- * Explicitly wake the camera from low-power sleep ahead of a
9674
- * streaming session start. Consumers that initiate a stream
9675
- * against a sleeping battery cam (HomeKit Secure Video, Alexa
9676
- * RTCSession, snapshot wrappers) call this with a short timeout
9677
- * before establishing the media pipeline — the broker's own
9678
- * passive wake-on-dial works but adds 5–7 seconds to first-frame,
9679
- * during which the consumer renders a black screen. Pre-waking
9680
- * compresses that gap.
9681
- *
9682
- * Returns `awoke: true` when the firmware acknowledged the wake
9683
- * before `timeoutMs`. Returns `awoke: false` when it timed out OR
9684
- * the cap surface is unavailable (no Baichuan / firmware
9685
- * channel); the caller should still attempt the stream — the
9686
- * passive broker wake remains as fallback.
9687
- */
9688
- wakeForStream: method(object({
9689
- deviceId: number(),
9690
- /** Bound on the wait. Sensible range 3000–10000ms. */
9691
- timeoutMs: number().int().min(500).max(3e4).default(8e3)
9692
- }), object({
9693
- awoke: boolean(),
9694
- durationMs: number()
9695
- }), { kind: "mutation" }) },
9901
+ methods: {},
9696
9902
  events: {
9697
- /**
9698
- * Emitted whenever the cached status changes (firmware push OR
9699
- * poll observes a delta). The DeviceEventPropagator mirrors this
9700
- * event on the parent chain — subscribing to a camera's source
9701
- * receives battery events from child accessories automatically.
9702
- */
9703
- onStatusChanged: { data: object({
9903
+ /** Fires whenever a fresh probe completes (kernel-driven `reprobe()`
9904
+ * or driver-initiated re-detect after a state change). */
9905
+ onProbeChanged: { data: object({
9704
9906
  deviceId: number(),
9705
- status: BatteryStatusSchema
9907
+ status: FeatureProbeStatusSchema
9706
9908
  }) } },
9707
9909
  status: {
9708
- schema: BatteryStatusSchema,
9709
- kind: "push",
9710
- empty: {
9711
- percentage: 0,
9712
- charging: "none",
9713
- sleeping: false,
9714
- lastUpdated: 0
9715
- }
9910
+ schema: FeatureProbeStatusSchema,
9911
+ kind: "push"
9716
9912
  },
9717
- /**
9718
- * Runtime-state slice — every provider that registers this cap
9719
- * stores the same shape under `device.runtimeState[battery]`.
9720
- * Cross-provider uniformity: a Reolink Argus, a Frigate sensor
9721
- * proxy, an ONVIF battery cam all read/write the same keys.
9722
- * Consumers (BatteryBadge, snapshot wrapper sleep gate) read once
9723
- * via `device.runtimeState.getCapState('battery')` regardless of
9724
- * the underlying driver.
9725
- */
9726
- runtimeState: BatteryStatusSchema
9913
+ runtimeState: FeatureProbeStatusSchema
9727
9914
  };
9728
9915
  /**
9729
- * Generic boolean sensor — last-resort fallback when no domain-
9730
- * specific binary cap fits (Home Assistant `binary_sensor` without a
9731
- * known `device_class`, or a domain we haven't typed yet). Pure
9732
- * pass-through: just the bool + timestamp. Push-driven.
9733
- *
9734
- * Prefer the typed alternatives (`contact`, `flood`, `smoke`,
9735
- * `carbon-monoxide`, `gas`, `tamper`, `vibration`, `connectivity`,
9736
- * `motion`) when the semantics match — export adapters render those
9737
- * with the right HomeKit / Alexa display category.
9916
+ * Multi-metric air-quality slice. Covers CO₂, total VOCs, particulate
9917
+ * matter at PM2.5 / PM10, and a derived AQI index — all optional so
9918
+ * a single-metric source populates only what it observes. Mirrors
9919
+ * the HA `sensor` device_class set (`co2`, `volatile_organic_compounds`,
9920
+ * `pm25`, `pm10`, `aqi`) collapsed into one cap because a typical
9921
+ * air-quality node reports several of these together; modelling them
9922
+ * as siblings keeps a single timestamp + one slice subscription.
9738
9923
  */
9739
- var BinaryStatusSchema = object({
9740
- on: boolean(),
9741
- /** Ms epoch of the last transition. 0 if never observed. */
9742
- lastChangedAt: number()
9924
+ var AirQualitySensorStatusSchema = object({
9925
+ /** Carbon dioxide concentration in ppm. */
9926
+ co2Ppm: number().min(0).optional(),
9927
+ /** Total volatile organic compounds in ppb. */
9928
+ vocPpb: number().min(0).optional(),
9929
+ /** Particulate matter ≤ 2.5 μm in µg/m³. */
9930
+ pm25: number().min(0).optional(),
9931
+ /** Particulate matter ≤ 10 μm in µg/m³. */
9932
+ pm10: number().min(0).optional(),
9933
+ /** Composite AQI value (typically 0..500). */
9934
+ aqi: number().optional(),
9935
+ /** Ms epoch when the slice was last updated. */
9936
+ lastFetchedAt: number(),
9937
+ /** Live display unit of the single metric this slice carries (e.g. HA
9938
+ * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
9939
+ * upstream `sensor.*` entity surfaces ONE device_class, so one unit
9940
+ * per slice is unambiguous. */
9941
+ unit: string().optional(),
9942
+ /** Suggested decimal places for numeric display.
9943
+ * Populated live from the upstream source when provided (e.g. HA
9944
+ * `attributes.suggested_display_precision`). Falls back to
9945
+ * auto-formatting when absent. */
9946
+ precision: number().int().min(0).max(10).optional()
9743
9947
  });
9744
- var binaryCapability = {
9745
- name: "binary",
9948
+ var airQualitySensorCapability = {
9949
+ name: "air-quality-sensor",
9746
9950
  scope: "device",
9747
9951
  deviceNative: true,
9748
9952
  mode: "singleton",
9749
9953
  deviceTypes: [DeviceType.Sensor],
9750
9954
  methods: {},
9751
9955
  status: {
9752
- schema: BinaryStatusSchema,
9956
+ schema: AirQualitySensorStatusSchema,
9753
9957
  kind: "push"
9754
9958
  },
9755
- runtimeState: BinaryStatusSchema
9959
+ runtimeState: AirQualitySensorStatusSchema
9756
9960
  };
9757
9961
  /**
9758
- * Dimmable-light brightness control. Co-exists with `switch` on the
9759
- * same device — the switch toggles on/off, this cap sets the level
9760
- * applied when the light is on. Drivers map their per-vendor dim
9761
- * controls to this single-method surface.
9962
+ * Alarm-panel cap. Models HA `alarm_control_panel.*` on
9963
+ * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
9964
+ * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
9965
+ * arming / pending / triggered / disarming.
9762
9966
  *
9763
- * The cap is intentionally minimal: a single `setBrightness({deviceId,
9764
- * percentage})` mutation plus the auto-injected `getStatus`. Drivers
9765
- * that expose richer controls (color temperature, scenes, schedules)
9766
- * should surface those via the device's `getSettingsUISchema()`
9767
- * instead of bloating this cap.
9967
+ * Many panels require a PIN code on arm / disarm — the optional
9968
+ * `code` field on the methods passes it through to the upstream
9969
+ * service; it's NEVER persisted in the runtime slice or any event
9970
+ * payload. The presence of a required code is signalled by
9971
+ * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
9972
+ * field without a slice fetch.
9973
+ *
9974
+ * `availableModes` mirrors HA's `supported_features`-derived arm
9975
+ * mode list — the UI renders only the buttons the panel accepts.
9768
9976
  */
9769
- var BrightnessStatusSchema = object({
9770
- /** Current level as 0..100 inclusive. Firmware-reported. */
9771
- percentage: number().min(0).max(100),
9772
- /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
9977
+ var AlarmStateSchema = _enum([
9978
+ "disarmed",
9979
+ "armed_home",
9980
+ "armed_away",
9981
+ "armed_night",
9982
+ "armed_vacation",
9983
+ "armed_custom_bypass",
9984
+ "arming",
9985
+ "disarming",
9986
+ "pending",
9987
+ "triggered"
9988
+ ]);
9989
+ var AlarmArmModeSchema = _enum([
9990
+ "home",
9991
+ "away",
9992
+ "night",
9993
+ "vacation",
9994
+ "custom_bypass"
9995
+ ]);
9996
+ var AlarmPanelStatusSchema = object({
9997
+ /** Current lifecycle state. */
9998
+ state: AlarmStateSchema,
9999
+ /** Subset of arm modes the panel accepts. UI renders one button per
10000
+ * mode in this list. */
10001
+ availableModes: array(AlarmArmModeSchema),
10002
+ /** Whether the panel requires a PIN on arm / disarm. Mirrors
10003
+ * `DeviceFeature.AlarmPinRequired` for slice consumers. */
10004
+ requiresCode: boolean(),
10005
+ /** Ms epoch when the slice was last updated. */
9773
10006
  lastChangedAt: number()
9774
10007
  });
9775
- var brightnessCapability = {
9776
- name: "brightness",
10008
+ var alarmPanelCapability = {
10009
+ name: "alarm-panel",
9777
10010
  scope: "device",
9778
10011
  deviceNative: true,
9779
10012
  mode: "singleton",
9780
- deviceTypes: [DeviceType.Light],
9781
- methods: { setBrightness: method(object({
9782
- deviceId: number().int().nonnegative(),
9783
- percentage: number().min(0).max(100)
9784
- }), _void(), {
9785
- kind: "mutation",
9786
- auth: "admin"
9787
- }) },
9788
- events: {
9789
- /**
9790
- * Emitted whenever the brightness changes — operator action OR
9791
- * firmware push. Subscribers (UI sliders, automation engines) react
9792
- * without polling.
9793
- */
9794
- onBrightnessChanged: { data: object({
9795
- deviceId: number(),
9796
- percentage: number().min(0).max(100),
9797
- lastChangedAt: number()
9798
- }) } },
9799
- status: {
9800
- schema: BrightnessStatusSchema,
9801
- kind: "command-driven"
9802
- },
9803
- /**
9804
- * Runtime-state slice — the last applied brightness level, mirrored
9805
- * by the kernel. Read via `device.state.brightness.value` so UI
9806
- * sliders surface the current level without polling the provider.
9807
- */
9808
- runtimeState: BrightnessStatusSchema
9809
- };
9810
- /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
10013
+ deviceTypes: [DeviceType.AlarmPanel],
10014
+ methods: {
10015
+ arm: method(object({
10016
+ deviceId: number().int().nonnegative(),
10017
+ mode: AlarmArmModeSchema,
10018
+ /** Optional PIN code. Required when `requiresCode === true`.
10019
+ * Passed through to the upstream service; never persisted. */
10020
+ code: string().min(1).optional()
10021
+ }), _void(), {
10022
+ kind: "mutation",
10023
+ auth: "admin"
10024
+ }),
10025
+ disarm: method(object({
10026
+ deviceId: number().int().nonnegative(),
10027
+ code: string().min(1).optional()
10028
+ }), _void(), {
10029
+ kind: "mutation",
10030
+ auth: "admin"
10031
+ }),
10032
+ /**
10033
+ * Force the panel into the `triggered` state — used by HA
10034
+ * automations to surface external sensor events through the panel
10035
+ * (e.g. a Reolink camera intrusion event firing the security
10036
+ * system). Provider rejects when the panel hardware doesn't
10037
+ * support a software-initiated trigger.
10038
+ */
10039
+ trigger: method(object({ deviceId: number().int().nonnegative() }), _void(), {
10040
+ kind: "mutation",
10041
+ auth: "admin"
10042
+ })
10043
+ },
10044
+ status: {
10045
+ schema: AlarmPanelStatusSchema,
10046
+ kind: "push"
10047
+ },
10048
+ /**
10049
+ * Runtime-state slice — mirrored by the kernel. UI panel reads the
10050
+ * full slice; renders an arm button per `availableModes` entry and
10051
+ * a PIN field iff `requiresCode === true`.
10052
+ */
10053
+ runtimeState: AlarmPanelStatusSchema
10054
+ };
10055
+ /**
10056
+ * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
10057
+ * entries with `device_class: illuminance`.
10058
+ */
10059
+ var AmbientLightSensorStatusSchema = object({
10060
+ /** Current illuminance in lux (lx). */
10061
+ lux: number().min(0),
10062
+ /** Ms epoch when the slice was last updated. */
10063
+ lastFetchedAt: number(),
10064
+ /** Live display unit from the upstream source (e.g. HA
10065
+ * `attributes.unit_of_measurement`). The UI prefers this over the
10066
+ * role's canonical unit. Absent → fall back to the canonical unit. */
10067
+ unit: string().optional(),
10068
+ /** Suggested decimal places for numeric display.
10069
+ * Populated live from the upstream source when provided (e.g. HA
10070
+ * `attributes.suggested_display_precision`). Falls back to
10071
+ * auto-formatting when absent. */
10072
+ precision: number().int().min(0).max(10).optional()
10073
+ });
10074
+ var ambientLightSensorCapability = {
10075
+ name: "ambient-light-sensor",
10076
+ scope: "device",
10077
+ deviceNative: true,
10078
+ mode: "singleton",
10079
+ deviceTypes: [DeviceType.Sensor],
10080
+ methods: {},
10081
+ status: {
10082
+ schema: AmbientLightSensorStatusSchema,
10083
+ kind: "push"
10084
+ },
10085
+ runtimeState: AmbientLightSensorStatusSchema
10086
+ };
10087
+ /**
10088
+ * Per-class audio metrics aggregated over a sliding window.
10089
+ */
10090
+ var AudioClassSummarySchema = object({
10091
+ className: string(),
10092
+ /** Number of windows (chunks) where this class was the top hit. */
10093
+ hits: number().int().nonnegative(),
10094
+ /** Mean score across those hits, clamped to [0,1]. */
10095
+ avgScore: number().min(0).max(1),
10096
+ /** Peak score in the window. */
10097
+ peakScore: number().min(0).max(1)
10098
+ });
10099
+ /**
10100
+ * Per-camera audio metrics snapshot — emitted by the analytics frame
10101
+ * handler on every `pipeline.audio-inference-result` event and
10102
+ * mirrored into the `audio-metrics` device-state slice. Symmetric
10103
+ * with `zone-analytics` snapshots for video — every consumer
10104
+ * (admin UI panel, automations, alert rules) reads via the
10105
+ * canonical `device.state.audioMetrics.value` reactive handle.
10106
+ *
10107
+ * Aggregates are computed over a rolling `windowSec` window
10108
+ * (default 60s). Past that window, classes drop out of `byClass`
10109
+ * and the level history shifts forward.
10110
+ */
10111
+ var AudioMetricsSnapshotSchema = object({
10112
+ /** Wall-clock timestamp (ms) of the most recent audio window. */
10113
+ ts: number().int(),
10114
+ /** Sliding-window length (seconds) used for aggregation. */
10115
+ windowSec: number().int().positive(),
10116
+ /** Latest level reading from the most recent window. */
10117
+ level: object({
10118
+ rms: number(),
10119
+ dbfs: number()
10120
+ }),
10121
+ /** Peak dBFS observed across the rolling window. */
10122
+ peakDbfs: number(),
10123
+ /** Mean dBFS across the rolling window. */
10124
+ avgDbfs: number(),
10125
+ /** Most recent above-threshold classification, or null on silence. */
10126
+ current: object({
10127
+ className: string(),
10128
+ score: number().min(0).max(1),
10129
+ timestamp: number().int()
10130
+ }).nullable(),
10131
+ /** Per-class summary across the rolling window — keys are
10132
+ * `macroClass` strings (e.g. `dog_bark`, `speech`, `glass_break`). */
10133
+ byClass: array(AudioClassSummarySchema).readonly()
10134
+ });
10135
+ /**
10136
+ * Audio-metrics history payload — a series of `AudioMetricsHistoryPoint`
10137
+ * samples capped at `maxPoints` (default 1024). When the requested
10138
+ * `windowSec / sampleEveryMs` would exceed the cap, the provider
10139
+ * subsamples by bucketed averaging and reports the effective sample
10140
+ * spacing on `effectiveSampleEveryMs` so the UI can label the x-axis.
10141
+ */
10142
+ var AudioMetricsHistorySchema = object({
10143
+ points: array(object({
10144
+ /** Wall-clock ms when this sample was recorded. */
10145
+ ts: number().int(),
10146
+ /** Instantaneous dBFS level at sample time. `null` for windows where
10147
+ * the source had no level reading (rare; happens at decode startup). */
10148
+ dbfs: number().nullable(),
10149
+ /** Rolling-window peak dBFS at sample time. Same window the live
10150
+ * snapshot reports. */
10151
+ peakDbfs: number(),
10152
+ /** Rolling-window mean dBFS at sample time. */
10153
+ avgDbfs: number(),
10154
+ /** Dominant above-threshold class at sample time, or null on silence. */
10155
+ topClass: string().nullable(),
10156
+ /** Score of the dominant class (`null` whenever `topClass` is null). */
10157
+ topScore: number().min(0).max(1).nullable()
10158
+ })).readonly(),
10159
+ /** Actual ms between adjacent samples after any subsampling. */
10160
+ effectiveSampleEveryMs: number().int().positive(),
10161
+ /** Wall-clock window covered by `points` (`points[N-1].ts - points[0].ts`),
10162
+ * or `0` when there's fewer than 2 samples. */
10163
+ windowMsActual: number().int().nonnegative()
10164
+ });
10165
+ /**
10166
+ * Audio Metrics capability — sliding-window aggregates over the
10167
+ * pipeline audio inference results. Hosted by `addon-pipeline-analytics`
10168
+ * (same addon that owns `zone-analytics`); the runtime-state slice
10169
+ * gives operators a live read on dB level + dominant classes without
10170
+ * a custom event subscription.
10171
+ */
10172
+ var audioMetricsCapability = {
10173
+ name: "audio-metrics",
10174
+ scope: "device",
10175
+ mode: "singleton",
10176
+ deviceTypes: [DeviceType.Camera],
10177
+ methods: {
10178
+ /** Latest snapshot for this device. Null until the analytics
10179
+ * pipeline has processed at least one audio window. */
10180
+ getCurrentSnapshot: method(object({ deviceId: number() }), AudioMetricsSnapshotSchema.nullable()),
10181
+ /**
10182
+ * Time-series view of recent audio-metrics samples. The provider
10183
+ * keeps an in-memory ring of ~1Hz samples (matching the slice-
10184
+ * write rate) capped at `MAX_HISTORY_POINTS_KEPT` (provider-side).
10185
+ * `windowSec` selects how far back to read; `sampleEveryMs`
10186
+ * downsamples by bucketed averaging when finer than the kept
10187
+ * granularity. Empty `points` array on freshly-booted providers
10188
+ * with no audio yet — same convention as `getCurrentSnapshot`.
10189
+ */
10190
+ getHistory: method(object({
10191
+ deviceId: number(),
10192
+ /** History window in seconds. Default 300 (5 minutes).
10193
+ * Provider clamps to its retention cap if larger. */
10194
+ windowSec: number().int().positive().optional(),
10195
+ /** Target sample interval in ms. Default 1000 (1 sample/second).
10196
+ * Provider clamps to natural sample rate if smaller, and
10197
+ * bucket-averages when bigger than the requested window
10198
+ * would produce more than `maxPoints` samples. */
10199
+ sampleEveryMs: number().int().positive().optional()
10200
+ }), AudioMetricsHistorySchema)
10201
+ },
10202
+ /** Reactive runtime-state mirror — live `device.state.audioMetrics.value`. */
10203
+ runtimeState: AudioMetricsSnapshotSchema
10204
+ };
10205
+ /**
10206
+ * Automation-control cap. Models HA `automation.*` entities on
10207
+ * `DeviceType.Automation`. An automation is a trigger+condition+
10208
+ * action rule that can be enabled / disabled and manually fired
10209
+ * via the `trigger` method.
10210
+ *
10211
+ * `trigger` accepts an optional `skipCondition` flag — when true,
10212
+ * the automation's action block runs WITHOUT evaluating its
10213
+ * condition block. Pair with `DeviceFeature.AutomationSkipCondition`
10214
+ * to gate the UI checkbox for the manual-trigger dialog.
10215
+ */
10216
+ var AutomationControlStatusSchema = object({
10217
+ /** Whether the automation is currently enabled. Disabled automations
10218
+ * ignore their trigger block — manual `trigger` still works. */
10219
+ enabled: boolean(),
10220
+ /** Whether the automation is currently executing its action block. */
10221
+ isRunning: boolean(),
10222
+ /** Ms epoch of the last successful run. 0 when never run. */
10223
+ lastTriggeredAt: number(),
10224
+ /** Failure description from the last completed run. Null on success
10225
+ * or when never run. */
10226
+ lastError: string().nullable(),
10227
+ /** Ms epoch when the slice was last updated. */
10228
+ lastChangedAt: number()
10229
+ });
10230
+ var automationControlCapability = {
10231
+ name: "automation-control",
10232
+ scope: "device",
10233
+ deviceNative: true,
10234
+ mode: "singleton",
10235
+ deviceTypes: [DeviceType.Automation],
10236
+ methods: {
10237
+ enable: method(object({ deviceId: number().int().nonnegative() }), _void(), {
10238
+ kind: "mutation",
10239
+ auth: "admin"
10240
+ }),
10241
+ disable: method(object({ deviceId: number().int().nonnegative() }), _void(), {
10242
+ kind: "mutation",
10243
+ auth: "admin"
10244
+ }),
10245
+ trigger: method(object({
10246
+ deviceId: number().int().nonnegative(),
10247
+ /** When true, fires the action block while bypassing the
10248
+ * automation's condition evaluation. Gated by
10249
+ * `DeviceFeature.AutomationSkipCondition`. */
10250
+ skipCondition: boolean().optional()
10251
+ }), _void(), {
10252
+ kind: "mutation",
10253
+ auth: "admin"
10254
+ })
10255
+ },
10256
+ status: {
10257
+ schema: AutomationControlStatusSchema,
10258
+ kind: "push"
10259
+ },
10260
+ /**
10261
+ * Runtime-state slice — mirrored by the kernel. UI automation tile
10262
+ * reads `enabled` (toggle) + `isRunning` (spinner) + `lastError`
10263
+ * (badge) directly.
10264
+ */
10265
+ runtimeState: AutomationControlStatusSchema
10266
+ };
10267
+ /**
10268
+ * Battery status snapshot. Emitted by providers whose device is
10269
+ * battery-operated (cameras with `DeviceFeature.BatteryOperated`,
10270
+ * future sensor/button accessories). Consumers build their own "low
10271
+ * battery" alerting on top — the cap deliberately does NOT enforce a
10272
+ * threshold.
10273
+ */
10274
+ var BatteryStatusSchema = object({
10275
+ /** 0..100 inclusive. Firmware-reported. */
10276
+ percentage: number().min(0).max(100),
10277
+ /**
10278
+ * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
10279
+ * Reolink-specific for the Solar Panel 2 accessory (will become
10280
+ * common on other battery cams). `'none'` means running on battery
10281
+ * alone.
10282
+ */
10283
+ charging: _enum([
10284
+ "dc",
10285
+ "solar",
10286
+ "none"
10287
+ ]),
10288
+ /**
10289
+ * True when the camera firmware has gone into low-power mode. Battery
10290
+ * providers MUST avoid polling during sleep — reading the battery
10291
+ * wakes the camera up and drains charge.
10292
+ */
10293
+ sleeping: boolean(),
10294
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
10295
+ lastUpdated: number(),
10296
+ /**
10297
+ * True when the source is a BINARY low-battery indicator (HA
10298
+ * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
10299
+ * charge level — `percentage` is then a coarse stand-in (100 = normal,
10300
+ * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
10301
+ * misleading exact percentage. Absent/false → genuine 0–100 % reading.
10302
+ */
10303
+ binary: boolean().optional()
10304
+ });
10305
+ var batteryCapability = {
10306
+ name: "battery",
10307
+ scope: "device",
10308
+ deviceNative: true,
10309
+ mode: "singleton",
10310
+ deviceTypes: [
10311
+ DeviceType.Camera,
10312
+ DeviceType.Sensor,
10313
+ DeviceType.Button,
10314
+ DeviceType.Switch
10315
+ ],
10316
+ methods: {
10317
+ /**
10318
+ * Explicitly wake the camera from low-power sleep ahead of a
10319
+ * streaming session start. Consumers that initiate a stream
10320
+ * against a sleeping battery cam (HomeKit Secure Video, Alexa
10321
+ * RTCSession, snapshot wrappers) call this with a short timeout
10322
+ * before establishing the media pipeline — the broker's own
10323
+ * passive wake-on-dial works but adds 5–7 seconds to first-frame,
10324
+ * during which the consumer renders a black screen. Pre-waking
10325
+ * compresses that gap.
10326
+ *
10327
+ * Returns `awoke: true` when the firmware acknowledged the wake
10328
+ * before `timeoutMs`. Returns `awoke: false` when it timed out OR
10329
+ * the cap surface is unavailable (no Baichuan / firmware
10330
+ * channel); the caller should still attempt the stream — the
10331
+ * passive broker wake remains as fallback.
10332
+ */
10333
+ wakeForStream: method(object({
10334
+ deviceId: number(),
10335
+ /** Bound on the wait. Sensible range 3000–10000ms. */
10336
+ timeoutMs: number().int().min(500).max(3e4).default(8e3)
10337
+ }), object({
10338
+ awoke: boolean(),
10339
+ durationMs: number()
10340
+ }), { kind: "mutation" }) },
10341
+ events: {
10342
+ /**
10343
+ * Emitted whenever the cached status changes (firmware push OR
10344
+ * poll observes a delta). The DeviceEventPropagator mirrors this
10345
+ * event on the parent chain — subscribing to a camera's source
10346
+ * receives battery events from child accessories automatically.
10347
+ */
10348
+ onStatusChanged: { data: object({
10349
+ deviceId: number(),
10350
+ status: BatteryStatusSchema
10351
+ }) } },
10352
+ status: {
10353
+ schema: BatteryStatusSchema,
10354
+ kind: "push",
10355
+ empty: {
10356
+ percentage: 0,
10357
+ charging: "none",
10358
+ sleeping: false,
10359
+ lastUpdated: 0
10360
+ }
10361
+ },
10362
+ /**
10363
+ * Runtime-state slice — every provider that registers this cap
10364
+ * stores the same shape under `device.runtimeState[battery]`.
10365
+ * Cross-provider uniformity: a Reolink Argus, a Frigate sensor
10366
+ * proxy, an ONVIF battery cam all read/write the same keys.
10367
+ * Consumers (BatteryBadge, snapshot wrapper sleep gate) read once
10368
+ * via `device.runtimeState.getCapState('battery')` regardless of
10369
+ * the underlying driver.
10370
+ */
10371
+ runtimeState: BatteryStatusSchema
10372
+ };
10373
+ /**
10374
+ * Generic boolean sensor — last-resort fallback when no domain-
10375
+ * specific binary cap fits (Home Assistant `binary_sensor` without a
10376
+ * known `device_class`, or a domain we haven't typed yet). Pure
10377
+ * pass-through: just the bool + timestamp. Push-driven.
10378
+ *
10379
+ * Prefer the typed alternatives (`contact`, `flood`, `smoke`,
10380
+ * `carbon-monoxide`, `gas`, `tamper`, `vibration`, `connectivity`,
10381
+ * `motion`) when the semantics match — export adapters render those
10382
+ * with the right HomeKit / Alexa display category.
10383
+ */
10384
+ var BinaryStatusSchema = object({
10385
+ on: boolean(),
10386
+ /** Ms epoch of the last transition. 0 if never observed. */
10387
+ lastChangedAt: number()
10388
+ });
10389
+ var binaryCapability = {
10390
+ name: "binary",
10391
+ scope: "device",
10392
+ deviceNative: true,
10393
+ mode: "singleton",
10394
+ deviceTypes: [DeviceType.Sensor],
10395
+ methods: {},
10396
+ status: {
10397
+ schema: BinaryStatusSchema,
10398
+ kind: "push"
10399
+ },
10400
+ runtimeState: BinaryStatusSchema
10401
+ };
10402
+ /**
10403
+ * Dimmable-light brightness control. Co-exists with `switch` on the
10404
+ * same device — the switch toggles on/off, this cap sets the level
10405
+ * applied when the light is on. Drivers map their per-vendor dim
10406
+ * controls to this single-method surface.
10407
+ *
10408
+ * The cap is intentionally minimal: a single `setBrightness({deviceId,
10409
+ * percentage})` mutation plus the auto-injected `getStatus`. Drivers
10410
+ * that expose richer controls (color temperature, scenes, schedules)
10411
+ * should surface those via the device's `getSettingsUISchema()`
10412
+ * instead of bloating this cap.
10413
+ */
10414
+ var BrightnessStatusSchema = object({
10415
+ /** Current level as 0..100 inclusive. Firmware-reported. */
10416
+ percentage: number().min(0).max(100),
10417
+ /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
10418
+ lastChangedAt: number()
10419
+ });
10420
+ var brightnessCapability = {
10421
+ name: "brightness",
10422
+ scope: "device",
10423
+ deviceNative: true,
10424
+ mode: "singleton",
10425
+ deviceTypes: [DeviceType.Light],
10426
+ methods: { setBrightness: method(object({
10427
+ deviceId: number().int().nonnegative(),
10428
+ percentage: number().min(0).max(100)
10429
+ }), _void(), {
10430
+ kind: "mutation",
10431
+ auth: "admin"
10432
+ }) },
10433
+ events: {
10434
+ /**
10435
+ * Emitted whenever the brightness changes — operator action OR
10436
+ * firmware push. Subscribers (UI sliders, automation engines) react
10437
+ * without polling.
10438
+ */
10439
+ onBrightnessChanged: { data: object({
10440
+ deviceId: number(),
10441
+ percentage: number().min(0).max(100),
10442
+ lastChangedAt: number()
10443
+ }) } },
10444
+ status: {
10445
+ schema: BrightnessStatusSchema,
10446
+ kind: "command-driven"
10447
+ },
10448
+ /**
10449
+ * Runtime-state slice — the last applied brightness level, mirrored
10450
+ * by the kernel. Read via `device.state.brightness.value` so UI
10451
+ * sliders surface the current level without polling the provider.
10452
+ */
10453
+ runtimeState: BrightnessStatusSchema
10454
+ };
10455
+ /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9811
10456
  var StreamFormatSchema = _enum([
9812
10457
  "webrtc",
9813
10458
  "hls",
@@ -13258,104 +13903,43 @@ var MotionTriggerStatusSchema = object({
13258
13903
  /**
13259
13904
  * Persistent slice mirrored across restarts. The provider writes here
13260
13905
  * on every successful firmware fetch / setMotionTrigger push; the cap
13261
- * router and admin-ui hero read straight from this snapshot via
13262
- * `device.state.motionTrigger.value` instead of re-issuing a firmware
13263
- * round-trip on every UI mount. `lastFetchedAt` lets the framework
13264
- * helper (`createRuntimeStateBridge`) stale-check before deciding
13265
- * whether to refresh from the camera.
13266
- */
13267
- var MotionTriggerRuntimeStateSchema = MotionTriggerStatusSchema.extend({
13268
- /** Ms epoch of the last successful camera fetch (0 = never). */
13269
- lastFetchedAt: number() });
13270
- var motionTriggerCapability = {
13271
- name: "motion-trigger",
13272
- scope: "device",
13273
- deviceNative: true,
13274
- mode: "singleton",
13275
- deviceTypes: [
13276
- DeviceType.Light,
13277
- DeviceType.Siren,
13278
- DeviceType.Switch
13279
- ],
13280
- methods: { setMotionTrigger: method(object({
13281
- deviceId: number().int().nonnegative(),
13282
- enabled: boolean()
13283
- }), _void(), {
13284
- kind: "mutation",
13285
- auth: "admin"
13286
- }) },
13287
- events: { onMotionTriggerChanged: { data: object({
13288
- deviceId: number(),
13289
- enabled: boolean(),
13290
- lastChangedAt: number()
13291
- }) } },
13292
- status: {
13293
- schema: MotionTriggerStatusSchema,
13294
- kind: "command-driven"
13295
- },
13296
- runtimeState: MotionTriggerRuntimeStateSchema
13297
- };
13298
- /**
13299
- * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
13300
- * motion-zones, and the detection zones/lines editor all speak this one
13301
- * language so a single drawing-plane editor and the providers stay
13302
- * decoupled from each cap's storage.
13303
- *
13304
- * All coordinates are normalized 0..1 of the camera frame (top-left
13305
- * origin). Each cap composes the SUBSET of shape kinds it supports and
13306
- * advertises it via `supportedShapes` in its `getOptions`.
13307
- */
13308
- /** A normalized 0..1 point (top-left origin). */
13309
- var MaskPointSchema = object({
13310
- x: number(),
13311
- y: number()
13312
- });
13313
- /** Axis-aligned rectangle (normalized 0..1). */
13314
- var MaskRectShapeSchema = object({
13315
- kind: literal("rect"),
13316
- x: number(),
13317
- y: number(),
13318
- width: number(),
13319
- height: number()
13320
- });
13321
- /** Free polygon — an ordered list of normalized vertices (≥3). */
13322
- var MaskPolygonShapeSchema = object({
13323
- kind: literal("polygon"),
13324
- points: array(MaskPointSchema)
13325
- });
13326
- /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
13327
- var MaskGridShapeSchema = object({
13328
- kind: literal("grid"),
13329
- gridWidth: number(),
13330
- gridHeight: number(),
13331
- cells: array(boolean())
13332
- });
13333
- discriminatedUnion("kind", [
13334
- MaskRectShapeSchema,
13335
- MaskPolygonShapeSchema,
13336
- MaskGridShapeSchema,
13337
- object({
13338
- kind: literal("line"),
13339
- points: array(MaskPointSchema)
13340
- })
13341
- ]);
13342
- /** Every shape-kind discriminant, for `supportedShapes` advertisement. */
13343
- var MaskShapeKindSchema = _enum([
13344
- "rect",
13345
- "polygon",
13346
- "grid",
13347
- "line"
13348
- ]);
13349
- /** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
13350
- var MaskPolygonVerticesSchema = object({
13351
- min: number(),
13352
- max: number()
13353
- });
13354
- /** Grid dimensions when a cap supports 'grid'. */
13355
- var MaskGridDimsSchema = object({
13356
- width: number(),
13357
- height: number()
13358
- });
13906
+ * router and admin-ui hero read straight from this snapshot via
13907
+ * `device.state.motionTrigger.value` instead of re-issuing a firmware
13908
+ * round-trip on every UI mount. `lastFetchedAt` lets the framework
13909
+ * helper (`createRuntimeStateBridge`) stale-check before deciding
13910
+ * whether to refresh from the camera.
13911
+ */
13912
+ var MotionTriggerRuntimeStateSchema = MotionTriggerStatusSchema.extend({
13913
+ /** Ms epoch of the last successful camera fetch (0 = never). */
13914
+ lastFetchedAt: number() });
13915
+ var motionTriggerCapability = {
13916
+ name: "motion-trigger",
13917
+ scope: "device",
13918
+ deviceNative: true,
13919
+ mode: "singleton",
13920
+ deviceTypes: [
13921
+ DeviceType.Light,
13922
+ DeviceType.Siren,
13923
+ DeviceType.Switch
13924
+ ],
13925
+ methods: { setMotionTrigger: method(object({
13926
+ deviceId: number().int().nonnegative(),
13927
+ enabled: boolean()
13928
+ }), _void(), {
13929
+ kind: "mutation",
13930
+ auth: "admin"
13931
+ }) },
13932
+ events: { onMotionTriggerChanged: { data: object({
13933
+ deviceId: number(),
13934
+ enabled: boolean(),
13935
+ lastChangedAt: number()
13936
+ }) } },
13937
+ status: {
13938
+ schema: MotionTriggerStatusSchema,
13939
+ kind: "command-driven"
13940
+ },
13941
+ runtimeState: MotionTriggerRuntimeStateSchema
13942
+ };
13359
13943
  /**
13360
13944
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
13361
13945
  * on-camera motion-detection mask is a single `grid` region (a row-major
@@ -16793,6 +17377,55 @@ method(object({
16793
17377
  password: string()
16794
17378
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
16795
17379
  /**
17380
+ * A live terminal session hosted by the provider addon. Output and input do
17381
+ * NOT flow through the capability — they use the addon data plane
17382
+ * (`GET /addon/terminal/<id>/out` SSE, `POST /addon/terminal/<id>/in`) because
17383
+ * terminal output must be ordered and lossless. The event bus is telemetry and
17384
+ * may drop chunks ([D8]), and a dropped chunk desynchronises the vt parser
17385
+ * permanently until a full repaint. The capability owns only lifecycle.
17386
+ */
17387
+ var TerminalSessionInfoSchema = object({
17388
+ /** Opaque session id minted by the provider on `openSession`. */
17389
+ sessionId: string(),
17390
+ /** The pre-declared profile this session runs (never a free-form command). */
17391
+ profileId: string(),
17392
+ /** Human-readable profile label for the UI session list. */
17393
+ label: string(),
17394
+ cols: number().int().positive(),
17395
+ rows: number().int().positive(),
17396
+ /** ms-epoch the session's pty was spawned. */
17397
+ startedAt: number()
17398
+ });
17399
+ /**
17400
+ * A profile the operator may open — a pre-declared, allowlisted program
17401
+ * (`monitor` → `btm`). The capability accepts only these ids; a free-form
17402
+ * command string would be remote code execution as the server's user, so it is
17403
+ * deliberately not part of the contract.
17404
+ */
17405
+ var TerminalProfileInfoSchema = object({
17406
+ profileId: string(),
17407
+ label: string(),
17408
+ description: string().optional()
17409
+ });
17410
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
17411
+ profileId: string(),
17412
+ cols: number().int().positive(),
17413
+ rows: number().int().positive()
17414
+ }), TerminalSessionInfoSchema, {
17415
+ kind: "mutation",
17416
+ auth: "admin"
17417
+ }), method(object({
17418
+ sessionId: string(),
17419
+ cols: number().int().positive(),
17420
+ rows: number().int().positive()
17421
+ }), _void(), {
17422
+ kind: "mutation",
17423
+ auth: "admin"
17424
+ }), method(object({ sessionId: string() }), _void(), {
17425
+ kind: "mutation",
17426
+ auth: "admin"
17427
+ });
17428
+ /**
16796
17429
  * Orchestrator-side destination metadata. The orchestrator computes
16797
17430
  * `id = <addonId>:<subId>` from its provider lookup so consumers
16798
17431
  * (admin UI, restore flow) see one canonical key.
@@ -16893,11 +17526,53 @@ var LocationStatSchema = object({
16893
17526
  fileCount: number(),
16894
17527
  present: boolean()
16895
17528
  });
17529
+ /**
17530
+ * A backup schedule — the N:M "entry" that binds one cron cadence to a
17531
+ * SET of destination locations. Supersedes the per-location cron on
17532
+ * `BackupDestinationPolicy`: an operator creates a schedule, picks the
17533
+ * `backups` locations it should write to, and the orchestrator fans a
17534
+ * single archive out to all of them when the cron fires.
17535
+ *
17536
+ * `retentionCount` is per-schedule (D-decision 2026-07-28): every
17537
+ * location targeted by this schedule keeps this many archives from
17538
+ * this schedule's runs.
17539
+ *
17540
+ * `dataSources` optionally narrows which top-level state locations
17541
+ * (db, addons, tls, …) are archived; omitted = the orchestrator's
17542
+ * default full set.
17543
+ */
17544
+ var BackupScheduleSchema = object({
17545
+ /** Stable id. Generated by the orchestrator on first upsert if absent. */
17546
+ id: string(),
17547
+ /** Operator-facing display name. */
17548
+ label: string(),
17549
+ /** 5-field POSIX cron. Empty = disabled cadence (kept for editing). */
17550
+ cron: string(),
17551
+ /** Master on/off toggle for the whole schedule. */
17552
+ enabled: boolean(),
17553
+ /** `backups`-location ids this schedule writes to (fan-out set). */
17554
+ locationIds: array(string()).readonly(),
17555
+ /** Archives kept per targeted location for this schedule. */
17556
+ retentionCount: number().int().min(1).max(1e3),
17557
+ /** Optional subset of source locations to include; omitted = all. */
17558
+ dataSources: array(string()).readonly().optional(),
17559
+ /** ms-epoch of last successful run. */
17560
+ lastRunAt: number().optional(),
17561
+ /** ms-epoch of next computed firing (read-only, filled on list). */
17562
+ nextRunAt: number().optional()
17563
+ });
16896
17564
  method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }), method(object({
16897
17565
  /** Subset of registered `backup-destination` addon ids to write to. */
16898
17566
  destinations: array(string()).optional(),
16899
17567
  locations: array(string()).optional(),
16900
- label: string().optional()
17568
+ label: string().optional(),
17569
+ /**
17570
+ * Per-run retention override applied to every targeted
17571
+ * destination. Used by schedule-driven runs (per-entry
17572
+ * retention). Omitted = each destination's own policy
17573
+ * retention (manual runs).
17574
+ */
17575
+ retentionCount: number().int().min(1).max(1e3).optional()
16901
17576
  }).optional(), array(BackupEntrySchema).readonly(), {
16902
17577
  kind: "mutation",
16903
17578
  auth: "admin"
@@ -16946,7 +17621,21 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
16946
17621
  ok: boolean(),
16947
17622
  error: string().optional(),
16948
17623
  nextRuns: array(number()).readonly()
16949
- }));
17624
+ })), method(_void(), array(BackupScheduleSchema).readonly(), { auth: "admin" }), method(object({
17625
+ id: string().optional(),
17626
+ label: string(),
17627
+ cron: string(),
17628
+ enabled: boolean(),
17629
+ locationIds: array(string()).readonly(),
17630
+ retentionCount: number().int().min(1).max(1e3),
17631
+ dataSources: array(string()).readonly().optional()
17632
+ }), BackupScheduleSchema, {
17633
+ kind: "mutation",
17634
+ auth: "admin"
17635
+ }), method(object({ id: string() }), _void(), {
17636
+ kind: "mutation",
17637
+ auth: "admin"
17638
+ });
16950
17639
  /**
16951
17640
  * `broker` — unified pub/sub broker registry, system-scoped collection.
16952
17641
  *
@@ -18028,1596 +18717,1108 @@ method(object({
18028
18717
  active: boolean()
18029
18718
  }), _void(), {
18030
18719
  kind: "mutation",
18031
- auth: "admin"
18032
- }), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
18033
- capName: string(),
18034
- wrappers: array(string())
18035
- }))), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), object({
18036
- settings: SettingsSchemaWithValuesSchema.nullable(),
18037
- live: SettingsSchemaWithValuesSchema.nullable()
18038
- })), method(object({
18039
- deviceId: number().int().nonnegative(),
18040
- action: string().min(1),
18041
- input: unknown()
18042
- }), unknown(), { kind: "mutation" }), method(object({
18043
- deviceId: number(),
18044
- writerCapName: string(),
18045
- writerAddonId: string(),
18046
- key: string(),
18047
- value: unknown()
18048
- }), object({ success: literal(true) }), {
18049
- kind: "mutation",
18050
- auth: "admin"
18051
- }), method(object({
18052
- deviceId: number(),
18053
- changes: array(object({
18054
- writerCapName: string(),
18055
- writerAddonId: string(),
18056
- key: string(),
18057
- value: unknown()
18058
- }))
18059
- }), object({
18060
- success: literal(true),
18061
- failures: array(object({
18062
- writerCapName: string(),
18063
- writerAddonId: string(),
18064
- error: string()
18065
- }))
18066
- }), {
18067
- kind: "mutation",
18068
- auth: "admin"
18069
- }), method(object({ addonId: string() }), array(DiscoveryCandidateSchema), {
18070
- kind: "mutation",
18071
- auth: "admin"
18072
- }), method(object({
18073
- addonId: string(),
18074
- candidate: DiscoveryCandidateSchema,
18075
- /** Owning integration id, stamped onto the new device's meta by the
18076
- * device-manager forwarder so `removeByIntegration` can cascade it.
18077
- * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
18078
- integrationId: string().optional()
18079
- }), DeviceSummarySchema, {
18080
- kind: "mutation",
18081
- auth: "admin"
18082
- }), method(object({
18083
- addonId: string(),
18084
- type: _enum(DeviceType)
18085
- }), unknown().nullable()), method(object({
18086
- addonId: string(),
18087
- type: _enum(DeviceType),
18088
- config: record(string(), unknown()),
18089
- /** Owning integration id, stamped onto the new device's meta by the
18090
- * device-manager forwarder so `removeByIntegration` can cascade it.
18091
- * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
18092
- integrationId: string().optional()
18093
- }), DeviceSummarySchema, {
18094
- kind: "mutation",
18095
- auth: "admin"
18096
- }), method(object({
18097
- addonId: string(),
18098
- type: _enum(DeviceType),
18099
- key: string(),
18100
- value: unknown(),
18101
- formValues: record(string(), unknown()).optional()
18102
- }), FieldProbeResultSchema, {
18103
- kind: "mutation",
18104
- auth: "admin"
18105
- }), method(object({
18106
- addonId: string(),
18107
- integrationId: string()
18108
- }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
18109
- addonId: string(),
18110
- integrationId: string()
18111
- }), AdoptionStatusSchema, {
18112
- kind: "mutation",
18113
- auth: "admin"
18114
- }), method(AdoptInputSchema.extend({ addonId: string() }), AdoptResultSchema, {
18115
- kind: "mutation",
18116
- auth: "admin"
18117
- }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
18118
- kind: "mutation",
18119
- auth: "admin"
18120
- }), method(ResyncInputSchema, ResyncResultSchema, {
18121
- kind: "mutation",
18122
- auth: "admin"
18123
- }), method(object({}), object({ providers: array(object({
18124
- addonId: string(),
18125
- label: string()
18126
- })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
18127
- addonId: string(),
18128
- label: string(),
18129
- candidates: array(DiscoveryCandidateSchema).readonly(),
18130
- error: string().nullable()
18131
- })).readonly() }), {
18132
- kind: "mutation",
18133
- auth: "admin"
18134
- }), method(object({
18135
- addonId: string(),
18136
- params: record(string(), unknown()).optional()
18137
- }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
18138
- kind: "mutation",
18139
- auth: "admin"
18140
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
18141
- deviceId: number(),
18142
- key: string(),
18143
- value: unknown()
18144
- }), FieldProbeResultSchema, {
18145
- kind: "mutation",
18146
- auth: "admin"
18147
- }), method(object({
18148
- deviceId: number(),
18149
- caps: array(string()).readonly().optional()
18150
- }), record(string(), unknown().nullable()));
18151
- method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
18152
- deviceId: number(),
18153
- capName: string()
18154
- }), record(string(), unknown()).nullable()), method(object({}), record(string(), record(string(), record(string(), unknown())))), method(object({
18155
- deviceId: number(),
18156
- capName: string(),
18157
- slice: record(string(), unknown())
18158
- }), _void(), { kind: "mutation" }), object({
18159
- deviceId: number(),
18160
- capName: string(),
18161
- slice: record(string(), unknown())
18162
- });
18163
- /**
18164
- * Embedding output. `embedding` is wire-encoded as `number[]` so the
18165
- * Zod-validated tRPC surface round-trips cleanly; consumers that need a
18166
- * `Float32Array` can wrap it on the way out (in-process, no marshalling
18167
- * is involved). `inferenceMs` mirrors the runtime field used by the
18168
- * post-analysis enrichment-engine.
18169
- */
18170
- var EmbeddingResultSchema = object({
18171
- embedding: array(number()),
18172
- inferenceMs: number()
18173
- });
18174
- var EmbeddingInfoSchema = object({
18175
- modelId: string(),
18176
- embeddingDim: number(),
18177
- ready: boolean()
18178
- });
18179
- method(object({
18180
- crop: _instanceof(Uint8Array),
18181
- width: number(),
18182
- height: number()
18183
- }), EmbeddingResultSchema), method(object({ text: string() }), EmbeddingResultSchema), method(_void(), EmbeddingInfoSchema);
18184
- /**
18185
- * filesystem-browse — per-node capability for browsing the node's local
18186
- * filesystem, sandboxed to operator-configured allowed roots. Used by the
18187
- * admin "Add filesystem location" flow to pick a node + path. `mode:'per-node'`
18188
- * (one provider per node); the hub calls it with `{nodeId}` so the codegen
18189
- * routes to that exact node (default `nodeIdMode:'routing'`).
18190
- */
18191
- var DirEntrySchema = object({
18192
- name: string(),
18193
- path: string()
18194
- });
18195
- var BrowseResultSchema = object({
18196
- path: string(),
18197
- entries: array(DirEntrySchema).readonly(),
18198
- freeBytes: number(),
18199
- totalBytes: number()
18200
- });
18201
- method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({ path: string() }), BrowseResultSchema, { auth: "admin" }), method(object({ path: string() }), object({ path: string() }), {
18720
+ auth: "admin"
18721
+ }), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
18722
+ capName: string(),
18723
+ wrappers: array(string())
18724
+ }))), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), object({
18725
+ settings: SettingsSchemaWithValuesSchema.nullable(),
18726
+ live: SettingsSchemaWithValuesSchema.nullable()
18727
+ })), method(object({
18728
+ deviceId: number().int().nonnegative(),
18729
+ action: string().min(1),
18730
+ input: unknown()
18731
+ }), unknown(), { kind: "mutation" }), method(object({
18732
+ deviceId: number(),
18733
+ writerCapName: string(),
18734
+ writerAddonId: string(),
18735
+ key: string(),
18736
+ value: unknown()
18737
+ }), object({ success: literal(true) }), {
18202
18738
  kind: "mutation",
18203
18739
  auth: "admin"
18204
- });
18205
- /**
18206
- * Shared LLM generate contracts — imported by BOTH `llm.cap.ts` (consumer
18207
- * surface) and `llm-runtime.cap.ts` (node-side managed executor) so the two
18208
- * caps stay wire-compatible without a circular cap→cap import.
18209
- *
18210
- * Errors are a discriminated-union RESULT, never thrown: the shape survives
18211
- * every transport tier structurally, and failed calls still write usage rows.
18212
- * Token counts only in v1 — no costUsd (operator decision, 2026-07-15).
18213
- */
18214
- var LlmUsageSchema = object({
18215
- inputTokens: number(),
18216
- outputTokens: number()
18217
- });
18218
- var LlmErrorCodeSchema = _enum([
18219
- "timeout",
18220
- "rate-limited",
18221
- "auth",
18222
- "refusal",
18223
- "bad-request",
18224
- "unavailable",
18225
- "no-profile",
18226
- "budget-exceeded",
18227
- "adapter-error"
18228
- ]);
18229
- var LlmGenerateResultSchema = discriminatedUnion("ok", [object({
18230
- ok: literal(true),
18231
- text: string(),
18232
- model: string(),
18233
- usage: LlmUsageSchema,
18234
- truncated: boolean(),
18235
- latencyMs: number()
18740
+ }), method(object({
18741
+ deviceId: number(),
18742
+ changes: array(object({
18743
+ writerCapName: string(),
18744
+ writerAddonId: string(),
18745
+ key: string(),
18746
+ value: unknown()
18747
+ }))
18236
18748
  }), object({
18237
- ok: literal(false),
18238
- code: LlmErrorCodeSchema,
18239
- message: string(),
18240
- retryAfterMs: number().optional()
18241
- })]);
18242
- /**
18243
- * `Uint8Array` is the sanctioned binary convention — superjson + the UDS
18244
- * MsgPack channel round-trip typed arrays (embedding-encoder.cap.ts:29,
18245
- * notification-output.cap.ts:27-31 precedents).
18246
- */
18247
- var LlmImageSchema = object({
18248
- bytes: _instanceof(Uint8Array),
18249
- mimeType: string()
18250
- });
18251
- var LlmGenerateBaseInputSchema = object({
18252
- /** Collection routing (the notification-output posture). */
18253
- addonId: string().optional(),
18254
- /** Explicit profile; else the resolution chain (spec §3). */
18255
- profileId: string().optional(),
18256
- /** MANDATORY usage tag: 'ai-summary', 'notifier-rules', 'adhoc-ui', … */
18257
- consumer: string(),
18258
- system: string().optional(),
18259
- /** v1: single-turn. `messages[]` is a v2 additive field. */
18260
- prompt: string(),
18261
- /** Structured output — adapter-mapped (response_format / forced tool / responseSchema). */
18262
- jsonSchema: record(string(), unknown()).optional(),
18263
- /** Per-call override of the profile default. */
18264
- maxTokens: number().int().positive().optional(),
18265
- temperature: number().optional()
18266
- });
18267
- /**
18268
- * `llm-runtime` — node-side managed llama.cpp executor (spec §4). Registered
18269
- * on EVERY node where `addon-ai` is installed; the hub `llm` provider reaches
18270
- * a specific node's runtime with `nodePin(profile.runtime.nodeId)` — normal
18271
- * cap routing, zero bespoke plumbing. `internal: true`: the operator reaches
18272
- * this only through the `llm` cap's methods.
18273
- *
18274
- * One running llama-server child per node in v1 (models are RAM-heavy).
18275
- * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
18276
- * watchdog — operator decision #3).
18277
- */
18278
- var ManagedModelRefSchema = discriminatedUnion("kind", [
18279
- object({
18280
- kind: literal("catalog"),
18281
- catalogId: string()
18282
- }),
18283
- object({
18284
- kind: literal("url"),
18285
- url: string(),
18286
- sha256: string().optional()
18287
- }),
18288
- object({
18289
- kind: literal("path"),
18290
- path: string()
18291
- })
18292
- ]);
18293
- var ManagedRuntimeConfigSchema = object({
18294
- /** WHERE the runtime lives — hub or any agent. */
18295
- nodeId: string(),
18296
- /** Closed for v1; 'ollama' is a v2 candidate. */
18297
- engine: _enum(["llama-cpp"]),
18298
- model: ManagedModelRefSchema,
18299
- contextSize: number().int().default(4096),
18300
- /** 0 = CPU-only. */
18301
- gpuLayers: number().int().default(0),
18302
- /** Default: cpus-2, clamped ≥1 (resolved node-side). */
18303
- threads: number().int().optional(),
18304
- /** Concurrent slots. */
18305
- parallel: number().int().default(1),
18306
- /** Else lazy: first generate boots it. */
18307
- autoStart: boolean().default(false),
18308
- /** 0 = never; frees RAM after quiet periods. */
18309
- idleStopMinutes: number().int().default(30)
18310
- });
18311
- var LlmRuntimeStatusSchema = object({
18312
- /** Status is ALWAYS node-qualified. */
18313
- nodeId: string(),
18314
- state: _enum([
18315
- "stopped",
18316
- "downloading",
18317
- "starting",
18318
- "ready",
18319
- "crashed",
18320
- "failed"
18321
- ]),
18322
- pid: number().optional(),
18323
- port: number().optional(),
18324
- modelPath: string().optional(),
18325
- modelId: string().optional(),
18326
- downloadProgress: number().min(0).max(1).optional(),
18327
- lastError: string().optional(),
18328
- crashesInWindow: number(),
18329
- /** Child RSS (sampled best-effort). */
18330
- memoryBytes: number().optional(),
18331
- vramBytes: number().optional()
18332
- });
18333
- var LlmNodeModelSchema = object({
18334
- file: string(),
18335
- sizeBytes: number(),
18336
- catalogId: string().optional(),
18337
- installedAt: number().optional()
18338
- });
18339
- var LlmRuntimeDiskUsageSchema = object({
18340
- nodeId: string(),
18341
- modelsBytes: number(),
18342
- freeBytes: number().optional()
18343
- });
18344
- method(LlmGenerateBaseInputSchema.extend({
18345
- images: array(LlmImageSchema).optional(),
18346
- runtime: ManagedRuntimeConfigSchema,
18347
- /** The managed profile's timeout, threaded by the hub provider. */
18348
- timeoutMs: number().int().positive().optional()
18349
- }), LlmGenerateResultSchema, { kind: "mutation" }), method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
18749
+ success: literal(true),
18750
+ failures: array(object({
18751
+ writerCapName: string(),
18752
+ writerAddonId: string(),
18753
+ error: string()
18754
+ }))
18755
+ }), {
18350
18756
  kind: "mutation",
18351
18757
  auth: "admin"
18352
- }), method(object({}), _void(), {
18758
+ }), method(object({ addonId: string() }), array(DiscoveryCandidateSchema), {
18353
18759
  kind: "mutation",
18354
18760
  auth: "admin"
18355
- }), method(object({}), LlmRuntimeStatusSchema), method(object({ model: ManagedModelRefSchema }), _void(), {
18761
+ }), method(object({
18762
+ addonId: string(),
18763
+ candidate: DiscoveryCandidateSchema,
18764
+ /** Owning integration id, stamped onto the new device's meta by the
18765
+ * device-manager forwarder so `removeByIntegration` can cascade it.
18766
+ * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
18767
+ integrationId: string().optional()
18768
+ }), DeviceSummarySchema, {
18356
18769
  kind: "mutation",
18357
18770
  auth: "admin"
18358
- }), method(object({ file: string() }), _void(), {
18771
+ }), method(object({
18772
+ addonId: string(),
18773
+ type: _enum(DeviceType)
18774
+ }), unknown().nullable()), method(object({
18775
+ addonId: string(),
18776
+ type: _enum(DeviceType),
18777
+ config: record(string(), unknown()),
18778
+ /** Owning integration id, stamped onto the new device's meta by the
18779
+ * device-manager forwarder so `removeByIntegration` can cascade it.
18780
+ * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
18781
+ integrationId: string().optional()
18782
+ }), DeviceSummarySchema, {
18359
18783
  kind: "mutation",
18360
18784
  auth: "admin"
18361
- }), method(object({}), array(LlmNodeModelSchema)), method(object({}), LlmRuntimeDiskUsageSchema);
18362
- /**
18363
- * `llm` — consumer-facing LLM surface (spec §1-§3). Collection-mode: array
18364
- * methods concat-fan across providers; single-row methods route to ONE
18365
- * provider by the `addonId` in the call input (the notification-output
18366
- * posture, notification-output.cap.ts:215-250). Provided by `addon-ai`
18367
- * (hub-placed); the cap stays open for future providers.
18368
- *
18369
- * Profiles are ROWS (data), not addons: one row = one usable model endpoint.
18370
- * `apiKey` is a password field — providers REDACT it on read and merge on
18371
- * write; a stored key NEVER round-trips to a client.
18372
- */
18373
- var LlmProfileKindSchema = _enum([
18374
- "openai-compatible",
18375
- "openai",
18376
- "anthropic",
18377
- "google",
18378
- "managed-local"
18379
- ]);
18380
- var LlmProfileSchema = object({
18381
- id: string(),
18382
- name: string(),
18383
- kind: LlmProfileKindSchema,
18384
- /** Stamped by the provider — keeps the fanned catalog routable. */
18785
+ }), method(object({
18385
18786
  addonId: string(),
18386
- enabled: boolean(),
18387
- /** Vendor model id, or the managed runtime's loaded model. */
18388
- model: string(),
18389
- /** Required for openai-compatible; override for cloud kinds. */
18390
- baseUrl: string().optional(),
18391
- /** ConfigUISchema type:'password' — never round-trips (spec §5). */
18392
- apiKey: string().optional(),
18393
- supportsVision: boolean(),
18394
- temperature: number().min(0).max(2).optional(),
18395
- maxTokens: number().int().positive().optional(),
18396
- timeoutMs: number().int().positive().default(6e4),
18397
- extraHeaders: record(string(), string()).optional(),
18398
- /** kind === 'managed-local' only (spec §4). */
18399
- runtime: ManagedRuntimeConfigSchema.optional()
18400
- });
18401
- /** ConfigUISchema tree passed through untyped on the wire (the
18402
- * notification-output `ConfigSchemaPassthrough` precedent at
18403
- * notification-output.cap.ts:151); the exported TS type re-tightens it. */
18404
- var ConfigSchemaPassthrough$1 = unknown();
18405
- var LlmProfileKindDescriptorSchema = object({
18406
- kind: LlmProfileKindSchema,
18407
- label: string(),
18408
- icon: string(),
18409
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
18787
+ type: _enum(DeviceType),
18788
+ key: string(),
18789
+ value: unknown(),
18790
+ formValues: record(string(), unknown()).optional()
18791
+ }), FieldProbeResultSchema, {
18792
+ kind: "mutation",
18793
+ auth: "admin"
18794
+ }), method(object({
18410
18795
  addonId: string(),
18411
- configSchema: ConfigSchemaPassthrough$1
18412
- });
18413
- var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
18414
- var LlmDefaultSchema = object({
18415
- selector: LlmDefaultSelectorSchema,
18416
- profileId: string()
18417
- });
18418
- /** Server-side rollup row — getUsage never dumps raw call rows (spec §6). */
18419
- var LlmUsageRollupSchema = object({
18420
- day: string(),
18421
- consumer: string(),
18422
- profileId: string(),
18423
- calls: number(),
18424
- okCalls: number(),
18425
- errorCalls: number(),
18426
- inputTokens: number(),
18427
- outputTokens: number(),
18428
- avgLatencyMs: number()
18429
- });
18430
- /** LLM-facing view over the reused ModelCatalogEntry mechanism (spec §4.2). */
18431
- var ManagedModelCatalogEntrySchema = object({
18432
- id: string(),
18433
- label: string(),
18434
- family: string(),
18435
- purpose: _enum(["text", "vision"]),
18436
- url: string(),
18437
- sha256: string(),
18438
- sizeBytes: number(),
18439
- quantization: string(),
18440
- /** Load-time guidance shown in the picker. */
18441
- minRamBytes: number(),
18442
- contextSizeDefault: number().int(),
18443
- /** Vision models: companion projector file. */
18444
- mmprojUrl: string().optional()
18445
- });
18446
- var LlmRuntimeNodeSchema = object({
18447
- nodeId: string(),
18448
- reachable: boolean(),
18449
- status: LlmRuntimeStatusSchema.optional(),
18450
- disk: LlmRuntimeDiskUsageSchema.optional(),
18451
- error: string().optional()
18452
- });
18453
- var GenerateVisionInputSchema = LlmGenerateBaseInputSchema.extend({ images: array(LlmImageSchema).min(1) });
18454
- var ProfileRefInputSchema = object({
18796
+ integrationId: string()
18797
+ }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
18455
18798
  addonId: string(),
18456
- profileId: string()
18457
- });
18458
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
18799
+ integrationId: string()
18800
+ }), AdoptionStatusSchema, {
18459
18801
  kind: "mutation",
18460
18802
  auth: "admin"
18461
- }), method(ProfileRefInputSchema, _void(), {
18803
+ }), method(AdoptInputSchema.extend({ addonId: string() }), AdoptResultSchema, {
18462
18804
  kind: "mutation",
18463
18805
  auth: "admin"
18464
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
18806
+ }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
18465
18807
  kind: "mutation",
18466
18808
  auth: "admin"
18467
- }), method(ProfileRefInputSchema, array(string())), method(object({}), array(LlmDefaultSchema)), method(object({
18468
- selector: LlmDefaultSelectorSchema,
18469
- profileId: string().nullable()
18470
- }), _void(), {
18809
+ }), method(ResyncInputSchema, ResyncResultSchema, {
18471
18810
  kind: "mutation",
18472
18811
  auth: "admin"
18473
- }), method(object({
18474
- since: number().optional(),
18475
- until: number().optional(),
18476
- consumer: string().optional(),
18477
- profileId: string().optional()
18478
- }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
18479
- nodeId: string(),
18480
- model: ManagedModelRefSchema
18481
- }), _void(), {
18812
+ }), method(object({}), object({ providers: array(object({
18813
+ addonId: string(),
18814
+ label: string()
18815
+ })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
18816
+ addonId: string(),
18817
+ label: string(),
18818
+ candidates: array(DiscoveryCandidateSchema).readonly(),
18819
+ error: string().nullable()
18820
+ })).readonly() }), {
18482
18821
  kind: "mutation",
18483
18822
  auth: "admin"
18484
18823
  }), method(object({
18485
- nodeId: string(),
18486
- file: string()
18487
- }), _void(), {
18824
+ addonId: string(),
18825
+ params: record(string(), unknown()).optional()
18826
+ }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
18488
18827
  kind: "mutation",
18489
18828
  auth: "admin"
18490
- }), method(ProfileRefInputSchema, LlmRuntimeStatusSchema), method(ProfileRefInputSchema, LlmRuntimeStatusSchema, {
18829
+ }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
18830
+ deviceId: number(),
18831
+ key: string(),
18832
+ value: unknown()
18833
+ }), FieldProbeResultSchema, {
18491
18834
  kind: "mutation",
18492
18835
  auth: "admin"
18493
- }), method(ProfileRefInputSchema, _void(), {
18836
+ }), method(object({
18837
+ deviceId: number(),
18838
+ caps: array(string()).readonly().optional()
18839
+ }), record(string(), unknown().nullable()));
18840
+ method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
18841
+ deviceId: number(),
18842
+ capName: string()
18843
+ }), record(string(), unknown()).nullable()), method(object({}), record(string(), record(string(), record(string(), unknown())))), method(object({
18844
+ deviceId: number(),
18845
+ capName: string(),
18846
+ slice: record(string(), unknown())
18847
+ }), _void(), { kind: "mutation" }), object({
18848
+ deviceId: number(),
18849
+ capName: string(),
18850
+ slice: record(string(), unknown())
18851
+ });
18852
+ /**
18853
+ * Embedding output. `embedding` is wire-encoded as `number[]` so the
18854
+ * Zod-validated tRPC surface round-trips cleanly; consumers that need a
18855
+ * `Float32Array` can wrap it on the way out (in-process, no marshalling
18856
+ * is involved). `inferenceMs` mirrors the runtime field used by the
18857
+ * post-analysis enrichment-engine.
18858
+ */
18859
+ var EmbeddingResultSchema = object({
18860
+ embedding: array(number()),
18861
+ inferenceMs: number()
18862
+ });
18863
+ var EmbeddingInfoSchema = object({
18864
+ modelId: string(),
18865
+ embeddingDim: number(),
18866
+ ready: boolean()
18867
+ });
18868
+ method(object({
18869
+ crop: _instanceof(Uint8Array),
18870
+ width: number(),
18871
+ height: number()
18872
+ }), EmbeddingResultSchema), method(object({ text: string() }), EmbeddingResultSchema), method(_void(), EmbeddingInfoSchema);
18873
+ /**
18874
+ * filesystem-browse — per-node capability for browsing the node's local
18875
+ * filesystem, sandboxed to operator-configured allowed roots. Used by the
18876
+ * admin "Add filesystem location" flow to pick a node + path. `mode:'per-node'`
18877
+ * (one provider per node); the hub calls it with `{nodeId}` so the codegen
18878
+ * routes to that exact node (default `nodeIdMode:'routing'`).
18879
+ */
18880
+ var DirEntrySchema = object({
18881
+ name: string(),
18882
+ path: string()
18883
+ });
18884
+ var BrowseResultSchema = object({
18885
+ path: string(),
18886
+ entries: array(DirEntrySchema).readonly(),
18887
+ freeBytes: number(),
18888
+ totalBytes: number()
18889
+ });
18890
+ method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({ path: string() }), BrowseResultSchema, { auth: "admin" }), method(object({ path: string() }), object({ path: string() }), {
18494
18891
  kind: "mutation",
18495
18892
  auth: "admin"
18496
18893
  });
18497
- var LogLevelSchema = _enum([
18498
- "debug",
18499
- "info",
18500
- "warn",
18501
- "error"
18894
+ /**
18895
+ * Shared LLM generate contracts — imported by BOTH `llm.cap.ts` (consumer
18896
+ * surface) and `llm-runtime.cap.ts` (node-side managed executor) so the two
18897
+ * caps stay wire-compatible without a circular cap→cap import.
18898
+ *
18899
+ * Errors are a discriminated-union RESULT, never thrown: the shape survives
18900
+ * every transport tier structurally, and failed calls still write usage rows.
18901
+ * Token counts only in v1 — no costUsd (operator decision, 2026-07-15).
18902
+ */
18903
+ var LlmUsageSchema = object({
18904
+ inputTokens: number(),
18905
+ outputTokens: number()
18906
+ });
18907
+ var LlmErrorCodeSchema = _enum([
18908
+ "timeout",
18909
+ "rate-limited",
18910
+ "auth",
18911
+ "refusal",
18912
+ "bad-request",
18913
+ "unavailable",
18914
+ "no-profile",
18915
+ "budget-exceeded",
18916
+ "adapter-error"
18502
18917
  ]);
18503
- var LogEntrySchema = object({
18504
- timestamp: date(),
18505
- level: LogLevelSchema,
18506
- scope: array(string()),
18918
+ var LlmGenerateResultSchema = discriminatedUnion("ok", [object({
18919
+ ok: literal(true),
18920
+ text: string(),
18921
+ model: string(),
18922
+ usage: LlmUsageSchema,
18923
+ truncated: boolean(),
18924
+ latencyMs: number()
18925
+ }), object({
18926
+ ok: literal(false),
18927
+ code: LlmErrorCodeSchema,
18507
18928
  message: string(),
18508
- meta: record(string(), unknown()).optional(),
18509
- tags: record(string(), string()).optional()
18510
- });
18511
- method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
18512
- scope: array(string()).optional(),
18513
- level: LogLevelSchema.optional(),
18514
- since: date().optional(),
18515
- until: date().optional(),
18516
- limit: number().optional(),
18517
- tags: record(string(), string()).optional()
18518
- }), array(LogEntrySchema).readonly());
18929
+ retryAfterMs: number().optional()
18930
+ })]);
18519
18931
  /**
18520
- * `login-method` — collection cap through which auth addons contribute
18521
- * their pre-auth login surfaces to the login page. This is the SINGLE,
18522
- * generic mechanism that supersedes the dead `auth.listProviders` reader:
18523
- * every auth addon (OIDC, magic-link, WebAuthn/passkey) registers a
18524
- * `login-method` provider and the PUBLIC `auth.listLoginMethods`
18525
- * procedure aggregates them for the unauthenticated login page.
18526
- *
18527
- * A contribution is a discriminated union on `kind`:
18528
- *
18529
- * - `redirect` — a declarative button. The login page renders a generic
18530
- * button that navigates to `startUrl` (an addon-owned HTTP route).
18531
- * Covers OIDC (`/addon/auth-oidc/<id>/start`) and magic-link with
18532
- * ZERO shell-side JS. A future SSO addon plugs in the same way — the
18533
- * login page needs NO change.
18534
- *
18535
- * - `widget` — a Module-Federation widget the login page mounts (via
18536
- * `loadRemoteBundle`) for an in-page ceremony. `auth.listLoginMethods`
18537
- * stamps a public `bundleUrl` from `addonId` + `bundle`. Generic
18538
- * mechanism kept for future use; no shipped addon uses it on the login
18539
- * page (the passkey ceremony below runs natively in the shell instead).
18540
- *
18541
- * - `passkey` — a declarative WebAuthn ceremony the shell renders
18542
- * natively (`@simplewebauthn/browser` lives in `addon-admin-ui`, not in
18543
- * a remotely-loaded bundle). Carries the addon's effective `rpId` /
18544
- * `origin` (from its `resolveRpID()` / `resolveOrigin()`) so the shell
18545
- * can gate visibility (IP-literal origin, hostname/rpId mismatch) WITHOUT
18546
- * fetching any remote code pre-auth. Contribution stays unconditional —
18547
- * enrollment state is never leaked pre-auth; visibility is a shell
18548
- * decision.
18549
- *
18550
- * Every contribution carries a `stage`:
18551
- * - `primary` — shown on the first credentials screen (OIDC /
18552
- * magic-link buttons; a future usernameless passkey).
18553
- * - `second-factor` — shown AFTER the password leg, gated on the
18554
- * returned `factors` (passkey-as-2FA today).
18932
+ * `Uint8Array` is the sanctioned binary convention — superjson + the UDS
18933
+ * MsgPack channel round-trip typed arrays (embedding-encoder.cap.ts:29,
18934
+ * notification-output.cap.ts:27-31 precedents).
18935
+ */
18936
+ var LlmImageSchema = object({
18937
+ bytes: _instanceof(Uint8Array),
18938
+ mimeType: string()
18939
+ });
18940
+ var LlmGenerateBaseInputSchema = object({
18941
+ /** Collection routing (the notification-output posture). */
18942
+ addonId: string().optional(),
18943
+ /** Explicit profile; else the resolution chain (spec §3). */
18944
+ profileId: string().optional(),
18945
+ /** MANDATORY usage tag: 'ai-summary', 'notifier-rules', 'adhoc-ui', … */
18946
+ consumer: string(),
18947
+ system: string().optional(),
18948
+ /** v1: single-turn. `messages[]` is a v2 additive field. */
18949
+ prompt: string(),
18950
+ /** Structured output — adapter-mapped (response_format / forced tool / responseSchema). */
18951
+ jsonSchema: record(string(), unknown()).optional(),
18952
+ /** Per-call override of the profile default. */
18953
+ maxTokens: number().int().positive().optional(),
18954
+ temperature: number().optional()
18955
+ });
18956
+ /**
18957
+ * `llm-runtime` — node-side managed llama.cpp executor (spec §4). Registered
18958
+ * on EVERY node where `addon-ai` is installed; the hub `llm` provider reaches
18959
+ * a specific node's runtime with `nodePin(profile.runtime.nodeId)` — normal
18960
+ * cap routing, zero bespoke plumbing. `internal: true`: the operator reaches
18961
+ * this only through the `llm` cap's methods.
18555
18962
  *
18556
- * `mount: skip` — the cap is read server-side by the core auth router
18557
- * (`registry.getCollection('login-method')`), never mounted as its own
18558
- * tRPC router.
18963
+ * One running llama-server child per node in v1 (models are RAM-heavy).
18964
+ * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
18965
+ * watchdog — operator decision #3).
18559
18966
  */
18560
- /** When a login method renders in the two-phase login flow. */
18561
- var LoginStageEnum = _enum(["primary", "second-factor"]);
18562
- /** One login-method contribution — redirect button, pre-auth widget, or native passkey ceremony. */
18563
- var LoginMethodContributionSchema = discriminatedUnion("kind", [
18967
+ var ManagedModelRefSchema = discriminatedUnion("kind", [
18564
18968
  object({
18565
- kind: literal("redirect"),
18566
- /** Stable id within the login-method set (e.g. `auth-oidc/google`). */
18567
- id: string(),
18568
- /** Operator-facing button label. */
18569
- label: string(),
18570
- /** lucide-react icon name. */
18571
- icon: string().optional(),
18572
- /** Addon-owned HTTP route the button navigates to (GET). */
18573
- startUrl: string(),
18574
- stage: LoginStageEnum
18969
+ kind: literal("catalog"),
18970
+ catalogId: string()
18575
18971
  }),
18576
18972
  object({
18577
- kind: literal("widget"),
18578
- /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-login`). */
18579
- id: string(),
18580
- /** Owning addon id — drives the public bundle URL + the MF namespace. */
18581
- addonId: string(),
18582
- /** Bundle filename inside the addon's dist dir (`remoteEntry.js`). */
18583
- bundle: string(),
18584
- /** MF remote descriptor — `{ remoteName, exposedModule, componentKey }`. */
18585
- remote: WidgetRemoteSchema,
18586
- stage: LoginStageEnum
18973
+ kind: literal("url"),
18974
+ url: string(),
18975
+ sha256: string().optional()
18587
18976
  }),
18588
18977
  object({
18589
- kind: literal("passkey"),
18590
- /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-direct-login`). */
18591
- id: string(),
18592
- /** Operator-facing button label. */
18593
- label: string(),
18594
- stage: LoginStageEnum,
18595
- /** Effective WebAuthn RP ID (`resolveRpID()`) — the shell gates visibility on it. */
18596
- rpId: string(),
18597
- /** Effective expected origin (`resolveOrigin()`), null when unconfigured. */
18598
- origin: string().nullable()
18978
+ kind: literal("path"),
18979
+ path: string()
18599
18980
  })
18600
18981
  ]);
18601
- method(_void(), array(LoginMethodContributionSchema).readonly());
18602
- var CpuBreakdownSchema = object({
18603
- total: number(),
18604
- user: number(),
18605
- system: number(),
18606
- irq: number(),
18607
- nice: number(),
18608
- loadAvg: tuple([
18609
- number(),
18610
- number(),
18611
- number()
18612
- ]),
18613
- cores: number()
18614
- });
18615
- var MemoryInfoSchema = object({
18616
- percent: number(),
18617
- totalBytes: number(),
18618
- usedBytes: number(),
18619
- availableBytes: number(),
18620
- swapUsedBytes: number(),
18621
- swapTotalBytes: number()
18622
- });
18623
- var DiskIoSnapshotSchema = object({
18624
- readBytes: number(),
18625
- writeBytes: number(),
18626
- readOps: number(),
18627
- writeOps: number(),
18628
- timestampMs: number()
18629
- });
18630
- var NetworkIoSnapshotSchema = object({
18631
- rxBytes: number(),
18632
- txBytes: number(),
18633
- rxPackets: number(),
18634
- txPackets: number(),
18635
- rxErrors: number(),
18636
- txErrors: number(),
18637
- timestampMs: number()
18638
- });
18639
- var MetricsGpuInfoSchema = object({
18640
- utilization: number(),
18641
- model: string(),
18642
- memoryUsedBytes: number(),
18643
- memoryTotalBytes: number(),
18644
- temperature: number().nullable()
18645
- });
18646
- var ProcessResourceInfoSchema = object({
18647
- openFds: number(),
18648
- threadCount: number(),
18649
- activeHandles: number(),
18650
- activeRequests: number()
18651
- });
18652
- var PressureAvgsSchema = object({
18653
- avg10: number(),
18654
- avg60: number(),
18655
- avg300: number()
18656
- });
18657
- var PressureInfoSchema = object({
18658
- some: PressureAvgsSchema,
18659
- full: PressureAvgsSchema.nullable()
18660
- });
18661
- var SystemResourceSnapshotSchema = object({
18662
- cpu: CpuBreakdownSchema,
18663
- memory: MemoryInfoSchema,
18664
- gpu: MetricsGpuInfoSchema.nullable(),
18665
- network: NetworkIoSnapshotSchema,
18666
- disk: DiskIoSnapshotSchema,
18667
- pressure: object({
18668
- cpu: PressureInfoSchema.nullable(),
18669
- memory: PressureInfoSchema.nullable(),
18670
- io: PressureInfoSchema.nullable()
18671
- }),
18672
- process: ProcessResourceInfoSchema,
18673
- cpuTemperature: number().nullable(),
18674
- timestampMs: number()
18675
- });
18676
- var DiskSpaceInfoSchema = object({
18677
- path: string(),
18678
- totalBytes: number(),
18679
- usedBytes: number(),
18680
- availableBytes: number(),
18681
- percent: number()
18682
- });
18683
- var PidResourceStatsSchema = object({
18684
- pid: number(),
18685
- cpu: number(),
18686
- memory: number(),
18687
- /**
18688
- * Private (anonymous) resident bytes — the per-process V8 heap + native
18689
- * allocations NOT shared with other processes (Linux RssAnon). This is the
18690
- * "real" per-runner cost; summing it across runners is meaningful, unlike
18691
- * `memory` (RSS), which double-counts the shared mmap'd framework code.
18692
- * Undefined where /proc is unavailable (e.g. macOS).
18693
- */
18694
- privateBytes: number().optional(),
18695
- /**
18696
- * Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
18697
- * code shared copy-on-write across runners. Undefined on macOS.
18698
- */
18699
- sharedBytes: number().optional()
18982
+ var ManagedRuntimeConfigSchema = object({
18983
+ /** WHERE the runtime lives — hub or any agent. */
18984
+ nodeId: string(),
18985
+ /** Closed for v1; 'ollama' is a v2 candidate. */
18986
+ engine: _enum(["llama-cpp"]),
18987
+ model: ManagedModelRefSchema,
18988
+ contextSize: number().int().default(4096),
18989
+ /** 0 = CPU-only. */
18990
+ gpuLayers: number().int().default(0),
18991
+ /** Default: cpus-2, clamped ≥1 (resolved node-side). */
18992
+ threads: number().int().optional(),
18993
+ /** Concurrent slots. */
18994
+ parallel: number().int().default(1),
18995
+ /** Else lazy: first generate boots it. */
18996
+ autoStart: boolean().default(false),
18997
+ /** 0 = never; frees RAM after quiet periods. */
18998
+ idleStopMinutes: number().int().default(30)
18700
18999
  });
18701
- var AddonInstanceSchema = object({
18702
- addonId: string(),
19000
+ var LlmRuntimeStatusSchema = object({
19001
+ /** Status is ALWAYS node-qualified. */
18703
19002
  nodeId: string(),
18704
- role: _enum(["hub", "worker"]),
18705
- pid: number(),
18706
19003
  state: _enum([
18707
- "starting",
18708
- "running",
18709
- "stopping",
18710
19004
  "stopped",
18711
- "crashed"
18712
- ]),
18713
- uptimeSec: number()
18714
- });
18715
- var NodeProcessSchema = object({
18716
- pid: number(),
18717
- ppid: number(),
18718
- pgid: number(),
18719
- classification: _enum([
18720
- "root",
18721
- "managed",
18722
- "system",
18723
- "ghost"
19005
+ "downloading",
19006
+ "starting",
19007
+ "ready",
19008
+ "crashed",
19009
+ "failed"
18724
19010
  ]),
18725
- /** `$process` addon binding when `managed`, else null. */
18726
- addonId: string().nullable(),
18727
- /** Kernel-reported nodeId when the process is a known agent/worker. */
18728
- nodeId: string().nullable(),
18729
- /** Truncated command line. */
18730
- command: string(),
18731
- cpuPercent: number(),
18732
- memoryRssBytes: number(),
18733
- /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
18734
- uptimeSec: number(),
18735
- /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
18736
- orphaned: boolean()
18737
- });
18738
- var KillProcessInputSchema = object({
18739
- pid: number(),
18740
- /** Force = SIGKILL. Default is SIGTERM. */
18741
- force: boolean().optional()
18742
- });
18743
- var KillProcessResultSchema = object({
18744
- success: boolean(),
18745
- reason: string().optional(),
18746
- signal: _enum(["SIGTERM", "SIGKILL"]).optional()
18747
- });
18748
- var DumpHeapSnapshotInputSchema = object({
18749
- /** The addon whose runner should dump a heap snapshot. */
18750
- addonId: string() });
18751
- var DumpHeapSnapshotResultSchema = object({
18752
- success: boolean(),
18753
- /** Path of the written .heapsnapshot inside the runner's container/host. */
18754
- path: string().optional(),
18755
- /** Process pid that was signalled. */
18756
19011
  pid: number().optional(),
18757
- reason: string().optional()
19012
+ port: number().optional(),
19013
+ modelPath: string().optional(),
19014
+ modelId: string().optional(),
19015
+ downloadProgress: number().min(0).max(1).optional(),
19016
+ lastError: string().optional(),
19017
+ crashesInWindow: number(),
19018
+ /** Child RSS (sampled best-effort). */
19019
+ memoryBytes: number().optional(),
19020
+ vramBytes: number().optional()
18758
19021
  });
18759
- var SystemMetricsSchema = object({
18760
- cpuPercent: number(),
18761
- memoryPercent: number(),
18762
- memoryUsedMB: number(),
18763
- memoryTotalMB: number(),
18764
- diskPercent: number().optional(),
18765
- temperature: number().optional(),
18766
- gpuPercent: number().optional(),
18767
- gpuMemoryPercent: number().optional()
19022
+ var LlmNodeModelSchema = object({
19023
+ file: string(),
19024
+ sizeBytes: number(),
19025
+ catalogId: string().optional(),
19026
+ installedAt: number().optional()
18768
19027
  });
18769
- method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(KillProcessInputSchema, KillProcessResultSchema, {
19028
+ var LlmRuntimeDiskUsageSchema = object({
19029
+ nodeId: string(),
19030
+ modelsBytes: number(),
19031
+ freeBytes: number().optional()
19032
+ });
19033
+ method(LlmGenerateBaseInputSchema.extend({
19034
+ images: array(LlmImageSchema).optional(),
19035
+ runtime: ManagedRuntimeConfigSchema,
19036
+ /** The managed profile's timeout, threaded by the hub provider. */
19037
+ timeoutMs: number().int().positive().optional()
19038
+ }), LlmGenerateResultSchema, { kind: "mutation" }), method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
18770
19039
  kind: "mutation",
18771
19040
  auth: "admin"
18772
- }), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
19041
+ }), method(object({}), _void(), {
18773
19042
  kind: "mutation",
18774
19043
  auth: "admin"
18775
- });
18776
- method(object({
18777
- sourceUrl: string(),
18778
- metadata: ModelConvertMetadataSchema,
18779
- targets: array(ConvertTargetSchema).min(1).readonly(),
18780
- calibrationRef: string().optional(),
18781
- sessionId: string().optional()
18782
- }), ConvertResultSchema, {
19044
+ }), method(object({}), LlmRuntimeStatusSchema), method(object({ model: ManagedModelRefSchema }), _void(), {
18783
19045
  kind: "mutation",
18784
- auth: "admin",
18785
- timeoutMs: 6e5
18786
- });
18787
- method(object({
18788
- nodeId: string(),
18789
- modelId: string(),
18790
- format: _enum(MODEL_FORMATS),
18791
- entry: ModelCatalogEntrySchema
18792
- }), object({
18793
- ok: boolean(),
18794
- /** sha256 of the staged tarball (empty for a hub-local no-op). */
18795
- sha256: string(),
18796
- bytes: number(),
18797
- /** The target node's modelsDir the artifact landed in. */
18798
- path: string()
18799
- }), {
19046
+ auth: "admin"
19047
+ }), method(object({ file: string() }), _void(), {
18800
19048
  kind: "mutation",
18801
19049
  auth: "admin"
18802
- });
18803
- /**
18804
- * `mqtt-broker` — broker-registry cap.
18805
- *
18806
- * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
18807
- * MQTT brokers (external + optionally an embedded `aedes`-backed one)
18808
- * and (b) the connection details a consumer addon needs to spin up
18809
- * its OWN `mqtt.js` client.
18810
- *
18811
- * Why: pub/sub routing over the system event-bus loses fidelity
18812
- * (callback shape, QoS guarantees, will/retain semantics) and adds
18813
- * refcount bookkeeping that addons would rather own themselves. The
18814
- * canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
18815
- * features anyway — give it the connection config, get out of the way.
18816
- *
18817
- * Consumer flow:
18818
- * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
18819
- * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
18820
- * client.subscribe('zigbee2mqtt/+')
18821
- *
18822
- * Collection mode: multiple brokers (e.g. one local mosquitto + one
18823
- * cloud bridge). The "embedded" entry (when present) is just another
18824
- * broker in the registry — its lifecycle is owned by the addon that
18825
- * spawned it.
18826
- */
18827
- var BrokerKindSchema = _enum(["external", "embedded"]);
19050
+ }), method(object({}), array(LlmNodeModelSchema)), method(object({}), LlmRuntimeDiskUsageSchema);
18828
19051
  /**
18829
- * Broker live-probe status.
19052
+ * `llm` — consumer-facing LLM surface (spec §1-§3). Collection-mode: array
19053
+ * methods concat-fan across providers; single-row methods route to ONE
19054
+ * provider by the `addonId` in the call input (the notification-output
19055
+ * posture, notification-output.cap.ts:215-250). Provided by `addon-ai`
19056
+ * (hub-placed); the cap stays open for future providers.
18830
19057
  *
18831
- * - `connected` — last probe completed a clean CONNACK
18832
- * - `disconnected` — no probe has run yet (cold cache)
18833
- * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
18834
- * - `unreachable` — TCP connect timed out / refused
18835
- * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
19058
+ * Profiles are ROWS (data), not addons: one row = one usable model endpoint.
19059
+ * `apiKey` is a password field — providers REDACT it on read and merge on
19060
+ * write; a stored key NEVER round-trips to a client.
18836
19061
  */
18837
- var BrokerStatusSchema$1 = _enum([
18838
- "connected",
18839
- "disconnected",
18840
- "auth-failed",
18841
- "unreachable",
18842
- "tls-error"
19062
+ var LlmProfileKindSchema = _enum([
19063
+ "openai-compatible",
19064
+ "openai",
19065
+ "anthropic",
19066
+ "google",
19067
+ "managed-local"
18843
19068
  ]);
18844
- var BrokerInfoSchema = object({
19069
+ var LlmProfileSchema = object({
18845
19070
  id: string(),
18846
19071
  name: string(),
18847
- url: string(),
18848
- kind: BrokerKindSchema,
18849
- status: BrokerStatusSchema$1,
18850
- latencyMs: number().nullable(),
18851
- error: string().optional(),
18852
- /** Embedded brokers only: number of MQTT clients currently connected. */
18853
- connectedClients: number().int().nonnegative().optional(),
18854
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
18855
- lastCheckedAt: number().optional()
19072
+ kind: LlmProfileKindSchema,
19073
+ /** Stamped by the provider — keeps the fanned catalog routable. */
19074
+ addonId: string(),
19075
+ enabled: boolean(),
19076
+ /** Vendor model id, or the managed runtime's loaded model. */
19077
+ model: string(),
19078
+ /** Required for openai-compatible; override for cloud kinds. */
19079
+ baseUrl: string().optional(),
19080
+ /** ConfigUISchema type:'password' — never round-trips (spec §5). */
19081
+ apiKey: string().optional(),
19082
+ supportsVision: boolean(),
19083
+ temperature: number().min(0).max(2).optional(),
19084
+ maxTokens: number().int().positive().optional(),
19085
+ timeoutMs: number().int().positive().default(6e4),
19086
+ extraHeaders: record(string(), string()).optional(),
19087
+ /** kind === 'managed-local' only (spec §4). */
19088
+ runtime: ManagedRuntimeConfigSchema.optional()
18856
19089
  });
18857
- /**
18858
- * Connection details — what a consumer needs to call
18859
- * `mqtt.connect(url, options)`. We split URL + credentials so the
18860
- * consumer can pass them as `mqtt.connect(url, { username, password })`
18861
- * instead of stuffing creds into the URL (which leaks them into logs).
18862
- */
18863
- var BrokerConnectionDetailsSchema = object({
18864
- url: string(),
18865
- username: string().optional(),
18866
- password: string().optional(),
18867
- /**
18868
- * Suggested prefix for `clientId`. Each consumer should suffix this
18869
- * with its own discriminator (addon id, instance id) so reconnects
18870
- * don't kick each other off (MQTT spec: clientId must be unique per
18871
- * broker).
18872
- */
18873
- clientIdPrefix: string().optional()
19090
+ /** ConfigUISchema tree passed through untyped on the wire (the
19091
+ * notification-output `ConfigSchemaPassthrough` precedent at
19092
+ * notification-output.cap.ts:151); the exported TS type re-tightens it. */
19093
+ var ConfigSchemaPassthrough$1 = unknown();
19094
+ var LlmProfileKindDescriptorSchema = object({
19095
+ kind: LlmProfileKindSchema,
19096
+ label: string(),
19097
+ icon: string(),
19098
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
19099
+ addonId: string(),
19100
+ configSchema: ConfigSchemaPassthrough$1
18874
19101
  });
18875
- var AddBrokerInputSchema = object({
18876
- name: string().min(1),
18877
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
18878
- username: string().optional(),
18879
- password: string().optional(),
18880
- clientIdPrefix: string().optional()
19102
+ var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
19103
+ var LlmDefaultSchema = object({
19104
+ selector: LlmDefaultSelectorSchema,
19105
+ profileId: string()
18881
19106
  });
18882
- var AddBrokerResultSchema = object({ id: string() });
18883
- var IdInputSchema = object({ id: string() });
18884
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
18885
- ok: literal(true),
18886
- latencyMs: number()
18887
- }), object({
18888
- ok: literal(false),
18889
- error: string()
18890
- })]);
18891
- var StartEmbeddedInputSchema = object({
18892
- port: number().int().min(1).max(65535).default(1883),
18893
- /** Allow anonymous connect (no username/password). Default: false. */
18894
- allowAnonymous: boolean().default(false),
18895
- /** Optional shared username/password for clients. */
18896
- username: string().optional(),
18897
- password: string().optional()
19107
+ /** Server-side rollup row — getUsage never dumps raw call rows (spec §6). */
19108
+ var LlmUsageRollupSchema = object({
19109
+ day: string(),
19110
+ consumer: string(),
19111
+ profileId: string(),
19112
+ calls: number(),
19113
+ okCalls: number(),
19114
+ errorCalls: number(),
19115
+ inputTokens: number(),
19116
+ outputTokens: number(),
19117
+ avgLatencyMs: number()
18898
19118
  });
18899
- var StartEmbeddedResultSchema = object({
19119
+ /** LLM-facing view over the reused ModelCatalogEntry mechanism (spec §4.2). */
19120
+ var ManagedModelCatalogEntrySchema = object({
18900
19121
  id: string(),
18901
- url: string()
18902
- });
18903
- var StatusSchema = object({
18904
- brokerCount: number(),
18905
- embeddedRunning: boolean()
18906
- });
18907
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
18908
- var NetworkEndpointSchema = object({
19122
+ label: string(),
19123
+ family: string(),
19124
+ purpose: _enum(["text", "vision"]),
18909
19125
  url: string(),
18910
- hostname: string(),
18911
- port: number(),
18912
- protocol: _enum(["http", "https"])
19126
+ sha256: string(),
19127
+ sizeBytes: number(),
19128
+ quantization: string(),
19129
+ /** Load-time guidance shown in the picker. */
19130
+ minRamBytes: number(),
19131
+ contextSizeDefault: number().int(),
19132
+ /** Vision models: companion projector file. */
19133
+ mmprojUrl: string().optional()
18913
19134
  });
18914
- var NetworkAccessStatusSchema = object({
18915
- connected: boolean(),
18916
- endpoint: NetworkEndpointSchema.nullable(),
19135
+ var LlmRuntimeNodeSchema = object({
19136
+ nodeId: string(),
19137
+ reachable: boolean(),
19138
+ status: LlmRuntimeStatusSchema.optional(),
19139
+ disk: LlmRuntimeDiskUsageSchema.optional(),
18917
19140
  error: string().optional()
18918
19141
  });
18919
- /**
18920
- * Optional, richer endpoint shape returned by providers that expose
18921
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
18922
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
18923
- * the originating provider config (mode + sourcePort) so the
18924
- * orchestrator UI can label rows distinctly. Providers that expose only
18925
- * one endpoint just omit `listEndpoints` from their provider impl.
18926
- */
18927
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
18928
- /**
18929
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
18930
- * the orchestrator can dedupe across `listEndpoints` polls.
18931
- */
18932
- id: string(),
18933
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
18934
- label: string(),
18935
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
18936
- mode: string().optional(),
18937
- /** Originating local port the ingress fronts (informational). */
18938
- sourcePort: number().optional()
19142
+ var GenerateVisionInputSchema = LlmGenerateBaseInputSchema.extend({ images: array(LlmImageSchema).min(1) });
19143
+ var ProfileRefInputSchema = object({
19144
+ addonId: string(),
19145
+ profileId: string()
19146
+ });
19147
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
19148
+ kind: "mutation",
19149
+ auth: "admin"
19150
+ }), method(ProfileRefInputSchema, _void(), {
19151
+ kind: "mutation",
19152
+ auth: "admin"
19153
+ }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
19154
+ kind: "mutation",
19155
+ auth: "admin"
19156
+ }), method(ProfileRefInputSchema, array(string())), method(object({}), array(LlmDefaultSchema)), method(object({
19157
+ selector: LlmDefaultSelectorSchema,
19158
+ profileId: string().nullable()
19159
+ }), _void(), {
19160
+ kind: "mutation",
19161
+ auth: "admin"
19162
+ }), method(object({
19163
+ since: number().optional(),
19164
+ until: number().optional(),
19165
+ consumer: string().optional(),
19166
+ profileId: string().optional()
19167
+ }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
19168
+ nodeId: string(),
19169
+ model: ManagedModelRefSchema
19170
+ }), _void(), {
19171
+ kind: "mutation",
19172
+ auth: "admin"
19173
+ }), method(object({
19174
+ nodeId: string(),
19175
+ file: string()
19176
+ }), _void(), {
19177
+ kind: "mutation",
19178
+ auth: "admin"
19179
+ }), method(ProfileRefInputSchema, LlmRuntimeStatusSchema), method(ProfileRefInputSchema, LlmRuntimeStatusSchema, {
19180
+ kind: "mutation",
19181
+ auth: "admin"
19182
+ }), method(ProfileRefInputSchema, _void(), {
19183
+ kind: "mutation",
19184
+ auth: "admin"
19185
+ });
19186
+ var LogLevelSchema = _enum([
19187
+ "debug",
19188
+ "info",
19189
+ "warn",
19190
+ "error"
19191
+ ]);
19192
+ var LogEntrySchema = object({
19193
+ timestamp: date(),
19194
+ level: LogLevelSchema,
19195
+ scope: array(string()),
19196
+ message: string(),
19197
+ meta: record(string(), unknown()).optional(),
19198
+ tags: record(string(), string()).optional()
18939
19199
  });
18940
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
19200
+ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
19201
+ scope: array(string()).optional(),
19202
+ level: LogLevelSchema.optional(),
19203
+ since: date().optional(),
19204
+ until: date().optional(),
19205
+ limit: number().optional(),
19206
+ tags: record(string(), string()).optional()
19207
+ }), array(LogEntrySchema).readonly());
18941
19208
  /**
18942
- * notification-output — canonical, capability-gated notification delivery.
19209
+ * `login-method` — collection cap through which auth addons contribute
19210
+ * their pre-auth login surfaces to the login page. This is the SINGLE,
19211
+ * generic mechanism that supersedes the dead `auth.listProviders` reader:
19212
+ * every auth addon (OIDC, magic-link, WebAuthn/passkey) registers a
19213
+ * `login-method` provider and the PUBLIC `auth.listLoginMethods`
19214
+ * procedure aggregates them for the unauthenticated login page.
18943
19215
  *
18944
- * Apprise-derived model (see
18945
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
18946
- * callers emit ONE canonical `Notification`; each provider declares a
18947
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
18948
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
18949
- * message to what the kind supports — callers never special-case a service.
19216
+ * A contribution is a discriminated union on `kind`:
18950
19217
  *
18951
- * DESIGN DECISIONS (locked):
18952
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
18953
- * `setTargetEnabled`), each provider persisting via the `settings-store`
18954
- * cap. Rationale: the admin UI needs one uniform surface across the
18955
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
18956
- * alternative would fork the UI per addon and cannot host the
18957
- * discovery→adopt flow.
18958
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
18959
- * the generated cap-mount auto-`concatCollection`-fans them across every
18960
- * registered provider (notifiers addon + HA addon) so one catalog is
18961
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
18962
- * `addonId` the generated collection router extracts from the call input.
18963
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
18964
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
18965
- * `storage` / `storage-provider` / `recording` caps over the same path. No
18966
- * base64 fallback needed.
19218
+ * - `redirect` — a declarative button. The login page renders a generic
19219
+ * button that navigates to `startUrl` (an addon-owned HTTP route).
19220
+ * Covers OIDC (`/addon/auth-oidc/<id>/start`) and magic-link with
19221
+ * ZERO shell-side JS. A future SSO addon plugs in the same way — the
19222
+ * login page needs NO change.
18967
19223
  *
18968
- * TODO (deferred, closed-set change — separate decision): add
18969
- * `providerKind: 'notify'` so notification providers surface on the unified
18970
- * admin "Integrations" page.
18971
- */
18972
- /**
18973
- * Zentik-derived typed-media enum — the superset across every kind. Each
18974
- * adapter picks what it supports and the degrade engine filters the rest.
18975
- */
18976
- var AttachmentMediaTypeSchema = _enum([
18977
- "image",
18978
- "video",
18979
- "gif",
18980
- "audio",
18981
- "icon"
18982
- ]);
18983
- /**
18984
- * A single attachment. Exactly one of `url` (remote source, most adapters
18985
- * prefer this) or `bytes` (inline source; required for Pushover-style
18986
- * bytes-only kinds) MUST be present — the degrade engine expresses a
18987
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
19224
+ * - `widget` — a Module-Federation widget the login page mounts (via
19225
+ * `loadRemoteBundle`) for an in-page ceremony. `auth.listLoginMethods`
19226
+ * stamps a public `bundleUrl` from `addonId` + `bundle`. Generic
19227
+ * mechanism kept for future use; no shipped addon uses it on the login
19228
+ * page (the passkey ceremony below runs natively in the shell instead).
19229
+ *
19230
+ * - `passkey` — a declarative WebAuthn ceremony the shell renders
19231
+ * natively (`@simplewebauthn/browser` lives in `addon-admin-ui`, not in
19232
+ * a remotely-loaded bundle). Carries the addon's effective `rpId` /
19233
+ * `origin` (from its `resolveRpID()` / `resolveOrigin()`) so the shell
19234
+ * can gate visibility (IP-literal origin, hostname/rpId mismatch) WITHOUT
19235
+ * fetching any remote code pre-auth. Contribution stays unconditional —
19236
+ * enrollment state is never leaked pre-auth; visibility is a shell
19237
+ * decision.
19238
+ *
19239
+ * Every contribution carries a `stage`:
19240
+ * - `primary` — shown on the first credentials screen (OIDC /
19241
+ * magic-link buttons; a future usernameless passkey).
19242
+ * - `second-factor` — shown AFTER the password leg, gated on the
19243
+ * returned `factors` (passkey-as-2FA today).
19244
+ *
19245
+ * `mount: skip` — the cap is read server-side by the core auth router
19246
+ * (`registry.getCollection('login-method')`), never mounted as its own
19247
+ * tRPC router.
18988
19248
  */
18989
- var AttachmentSchema = object({
18990
- mediaType: AttachmentMediaTypeSchema,
18991
- url: string().optional(),
18992
- bytes: _instanceof(Uint8Array).optional(),
18993
- mime: string().optional(),
18994
- name: string().optional()
18995
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
18996
- var NotificationFormatSchema = _enum([
18997
- "text",
18998
- "markdown",
18999
- "html"
19249
+ /** When a login method renders in the two-phase login flow. */
19250
+ var LoginStageEnum = _enum(["primary", "second-factor"]);
19251
+ /** One login-method contribution — redirect button, pre-auth widget, or native passkey ceremony. */
19252
+ var LoginMethodContributionSchema = discriminatedUnion("kind", [
19253
+ object({
19254
+ kind: literal("redirect"),
19255
+ /** Stable id within the login-method set (e.g. `auth-oidc/google`). */
19256
+ id: string(),
19257
+ /** Operator-facing button label. */
19258
+ label: string(),
19259
+ /** lucide-react icon name. */
19260
+ icon: string().optional(),
19261
+ /** Addon-owned HTTP route the button navigates to (GET). */
19262
+ startUrl: string(),
19263
+ stage: LoginStageEnum
19264
+ }),
19265
+ object({
19266
+ kind: literal("widget"),
19267
+ /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-login`). */
19268
+ id: string(),
19269
+ /** Owning addon id — drives the public bundle URL + the MF namespace. */
19270
+ addonId: string(),
19271
+ /** Bundle filename inside the addon's dist dir (`remoteEntry.js`). */
19272
+ bundle: string(),
19273
+ /** MF remote descriptor — `{ remoteName, exposedModule, componentKey }`. */
19274
+ remote: WidgetRemoteSchema,
19275
+ stage: LoginStageEnum
19276
+ }),
19277
+ object({
19278
+ kind: literal("passkey"),
19279
+ /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-direct-login`). */
19280
+ id: string(),
19281
+ /** Operator-facing button label. */
19282
+ label: string(),
19283
+ stage: LoginStageEnum,
19284
+ /** Effective WebAuthn RP ID (`resolveRpID()`) — the shell gates visibility on it. */
19285
+ rpId: string(),
19286
+ /** Effective expected origin (`resolveOrigin()`), null when unconfigured. */
19287
+ origin: string().nullable()
19288
+ })
19000
19289
  ]);
19001
- /** A single tap-through action button. */
19002
- var NotificationActionSchema = object({
19003
- id: string(),
19004
- label: string(),
19005
- url: string().optional()
19290
+ method(_void(), array(LoginMethodContributionSchema).readonly());
19291
+ var CpuBreakdownSchema = object({
19292
+ total: number(),
19293
+ user: number(),
19294
+ system: number(),
19295
+ irq: number(),
19296
+ nice: number(),
19297
+ loadAvg: tuple([
19298
+ number(),
19299
+ number(),
19300
+ number()
19301
+ ]),
19302
+ cores: number()
19006
19303
  });
19007
- /**
19008
- * The canonical notification. `body` is the only hard field (Apprise model).
19009
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
19010
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
19011
- * the adapter maps this ordinal onto its native level. `level?` is an
19012
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
19013
- * `priority` for that one target.
19014
- */
19015
- var NotificationSchema = object({
19016
- body: string(),
19017
- title: string().optional(),
19018
- format: NotificationFormatSchema.default("text"),
19019
- priority: number().int().min(1).max(5).default(3),
19020
- level: string().optional(),
19021
- attachments: array(AttachmentSchema).optional(),
19022
- clickUrl: string().optional(),
19023
- actions: array(NotificationActionSchema).optional(),
19024
- sound: string().optional(),
19025
- ttl: number().optional(),
19026
- tag: string().optional(),
19027
- deviceId: number().optional(),
19028
- eventId: string().optional(),
19029
- metadata: record(string(), unknown()).optional()
19304
+ var MemoryInfoSchema = object({
19305
+ percent: number(),
19306
+ totalBytes: number(),
19307
+ usedBytes: number(),
19308
+ availableBytes: number(),
19309
+ swapUsedBytes: number(),
19310
+ swapTotalBytes: number()
19311
+ });
19312
+ var DiskIoSnapshotSchema = object({
19313
+ readBytes: number(),
19314
+ writeBytes: number(),
19315
+ readOps: number(),
19316
+ writeOps: number(),
19317
+ timestampMs: number()
19318
+ });
19319
+ var NetworkIoSnapshotSchema = object({
19320
+ rxBytes: number(),
19321
+ txBytes: number(),
19322
+ rxPackets: number(),
19323
+ txPackets: number(),
19324
+ rxErrors: number(),
19325
+ txErrors: number(),
19326
+ timestampMs: number()
19327
+ });
19328
+ var MetricsGpuInfoSchema = object({
19329
+ utilization: number(),
19330
+ model: string(),
19331
+ memoryUsedBytes: number(),
19332
+ memoryTotalBytes: number(),
19333
+ temperature: number().nullable()
19334
+ });
19335
+ var ProcessResourceInfoSchema = object({
19336
+ openFds: number(),
19337
+ threadCount: number(),
19338
+ activeHandles: number(),
19339
+ activeRequests: number()
19030
19340
  });
19031
- /** One declared native severity/priority level for a kind. */
19032
- var TargetKindLevelSchema = object({
19033
- id: string(),
19034
- label: string(),
19035
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
19036
- ordinal: number().int().min(1).max(5).nullable(),
19037
- flags: object({
19038
- critical: boolean().optional(),
19039
- silent: boolean().optional(),
19040
- noPush: boolean().optional()
19041
- }).optional(),
19042
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
19043
- requires: array(string()).optional(),
19044
- description: string().optional()
19341
+ var PressureAvgsSchema = object({
19342
+ avg10: number(),
19343
+ avg60: number(),
19344
+ avg300: number()
19045
19345
  });
19046
- /** The full capability block consulted before dispatch. */
19047
- var TargetKindCapsSchema = object({
19048
- attachments: object({
19049
- mediaTypes: array(AttachmentMediaTypeSchema),
19050
- mode: _enum([
19051
- "url",
19052
- "bytes",
19053
- "both"
19054
- ]),
19055
- max: number().int().nonnegative(),
19056
- maxBytes: number().int().positive().optional()
19346
+ var PressureInfoSchema = object({
19347
+ some: PressureAvgsSchema,
19348
+ full: PressureAvgsSchema.nullable()
19349
+ });
19350
+ var SystemResourceSnapshotSchema = object({
19351
+ cpu: CpuBreakdownSchema,
19352
+ memory: MemoryInfoSchema,
19353
+ gpu: MetricsGpuInfoSchema.nullable(),
19354
+ network: NetworkIoSnapshotSchema,
19355
+ disk: DiskIoSnapshotSchema,
19356
+ pressure: object({
19357
+ cpu: PressureInfoSchema.nullable(),
19358
+ memory: PressureInfoSchema.nullable(),
19359
+ io: PressureInfoSchema.nullable()
19057
19360
  }),
19058
- /** Max action buttons (0 = none). */
19059
- actions: number().int().nonnegative(),
19060
- levels: array(TargetKindLevelSchema),
19061
- format: array(NotificationFormatSchema),
19062
- clickUrl: boolean(),
19063
- sound: boolean(),
19064
- ttl: boolean(),
19065
- bodyMaxLen: number().int().positive()
19361
+ process: ProcessResourceInfoSchema,
19362
+ cpuTemperature: number().nullable(),
19363
+ timestampMs: number()
19066
19364
  });
19067
- /**
19068
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
19069
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
19070
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
19071
- * the union is large and not meant for runtime validation here; the exported
19072
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
19073
- */
19074
- var ConfigSchemaPassthrough = unknown();
19075
- var TargetKindSchema = object({
19076
- kind: string(),
19077
- label: string(),
19078
- icon: string(),
19079
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
19080
- addonId: string(),
19081
- configSchema: ConfigSchemaPassthrough,
19082
- supportsDiscovery: boolean(),
19083
- caps: TargetKindCapsSchema
19365
+ var DiskSpaceInfoSchema = object({
19366
+ path: string(),
19367
+ totalBytes: number(),
19368
+ usedBytes: number(),
19369
+ availableBytes: number(),
19370
+ percent: number()
19084
19371
  });
19085
- /**
19086
- * A persisted target. `config` holds secrets; providers REDACT secret fields
19087
- * (return a presence marker only) when serving `listTargets` — never
19088
- * round-trip a stored secret to the UI.
19089
- */
19090
- var TargetSchema = object({
19091
- id: string(),
19092
- name: string(),
19093
- kind: string(),
19372
+ var PidResourceStatsSchema = object({
19373
+ pid: number(),
19374
+ cpu: number(),
19375
+ memory: number(),
19376
+ /**
19377
+ * Private (anonymous) resident bytes — the per-process V8 heap + native
19378
+ * allocations NOT shared with other processes (Linux RssAnon). This is the
19379
+ * "real" per-runner cost; summing it across runners is meaningful, unlike
19380
+ * `memory` (RSS), which double-counts the shared mmap'd framework code.
19381
+ * Undefined where /proc is unavailable (e.g. macOS).
19382
+ */
19383
+ privateBytes: number().optional(),
19384
+ /**
19385
+ * Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
19386
+ * code shared copy-on-write across runners. Undefined on macOS.
19387
+ */
19388
+ sharedBytes: number().optional()
19389
+ });
19390
+ var AddonInstanceSchema = object({
19094
19391
  addonId: string(),
19095
- enabled: boolean(),
19096
- config: record(string(), unknown())
19392
+ nodeId: string(),
19393
+ role: _enum(["hub", "worker"]),
19394
+ pid: number(),
19395
+ state: _enum([
19396
+ "starting",
19397
+ "running",
19398
+ "stopping",
19399
+ "stopped",
19400
+ "crashed"
19401
+ ]),
19402
+ uptimeSec: number()
19097
19403
  });
19098
- /** A discovery-surfaced candidate (config is partial + non-secret). */
19099
- var DiscoveredTargetSchema = object({
19100
- kind: string(),
19101
- suggestedName: string(),
19102
- config: record(string(), unknown())
19404
+ var NodeProcessSchema = object({
19405
+ pid: number(),
19406
+ ppid: number(),
19407
+ pgid: number(),
19408
+ classification: _enum([
19409
+ "root",
19410
+ "managed",
19411
+ "system",
19412
+ "ghost"
19413
+ ]),
19414
+ /** `$process` addon binding when `managed`, else null. */
19415
+ addonId: string().nullable(),
19416
+ /** Kernel-reported nodeId when the process is a known agent/worker. */
19417
+ nodeId: string().nullable(),
19418
+ /** Truncated command line. */
19419
+ command: string(),
19420
+ cpuPercent: number(),
19421
+ memoryRssBytes: number(),
19422
+ /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
19423
+ uptimeSec: number(),
19424
+ /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
19425
+ orphaned: boolean()
19103
19426
  });
19104
- /** The degrade engine's report — what was resolved / dropped / degraded. */
19105
- var RenderedAsSchema = object({
19106
- level: string(),
19107
- format: NotificationFormatSchema,
19108
- attachmentsSent: number().int().nonnegative(),
19109
- actionsSent: number().int().nonnegative(),
19110
- truncated: boolean(),
19111
- dropped: array(string())
19427
+ var KillProcessInputSchema = object({
19428
+ pid: number(),
19429
+ /** Force = SIGKILL. Default is SIGTERM. */
19430
+ force: boolean().optional()
19112
19431
  });
19113
- var SendResultSchema = object({
19432
+ var KillProcessResultSchema = object({
19114
19433
  success: boolean(),
19115
- error: string().optional(),
19116
- renderedAs: RenderedAsSchema.optional()
19434
+ reason: string().optional(),
19435
+ signal: _enum(["SIGTERM", "SIGKILL"]).optional()
19436
+ });
19437
+ var DumpHeapSnapshotInputSchema = object({
19438
+ /** The addon whose runner should dump a heap snapshot. */
19439
+ addonId: string() });
19440
+ var DumpHeapSnapshotResultSchema = object({
19441
+ success: boolean(),
19442
+ /** Path of the written .heapsnapshot inside the runner's container/host. */
19443
+ path: string().optional(),
19444
+ /** Process pid that was signalled. */
19445
+ pid: number().optional(),
19446
+ reason: string().optional()
19447
+ });
19448
+ var SystemMetricsSchema = object({
19449
+ cpuPercent: number(),
19450
+ memoryPercent: number(),
19451
+ memoryUsedMB: number(),
19452
+ memoryTotalMB: number(),
19453
+ diskPercent: number().optional(),
19454
+ temperature: number().optional(),
19455
+ gpuPercent: number().optional(),
19456
+ gpuMemoryPercent: number().optional()
19457
+ });
19458
+ method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(KillProcessInputSchema, KillProcessResultSchema, {
19459
+ kind: "mutation",
19460
+ auth: "admin"
19461
+ }), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
19462
+ kind: "mutation",
19463
+ auth: "admin"
19464
+ });
19465
+ method(object({
19466
+ sourceUrl: string(),
19467
+ metadata: ModelConvertMetadataSchema,
19468
+ targets: array(ConvertTargetSchema).min(1).readonly(),
19469
+ calibrationRef: string().optional(),
19470
+ sessionId: string().optional()
19471
+ }), ConvertResultSchema, {
19472
+ kind: "mutation",
19473
+ auth: "admin",
19474
+ timeoutMs: 6e5
19475
+ });
19476
+ method(object({
19477
+ nodeId: string(),
19478
+ modelId: string(),
19479
+ format: _enum(MODEL_FORMATS),
19480
+ entry: ModelCatalogEntrySchema
19481
+ }), object({
19482
+ ok: boolean(),
19483
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
19484
+ sha256: string(),
19485
+ bytes: number(),
19486
+ /** The target node's modelsDir the artifact landed in. */
19487
+ path: string()
19488
+ }), {
19489
+ kind: "mutation",
19490
+ auth: "admin"
19117
19491
  });
19118
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
19119
- var TestResultSchema = SendResultSchema;
19120
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
19121
- kind: string(),
19122
- config: record(string(), unknown()).optional()
19123
- }), array(DiscoveredTargetSchema)), method(object({
19124
- targetId: string(),
19125
- notification: NotificationSchema
19126
- }), SendResultSchema, { kind: "mutation" }), method(object({
19127
- targetId: string(),
19128
- sample: NotificationSchema.optional()
19129
- }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
19130
- targetId: string(),
19131
- enabled: boolean()
19132
- }), _void(), { kind: "mutation" });
19133
19492
  /**
19134
- * notification-rules — the Notification Center rule surface (P1 core).
19135
- *
19136
- * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
19137
- * (operator decisions D-1/D-2/D-3 are binding):
19493
+ * `mqtt-broker` — broker-registry cap.
19138
19494
  *
19139
- * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
19140
- * `notification-center` module), hooked on the durable persistence
19141
- * moments (object-event insert, TrackCloser.closeExpired) with a
19142
- * persisted outbox + retry — never the lossy telemetry bus (D8).
19143
- * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
19144
- * FIRST persisted detection matching the conditions (per-track dedup,
19145
- * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
19146
- * `delivery: 'track-end'` evaluates the finalized track record at close.
19147
- * - DISPATCH stays behind `notification-output` (rules reference targets
19148
- * by id; per-backend params are a passthrough blob capped by the
19149
- * target kind's own caps/degrade engine).
19495
+ * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
19496
+ * MQTT brokers (external + optionally an embedded `aedes`-backed one)
19497
+ * and (b) the connection details a consumer addon needs to spin up
19498
+ * its OWN `mqtt.js` client.
19150
19499
  *
19151
- * P1 scope: admin-authored rules only (`createdBy` stamped from the
19152
- * server-injected caller identity — the first `caller: 'required'`
19153
- * adopter). The P1 condition subset is: devices, classes(+exclude),
19154
- * minConfidence, admin zones (any/all + exclude), weekly schedule
19155
- * windows, and the optional label/identity/plate matchers. User rules,
19156
- * private zones, per-recipient fan-out and the wider condition table are
19157
- * P2+ (see spec §7).
19500
+ * Why: pub/sub routing over the system event-bus loses fidelity
19501
+ * (callback shape, QoS guarantees, will/retain semantics) and adds
19502
+ * refcount bookkeeping that addons would rather own themselves. The
19503
+ * canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
19504
+ * features anyway — give it the connection config, get out of the way.
19158
19505
  *
19159
- * All schemas here are the single source of truth — `NcRule` etc. are
19160
- * `z.infer` exports; no duplicate interfaces (the advanced-notifier
19161
- * schema/interface drift is explicitly not repeated).
19506
+ * Consumer flow:
19507
+ * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
19508
+ * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
19509
+ * client.subscribe('zigbee2mqtt/+')
19510
+ *
19511
+ * Collection mode: multiple brokers (e.g. one local mosquitto + one
19512
+ * cloud bridge). The "embedded" entry (when present) is just another
19513
+ * broker in the registry — its lifecycle is owned by the addon that
19514
+ * spawned it.
19162
19515
  */
19516
+ var BrokerKindSchema = _enum(["external", "embedded"]);
19163
19517
  /**
19164
- * D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
19165
- * The value maps 1:1 onto the evaluated record kind:
19166
- * - `immediate` ↔ object-event persist (lowest-latency detection burst)
19167
- * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
19168
- * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
19169
- * change of a LINKED device, one row per linked camera)
19170
- * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
19171
- * delivery / pick-up)
19518
+ * Broker live-probe status.
19172
19519
  *
19173
- * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
19174
- * `package-event` are pure trigger kinds (no urgency dimension). Extending
19175
- * this one field keeps the schema additive — a rule still declares exactly
19176
- * one trigger.
19520
+ * - `connected` — last probe completed a clean CONNACK
19521
+ * - `disconnected` — no probe has run yet (cold cache)
19522
+ * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
19523
+ * - `unreachable` — TCP connect timed out / refused
19524
+ * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
19177
19525
  */
19178
- var NcDeliverySchema = _enum([
19179
- "immediate",
19180
- "track-end",
19181
- "device-event",
19182
- "package-event"
19526
+ var BrokerStatusSchema$1 = _enum([
19527
+ "connected",
19528
+ "disconnected",
19529
+ "auth-failed",
19530
+ "unreachable",
19531
+ "tls-error"
19183
19532
  ]);
19184
- /** Weekly schedule — OR of windows; absence on the rule = always active. */
19185
- var NcScheduleSchema = object({
19186
- windows: array(object({
19187
- /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
19188
- days: array(number().int().min(0).max(6)).min(1),
19189
- startMinute: number().int().min(0).max(1439),
19190
- endMinute: number().int().min(0).max(1439)
19191
- })).min(1),
19192
- /** IANA timezone; default = hub host timezone. */
19193
- timezone: string().optional(),
19194
- /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
19195
- invert: boolean().optional()
19196
- });
19197
- /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
19198
- var NcPlateMatcherSchema = object({
19199
- values: array(string().min(1)).min(1),
19200
- /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
19201
- maxDistance: number().int().min(0).max(3).default(1)
19202
- });
19203
- /**
19204
- * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
19205
- * occupancy edge for a device — optionally narrowed to a single admin
19206
- * `zoneId` and/or object `className`. `op` selects the edge/threshold:
19207
- * - `became-occupied` (default) — count crossed 0 → ≥ `count`
19208
- * - `became-free` — count crossed ≥ `count` → below it
19209
- * - `>=` / `<=` — count is at/over or at/under `count`
19210
- * `sustainSeconds` requires the condition hold continuously that long
19211
- * before firing (debounces flicker; 0 = fire on the first matching edge).
19212
- * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
19213
- * the condition never matches. Confirmed edge-state survives addon restarts
19214
- * (declared SQLite collection, reseeded on boot).
19215
- */
19216
- var NcOccupancyConditionSchema = object({
19217
- /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
19218
- zoneId: string().optional(),
19219
- /** Object class to count; absent = any class. */
19220
- className: string().optional(),
19221
- op: _enum([
19222
- "became-occupied",
19223
- "became-free",
19224
- ">=",
19225
- "<="
19226
- ]).default("became-occupied"),
19227
- count: number().int().min(0).default(1),
19228
- sustainSeconds: number().int().min(0).max(3600).default(15)
19229
- });
19230
- /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
19231
- var NcZoneConditionSchema = object({
19232
- ids: array(string().min(1)).min(1),
19233
- /** Quantifier over `ids` — at least one / every one visited. */
19234
- match: _enum(["any", "all"]).default("any")
19235
- });
19236
- /**
19237
- * The P1 condition set — a flat AND of groups; absent group = pass;
19238
- * membership lists are OR within the list (spec §2.3).
19239
- */
19240
- var NcConditionsSchema = object({
19241
- /** Device scope — absent = all devices. */
19242
- devices: array(number()).optional(),
19243
- /** Detector class names (any overlap with the record's class set). */
19244
- classes: array(string().min(1)).optional(),
19245
- /** Veto classes — any overlap fails the rule. */
19246
- classesExclude: array(string().min(1)).optional(),
19247
- /** Minimum detection confidence 0–1 (fails when the record has none). */
19248
- minConfidence: number().min(0).max(1).optional(),
19249
- /** Admin zone membership over event `zones` / track `zonesVisited`. */
19250
- zones: NcZoneConditionSchema.optional(),
19251
- /** Veto zones — any hit fails the rule. */
19252
- zonesExclude: array(string().min(1)).optional(),
19253
- /**
19254
- * Exact (case-insensitive) match on the record's collapsed `label`
19255
- * (identity name / plate text / subclass).
19256
- */
19257
- labelEquals: array(string().min(1)).optional(),
19258
- /**
19259
- * Identity matcher. P1 boundary: matched against the record's collapsed
19260
- * `label` (the identity display name propagated by the face pipeline) —
19261
- * identity-ID matching rides in P2 when identity ids reach the record.
19262
- */
19263
- identities: array(string().min(1)).optional(),
19264
- /** Fuzzy plate matcher against the record's `label` (plate text). */
19265
- plates: NcPlateMatcherSchema.optional(),
19266
- /**
19267
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
19268
- * Same P1 boundary: matched against the record's collapsed `label` (the
19269
- * identity display name). A record with NO label passes (nothing to
19270
- * exclude), unlike the include variant which fails on an absent label.
19271
- */
19272
- identitiesExclude: array(string().min(1)).optional(),
19273
- /**
19274
- * Minimum server-computed key-event importance in [0,1] (`Track.importance`).
19275
- * TRACK-END only: importance is scored at track close, so it does not exist
19276
- * at immediate / object-event evaluation time (see catalog `appliesTo`). At
19277
- * close the value is threaded via the close-time info (the `Track` clone is
19278
- * captured before the DB row is updated, so it would otherwise read stale).
19279
- * Fails when the record carries no importance (never guess quality — the
19280
- * `minConfidence` precedent). MVP cut: a single scalar threshold.
19281
- */
19282
- minImportance: number().min(0).max(1).optional(),
19283
- /**
19284
- * Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
19285
- * TRACK-END only: an `immediate` / object-event subject has no closed
19286
- * lifespan, so a dwell condition never matches immediate delivery
19287
- * (documented choice — the object-event record carries no `firstSeen`,
19288
- * so dwell cannot be computed from what the subject actually carries).
19289
- */
19290
- minDwellSeconds: number().min(0).optional(),
19291
- /**
19292
- * Detection provenance filter. `any` (default / absent) matches every
19293
- * source; otherwise the subject's source must equal it. Legacy records
19294
- * with no stamped source are treated as `pipeline`. The union spans both
19295
- * record kinds — object events carry `pipeline` | `onboard`, synthetic
19296
- * tracks carry `sensor`.
19297
- */
19298
- source: _enum([
19299
- "pipeline",
19300
- "onboard",
19301
- "sensor",
19302
- "any"
19303
- ]).optional(),
19304
- /**
19305
- * Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
19306
- * detector `minConfidence` (that gates the object-detection score; this
19307
- * gates the recognition/OCR match score). Fails when the subject carries
19308
- * no label-match confidence (never guess). TRACK-END only: the confidence
19309
- * lives on the recognition result and reaches the subject at track close.
19310
- *
19311
- * What it measures precisely (plumbed at track close — the closer threads
19312
- * the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
19313
- * `importance`): the BEST recognition match confidence observed for the
19314
- * label the track carries at close — for a face, the peak cosine similarity
19315
- * of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
19316
- * for a plate, the peak OCR read score of the best-held plate
19317
- * (`plateText.confidence`). When BOTH a face and a plate were recognized on
19318
- * one track the higher of the two is used. A track that ended with no
19319
- * confident identity/plate match carries no value, so the condition fails
19320
- * closed for it (an un-recognized subject).
19321
- */
19322
- minLabelConfidence: number().min(0).max(1).optional(),
19323
- /**
19324
- * DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
19325
- * e.g. a doorbell `press` / `press_long`) — matched case-insensitively
19326
- * against the token carried on the device-event subject (extracted from the
19327
- * event-emitter runtime slice's `lastEvent.eventType`). Fails when the
19328
- * subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
19329
- * eventType, so gate those with {@link sensorKinds} instead.
19330
- */
19331
- eventTypeTokens: array(string().min(1)).optional(),
19332
- /**
19333
- * DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
19334
- * `contact`, `button`, `device-event`) — matched against the persisted
19335
- * `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
19336
- */
19337
- sensorKinds: array(string().min(1)).optional(),
19338
- /**
19339
- * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
19340
- * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
19341
- * when the subject's phase does not match (a subject always carries a phase
19342
- * on the package-event trigger).
19343
- */
19344
- packagePhase: _enum([
19345
- "delivered",
19346
- "picked-up",
19347
- "both"
19348
- ]).optional(),
19349
- /**
19350
- * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
19351
- * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
19352
- * listed polygon (ZoneEngine membership semantics). Evaluated only when
19353
- * the subject carries a bbox; absent bbox ⇒ the condition FAILS.
19354
- */
19355
- customZones: array(MaskPolygonShapeSchema).optional(),
19356
- /**
19357
- * DEVICE-EVENT only. ZoneAnalytics occupancy edge — fires when a device's
19358
- * (optionally zone/class-scoped) occupancy count crosses the configured
19359
- * threshold and holds for `sustainSeconds`. Fail-closed on missing
19360
- * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
19361
- */
19362
- occupancy: NcOccupancyConditionSchema.optional()
19363
- });
19364
- /** One delivery target: a `notification-output` Target ref + passthrough params. */
19365
- var NcRuleTargetSchema = object({
19366
- /** `notification-output` Target id. */
19367
- targetId: string().min(1),
19368
- /**
19369
- * Per-backend passthrough. Recognized keys are mapped onto the canonical
19370
- * Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
19371
- * degrade engine drops what the backend can't render.
19372
- */
19373
- params: record(string(), unknown()).optional()
19533
+ var BrokerInfoSchema = object({
19534
+ id: string(),
19535
+ name: string(),
19536
+ url: string(),
19537
+ kind: BrokerKindSchema,
19538
+ status: BrokerStatusSchema$1,
19539
+ latencyMs: number().nullable(),
19540
+ error: string().optional(),
19541
+ /** Embedded brokers only: number of MQTT clients currently connected. */
19542
+ connectedClients: number().int().nonnegative().optional(),
19543
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
19544
+ lastCheckedAt: number().optional()
19374
19545
  });
19375
19546
  /**
19376
- * Media attachment policy (P1 still-image subset).
19377
- * - `best` — the best AVAILABLE subject image at dispatch time (D-3).
19378
- * - `best-matching` — the media that explains WHY the rule fired: a rule
19379
- * matched on identities attaches the subject's `faceCrop`, one matched on
19380
- * plates attaches the `plateCrop`; a rule with no identity/plate condition
19381
- * (or when the specific crop is missing) degrades to `best`, then
19382
- * `keyFrame`, then no attachment — never delaying the send. The matched
19383
- * condition summary is frozen on the outbox row at enqueue (like the rule
19384
- * name), so the choice never drifts from the record that fired it.
19385
- * - `keyFrame` — the clean scene frame (no subject box).
19386
- * - `none` — no attachment.
19547
+ * Connection details — what a consumer needs to call
19548
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
19549
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
19550
+ * instead of stuffing creds into the URL (which leaks them into logs).
19387
19551
  */
19388
- var NcMediaPolicySchema = object({ attach: _enum([
19389
- "best",
19390
- "best-matching",
19391
- "keyFrame",
19392
- "none"
19393
- ]).default("best") });
19394
- /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
19395
- var NcThrottleSchema = object({
19396
- cooldownSec: number().int().min(0).max(86400).default(60),
19397
- /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
19398
- scope: _enum(["rule", "rule-device"]).default("rule-device")
19399
- });
19400
- /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
19401
- var NcRuleInputSchema = object({
19402
- name: string().min(1).max(200),
19403
- enabled: boolean().default(true),
19404
- delivery: NcDeliverySchema,
19405
- conditions: NcConditionsSchema.default({}),
19406
- schedule: NcScheduleSchema.optional(),
19407
- targets: array(NcRuleTargetSchema).min(1),
19408
- media: NcMediaPolicySchema.default({ attach: "best" }),
19409
- throttle: NcThrottleSchema.default({
19410
- cooldownSec: 60,
19411
- scope: "rule-device"
19412
- }),
19413
- /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
19414
- template: object({
19415
- title: string().max(500).optional(),
19416
- body: string().max(2e3).optional()
19417
- }).optional(),
19418
- /** Canonical notification priority ordinal (1..5); per-target overridable. */
19419
- priority: number().int().min(1).max(5).default(3),
19552
+ var BrokerConnectionDetailsSchema = object({
19553
+ url: string(),
19554
+ username: string().optional(),
19555
+ password: string().optional(),
19420
19556
  /**
19421
- * Ownership/visibility key. Absent = admin/global rule (unchanged legacy
19422
- * behaviour, visible to all, read-only in the viewer). Present = personal
19423
- * rule owned by this userId. Server-stamped; never trusted from a client.
19557
+ * Suggested prefix for `clientId`. Each consumer should suffix this
19558
+ * with its own discriminator (addon id, instance id) so reconnects
19559
+ * don't kick each other off (MQTT spec: clientId must be unique per
19560
+ * broker).
19424
19561
  */
19425
- ownerUserId: string().optional()
19562
+ clientIdPrefix: string().optional()
19563
+ });
19564
+ var AddBrokerInputSchema = object({
19565
+ name: string().min(1),
19566
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
19567
+ username: string().optional(),
19568
+ password: string().optional(),
19569
+ clientIdPrefix: string().optional()
19570
+ });
19571
+ var AddBrokerResultSchema = object({ id: string() });
19572
+ var IdInputSchema = object({ id: string() });
19573
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
19574
+ ok: literal(true),
19575
+ latencyMs: number()
19576
+ }), object({
19577
+ ok: literal(false),
19578
+ error: string()
19579
+ })]);
19580
+ var StartEmbeddedInputSchema = object({
19581
+ port: number().int().min(1).max(65535).default(1883),
19582
+ /** Allow anonymous connect (no username/password). Default: false. */
19583
+ allowAnonymous: boolean().default(false),
19584
+ /** Optional shared username/password for clients. */
19585
+ username: string().optional(),
19586
+ password: string().optional()
19587
+ });
19588
+ var StartEmbeddedResultSchema = object({
19589
+ id: string(),
19590
+ url: string()
19591
+ });
19592
+ var StatusSchema = object({
19593
+ brokerCount: number(),
19594
+ embeddedRunning: boolean()
19595
+ });
19596
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
19597
+ var NetworkEndpointSchema = object({
19598
+ url: string(),
19599
+ hostname: string(),
19600
+ port: number(),
19601
+ protocol: _enum(["http", "https"])
19602
+ });
19603
+ var NetworkAccessStatusSchema = object({
19604
+ connected: boolean(),
19605
+ endpoint: NetworkEndpointSchema.nullable(),
19606
+ error: string().optional()
19426
19607
  });
19427
19608
  /**
19428
- * Partial patch for `updateRule` — any subset of the input fields, plus the
19429
- * persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
19430
- * NOT a client-authored input field (it lives on the persisted rule, not the
19431
- * input), so it is added here explicitly to let the store's per-target opt-out
19432
- * toggle round-trip through the shared `update` path. Viewer opt-out mutations
19433
- * still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
19434
- * `updateRule` patch.
19609
+ * Optional, richer endpoint shape returned by providers that expose
19610
+ * MORE than one ingress concurrently (Tailscale Ingress with mixed
19611
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
19612
+ * the originating provider config (mode + sourcePort) so the
19613
+ * orchestrator UI can label rows distinctly. Providers that expose only
19614
+ * one endpoint just omit `listEndpoints` from their provider impl.
19435
19615
  */
19436
- var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
19437
- /** A persisted rule. */
19438
- var NcRuleSchema = NcRuleInputSchema.extend({
19439
- id: string(),
19440
- /** userId of the admin who created the rule (server-stamped caller). */
19441
- createdBy: string(),
19442
- createdAt: number(),
19443
- updatedAt: number(),
19616
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
19444
19617
  /**
19445
- * Per-target opt-out set. A targetId here is suppressed for THIS rule at
19446
- * send time. Only a target's OWNER may add/remove its id (server-checked
19447
- * in `nc.setRuleTargetEnabled`). Defaults to empty.
19618
+ * Stable id within the provider — typically `<mode>-<sourcePort>` so
19619
+ * the orchestrator can dedupe across `listEndpoints` polls.
19448
19620
  */
19449
- disabledTargetIds: array(string()).default([])
19450
- });
19451
- var NcTestResultSchema = object({
19452
- recordId: string(),
19453
- recordKind: _enum([
19454
- "object-event",
19455
- "track",
19456
- "device-event",
19457
- "package-event"
19458
- ]),
19459
- deviceId: number(),
19460
- timestamp: number(),
19461
- wouldFire: boolean(),
19462
- /** Condition id that failed (first failing group), when `wouldFire` is false. */
19463
- failedCondition: string().optional(),
19464
- className: string().optional(),
19465
- label: string().optional()
19621
+ id: string(),
19622
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
19623
+ label: string(),
19624
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
19625
+ mode: string().optional(),
19626
+ /** Originating local port the ingress fronts (informational). */
19627
+ sourcePort: number().optional()
19466
19628
  });
19467
- var NcConditionDescriptorSchema = object({
19468
- /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
19629
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
19630
+ /**
19631
+ * notification-output — canonical, capability-gated notification delivery.
19632
+ *
19633
+ * Apprise-derived model (see
19634
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
19635
+ * callers emit ONE canonical `Notification`; each provider declares a
19636
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
19637
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
19638
+ * message to what the kind supports — callers never special-case a service.
19639
+ *
19640
+ * DESIGN DECISIONS (locked):
19641
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
19642
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
19643
+ * cap. Rationale: the admin UI needs one uniform surface across the
19644
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
19645
+ * alternative would fork the UI per addon and cannot host the
19646
+ * discovery→adopt flow.
19647
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
19648
+ * the generated cap-mount auto-`concatCollection`-fans them across every
19649
+ * registered provider (notifiers addon + HA addon) so one catalog is
19650
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
19651
+ * `addonId` the generated collection router extracts from the call input.
19652
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
19653
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
19654
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
19655
+ * base64 fallback needed.
19656
+ *
19657
+ * TODO (deferred, closed-set change — separate decision): add
19658
+ * `providerKind: 'notify'` so notification providers surface on the unified
19659
+ * admin "Integrations" page.
19660
+ */
19661
+ /**
19662
+ * Zentik-derived typed-media enum — the superset across every kind. Each
19663
+ * adapter picks what it supports and the degrade engine filters the rest.
19664
+ */
19665
+ var AttachmentMediaTypeSchema = _enum([
19666
+ "image",
19667
+ "video",
19668
+ "gif",
19669
+ "audio",
19670
+ "icon"
19671
+ ]);
19672
+ /**
19673
+ * A single attachment. Exactly one of `url` (remote source, most adapters
19674
+ * prefer this) or `bytes` (inline source; required for Pushover-style
19675
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
19676
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
19677
+ */
19678
+ var AttachmentSchema = object({
19679
+ mediaType: AttachmentMediaTypeSchema,
19680
+ url: string().optional(),
19681
+ bytes: _instanceof(Uint8Array).optional(),
19682
+ mime: string().optional(),
19683
+ name: string().optional()
19684
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
19685
+ var NotificationFormatSchema = _enum([
19686
+ "text",
19687
+ "markdown",
19688
+ "html"
19689
+ ]);
19690
+ /** A single tap-through action button. */
19691
+ var NotificationActionSchema = object({
19692
+ id: string(),
19693
+ label: string(),
19694
+ url: string().optional()
19695
+ });
19696
+ /**
19697
+ * The canonical notification. `body` is the only hard field (Apprise model).
19698
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
19699
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
19700
+ * the adapter maps this ordinal onto its native level. `level?` is an
19701
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
19702
+ * `priority` for that one target.
19703
+ */
19704
+ var NotificationSchema = object({
19705
+ body: string(),
19706
+ title: string().optional(),
19707
+ format: NotificationFormatSchema.default("text"),
19708
+ priority: number().int().min(1).max(5).default(3),
19709
+ level: string().optional(),
19710
+ attachments: array(AttachmentSchema).optional(),
19711
+ clickUrl: string().optional(),
19712
+ actions: array(NotificationActionSchema).optional(),
19713
+ sound: string().optional(),
19714
+ ttl: number().optional(),
19715
+ tag: string().optional(),
19716
+ deviceId: number().optional(),
19717
+ eventId: string().optional(),
19718
+ metadata: record(string(), unknown()).optional()
19719
+ });
19720
+ /** One declared native severity/priority level for a kind. */
19721
+ var TargetKindLevelSchema = object({
19469
19722
  id: string(),
19470
- group: _enum([
19471
- "scope",
19472
- "class",
19473
- "zones",
19474
- "quality",
19475
- "label",
19476
- "schedule",
19477
- "device",
19478
- "package",
19479
- "occupancy"
19480
- ]),
19481
19723
  label: string(),
19482
- /** Editor widget the UI renders — never hardcode per-condition forms. */
19483
- valueType: _enum([
19484
- "deviceIdList",
19485
- "stringList",
19486
- "number01",
19487
- "number",
19488
- "sourceSelect",
19489
- "zoneSelection",
19490
- "zoneIdList",
19491
- "schedule",
19492
- "plateMatcher",
19493
- "packagePhase",
19494
- "polygonDraw",
19495
- "occupancy"
19496
- ]),
19497
- operator: _enum([
19498
- "in",
19499
- "notIn",
19500
- "anyOf",
19501
- "allOf",
19502
- "gte",
19503
- "fuzzyIn",
19504
- "withinSchedule"
19505
- ]),
19506
- /** Which delivery kinds the condition applies to. */
19507
- appliesTo: array(NcDeliverySchema),
19508
- phase: string(),
19724
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
19725
+ ordinal: number().int().min(1).max(5).nullable(),
19726
+ flags: object({
19727
+ critical: boolean().optional(),
19728
+ silent: boolean().optional(),
19729
+ noPush: boolean().optional()
19730
+ }).optional(),
19731
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
19732
+ requires: array(string()).optional(),
19509
19733
  description: string().optional()
19510
19734
  });
19735
+ /** The full capability block consulted before dispatch. */
19736
+ var TargetKindCapsSchema = object({
19737
+ attachments: object({
19738
+ mediaTypes: array(AttachmentMediaTypeSchema),
19739
+ mode: _enum([
19740
+ "url",
19741
+ "bytes",
19742
+ "both"
19743
+ ]),
19744
+ max: number().int().nonnegative(),
19745
+ maxBytes: number().int().positive().optional()
19746
+ }),
19747
+ /** Max action buttons (0 = none). */
19748
+ actions: number().int().nonnegative(),
19749
+ levels: array(TargetKindLevelSchema),
19750
+ format: array(NotificationFormatSchema),
19751
+ clickUrl: boolean(),
19752
+ sound: boolean(),
19753
+ ttl: boolean(),
19754
+ bodyMaxLen: number().int().positive()
19755
+ });
19511
19756
  /**
19512
- * The delivery lifecycle status of a history row — a straight read of the
19513
- * durable outbox row's own status (single source of truth):
19514
- * - `pending` — enqueued, in-flight or retrying with backoff
19515
- * - `sent` — delivered (terminal)
19516
- * - `dead` — dead-lettered after exhausting retries / a permanent
19517
- * backend rejection / a deleted target (terminal; carries
19518
- * the failure `error`)
19519
- *
19520
- * P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
19521
- * user dimension (quiet hours / snooze) and are additive when they land.
19757
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
19758
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
19759
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
19760
+ * the union is large and not meant for runtime validation here; the exported
19761
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
19522
19762
  */
19523
- var NcHistoryStatusSchema = _enum([
19524
- "pending",
19525
- "sent",
19526
- "dead"
19527
- ]);
19528
- /** The evaluated record kind a history row descends from (one per trigger). */
19529
- var NcHistoryRecordKindSchema = _enum([
19530
- "object-event",
19531
- "track-end",
19532
- "device-event",
19533
- "package-event"
19534
- ]);
19535
- /** Subject summary frozen on the row at fire time (survives rule/record edits). */
19536
- var NcHistorySubjectSchema = object({
19537
- className: string(),
19538
- label: string().optional(),
19539
- confidence: number().optional(),
19540
- zones: array(string()),
19541
- timestamp: number()
19763
+ var ConfigSchemaPassthrough = unknown();
19764
+ var TargetKindSchema = object({
19765
+ kind: string(),
19766
+ label: string(),
19767
+ icon: string(),
19768
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
19769
+ addonId: string(),
19770
+ configSchema: ConfigSchemaPassthrough,
19771
+ supportsDiscovery: boolean(),
19772
+ caps: TargetKindCapsSchema
19542
19773
  });
19543
19774
  /**
19544
- * One delivery-history row. This is a read-only VIEW over the durable
19545
- * outbox row (single source of truth — the same row the drain loop drives;
19546
- * NO second write path, so history can never drift from delivery state).
19547
- * The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
19548
- * `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
19549
- * (fire) / `updatedAt` (last transition), `status` + `error` are the
19550
- * lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
19551
- * enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
19552
- * P1 (admin scope only).
19775
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
19776
+ * (return a presence marker only) when serving `listTargets` — never
19777
+ * round-trip a stored secret to the UI.
19553
19778
  */
19554
- var NcHistoryEntrySchema = object({
19555
- /** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
19779
+ var TargetSchema = object({
19556
19780
  id: string(),
19557
- ruleId: string(),
19558
- /** Rule name frozen at fire time (outlives a later rename / delete). */
19559
- ruleName: string(),
19560
- /** The rule urgency/trigger that produced this delivery. */
19561
- delivery: NcDeliverySchema,
19562
- targetId: string(),
19563
- deviceId: number(),
19564
- recordKind: NcHistoryRecordKindSchema,
19565
- /** Event / track ref of the evaluated record (§3.2 `eventRef`). */
19566
- recordId: string(),
19567
- /** Present for track-scoped deliveries (object-event / track-end). */
19568
- trackId: string().optional(),
19569
- status: NcHistoryStatusSchema,
19570
- /** Delivery attempts made so far. */
19571
- attempts: number().int(),
19572
- /** Fire time (outbox enqueue). */
19573
- createdAt: number(),
19574
- /** Last transition time (terminal for sent / dead). */
19575
- updatedAt: number(),
19576
- /** Failure detail — present on a `dead` row. */
19577
- error: string().optional(),
19578
- subject: NcHistorySubjectSchema
19781
+ name: string(),
19782
+ kind: string(),
19783
+ addonId: string(),
19784
+ enabled: boolean(),
19785
+ config: record(string(), unknown())
19579
19786
  });
19580
- /**
19581
- * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
19582
- * AND; absent = unbounded on that axis. `since`/`until` bound the fire time
19583
- * (`createdAt`, epoch ms, inclusive). `limit` is clamped to
19584
- * {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
19585
- */
19586
- var NcHistoryFilterSchema = object({
19587
- ruleId: string().optional(),
19588
- deviceId: number().optional(),
19589
- status: NcHistoryStatusSchema.optional(),
19590
- since: number().optional(),
19591
- until: number().optional(),
19592
- limit: number().int().min(1).max(500).default(100)
19787
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
19788
+ var DiscoveredTargetSchema = object({
19789
+ kind: string(),
19790
+ suggestedName: string(),
19791
+ config: record(string(), unknown())
19593
19792
  });
19594
- method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), method(object({ ruleId: string() }), object({ rule: NcRuleSchema.nullable() }), { auth: "admin" }), method(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
19595
- kind: "mutation",
19596
- auth: "admin",
19597
- caller: "required"
19598
- }), method(object({
19599
- ruleId: string(),
19600
- patch: NcRulePatchSchema
19601
- }), object({ rule: NcRuleSchema }), {
19602
- kind: "mutation",
19603
- auth: "admin",
19604
- caller: "required"
19605
- }), method(object({ ruleId: string() }), object({ success: literal(true) }), {
19606
- kind: "mutation",
19607
- auth: "admin"
19608
- }), method(object({
19609
- ruleId: string(),
19793
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
19794
+ var RenderedAsSchema = object({
19795
+ level: string(),
19796
+ format: NotificationFormatSchema,
19797
+ attachmentsSent: number().int().nonnegative(),
19798
+ actionsSent: number().int().nonnegative(),
19799
+ truncated: boolean(),
19800
+ dropped: array(string())
19801
+ });
19802
+ var SendResultSchema = object({
19803
+ success: boolean(),
19804
+ error: string().optional(),
19805
+ renderedAs: RenderedAsSchema.optional()
19806
+ });
19807
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
19808
+ var TestResultSchema = SendResultSchema;
19809
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
19810
+ kind: string(),
19811
+ config: record(string(), unknown()).optional()
19812
+ }), array(DiscoveredTargetSchema)), method(object({
19813
+ targetId: string(),
19814
+ notification: NotificationSchema
19815
+ }), SendResultSchema, { kind: "mutation" }), method(object({
19816
+ targetId: string(),
19817
+ sample: NotificationSchema.optional()
19818
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
19819
+ targetId: string(),
19610
19820
  enabled: boolean()
19611
- }), object({ success: literal(true) }), {
19612
- kind: "mutation",
19613
- auth: "admin"
19614
- }), method(object({
19615
- rule: NcRuleInputSchema,
19616
- lookbackMinutes: number().int().min(1).max(1440).default(60)
19617
- }), object({ results: array(NcTestResultSchema) }), {
19618
- kind: "mutation",
19619
- auth: "admin"
19620
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
19821
+ }), _void(), { kind: "mutation" });
19621
19822
  /**
19622
19823
  * Zod schemas for persisted record types.
19623
19824
  *
@@ -24622,6 +24823,12 @@ Object.freeze({
24622
24823
  addonId: null,
24623
24824
  access: "delete"
24624
24825
  },
24826
+ "backup.deleteSchedule": {
24827
+ capName: "backup",
24828
+ capScope: "system",
24829
+ addonId: null,
24830
+ access: "delete"
24831
+ },
24625
24832
  "backup.getEntries": {
24626
24833
  capName: "backup",
24627
24834
  capScope: "system",
@@ -24652,6 +24859,12 @@ Object.freeze({
24652
24859
  addonId: null,
24653
24860
  access: "view"
24654
24861
  },
24862
+ "backup.listSchedules": {
24863
+ capName: "backup",
24864
+ capScope: "system",
24865
+ addonId: null,
24866
+ access: "view"
24867
+ },
24655
24868
  "backup.previewSchedule": {
24656
24869
  capName: "backup",
24657
24870
  capScope: "system",
@@ -24676,6 +24889,12 @@ Object.freeze({
24676
24889
  addonId: null,
24677
24890
  access: "create"
24678
24891
  },
24892
+ "backup.upsertSchedule": {
24893
+ capName: "backup",
24894
+ capScope: "system",
24895
+ addonId: null,
24896
+ access: "create"
24897
+ },
24679
24898
  "battery.wakeForStream": {
24680
24899
  capName: "battery",
24681
24900
  capScope: "device",
@@ -28510,6 +28729,36 @@ Object.freeze({
28510
28729
  addonId: null,
28511
28730
  access: "create"
28512
28731
  },
28732
+ "terminalSession.close": {
28733
+ capName: "terminal-session",
28734
+ capScope: "system",
28735
+ addonId: null,
28736
+ access: "create"
28737
+ },
28738
+ "terminalSession.listProfiles": {
28739
+ capName: "terminal-session",
28740
+ capScope: "system",
28741
+ addonId: null,
28742
+ access: "view"
28743
+ },
28744
+ "terminalSession.listSessions": {
28745
+ capName: "terminal-session",
28746
+ capScope: "system",
28747
+ addonId: null,
28748
+ access: "view"
28749
+ },
28750
+ "terminalSession.openSession": {
28751
+ capName: "terminal-session",
28752
+ capScope: "system",
28753
+ addonId: null,
28754
+ access: "create"
28755
+ },
28756
+ "terminalSession.resize": {
28757
+ capName: "terminal-session",
28758
+ capScope: "system",
28759
+ addonId: null,
28760
+ access: "create"
28761
+ },
28513
28762
  "toast.onToast": {
28514
28763
  capName: "toast",
28515
28764
  capScope: "system",