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