@camstack/addon-provider-vesync 0.2.5 → 0.2.6

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