@camstack/addon-pipeline-orchestrator 1.2.10 → 1.2.11

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.
package/dist/index.mjs CHANGED
@@ -6880,6 +6880,36 @@ function randomGeneration() {
6880
6880
  if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
6881
6881
  return Math.random().toString(36).slice(2, 14);
6882
6882
  }
6883
+ /**
6884
+ * Per-call node pinning for `ctx.api` capability calls.
6885
+ *
6886
+ * A capability call normally resolves to its DEFAULT provider — a `singleton`
6887
+ * cap resolves to the hub, a device-scoped cap to the device's owning node. To
6888
+ * query a SPECIFIC node's provider instead (e.g. a remote agent's own
6889
+ * in-process `platform-probe` hardware, which the hub cannot probe), pin the
6890
+ * call to that node.
6891
+ *
6892
+ * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
6893
+ * method args), so capability method signatures stay `nodeId`-free — node
6894
+ * targeting is a property of the CALL, not of the method. The transport lifts
6895
+ * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
6896
+ * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
6897
+ * which classifies a pinned agent node as `agent-child-forward`
6898
+ * (`$agent-cap-fwd.forward` → the agent's in-process provider).
6899
+ *
6900
+ * Usage at a call site:
6901
+ *
6902
+ * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
6903
+ */
6904
+ /** tRPC `op.context` key carrying a per-call node pin. */
6905
+ var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
6906
+ /**
6907
+ * Build the tRPC request options that pin a single capability call to `nodeId`.
6908
+ * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
6909
+ */
6910
+ function nodePin(nodeId) {
6911
+ return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
6912
+ }
6883
6913
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
6884
6914
  DeviceType["Camera"] = "camera";
6885
6915
  DeviceType["Hub"] = "hub";
@@ -7983,16 +8013,23 @@ var StorageLocationDeclarationSchema = object({
7983
8013
  * Which node root the seeded `<id>:default` instance is placed under on a
7984
8014
  * FRESH install:
7985
8015
  * - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
7986
- * the appData volume. Right for small/durable data (backups, logs, models).
8016
+ * the appData volume. Right for small/durable data (logs, models).
7987
8017
  * - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
7988
8018
  * env is set, else falls back to the data root. Right for bulky, hot media
7989
8019
  * (recordings, event media) that should stay off the appData disk.
8020
+ * - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
8021
+ * `/backups` in the image) so archives live on their own mount rather than
8022
+ * filling the appData disk. Falls back to the data root when unset.
7990
8023
  *
7991
8024
  * Only affects the seeded default's `basePath`; operators can repoint any
7992
8025
  * location afterwards, and a `defaultsTo` slot inherits its parent's root
7993
8026
  * regardless of this field. Absent (the common case) is treated as `'data'`.
7994
8027
  */
7995
- defaultRoot: _enum(["data", "media"]).optional()
8028
+ defaultRoot: _enum([
8029
+ "data",
8030
+ "media",
8031
+ "backup"
8032
+ ]).optional()
7996
8033
  });
7997
8034
  var DecoderStatsSchema = object({
7998
8035
  inputFps: number(),
@@ -8117,36 +8154,6 @@ var EncodeProfileSchema = object({
8117
8154
  outputArgs: array(string()).optional()
8118
8155
  });
8119
8156
  /**
8120
- * Per-call node pinning for `ctx.api` capability calls.
8121
- *
8122
- * A capability call normally resolves to its DEFAULT provider — a `singleton`
8123
- * cap resolves to the hub, a device-scoped cap to the device's owning node. To
8124
- * query a SPECIFIC node's provider instead (e.g. a remote agent's own
8125
- * in-process `platform-probe` hardware, which the hub cannot probe), pin the
8126
- * call to that node.
8127
- *
8128
- * The nodeId rides OUT-OF-BAND in the tRPC call context (NOT in the validated
8129
- * method args), so capability method signatures stay `nodeId`-free — node
8130
- * targeting is a property of the CALL, not of the method. The transport lifts
8131
- * it from `op.context` onto the `CapCallInput.nodeId` field (`ipcParentLink`),
8132
- * and the hub parent's `onUnownedCall` passes it to the `CapRouteResolver`,
8133
- * which classifies a pinned agent node as `agent-child-forward`
8134
- * (`$agent-cap-fwd.forward` → the agent's in-process provider).
8135
- *
8136
- * Usage at a call site:
8137
- *
8138
- * await api.platformProbe.getCapabilities.query(undefined, nodePin(nodeId))
8139
- */
8140
- /** tRPC `op.context` key carrying a per-call node pin. */
8141
- var CAP_NODE_PIN_CONTEXT_KEY = "__camstackNodePin";
8142
- /**
8143
- * Build the tRPC request options that pin a single capability call to `nodeId`.
8144
- * Pass as the second argument to `.query(input, …)` / `.mutate(input, …)`.
8145
- */
8146
- function nodePin(nodeId) {
8147
- return { context: { [CAP_NODE_PIN_CONTEXT_KEY]: nodeId } };
8148
- }
8149
- /**
8150
8157
  * DEVICE-POOL backend → model format — the SSOT for the multi-device inference
8151
8158
  * DESCRIPTOR path (Python per-device pools), distinct from {@link BACKEND_TO_FORMAT}
8152
8159
  * above which is the legacy NODE runtime map (where `coreml → onnx` because
@@ -9429,529 +9436,1167 @@ function resolveDeviceProfile(features) {
9429
9436
  return null;
9430
9437
  }
9431
9438
  /**
9432
- * Generic device-level status snapshot. Auto-registered by `BaseDevice`
9433
- * for every device, regardless of provider the kernel needs a uniform
9434
- * cap-keyed slice for the basic device flags every consumer expects to
9435
- * read across processes (the `online` flag in particular). Driver-specific
9436
- * caps (`battery`, `doorbell`, …) carry their domain-specific state on
9437
- * their own slices.
9439
+ * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9440
+ * motion-zones, and the detection zones/lines editor all speak this one
9441
+ * language so a single drawing-plane editor and the providers stay
9442
+ * decoupled from each cap's storage.
9438
9443
  *
9439
- * Pattern is identical to `battery`: schema-bearing `runtimeState`,
9440
- * empty `methods`, single change event. Reads land at
9441
- * `runtimeState.getCapState('device-status')`; writes at
9442
- * `runtimeState.setCapState('device-status', …)`. Cross-process
9443
- * consumers reach the same data via the `device-state` cap router
9444
- * (`getCapSlice({deviceId, capName: 'device-status'})`).
9444
+ * All coordinates are normalized 0..1 of the camera frame (top-left
9445
+ * origin). Each cap composes the SUBSET of shape kinds it supports and
9446
+ * advertises it via `supportedShapes` in its `getOptions`.
9445
9447
  */
9446
- var DeviceStatusSchema = object({
9447
- /**
9448
- * Device-level liveness. Drivers flip via `markOnline(boolean)` on
9449
- * `BaseDevice`. Provider semantics vary — RTSP aggregates broker
9450
- * stream-health, Reolink reads firmware push events, ONVIF tracks
9451
- * ping responses. This cap intentionally does NOT prescribe which
9452
- * signal drives the flag.
9453
- */
9454
- online: boolean(),
9455
- /** Ms epoch of the last `online` transition. Lets consumers tell
9456
- * apart "just came online" from "still online". */
9457
- lastChangedAt: number()
9448
+ /** A normalized 0..1 point (top-left origin). */
9449
+ var MaskPointSchema = object({
9450
+ x: number(),
9451
+ y: number()
9458
9452
  });
9459
- object({
9460
- deviceId: number(),
9461
- status: DeviceStatusSchema
9453
+ /** Axis-aligned rectangle (normalized 0..1). */
9454
+ var MaskRectShapeSchema = object({
9455
+ kind: literal("rect"),
9456
+ x: number(),
9457
+ y: number(),
9458
+ width: number(),
9459
+ height: number()
9462
9460
  });
9463
- /**
9464
- * Per-device feature/identity probe slice. Holds the runtime-resolved
9465
- * truth about what a device CAN do — which the kernel uses to:
9466
- * 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
9467
- * based on what the firmware actually advertises).
9468
- * 2. Compute the public `features: DeviceFeature[]` array surfaced via
9469
- * `device-manager.listAll`.
9470
- * 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
9471
- * to register on the device's capability surface.
9472
- *
9473
- * Auto-registered by `BaseDevice` for every device. Drivers populate the
9474
- * slice from `onProbe()` (kernel calls it once after register, before
9475
- * accessory reconciliation). Consumers read via:
9476
- * `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
9477
- *
9478
- * `flags` is an open record so each driver carries its own keys without
9479
- * a centralized schema bottleneck — Reolink writes `hasPtz/hasIntercom`,
9480
- * Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
9481
- *
9482
- * Replaces the older driver-local `deviceCache.has*` blob: the per-device
9483
- * config is for operator-edited overrides + UI snapshots; runtime probe
9484
- * results belong in runtime-state where the kernel handles persistence,
9485
- * cross-process mirroring, and reactive updates.
9486
- */
9487
- var FeatureProbeStatusSchema = object({
9488
- /**
9489
- * Driver-specific flag bag. Each driver picks its own key names — the
9490
- * cap deliberately does NOT enforce a closed enum here. Reolink keys:
9491
- * `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
9492
- * `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
9493
- * `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
9494
- */
9495
- flags: record(string(), unknown()),
9496
- /**
9497
- * Coarse driver-classification — lets cross-process consumers tell apart
9498
- * cameras / battery-cams / NVRs without re-running the probe. `null`
9499
- * before the first probe completes.
9500
- */
9501
- deviceType: string().nullable(),
9502
- /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
9503
- model: string().nullable(),
9504
- /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
9505
- channelCount: number().nullable(),
9506
- /**
9507
- * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
9508
- * completes — drivers' `getAccessoryChildren()` should treat zero as
9509
- * "probe not done yet, return empty" so accessories aren't spawned
9510
- * before the firmware is queried.
9511
- */
9512
- lastProbedAt: number(),
9513
- /**
9514
- * Framework convention: every runtime-state slice carries this for the
9515
- * createRuntimeStateBridge stale-check helper. We keep it in sync with
9516
- * `lastProbedAt` on every write.
9517
- */
9518
- lastFetchedAt: number()
9461
+ /** Free polygon — an ordered list of normalized vertices (≥3). */
9462
+ var MaskPolygonShapeSchema = object({
9463
+ kind: literal("polygon"),
9464
+ points: array(MaskPointSchema)
9519
9465
  });
9520
- object({
9521
- deviceId: number(),
9522
- status: FeatureProbeStatusSchema
9466
+ /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
9467
+ var MaskGridShapeSchema = object({
9468
+ kind: literal("grid"),
9469
+ gridWidth: number(),
9470
+ gridHeight: number(),
9471
+ cells: array(boolean())
9523
9472
  });
9524
- object({
9525
- /** Carbon dioxide concentration in ppm. */
9526
- co2Ppm: number().min(0).optional(),
9527
- /** Total volatile organic compounds in ppb. */
9528
- vocPpb: number().min(0).optional(),
9529
- /** Particulate matter ≤ 2.5 μm in µg/m³. */
9530
- pm25: number().min(0).optional(),
9531
- /** Particulate matter ≤ 10 μm in µg/m³. */
9532
- pm10: number().min(0).optional(),
9533
- /** Composite AQI value (typically 0..500). */
9534
- aqi: number().optional(),
9535
- /** Ms epoch when the slice was last updated. */
9536
- lastFetchedAt: number(),
9537
- /** Live display unit of the single metric this slice carries (e.g. HA
9538
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
9539
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
9540
- * per slice is unambiguous. */
9541
- unit: string().optional(),
9542
- /** Suggested decimal places for numeric display.
9543
- * Populated live from the upstream source when provided (e.g. HA
9544
- * `attributes.suggested_display_precision`). Falls back to
9545
- * auto-formatting when absent. */
9546
- precision: number().int().min(0).max(10).optional()
9473
+ discriminatedUnion("kind", [
9474
+ MaskRectShapeSchema,
9475
+ MaskPolygonShapeSchema,
9476
+ MaskGridShapeSchema,
9477
+ object({
9478
+ kind: literal("line"),
9479
+ points: array(MaskPointSchema)
9480
+ })
9481
+ ]);
9482
+ /** Every shape-kind discriminant, for `supportedShapes` advertisement. */
9483
+ var MaskShapeKindSchema = _enum([
9484
+ "rect",
9485
+ "polygon",
9486
+ "grid",
9487
+ "line"
9488
+ ]);
9489
+ /** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
9490
+ var MaskPolygonVerticesSchema = object({
9491
+ min: number(),
9492
+ max: number()
9493
+ });
9494
+ /** Grid dimensions when a cap supports 'grid'. */
9495
+ var MaskGridDimsSchema = object({
9496
+ width: number(),
9497
+ height: number()
9547
9498
  });
9548
- DeviceType.Sensor;
9549
9499
  /**
9550
- * Alarm-panel cap. Models HA `alarm_control_panel.*` on
9551
- * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
9552
- * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
9553
- * arming / pending / triggered / disarming.
9500
+ * notification-rules the Notification Center rule surface (P1 core).
9554
9501
  *
9555
- * Many panels require a PIN code on arm / disarm — the optional
9556
- * `code` field on the methods passes it through to the upstream
9557
- * service; it's NEVER persisted in the runtime slice or any event
9558
- * payload. The presence of a required code is signalled by
9559
- * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
9560
- * field without a slice fetch.
9502
+ * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
9503
+ * (operator decisions D-1/D-2/D-3 are binding):
9561
9504
  *
9562
- * `availableModes` mirrors HA's `supported_features`-derived arm
9563
- * mode list the UI renders only the buttons the panel accepts.
9564
- */
9565
- var AlarmStateSchema = _enum([
9566
- "disarmed",
9567
- "armed_home",
9568
- "armed_away",
9569
- "armed_night",
9570
- "armed_vacation",
9571
- "armed_custom_bypass",
9572
- "arming",
9573
- "disarming",
9574
- "pending",
9575
- "triggered"
9576
- ]);
9577
- var AlarmArmModeSchema = _enum([
9578
- "home",
9579
- "away",
9580
- "night",
9581
- "vacation",
9582
- "custom_bypass"
9583
- ]);
9584
- object({
9585
- /** Current lifecycle state. */
9586
- state: AlarmStateSchema,
9587
- /** Subset of arm modes the panel accepts. UI renders one button per
9588
- * mode in this list. */
9589
- availableModes: array(AlarmArmModeSchema),
9590
- /** Whether the panel requires a PIN on arm / disarm. Mirrors
9591
- * `DeviceFeature.AlarmPinRequired` for slice consumers. */
9592
- requiresCode: boolean(),
9593
- /** Ms epoch when the slice was last updated. */
9594
- lastChangedAt: number()
9595
- });
9596
- DeviceType.AlarmPanel, method(object({
9597
- deviceId: number().int().nonnegative(),
9598
- mode: AlarmArmModeSchema,
9599
- /** Optional PIN code. Required when `requiresCode === true`.
9600
- * Passed through to the upstream service; never persisted. */
9601
- code: string().min(1).optional()
9602
- }), _void(), {
9603
- kind: "mutation",
9604
- auth: "admin"
9605
- }), method(object({
9606
- deviceId: number().int().nonnegative(),
9607
- code: string().min(1).optional()
9608
- }), _void(), {
9609
- kind: "mutation",
9610
- auth: "admin"
9611
- }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
9612
- kind: "mutation",
9613
- auth: "admin"
9614
- });
9615
- object({
9616
- /** Current illuminance in lux (lx). */
9617
- lux: number().min(0),
9618
- /** Ms epoch when the slice was last updated. */
9619
- lastFetchedAt: number(),
9620
- /** Live display unit from the upstream source (e.g. HA
9621
- * `attributes.unit_of_measurement`). The UI prefers this over the
9622
- * role's canonical unit. Absent → fall back to the canonical unit. */
9623
- unit: string().optional(),
9624
- /** Suggested decimal places for numeric display.
9625
- * Populated live from the upstream source when provided (e.g. HA
9626
- * `attributes.suggested_display_precision`). Falls back to
9627
- * auto-formatting when absent. */
9628
- precision: number().int().min(0).max(10).optional()
9629
- });
9630
- DeviceType.Sensor;
9631
- /**
9632
- * Per-class audio metrics aggregated over a sliding window.
9505
+ * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
9506
+ * `notification-center` module), hooked on the durable persistence
9507
+ * moments (object-event insert, TrackCloser.closeExpired) with a
9508
+ * persisted outbox + retry — never the lossy telemetry bus (D8).
9509
+ * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
9510
+ * FIRST persisted detection matching the conditions (per-track dedup,
9511
+ * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
9512
+ * `delivery: 'track-end'` evaluates the finalized track record at close.
9513
+ * - DISPATCH stays behind `notification-output` (rules reference targets
9514
+ * by id; per-backend params are a passthrough blob capped by the
9515
+ * target kind's own caps/degrade engine).
9516
+ *
9517
+ * P1 scope: admin-authored rules only (`createdBy` stamped from the
9518
+ * server-injected caller identity — the first `caller: 'required'`
9519
+ * adopter). The P1 condition subset is: devices, classes(+exclude),
9520
+ * minConfidence, admin zones (any/all + exclude), weekly schedule
9521
+ * windows, and the optional label/identity/plate matchers. User rules,
9522
+ * private zones, per-recipient fan-out and the wider condition table are
9523
+ * P2+ (see spec §7).
9524
+ *
9525
+ * All schemas here are the single source of truth — `NcRule` etc. are
9526
+ * `z.infer` exports; no duplicate interfaces (the advanced-notifier
9527
+ * schema/interface drift is explicitly not repeated).
9633
9528
  */
9634
- var AudioClassSummarySchema = object({
9635
- className: string(),
9636
- /** Number of windows (chunks) where this class was the top hit. */
9637
- hits: number().int().nonnegative(),
9638
- /** Mean score across those hits, clamped to [0,1]. */
9639
- avgScore: number().min(0).max(1),
9640
- /** Peak score in the window. */
9641
- peakScore: number().min(0).max(1)
9642
- });
9643
9529
  /**
9644
- * Per-camera audio metrics snapshotemitted by the analytics frame
9645
- * handler on every `pipeline.audio-inference-result` event and
9646
- * mirrored into the `audio-metrics` device-state slice. Symmetric
9647
- * with `zone-analytics` snapshots for video — every consumer
9648
- * (admin UI panel, automations, alert rules) reads via the
9649
- * canonical `device.state.audioMetrics.value` reactive handle.
9530
+ * D-3: the trigger/urgency of a rule which persistence moment evaluates it.
9531
+ * The value maps 1:1 onto the evaluated record kind:
9532
+ * - `immediate` ↔ object-event persist (lowest-latency detection burst)
9533
+ * - `track-end` TrackCloser.closeExpired (finalized track record)
9534
+ * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
9535
+ * change of a LINKED device, one row per linked camera)
9536
+ * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
9537
+ * delivery / pick-up)
9650
9538
  *
9651
- * Aggregates are computed over a rolling `windowSec` window
9652
- * (default 60s). Past that window, classes drop out of `byClass`
9653
- * and the level history shifts forward.
9539
+ * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
9540
+ * `package-event` are pure trigger kinds (no urgency dimension). Extending
9541
+ * this one field keeps the schema additive a rule still declares exactly
9542
+ * one trigger.
9654
9543
  */
9655
- var AudioMetricsSnapshotSchema = object({
9656
- /** Wall-clock timestamp (ms) of the most recent audio window. */
9657
- ts: number().int(),
9658
- /** Sliding-window length (seconds) used for aggregation. */
9659
- windowSec: number().int().positive(),
9660
- /** Latest level reading from the most recent window. */
9661
- level: object({
9662
- rms: number(),
9663
- dbfs: number()
9664
- }),
9665
- /** Peak dBFS observed across the rolling window. */
9666
- peakDbfs: number(),
9667
- /** Mean dBFS across the rolling window. */
9668
- avgDbfs: number(),
9669
- /** Most recent above-threshold classification, or null on silence. */
9670
- current: object({
9671
- className: string(),
9672
- score: number().min(0).max(1),
9673
- timestamp: number().int()
9674
- }).nullable(),
9675
- /** Per-class summary across the rolling window — keys are
9676
- * `macroClass` strings (e.g. `dog_bark`, `speech`, `glass_break`). */
9677
- byClass: array(AudioClassSummarySchema).readonly()
9544
+ var NcDeliverySchema = _enum([
9545
+ "immediate",
9546
+ "track-end",
9547
+ "device-event",
9548
+ "package-event"
9549
+ ]);
9550
+ /** Weekly schedule — OR of windows; absence on the rule = always active. */
9551
+ var NcScheduleSchema = object({
9552
+ windows: array(object({
9553
+ /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
9554
+ days: array(number().int().min(0).max(6)).min(1),
9555
+ startMinute: number().int().min(0).max(1439),
9556
+ endMinute: number().int().min(0).max(1439)
9557
+ })).min(1),
9558
+ /** IANA timezone; default = hub host timezone. */
9559
+ timezone: string().optional(),
9560
+ /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
9561
+ invert: boolean().optional()
9562
+ });
9563
+ /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
9564
+ var NcPlateMatcherSchema = object({
9565
+ values: array(string().min(1)).min(1),
9566
+ /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
9567
+ maxDistance: number().int().min(0).max(3).default(1)
9678
9568
  });
9679
9569
  /**
9680
- * Audio-metrics history payload a series of `AudioMetricsHistoryPoint`
9681
- * samples capped at `maxPoints` (default 1024). When the requested
9682
- * `windowSec / sampleEveryMs` would exceed the cap, the provider
9683
- * subsamples by bucketed averaging and reports the effective sample
9684
- * spacing on `effectiveSampleEveryMs` so the UI can label the x-axis.
9570
+ * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
9571
+ * occupancy edge for a device optionally narrowed to a single admin
9572
+ * `zoneId` and/or object `className`. `op` selects the edge/threshold:
9573
+ * - `became-occupied` (default) count crossed 0 `count`
9574
+ * - `became-free` count crossed `count` below it
9575
+ * - `>=` / `<=` — count is at/over or at/under `count`
9576
+ * `sustainSeconds` requires the condition hold continuously that long
9577
+ * before firing (debounces flicker; 0 = fire on the first matching edge).
9578
+ * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
9579
+ * the condition never matches. Confirmed edge-state survives addon restarts
9580
+ * (declared SQLite collection, reseeded on boot).
9685
9581
  */
9686
- var AudioMetricsHistorySchema = object({
9687
- points: array(object({
9688
- /** Wall-clock ms when this sample was recorded. */
9689
- ts: number().int(),
9690
- /** Instantaneous dBFS level at sample time. `null` for windows where
9691
- * the source had no level reading (rare; happens at decode startup). */
9692
- dbfs: number().nullable(),
9693
- /** Rolling-window peak dBFS at sample time. Same window the live
9694
- * snapshot reports. */
9695
- peakDbfs: number(),
9696
- /** Rolling-window mean dBFS at sample time. */
9697
- avgDbfs: number(),
9698
- /** Dominant above-threshold class at sample time, or null on silence. */
9699
- topClass: string().nullable(),
9700
- /** Score of the dominant class (`null` whenever `topClass` is null). */
9701
- topScore: number().min(0).max(1).nullable()
9702
- })).readonly(),
9703
- /** Actual ms between adjacent samples after any subsampling. */
9704
- effectiveSampleEveryMs: number().int().positive(),
9705
- /** Wall-clock window covered by `points` (`points[N-1].ts - points[0].ts`),
9706
- * or `0` when there's fewer than 2 samples. */
9707
- windowMsActual: number().int().nonnegative()
9708
- });
9709
- DeviceType.Camera, method(object({ deviceId: number() }), AudioMetricsSnapshotSchema.nullable()), method(object({
9710
- deviceId: number(),
9711
- /** History window in seconds. Default 300 (5 minutes).
9712
- * Provider clamps to its retention cap if larger. */
9713
- windowSec: number().int().positive().optional(),
9714
- /** Target sample interval in ms. Default 1000 (1 sample/second).
9715
- * Provider clamps to natural sample rate if smaller, and
9716
- * bucket-averages when bigger than the requested window
9717
- * would produce more than `maxPoints` samples. */
9718
- sampleEveryMs: number().int().positive().optional()
9719
- }), AudioMetricsHistorySchema);
9720
- object({
9721
- /** Whether the automation is currently enabled. Disabled automations
9722
- * ignore their trigger block — manual `trigger` still works. */
9723
- enabled: boolean(),
9724
- /** Whether the automation is currently executing its action block. */
9725
- isRunning: boolean(),
9726
- /** Ms epoch of the last successful run. 0 when never run. */
9727
- lastTriggeredAt: number(),
9728
- /** Failure description from the last completed run. Null on success
9729
- * or when never run. */
9730
- lastError: string().nullable(),
9731
- /** Ms epoch when the slice was last updated. */
9732
- lastChangedAt: number()
9582
+ var NcOccupancyConditionSchema = object({
9583
+ /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
9584
+ zoneId: string().optional(),
9585
+ /** Object class to count; absent = any class. */
9586
+ className: string().optional(),
9587
+ op: _enum([
9588
+ "became-occupied",
9589
+ "became-free",
9590
+ ">=",
9591
+ "<="
9592
+ ]).default("became-occupied"),
9593
+ count: number().int().min(0).default(1),
9594
+ sustainSeconds: number().int().min(0).max(3600).default(15)
9733
9595
  });
9734
- DeviceType.Automation, method(object({ deviceId: number().int().nonnegative() }), _void(), {
9735
- kind: "mutation",
9736
- auth: "admin"
9737
- }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
9738
- kind: "mutation",
9739
- auth: "admin"
9740
- }), method(object({
9741
- deviceId: number().int().nonnegative(),
9742
- /** When true, fires the action block while bypassing the
9743
- * automation's condition evaluation. Gated by
9744
- * `DeviceFeature.AutomationSkipCondition`. */
9745
- skipCondition: boolean().optional()
9746
- }), _void(), {
9747
- kind: "mutation",
9748
- auth: "admin"
9596
+ /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9597
+ var NcZoneConditionSchema = object({
9598
+ ids: array(string().min(1)).min(1),
9599
+ /** Quantifier over `ids` at least one / every one visited. */
9600
+ match: _enum(["any", "all"]).default("any")
9749
9601
  });
9750
9602
  /**
9751
- * Battery status snapshot. Emitted by providers whose device is
9752
- * battery-operated (cameras with `DeviceFeature.BatteryOperated`,
9753
- * future sensor/button accessories). Consumers build their own "low
9754
- * battery" alerting on top — the cap deliberately does NOT enforce a
9755
- * threshold.
9603
+ * The P1 condition set a flat AND of groups; absent group = pass;
9604
+ * membership lists are OR within the list (spec §2.3).
9756
9605
  */
9757
- var BatteryStatusSchema = object({
9758
- /** 0..100 inclusive. Firmware-reported. */
9759
- percentage: number().min(0).max(100),
9606
+ var NcConditionsSchema = object({
9607
+ /** Device scope — absent = all devices. */
9608
+ devices: array(number()).optional(),
9609
+ /** Detector class names (any overlap with the record's class set). */
9610
+ classes: array(string().min(1)).optional(),
9611
+ /** Veto classes — any overlap fails the rule. */
9612
+ classesExclude: array(string().min(1)).optional(),
9613
+ /** Minimum detection confidence 0–1 (fails when the record has none). */
9614
+ minConfidence: number().min(0).max(1).optional(),
9615
+ /** Admin zone membership over event `zones` / track `zonesVisited`. */
9616
+ zones: NcZoneConditionSchema.optional(),
9617
+ /** Veto zones — any hit fails the rule. */
9618
+ zonesExclude: array(string().min(1)).optional(),
9760
9619
  /**
9761
- * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
9762
- * Reolink-specific for the Solar Panel 2 accessory (will become
9763
- * common on other battery cams). `'none'` means running on battery
9764
- * alone.
9620
+ * Exact (case-insensitive) match on the record's collapsed `label`
9621
+ * (identity name / plate text / subclass).
9765
9622
  */
9766
- charging: _enum([
9767
- "dc",
9768
- "solar",
9769
- "none"
9770
- ]),
9623
+ labelEquals: array(string().min(1)).optional(),
9771
9624
  /**
9772
- * True when the camera firmware has gone into low-power mode. Battery
9773
- * providers MUST avoid polling during sleep reading the battery
9774
- * wakes the camera up and drains charge.
9625
+ * Identity matcher. P1 boundary: matched against the record's collapsed
9626
+ * `label` (the identity display name propagated by the face pipeline) —
9627
+ * identity-ID matching rides in P2 when identity ids reach the record.
9775
9628
  */
9776
- sleeping: boolean(),
9777
- /** Ms epoch of the last observation. Lets consumers reason about freshness. */
9778
- lastUpdated: number(),
9629
+ identities: array(string().min(1)).optional(),
9630
+ /** Fuzzy plate matcher against the record's `label` (plate text). */
9631
+ plates: NcPlateMatcherSchema.optional(),
9779
9632
  /**
9780
- * True when the source is a BINARY low-battery indicator (HA
9781
- * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
9782
- * charge level `percentage` is then a coarse stand-in (100 = normal,
9783
- * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
9784
- * misleading exact percentage. Absent/false → genuine 0–100 % reading.
9633
+ * Identity EXCLUDE mirror of {@link identities} with `notIn` semantics.
9634
+ * Same P1 boundary: matched against the record's collapsed `label` (the
9635
+ * identity display name). A record with NO label passes (nothing to
9636
+ * exclude), unlike the include variant which fails on an absent label.
9785
9637
  */
9786
- binary: boolean().optional()
9787
- });
9788
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, method(object({
9789
- deviceId: number(),
9790
- /** Bound on the wait. Sensible range 3000–10000ms. */
9791
- timeoutMs: number().int().min(500).max(3e4).default(8e3)
9792
- }), object({
9793
- awoke: boolean(),
9794
- durationMs: number()
9795
- }), { kind: "mutation" }), object({
9796
- deviceId: number(),
9797
- status: BatteryStatusSchema
9798
- });
9799
- object({
9800
- on: boolean(),
9801
- /** Ms epoch of the last transition. 0 if never observed. */
9802
- lastChangedAt: number()
9803
- });
9804
- DeviceType.Sensor;
9805
- object({
9806
- /** Current level as 0..100 inclusive. Firmware-reported. */
9807
- percentage: number().min(0).max(100),
9808
- /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
9809
- lastChangedAt: number()
9810
- });
9811
- DeviceType.Light, method(object({
9812
- deviceId: number().int().nonnegative(),
9813
- percentage: number().min(0).max(100)
9814
- }), _void(), {
9815
- kind: "mutation",
9816
- auth: "admin"
9817
- }), object({
9818
- deviceId: number(),
9819
- percentage: number().min(0).max(100),
9820
- lastChangedAt: number()
9821
- });
9822
- /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9823
- var StreamFormatSchema = _enum([
9824
- "webrtc",
9825
- "hls",
9826
- "mjpeg",
9827
- "rtsp"
9828
- ]);
9829
- var RtspRestreamEntrySchema = object({
9830
- brokerId: string(),
9831
- url: string(),
9832
- mutedUrl: string(),
9833
- enabled: boolean(),
9638
+ identitiesExclude: array(string().min(1)).optional(),
9834
9639
  /**
9835
- * Source-stream codec / resolution for the camStream this entry serves
9836
- * (the broker's "high"/"mid"/"low" profile slot for this device).
9837
- * Used by exporter pickers (`pickPreferredRtspEntry`) to resolve
9838
- * `streamPreference: 'auto'` to the slot whose source is closest to
9839
- * the consumer's target Alexa wants ~720p, HomeKit wants ~1080p,
9840
- * and there's no point dialling the 4K slot for an Echo Show. Absent
9841
- * when the source publisher never advertised the field; pickers fall
9842
- * back to first-enabled in that case.
9640
+ * Minimum server-computed key-event importance in [0,1] (`Track.importance`).
9641
+ * TRACK-END only: importance is scored at track close, so it does not exist
9642
+ * at immediate / object-event evaluation time (see catalog `appliesTo`). At
9643
+ * close the value is threaded via the close-time info (the `Track` clone is
9644
+ * captured before the DB row is updated, so it would otherwise read stale).
9645
+ * Fails when the record carries no importance (never guess quality the
9646
+ * `minConfidence` precedent). MVP cut: a single scalar threshold.
9843
9647
  */
9844
- codec: string().optional(),
9845
- resolution: object({
9846
- width: number().int().positive(),
9847
- height: number().int().positive()
9848
- }).optional()
9849
- });
9850
- var BrokerRtspClientSchema = object({
9851
- sessionId: string(),
9852
- remoteAddr: string(),
9853
- /** Client `User-Agent` (e.g. `recorder` for the recorder's RTSP pull), or
9854
- * null/absent when the client sent none. Lets the UI label a consumer by
9855
- * purpose. Optional so a client built against an older schema stays valid. */
9856
- userAgent: string().nullish(),
9857
- playing: boolean(),
9858
- muted: boolean(),
9859
- connectedAt: number(),
9860
- lastRtpAt: number(),
9861
- bytesSent: number()
9862
- });
9863
- var BrokerDecodedClientSchema = object({
9864
- tag: string(),
9865
- subscribedAt: number(),
9866
- maxFps: number(),
9867
- framesDelivered: number(),
9868
- framesDropped: number()
9869
- });
9870
- var BrokerAudioClientSchema = object({
9871
- tag: string(),
9872
- subscribedAt: number(),
9873
- chunksDelivered: number()
9874
- });
9875
- var BrokerConsumerAttributionSchema = object({
9876
- kind: _enum([
9877
- "alexa",
9878
- "homekit",
9879
- "webrtc-browser",
9880
- "webrtc-mobile",
9881
- "webrtc-whep",
9882
- "rtsp-listen",
9883
- "derived-broker",
9884
- "recording",
9885
- "pipeline",
9886
- "snapshot",
9887
- "warmup",
9888
- "unknown"
9889
- ]),
9648
+ minImportance: number().min(0).max(1).optional(),
9890
9649
  /**
9891
- * Free-form label intended to disambiguate consumers OF THE SAME kind
9892
- * on the same broker e.g. user name, device alias. Should NOT repeat
9893
- * the kind / cam / cam-stream / sessionId (those are surfaced by
9894
- * dedicated fields). When empty the widget falls back to `${kind} ·
9895
- * <sessionId tail>`.
9650
+ * Minimum track dwell in SECONDS `(lastSeen firstSeen) / 1000`.
9651
+ * TRACK-END only: an `immediate` / object-event subject has no closed
9652
+ * lifespan, so a dwell condition never matches immediate delivery
9653
+ * (documented choice the object-event record carries no `firstSeen`,
9654
+ * so dwell cannot be computed from what the subject actually carries).
9896
9655
  */
9897
- label: string().optional(),
9656
+ minDwellSeconds: number().min(0).optional(),
9898
9657
  /**
9899
- * Which part of the encoded plane this subscription consumes:
9900
- * - `'audio'` audio packets only
9901
- * - `'video'` video packets only
9902
- * - `'both'` — both video + audio (typical for AnnexB push paths
9903
- * that don't split the source RTP)
9904
- *
9905
- * Missing field means "unknown" — older callers and the legacy
9906
- * paths that haven't been migrated yet.
9658
+ * Detection provenance filter. `any` (default / absent) matches every
9659
+ * source; otherwise the subject's source must equal it. Legacy records
9660
+ * with no stamped source are treated as `pipeline`. The union spans both
9661
+ * record kinds object events carry `pipeline` | `onboard`, synthetic
9662
+ * tracks carry `sensor`.
9907
9663
  */
9908
- media: _enum([
9909
- "audio",
9910
- "video",
9911
- "both"
9664
+ source: _enum([
9665
+ "pipeline",
9666
+ "onboard",
9667
+ "sensor",
9668
+ "any"
9912
9669
  ]).optional(),
9913
9670
  /**
9914
- * Codec the consumer receives AFTER any per-session re-encoding. For
9915
- * a WebRTC session this is the negotiated egress codec (e.g. `H264`,
9916
- * `H265`, `Opus`, `Pcmu`); for an RTSP restreamer it mirrors the
9917
- * source codec. Surfaced in the widget chip so operators see at a
9918
- * glance whether a viewer is on H.264 (Echo) or H.265 (Safari).
9671
+ * Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
9672
+ * detector `minConfidence` (that gates the object-detection score; this
9673
+ * gates the recognition/OCR match score). Fails when the subject carries
9674
+ * no label-match confidence (never guess). TRACK-END only: the confidence
9675
+ * lives on the recognition result and reaches the subject at track close.
9676
+ *
9677
+ * What it measures precisely (plumbed at track close — the closer threads
9678
+ * the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
9679
+ * `importance`): the BEST recognition match confidence observed for the
9680
+ * label the track carries at close — for a face, the peak cosine similarity
9681
+ * of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
9682
+ * for a plate, the peak OCR read score of the best-held plate
9683
+ * (`plateText.confidence`). When BOTH a face and a plate were recognized on
9684
+ * one track the higher of the two is used. A track that ended with no
9685
+ * confident identity/plate match carries no value, so the condition fails
9686
+ * closed for it (an un-recognized subject).
9919
9687
  */
9920
- targetCodec: string().optional(),
9688
+ minLabelConfidence: number().min(0).max(1).optional(),
9921
9689
  /**
9922
- * Whether the broker is re-encoding for this consumer:
9923
- * - `'passthrough'` bytes flow source→consumer without ffmpeg
9924
- * - `'repacketize'` RTP payload is re-packetized but NOT re-encoded
9925
- * (preserves frames, just rebuilds headers; e.g.
9926
- * the H.265 repacketizer path)
9927
- * - `'transcode'` — a full encode/decode round (ffmpeg, libx264,
9928
- * libav…); the most expensive path
9690
+ * DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
9691
+ * e.g. a doorbell `press` / `press_long`) matched case-insensitively
9692
+ * against the token carried on the device-event subject (extracted from the
9693
+ * event-emitter runtime slice's `lastEvent.eventType`). Fails when the
9694
+ * subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
9695
+ * eventType, so gate those with {@link sensorKinds} instead.
9929
9696
  */
9930
- transport: _enum([
9931
- "passthrough",
9932
- "repacketize",
9933
- "transcode"
9934
- ]).optional(),
9935
- /** Remote peer IP if the consumer terminates a network socket. */
9936
- remoteAddr: string().optional(),
9697
+ eventTypeTokens: array(string().min(1)).optional(),
9937
9698
  /**
9938
- * Server-read User-Agent of the originating client; enriched by the hub
9939
- * from the tRPC request context (browser sessions).
9699
+ * DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
9700
+ * `contact`, `button`, `device-event`) matched against the persisted
9701
+ * `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
9940
9702
  */
9941
- userAgent: string().optional(),
9942
- /** Authenticated user id (CamStack OAuth subject) when known. */
9943
- userId: string().optional(),
9944
- /** Higher-level session identifier (Alexa Echo sessionId, HAP session, …). */
9945
- sessionId: string().optional(),
9946
- /** Free-form key/value extras (e.g. clientHints from a WebRTC offer). */
9947
- extra: record(string(), string()).optional()
9948
- }).readonly();
9949
- var BrokerEncodedClientSchema = object({
9950
- /** Stable id assigned on attach; used by `killClient`. */
9951
- id: string(),
9952
- /** Which broker fanout the subscriber rides — annexB encoded packets vs raw RTP. */
9953
- channel: _enum(["annexb", "rtp"]),
9954
- attribution: BrokerConsumerAttributionSchema,
9703
+ sensorKinds: array(string().min(1)).optional(),
9704
+ /**
9705
+ * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
9706
+ * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
9707
+ * when the subject's phase does not match (a subject always carries a phase
9708
+ * on the package-event trigger).
9709
+ */
9710
+ packagePhase: _enum([
9711
+ "delivered",
9712
+ "picked-up",
9713
+ "both"
9714
+ ]).optional(),
9715
+ /**
9716
+ * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
9717
+ * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
9718
+ * listed polygon (ZoneEngine membership semantics). Evaluated only when
9719
+ * the subject carries a bbox; absent bbox ⇒ the condition FAILS.
9720
+ */
9721
+ customZones: array(MaskPolygonShapeSchema).optional(),
9722
+ /**
9723
+ * DEVICE-EVENT only. ZoneAnalytics occupancy edge — fires when a device's
9724
+ * (optionally zone/class-scoped) occupancy count crosses the configured
9725
+ * threshold and holds for `sustainSeconds`. Fail-closed on missing
9726
+ * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
9727
+ */
9728
+ occupancy: NcOccupancyConditionSchema.optional()
9729
+ });
9730
+ /** One delivery target: a `notification-output` Target ref + passthrough params. */
9731
+ var NcRuleTargetSchema = object({
9732
+ /** `notification-output` Target id. */
9733
+ targetId: string().min(1),
9734
+ /**
9735
+ * Per-backend passthrough. Recognized keys are mapped onto the canonical
9736
+ * Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
9737
+ * degrade engine drops what the backend can't render.
9738
+ */
9739
+ params: record(string(), unknown()).optional()
9740
+ });
9741
+ /**
9742
+ * Media attachment policy (P1 still-image subset).
9743
+ * - `best` — the best AVAILABLE subject image at dispatch time (D-3).
9744
+ * - `best-matching` — the media that explains WHY the rule fired: a rule
9745
+ * matched on identities attaches the subject's `faceCrop`, one matched on
9746
+ * plates attaches the `plateCrop`; a rule with no identity/plate condition
9747
+ * (or when the specific crop is missing) degrades to `best`, then
9748
+ * `keyFrame`, then no attachment — never delaying the send. The matched
9749
+ * condition summary is frozen on the outbox row at enqueue (like the rule
9750
+ * name), so the choice never drifts from the record that fired it.
9751
+ * - `keyFrame` — the clean scene frame (no subject box).
9752
+ * - `none` — no attachment.
9753
+ */
9754
+ var NcMediaPolicySchema = object({ attach: _enum([
9755
+ "best",
9756
+ "best-matching",
9757
+ "keyFrame",
9758
+ "none"
9759
+ ]).default("best") });
9760
+ /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9761
+ var NcThrottleSchema = object({
9762
+ cooldownSec: number().int().min(0).max(86400).default(60),
9763
+ /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
9764
+ scope: _enum(["rule", "rule-device"]).default("rule-device")
9765
+ });
9766
+ /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
9767
+ var NcRuleInputSchema = object({
9768
+ name: string().min(1).max(200),
9769
+ enabled: boolean().default(true),
9770
+ delivery: NcDeliverySchema,
9771
+ conditions: NcConditionsSchema.default({}),
9772
+ schedule: NcScheduleSchema.optional(),
9773
+ targets: array(NcRuleTargetSchema).min(1),
9774
+ media: NcMediaPolicySchema.default({ attach: "best" }),
9775
+ throttle: NcThrottleSchema.default({
9776
+ cooldownSec: 60,
9777
+ scope: "rule-device"
9778
+ }),
9779
+ /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
9780
+ template: object({
9781
+ title: string().max(500).optional(),
9782
+ body: string().max(2e3).optional()
9783
+ }).optional(),
9784
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
9785
+ priority: number().int().min(1).max(5).default(3),
9786
+ /**
9787
+ * Ownership/visibility key. Absent = admin/global rule (unchanged legacy
9788
+ * behaviour, visible to all, read-only in the viewer). Present = personal
9789
+ * rule owned by this userId. Server-stamped; never trusted from a client.
9790
+ */
9791
+ ownerUserId: string().optional()
9792
+ });
9793
+ /**
9794
+ * Partial patch for `updateRule` — any subset of the input fields, plus the
9795
+ * persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
9796
+ * NOT a client-authored input field (it lives on the persisted rule, not the
9797
+ * input), so it is added here explicitly to let the store's per-target opt-out
9798
+ * toggle round-trip through the shared `update` path. Viewer opt-out mutations
9799
+ * still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
9800
+ * `updateRule` patch.
9801
+ */
9802
+ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
9803
+ /** A persisted rule. */
9804
+ var NcRuleSchema = NcRuleInputSchema.extend({
9805
+ id: string(),
9806
+ /** userId of the admin who created the rule (server-stamped caller). */
9807
+ createdBy: string(),
9808
+ createdAt: number(),
9809
+ updatedAt: number(),
9810
+ /**
9811
+ * Per-target opt-out set. A targetId here is suppressed for THIS rule at
9812
+ * send time. Only a target's OWNER may add/remove its id (server-checked
9813
+ * in `nc.setRuleTargetEnabled`). Defaults to empty.
9814
+ */
9815
+ disabledTargetIds: array(string()).default([])
9816
+ });
9817
+ var NcTestResultSchema = object({
9818
+ recordId: string(),
9819
+ recordKind: _enum([
9820
+ "object-event",
9821
+ "track",
9822
+ "device-event",
9823
+ "package-event"
9824
+ ]),
9825
+ deviceId: number(),
9826
+ timestamp: number(),
9827
+ wouldFire: boolean(),
9828
+ /** Condition id that failed (first failing group), when `wouldFire` is false. */
9829
+ failedCondition: string().optional(),
9830
+ className: string().optional(),
9831
+ label: string().optional()
9832
+ });
9833
+ var NcConditionDescriptorSchema = object({
9834
+ /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
9835
+ id: string(),
9836
+ group: _enum([
9837
+ "scope",
9838
+ "class",
9839
+ "zones",
9840
+ "quality",
9841
+ "label",
9842
+ "schedule",
9843
+ "device",
9844
+ "package",
9845
+ "occupancy"
9846
+ ]),
9847
+ label: string(),
9848
+ /** Editor widget the UI renders — never hardcode per-condition forms. */
9849
+ valueType: _enum([
9850
+ "deviceIdList",
9851
+ "stringList",
9852
+ "number01",
9853
+ "number",
9854
+ "sourceSelect",
9855
+ "zoneSelection",
9856
+ "zoneIdList",
9857
+ "schedule",
9858
+ "plateMatcher",
9859
+ "packagePhase",
9860
+ "polygonDraw",
9861
+ "occupancy"
9862
+ ]),
9863
+ operator: _enum([
9864
+ "in",
9865
+ "notIn",
9866
+ "anyOf",
9867
+ "allOf",
9868
+ "gte",
9869
+ "fuzzyIn",
9870
+ "withinSchedule"
9871
+ ]),
9872
+ /** Which delivery kinds the condition applies to. */
9873
+ appliesTo: array(NcDeliverySchema),
9874
+ phase: string(),
9875
+ description: string().optional()
9876
+ });
9877
+ /**
9878
+ * The delivery lifecycle status of a history row — a straight read of the
9879
+ * durable outbox row's own status (single source of truth):
9880
+ * - `pending` — enqueued, in-flight or retrying with backoff
9881
+ * - `sent` — delivered (terminal)
9882
+ * - `dead` — dead-lettered after exhausting retries / a permanent
9883
+ * backend rejection / a deleted target (terminal; carries
9884
+ * the failure `error`)
9885
+ *
9886
+ * P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
9887
+ * user dimension (quiet hours / snooze) and are additive when they land.
9888
+ */
9889
+ var NcHistoryStatusSchema = _enum([
9890
+ "pending",
9891
+ "sent",
9892
+ "dead"
9893
+ ]);
9894
+ /** The evaluated record kind a history row descends from (one per trigger). */
9895
+ var NcHistoryRecordKindSchema = _enum([
9896
+ "object-event",
9897
+ "track-end",
9898
+ "device-event",
9899
+ "package-event"
9900
+ ]);
9901
+ /** Subject summary frozen on the row at fire time (survives rule/record edits). */
9902
+ var NcHistorySubjectSchema = object({
9903
+ className: string(),
9904
+ label: string().optional(),
9905
+ confidence: number().optional(),
9906
+ zones: array(string()),
9907
+ timestamp: number()
9908
+ });
9909
+ /**
9910
+ * One delivery-history row. This is a read-only VIEW over the durable
9911
+ * outbox row (single source of truth — the same row the drain loop drives;
9912
+ * NO second write path, so history can never drift from delivery state).
9913
+ * The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
9914
+ * `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
9915
+ * (fire) / `updatedAt` (last transition), `status` + `error` are the
9916
+ * lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
9917
+ * enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
9918
+ * P1 (admin scope only).
9919
+ */
9920
+ var NcHistoryEntrySchema = object({
9921
+ /** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
9922
+ id: string(),
9923
+ ruleId: string(),
9924
+ /** Rule name frozen at fire time (outlives a later rename / delete). */
9925
+ ruleName: string(),
9926
+ /** The rule urgency/trigger that produced this delivery. */
9927
+ delivery: NcDeliverySchema,
9928
+ targetId: string(),
9929
+ deviceId: number(),
9930
+ recordKind: NcHistoryRecordKindSchema,
9931
+ /** Event / track ref of the evaluated record (§3.2 `eventRef`). */
9932
+ recordId: string(),
9933
+ /** Present for track-scoped deliveries (object-event / track-end). */
9934
+ trackId: string().optional(),
9935
+ status: NcHistoryStatusSchema,
9936
+ /** Delivery attempts made so far. */
9937
+ attempts: number().int(),
9938
+ /** Fire time (outbox enqueue). */
9939
+ createdAt: number(),
9940
+ /** Last transition time (terminal for sent / dead). */
9941
+ updatedAt: number(),
9942
+ /** Failure detail — present on a `dead` row. */
9943
+ error: string().optional(),
9944
+ subject: NcHistorySubjectSchema
9945
+ });
9946
+ /**
9947
+ * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
9948
+ * AND; absent = unbounded on that axis. `since`/`until` bound the fire time
9949
+ * (`createdAt`, epoch ms, inclusive). `limit` is clamped to
9950
+ * {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
9951
+ */
9952
+ var NcHistoryFilterSchema = object({
9953
+ ruleId: string().optional(),
9954
+ deviceId: number().optional(),
9955
+ status: NcHistoryStatusSchema.optional(),
9956
+ since: number().optional(),
9957
+ until: number().optional(),
9958
+ limit: number().int().min(1).max(500).default(100)
9959
+ });
9960
+ 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 }), {
9961
+ kind: "mutation",
9962
+ auth: "admin",
9963
+ caller: "required"
9964
+ }), method(object({
9965
+ ruleId: string(),
9966
+ patch: NcRulePatchSchema
9967
+ }), object({ rule: NcRuleSchema }), {
9968
+ kind: "mutation",
9969
+ auth: "admin",
9970
+ caller: "required"
9971
+ }), method(object({ ruleId: string() }), object({ success: literal(true) }), {
9972
+ kind: "mutation",
9973
+ auth: "admin"
9974
+ }), method(object({
9975
+ ruleId: string(),
9976
+ enabled: boolean()
9977
+ }), object({ success: literal(true) }), {
9978
+ kind: "mutation",
9979
+ auth: "admin"
9980
+ }), method(object({
9981
+ rule: NcRuleInputSchema,
9982
+ lookbackMinutes: number().int().min(1).max(1440).default(60)
9983
+ }), object({ results: array(NcTestResultSchema) }), {
9984
+ kind: "mutation",
9985
+ auth: "admin"
9986
+ }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
9987
+ /**
9988
+ * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9989
+ *
9990
+ * Spec: `docs/superpowers/specs/2026-07-24-nc-occupancy-timelapse-design.md`
9991
+ * §3.2/§3.3.
9992
+ *
9993
+ * Deliberately NOT a capability definition and NOT an `NcRule`:
9994
+ * - Every `NcDelivery` member is a *persisted-pipeline-record* trigger. A
9995
+ * timelapse fires on a SCHEDULE WINDOW BOUNDARY, evaluates no pipeline
9996
+ * record, and produces a video it assembled itself — so it rides no
9997
+ * delivery-enum member (the enum is frozen) and no cap method. This file is
9998
+ * a plain typed schema; it does NOT go through `npm run codegen`.
9999
+ * - It shares only the delivery leg (`notification-output.send`) and the
10000
+ * persistence/ownership patterns with the Notification Center, reusing
10001
+ * {@link NcScheduleSchema} (weekly windows, midnight-crossing, invertible)
10002
+ * and {@link NcRuleTargetSchema} (target ref + passthrough params).
10003
+ *
10004
+ * Ownership is SERVER-DERIVED. `ownerUserId` / `createdBy` / `createdAt` /
10005
+ * `updatedAt` / `id` / `lastGeneratedAt` live on the PERSISTED rule only —
10006
+ * {@link TimelapseRuleInputSchema} and {@link TimelapseRulePatchSchema} do not
10007
+ * carry them, so a forged client payload can never claim or re-own a rule
10008
+ * (Zod strips unknown keys). The store stamps them from the resolved caller.
10009
+ */
10010
+ /** `{{var}}` templating over camera/rule/time — same vocabulary as `NcRule`. */
10011
+ var TimelapseTemplateSchema = object({
10012
+ title: string().max(500).optional(),
10013
+ body: string().max(2e3).optional()
10014
+ });
10015
+ var NameField = string().min(1).max(200);
10016
+ var DeviceIdsField = array(number()).min(1);
10017
+ var CadenceSecField = number().int().min(2).max(3600);
10018
+ var FramerateField = number().int().min(1).max(60);
10019
+ var TargetsField = array(NcRuleTargetSchema).min(1);
10020
+ var PriorityField = number().int().min(1).max(5);
10021
+ /**
10022
+ * Client-supplied timelapse-rule fields. The server stamps id / createdBy /
10023
+ * createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
10024
+ * here (see the ownership note above).
10025
+ */
10026
+ var TimelapseRuleInputSchema = object({
10027
+ name: NameField,
10028
+ enabled: boolean().default(true),
10029
+ /** Cameras sampled by this rule — one scratch dir + one artifact per device. */
10030
+ deviceIds: DeviceIdsField,
10031
+ /**
10032
+ * Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
10033
+ * means "always active"): a timelapse is defined by its window boundaries —
10034
+ * open clears the scratch, close assembles and delivers.
10035
+ */
10036
+ schedule: NcScheduleSchema,
10037
+ /** Force-snapshot cadence inside the window, seconds (predecessor parity). */
10038
+ cadenceSec: CadenceSecField.default(15),
10039
+ /** Output frames per second of the assembled mp4 (predecessor parity). */
10040
+ framerate: FramerateField.default(10),
10041
+ /** `notification-output` targets the finished video/thumbnail is sent to. */
10042
+ targets: TargetsField,
10043
+ template: TimelapseTemplateSchema.optional(),
10044
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
10045
+ priority: PriorityField.default(3)
10046
+ });
10047
+ object({
10048
+ name: NameField.optional(),
10049
+ enabled: boolean().optional(),
10050
+ deviceIds: DeviceIdsField.optional(),
10051
+ schedule: NcScheduleSchema.optional(),
10052
+ cadenceSec: CadenceSecField.optional(),
10053
+ framerate: FramerateField.optional(),
10054
+ targets: TargetsField.optional(),
10055
+ template: TimelapseTemplateSchema.nullable().optional(),
10056
+ priority: PriorityField.optional()
10057
+ });
10058
+ TimelapseRuleInputSchema.extend({
10059
+ id: string(),
10060
+ /**
10061
+ * Ownership/visibility key. Absent = admin/global rule (visible to all).
10062
+ * Present = personal rule owned by this userId. Server-stamped from the
10063
+ * resolved caller; never trusted from a client payload.
10064
+ */
10065
+ ownerUserId: string().optional(),
10066
+ /**
10067
+ * Epoch-ms of the last successful generation — the 1-hour re-generation
10068
+ * guard's durable state (predecessor parity). Absent = never generated.
10069
+ */
10070
+ lastGeneratedAt: number().optional(),
10071
+ /** userId of the caller who created the rule (server-stamped). */
10072
+ createdBy: string(),
10073
+ createdAt: number(),
10074
+ updatedAt: number()
10075
+ });
10076
+ /**
10077
+ * Generic device-level status snapshot. Auto-registered by `BaseDevice`
10078
+ * for every device, regardless of provider — the kernel needs a uniform
10079
+ * cap-keyed slice for the basic device flags every consumer expects to
10080
+ * read across processes (the `online` flag in particular). Driver-specific
10081
+ * caps (`battery`, `doorbell`, …) carry their domain-specific state on
10082
+ * their own slices.
10083
+ *
10084
+ * Pattern is identical to `battery`: schema-bearing `runtimeState`,
10085
+ * empty `methods`, single change event. Reads land at
10086
+ * `runtimeState.getCapState('device-status')`; writes at
10087
+ * `runtimeState.setCapState('device-status', …)`. Cross-process
10088
+ * consumers reach the same data via the `device-state` cap router
10089
+ * (`getCapSlice({deviceId, capName: 'device-status'})`).
10090
+ */
10091
+ var DeviceStatusSchema = object({
10092
+ /**
10093
+ * Device-level liveness. Drivers flip via `markOnline(boolean)` on
10094
+ * `BaseDevice`. Provider semantics vary — RTSP aggregates broker
10095
+ * stream-health, Reolink reads firmware push events, ONVIF tracks
10096
+ * ping responses. This cap intentionally does NOT prescribe which
10097
+ * signal drives the flag.
10098
+ */
10099
+ online: boolean(),
10100
+ /** Ms epoch of the last `online` transition. Lets consumers tell
10101
+ * apart "just came online" from "still online". */
10102
+ lastChangedAt: number()
10103
+ });
10104
+ object({
10105
+ deviceId: number(),
10106
+ status: DeviceStatusSchema
10107
+ });
10108
+ /**
10109
+ * Per-device feature/identity probe slice. Holds the runtime-resolved
10110
+ * truth about what a device CAN do — which the kernel uses to:
10111
+ * 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
10112
+ * based on what the firmware actually advertises).
10113
+ * 2. Compute the public `features: DeviceFeature[]` array surfaced via
10114
+ * `device-manager.listAll`.
10115
+ * 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
10116
+ * to register on the device's capability surface.
10117
+ *
10118
+ * Auto-registered by `BaseDevice` for every device. Drivers populate the
10119
+ * slice from `onProbe()` (kernel calls it once after register, before
10120
+ * accessory reconciliation). Consumers read via:
10121
+ * `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
10122
+ *
10123
+ * `flags` is an open record so each driver carries its own keys without
10124
+ * a centralized schema bottleneck — Reolink writes `hasPtz/hasIntercom`,
10125
+ * Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
10126
+ *
10127
+ * Replaces the older driver-local `deviceCache.has*` blob: the per-device
10128
+ * config is for operator-edited overrides + UI snapshots; runtime probe
10129
+ * results belong in runtime-state where the kernel handles persistence,
10130
+ * cross-process mirroring, and reactive updates.
10131
+ */
10132
+ var FeatureProbeStatusSchema = object({
10133
+ /**
10134
+ * Driver-specific flag bag. Each driver picks its own key names — the
10135
+ * cap deliberately does NOT enforce a closed enum here. Reolink keys:
10136
+ * `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
10137
+ * `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
10138
+ * `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
10139
+ */
10140
+ flags: record(string(), unknown()),
10141
+ /**
10142
+ * Coarse driver-classification — lets cross-process consumers tell apart
10143
+ * cameras / battery-cams / NVRs without re-running the probe. `null`
10144
+ * before the first probe completes.
10145
+ */
10146
+ deviceType: string().nullable(),
10147
+ /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
10148
+ model: string().nullable(),
10149
+ /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
10150
+ channelCount: number().nullable(),
10151
+ /**
10152
+ * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
10153
+ * completes — drivers' `getAccessoryChildren()` should treat zero as
10154
+ * "probe not done yet, return empty" so accessories aren't spawned
10155
+ * before the firmware is queried.
10156
+ */
10157
+ lastProbedAt: number(),
10158
+ /**
10159
+ * Framework convention: every runtime-state slice carries this for the
10160
+ * createRuntimeStateBridge stale-check helper. We keep it in sync with
10161
+ * `lastProbedAt` on every write.
10162
+ */
10163
+ lastFetchedAt: number()
10164
+ });
10165
+ object({
10166
+ deviceId: number(),
10167
+ status: FeatureProbeStatusSchema
10168
+ });
10169
+ object({
10170
+ /** Carbon dioxide concentration in ppm. */
10171
+ co2Ppm: number().min(0).optional(),
10172
+ /** Total volatile organic compounds in ppb. */
10173
+ vocPpb: number().min(0).optional(),
10174
+ /** Particulate matter ≤ 2.5 μm in µg/m³. */
10175
+ pm25: number().min(0).optional(),
10176
+ /** Particulate matter ≤ 10 μm in µg/m³. */
10177
+ pm10: number().min(0).optional(),
10178
+ /** Composite AQI value (typically 0..500). */
10179
+ aqi: number().optional(),
10180
+ /** Ms epoch when the slice was last updated. */
10181
+ lastFetchedAt: number(),
10182
+ /** Live display unit of the single metric this slice carries (e.g. HA
10183
+ * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
10184
+ * upstream `sensor.*` entity surfaces ONE device_class, so one unit
10185
+ * per slice is unambiguous. */
10186
+ unit: string().optional(),
10187
+ /** Suggested decimal places for numeric display.
10188
+ * Populated live from the upstream source when provided (e.g. HA
10189
+ * `attributes.suggested_display_precision`). Falls back to
10190
+ * auto-formatting when absent. */
10191
+ precision: number().int().min(0).max(10).optional()
10192
+ });
10193
+ DeviceType.Sensor;
10194
+ /**
10195
+ * Alarm-panel cap. Models HA `alarm_control_panel.*` on
10196
+ * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
10197
+ * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
10198
+ * arming / pending / triggered / disarming.
10199
+ *
10200
+ * Many panels require a PIN code on arm / disarm — the optional
10201
+ * `code` field on the methods passes it through to the upstream
10202
+ * service; it's NEVER persisted in the runtime slice or any event
10203
+ * payload. The presence of a required code is signalled by
10204
+ * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
10205
+ * field without a slice fetch.
10206
+ *
10207
+ * `availableModes` mirrors HA's `supported_features`-derived arm
10208
+ * mode list — the UI renders only the buttons the panel accepts.
10209
+ */
10210
+ var AlarmStateSchema = _enum([
10211
+ "disarmed",
10212
+ "armed_home",
10213
+ "armed_away",
10214
+ "armed_night",
10215
+ "armed_vacation",
10216
+ "armed_custom_bypass",
10217
+ "arming",
10218
+ "disarming",
10219
+ "pending",
10220
+ "triggered"
10221
+ ]);
10222
+ var AlarmArmModeSchema = _enum([
10223
+ "home",
10224
+ "away",
10225
+ "night",
10226
+ "vacation",
10227
+ "custom_bypass"
10228
+ ]);
10229
+ object({
10230
+ /** Current lifecycle state. */
10231
+ state: AlarmStateSchema,
10232
+ /** Subset of arm modes the panel accepts. UI renders one button per
10233
+ * mode in this list. */
10234
+ availableModes: array(AlarmArmModeSchema),
10235
+ /** Whether the panel requires a PIN on arm / disarm. Mirrors
10236
+ * `DeviceFeature.AlarmPinRequired` for slice consumers. */
10237
+ requiresCode: boolean(),
10238
+ /** Ms epoch when the slice was last updated. */
10239
+ lastChangedAt: number()
10240
+ });
10241
+ DeviceType.AlarmPanel, method(object({
10242
+ deviceId: number().int().nonnegative(),
10243
+ mode: AlarmArmModeSchema,
10244
+ /** Optional PIN code. Required when `requiresCode === true`.
10245
+ * Passed through to the upstream service; never persisted. */
10246
+ code: string().min(1).optional()
10247
+ }), _void(), {
10248
+ kind: "mutation",
10249
+ auth: "admin"
10250
+ }), method(object({
10251
+ deviceId: number().int().nonnegative(),
10252
+ code: string().min(1).optional()
10253
+ }), _void(), {
10254
+ kind: "mutation",
10255
+ auth: "admin"
10256
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
10257
+ kind: "mutation",
10258
+ auth: "admin"
10259
+ });
10260
+ object({
10261
+ /** Current illuminance in lux (lx). */
10262
+ lux: number().min(0),
10263
+ /** Ms epoch when the slice was last updated. */
10264
+ lastFetchedAt: number(),
10265
+ /** Live display unit from the upstream source (e.g. HA
10266
+ * `attributes.unit_of_measurement`). The UI prefers this over the
10267
+ * role's canonical unit. Absent → fall back to the canonical unit. */
10268
+ unit: string().optional(),
10269
+ /** Suggested decimal places for numeric display.
10270
+ * Populated live from the upstream source when provided (e.g. HA
10271
+ * `attributes.suggested_display_precision`). Falls back to
10272
+ * auto-formatting when absent. */
10273
+ precision: number().int().min(0).max(10).optional()
10274
+ });
10275
+ DeviceType.Sensor;
10276
+ /**
10277
+ * Per-class audio metrics aggregated over a sliding window.
10278
+ */
10279
+ var AudioClassSummarySchema = object({
10280
+ className: string(),
10281
+ /** Number of windows (chunks) where this class was the top hit. */
10282
+ hits: number().int().nonnegative(),
10283
+ /** Mean score across those hits, clamped to [0,1]. */
10284
+ avgScore: number().min(0).max(1),
10285
+ /** Peak score in the window. */
10286
+ peakScore: number().min(0).max(1)
10287
+ });
10288
+ /**
10289
+ * Per-camera audio metrics snapshot — emitted by the analytics frame
10290
+ * handler on every `pipeline.audio-inference-result` event and
10291
+ * mirrored into the `audio-metrics` device-state slice. Symmetric
10292
+ * with `zone-analytics` snapshots for video — every consumer
10293
+ * (admin UI panel, automations, alert rules) reads via the
10294
+ * canonical `device.state.audioMetrics.value` reactive handle.
10295
+ *
10296
+ * Aggregates are computed over a rolling `windowSec` window
10297
+ * (default 60s). Past that window, classes drop out of `byClass`
10298
+ * and the level history shifts forward.
10299
+ */
10300
+ var AudioMetricsSnapshotSchema = object({
10301
+ /** Wall-clock timestamp (ms) of the most recent audio window. */
10302
+ ts: number().int(),
10303
+ /** Sliding-window length (seconds) used for aggregation. */
10304
+ windowSec: number().int().positive(),
10305
+ /** Latest level reading from the most recent window. */
10306
+ level: object({
10307
+ rms: number(),
10308
+ dbfs: number()
10309
+ }),
10310
+ /** Peak dBFS observed across the rolling window. */
10311
+ peakDbfs: number(),
10312
+ /** Mean dBFS across the rolling window. */
10313
+ avgDbfs: number(),
10314
+ /** Most recent above-threshold classification, or null on silence. */
10315
+ current: object({
10316
+ className: string(),
10317
+ score: number().min(0).max(1),
10318
+ timestamp: number().int()
10319
+ }).nullable(),
10320
+ /** Per-class summary across the rolling window — keys are
10321
+ * `macroClass` strings (e.g. `dog_bark`, `speech`, `glass_break`). */
10322
+ byClass: array(AudioClassSummarySchema).readonly()
10323
+ });
10324
+ /**
10325
+ * Audio-metrics history payload — a series of `AudioMetricsHistoryPoint`
10326
+ * samples capped at `maxPoints` (default 1024). When the requested
10327
+ * `windowSec / sampleEveryMs` would exceed the cap, the provider
10328
+ * subsamples by bucketed averaging and reports the effective sample
10329
+ * spacing on `effectiveSampleEveryMs` so the UI can label the x-axis.
10330
+ */
10331
+ var AudioMetricsHistorySchema = object({
10332
+ points: array(object({
10333
+ /** Wall-clock ms when this sample was recorded. */
10334
+ ts: number().int(),
10335
+ /** Instantaneous dBFS level at sample time. `null` for windows where
10336
+ * the source had no level reading (rare; happens at decode startup). */
10337
+ dbfs: number().nullable(),
10338
+ /** Rolling-window peak dBFS at sample time. Same window the live
10339
+ * snapshot reports. */
10340
+ peakDbfs: number(),
10341
+ /** Rolling-window mean dBFS at sample time. */
10342
+ avgDbfs: number(),
10343
+ /** Dominant above-threshold class at sample time, or null on silence. */
10344
+ topClass: string().nullable(),
10345
+ /** Score of the dominant class (`null` whenever `topClass` is null). */
10346
+ topScore: number().min(0).max(1).nullable()
10347
+ })).readonly(),
10348
+ /** Actual ms between adjacent samples after any subsampling. */
10349
+ effectiveSampleEveryMs: number().int().positive(),
10350
+ /** Wall-clock window covered by `points` (`points[N-1].ts - points[0].ts`),
10351
+ * or `0` when there's fewer than 2 samples. */
10352
+ windowMsActual: number().int().nonnegative()
10353
+ });
10354
+ DeviceType.Camera, method(object({ deviceId: number() }), AudioMetricsSnapshotSchema.nullable()), method(object({
10355
+ deviceId: number(),
10356
+ /** History window in seconds. Default 300 (5 minutes).
10357
+ * Provider clamps to its retention cap if larger. */
10358
+ windowSec: number().int().positive().optional(),
10359
+ /** Target sample interval in ms. Default 1000 (1 sample/second).
10360
+ * Provider clamps to natural sample rate if smaller, and
10361
+ * bucket-averages when bigger than the requested window
10362
+ * would produce more than `maxPoints` samples. */
10363
+ sampleEveryMs: number().int().positive().optional()
10364
+ }), AudioMetricsHistorySchema);
10365
+ object({
10366
+ /** Whether the automation is currently enabled. Disabled automations
10367
+ * ignore their trigger block — manual `trigger` still works. */
10368
+ enabled: boolean(),
10369
+ /** Whether the automation is currently executing its action block. */
10370
+ isRunning: boolean(),
10371
+ /** Ms epoch of the last successful run. 0 when never run. */
10372
+ lastTriggeredAt: number(),
10373
+ /** Failure description from the last completed run. Null on success
10374
+ * or when never run. */
10375
+ lastError: string().nullable(),
10376
+ /** Ms epoch when the slice was last updated. */
10377
+ lastChangedAt: number()
10378
+ });
10379
+ DeviceType.Automation, method(object({ deviceId: number().int().nonnegative() }), _void(), {
10380
+ kind: "mutation",
10381
+ auth: "admin"
10382
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
10383
+ kind: "mutation",
10384
+ auth: "admin"
10385
+ }), method(object({
10386
+ deviceId: number().int().nonnegative(),
10387
+ /** When true, fires the action block while bypassing the
10388
+ * automation's condition evaluation. Gated by
10389
+ * `DeviceFeature.AutomationSkipCondition`. */
10390
+ skipCondition: boolean().optional()
10391
+ }), _void(), {
10392
+ kind: "mutation",
10393
+ auth: "admin"
10394
+ });
10395
+ /**
10396
+ * Battery status snapshot. Emitted by providers whose device is
10397
+ * battery-operated (cameras with `DeviceFeature.BatteryOperated`,
10398
+ * future sensor/button accessories). Consumers build their own "low
10399
+ * battery" alerting on top — the cap deliberately does NOT enforce a
10400
+ * threshold.
10401
+ */
10402
+ var BatteryStatusSchema = object({
10403
+ /** 0..100 inclusive. Firmware-reported. */
10404
+ percentage: number().min(0).max(100),
10405
+ /**
10406
+ * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
10407
+ * Reolink-specific for the Solar Panel 2 accessory (will become
10408
+ * common on other battery cams). `'none'` means running on battery
10409
+ * alone.
10410
+ */
10411
+ charging: _enum([
10412
+ "dc",
10413
+ "solar",
10414
+ "none"
10415
+ ]),
10416
+ /**
10417
+ * True when the camera firmware has gone into low-power mode. Battery
10418
+ * providers MUST avoid polling during sleep — reading the battery
10419
+ * wakes the camera up and drains charge.
10420
+ */
10421
+ sleeping: boolean(),
10422
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
10423
+ lastUpdated: number(),
10424
+ /**
10425
+ * True when the source is a BINARY low-battery indicator (HA
10426
+ * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
10427
+ * charge level — `percentage` is then a coarse stand-in (100 = normal,
10428
+ * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
10429
+ * misleading exact percentage. Absent/false → genuine 0–100 % reading.
10430
+ */
10431
+ binary: boolean().optional()
10432
+ });
10433
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, method(object({
10434
+ deviceId: number(),
10435
+ /** Bound on the wait. Sensible range 3000–10000ms. */
10436
+ timeoutMs: number().int().min(500).max(3e4).default(8e3)
10437
+ }), object({
10438
+ awoke: boolean(),
10439
+ durationMs: number()
10440
+ }), { kind: "mutation" }), object({
10441
+ deviceId: number(),
10442
+ status: BatteryStatusSchema
10443
+ });
10444
+ object({
10445
+ on: boolean(),
10446
+ /** Ms epoch of the last transition. 0 if never observed. */
10447
+ lastChangedAt: number()
10448
+ });
10449
+ DeviceType.Sensor;
10450
+ object({
10451
+ /** Current level as 0..100 inclusive. Firmware-reported. */
10452
+ percentage: number().min(0).max(100),
10453
+ /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
10454
+ lastChangedAt: number()
10455
+ });
10456
+ DeviceType.Light, method(object({
10457
+ deviceId: number().int().nonnegative(),
10458
+ percentage: number().min(0).max(100)
10459
+ }), _void(), {
10460
+ kind: "mutation",
10461
+ auth: "admin"
10462
+ }), object({
10463
+ deviceId: number(),
10464
+ percentage: number().min(0).max(100),
10465
+ lastChangedAt: number()
10466
+ });
10467
+ /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
10468
+ var StreamFormatSchema = _enum([
10469
+ "webrtc",
10470
+ "hls",
10471
+ "mjpeg",
10472
+ "rtsp"
10473
+ ]);
10474
+ var RtspRestreamEntrySchema = object({
10475
+ brokerId: string(),
10476
+ url: string(),
10477
+ mutedUrl: string(),
10478
+ enabled: boolean(),
10479
+ /**
10480
+ * Source-stream codec / resolution for the camStream this entry serves
10481
+ * (the broker's "high"/"mid"/"low" profile slot for this device).
10482
+ * Used by exporter pickers (`pickPreferredRtspEntry`) to resolve
10483
+ * `streamPreference: 'auto'` to the slot whose source is closest to
10484
+ * the consumer's target — Alexa wants ~720p, HomeKit wants ~1080p,
10485
+ * and there's no point dialling the 4K slot for an Echo Show. Absent
10486
+ * when the source publisher never advertised the field; pickers fall
10487
+ * back to first-enabled in that case.
10488
+ */
10489
+ codec: string().optional(),
10490
+ resolution: object({
10491
+ width: number().int().positive(),
10492
+ height: number().int().positive()
10493
+ }).optional()
10494
+ });
10495
+ var BrokerRtspClientSchema = object({
10496
+ sessionId: string(),
10497
+ remoteAddr: string(),
10498
+ /** Client `User-Agent` (e.g. `recorder` for the recorder's RTSP pull), or
10499
+ * null/absent when the client sent none. Lets the UI label a consumer by
10500
+ * purpose. Optional so a client built against an older schema stays valid. */
10501
+ userAgent: string().nullish(),
10502
+ playing: boolean(),
10503
+ muted: boolean(),
10504
+ connectedAt: number(),
10505
+ lastRtpAt: number(),
10506
+ bytesSent: number()
10507
+ });
10508
+ var BrokerDecodedClientSchema = object({
10509
+ tag: string(),
10510
+ subscribedAt: number(),
10511
+ maxFps: number(),
10512
+ framesDelivered: number(),
10513
+ framesDropped: number()
10514
+ });
10515
+ var BrokerAudioClientSchema = object({
10516
+ tag: string(),
10517
+ subscribedAt: number(),
10518
+ chunksDelivered: number()
10519
+ });
10520
+ var BrokerConsumerAttributionSchema = object({
10521
+ kind: _enum([
10522
+ "alexa",
10523
+ "homekit",
10524
+ "webrtc-browser",
10525
+ "webrtc-mobile",
10526
+ "webrtc-whep",
10527
+ "rtsp-listen",
10528
+ "derived-broker",
10529
+ "recording",
10530
+ "pipeline",
10531
+ "snapshot",
10532
+ "warmup",
10533
+ "unknown"
10534
+ ]),
10535
+ /**
10536
+ * Free-form label intended to disambiguate consumers OF THE SAME kind
10537
+ * on the same broker — e.g. user name, device alias. Should NOT repeat
10538
+ * the kind / cam / cam-stream / sessionId (those are surfaced by
10539
+ * dedicated fields). When empty the widget falls back to `${kind} ·
10540
+ * <sessionId tail>`.
10541
+ */
10542
+ label: string().optional(),
10543
+ /**
10544
+ * Which part of the encoded plane this subscription consumes:
10545
+ * - `'audio'` — audio packets only
10546
+ * - `'video'` — video packets only
10547
+ * - `'both'` — both video + audio (typical for AnnexB push paths
10548
+ * that don't split the source RTP)
10549
+ *
10550
+ * Missing field means "unknown" — older callers and the legacy
10551
+ * paths that haven't been migrated yet.
10552
+ */
10553
+ media: _enum([
10554
+ "audio",
10555
+ "video",
10556
+ "both"
10557
+ ]).optional(),
10558
+ /**
10559
+ * Codec the consumer receives AFTER any per-session re-encoding. For
10560
+ * a WebRTC session this is the negotiated egress codec (e.g. `H264`,
10561
+ * `H265`, `Opus`, `Pcmu`); for an RTSP restreamer it mirrors the
10562
+ * source codec. Surfaced in the widget chip so operators see at a
10563
+ * glance whether a viewer is on H.264 (Echo) or H.265 (Safari).
10564
+ */
10565
+ targetCodec: string().optional(),
10566
+ /**
10567
+ * Whether the broker is re-encoding for this consumer:
10568
+ * - `'passthrough'` — bytes flow source→consumer without ffmpeg
10569
+ * - `'repacketize'` — RTP payload is re-packetized but NOT re-encoded
10570
+ * (preserves frames, just rebuilds headers; e.g.
10571
+ * the H.265 repacketizer path)
10572
+ * - `'transcode'` — a full encode/decode round (ffmpeg, libx264,
10573
+ * libav…); the most expensive path
10574
+ */
10575
+ transport: _enum([
10576
+ "passthrough",
10577
+ "repacketize",
10578
+ "transcode"
10579
+ ]).optional(),
10580
+ /** Remote peer IP if the consumer terminates a network socket. */
10581
+ remoteAddr: string().optional(),
10582
+ /**
10583
+ * Server-read User-Agent of the originating client; enriched by the hub
10584
+ * from the tRPC request context (browser sessions).
10585
+ */
10586
+ userAgent: string().optional(),
10587
+ /** Authenticated user id (CamStack OAuth subject) when known. */
10588
+ userId: string().optional(),
10589
+ /** Higher-level session identifier (Alexa Echo sessionId, HAP session, …). */
10590
+ sessionId: string().optional(),
10591
+ /** Free-form key/value extras (e.g. clientHints from a WebRTC offer). */
10592
+ extra: record(string(), string()).optional()
10593
+ }).readonly();
10594
+ var BrokerEncodedClientSchema = object({
10595
+ /** Stable id assigned on attach; used by `killClient`. */
10596
+ id: string(),
10597
+ /** Which broker fanout the subscriber rides — annexB encoded packets vs raw RTP. */
10598
+ channel: _enum(["annexb", "rtp"]),
10599
+ attribution: BrokerConsumerAttributionSchema,
9955
10600
  subscribedAt: number(),
9956
10601
  /** Total packets delivered (annex-B EncodedPackets or RTP byte-buffers). */
9957
10602
  packetsDelivered: number()
@@ -12696,101 +13341,40 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
12696
13341
  }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" });
12697
13342
  object({
12698
13343
  detected: boolean(),
12699
- /** Ms epoch of the last detected-true observation. Null if never detected. */
12700
- lastDetectedAt: number().nullable(),
12701
- /**
12702
- * Ms after which `detected` auto-reverts to false if no fresh push
12703
- * arrives. Null means the provider leaves detected state until a
12704
- * native "clear" event.
12705
- */
12706
- autoClearAfterMs: number().nullable()
12707
- });
12708
- object({
12709
- deviceId: number(),
12710
- detected: boolean(),
12711
- timestamp: number(),
12712
- source: MotionSourceEnum,
12713
- regions: array(MotionRegionSchema).readonly().optional()
12714
- });
12715
- DeviceType.Camera, DeviceType.Sensor, method(object({ deviceId: number() }), boolean());
12716
- object({
12717
- enabled: boolean(),
12718
- /** Ms epoch of the last operator-driven change. */
12719
- lastChangedAt: number()
12720
- }).extend({
12721
- /** Ms epoch of the last successful camera fetch (0 = never). */
12722
- lastFetchedAt: number() });
12723
- DeviceType.Light, DeviceType.Siren, DeviceType.Switch, method(object({
12724
- deviceId: number().int().nonnegative(),
12725
- enabled: boolean()
12726
- }), _void(), {
12727
- kind: "mutation",
12728
- auth: "admin"
12729
- }), object({
12730
- deviceId: number(),
12731
- enabled: boolean(),
12732
- lastChangedAt: number()
12733
- });
12734
- /**
12735
- * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
12736
- * motion-zones, and the detection zones/lines editor all speak this one
12737
- * language so a single drawing-plane editor and the providers stay
12738
- * decoupled from each cap's storage.
12739
- *
12740
- * All coordinates are normalized 0..1 of the camera frame (top-left
12741
- * origin). Each cap composes the SUBSET of shape kinds it supports and
12742
- * advertises it via `supportedShapes` in its `getOptions`.
12743
- */
12744
- /** A normalized 0..1 point (top-left origin). */
12745
- var MaskPointSchema = object({
12746
- x: number(),
12747
- y: number()
12748
- });
12749
- /** Axis-aligned rectangle (normalized 0..1). */
12750
- var MaskRectShapeSchema = object({
12751
- kind: literal("rect"),
12752
- x: number(),
12753
- y: number(),
12754
- width: number(),
12755
- height: number()
12756
- });
12757
- /** Free polygon — an ordered list of normalized vertices (≥3). */
12758
- var MaskPolygonShapeSchema = object({
12759
- kind: literal("polygon"),
12760
- points: array(MaskPointSchema)
12761
- });
12762
- /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
12763
- var MaskGridShapeSchema = object({
12764
- kind: literal("grid"),
12765
- gridWidth: number(),
12766
- gridHeight: number(),
12767
- cells: array(boolean())
12768
- });
12769
- discriminatedUnion("kind", [
12770
- MaskRectShapeSchema,
12771
- MaskPolygonShapeSchema,
12772
- MaskGridShapeSchema,
12773
- object({
12774
- kind: literal("line"),
12775
- points: array(MaskPointSchema)
12776
- })
12777
- ]);
12778
- /** Every shape-kind discriminant, for `supportedShapes` advertisement. */
12779
- var MaskShapeKindSchema = _enum([
12780
- "rect",
12781
- "polygon",
12782
- "grid",
12783
- "line"
12784
- ]);
12785
- /** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
12786
- var MaskPolygonVerticesSchema = object({
12787
- min: number(),
12788
- max: number()
13344
+ /** Ms epoch of the last detected-true observation. Null if never detected. */
13345
+ lastDetectedAt: number().nullable(),
13346
+ /**
13347
+ * Ms after which `detected` auto-reverts to false if no fresh push
13348
+ * arrives. Null means the provider leaves detected state until a
13349
+ * native "clear" event.
13350
+ */
13351
+ autoClearAfterMs: number().nullable()
12789
13352
  });
12790
- /** Grid dimensions when a cap supports 'grid'. */
12791
- var MaskGridDimsSchema = object({
12792
- width: number(),
12793
- height: number()
13353
+ object({
13354
+ deviceId: number(),
13355
+ detected: boolean(),
13356
+ timestamp: number(),
13357
+ source: MotionSourceEnum,
13358
+ regions: array(MotionRegionSchema).readonly().optional()
13359
+ });
13360
+ DeviceType.Camera, DeviceType.Sensor, method(object({ deviceId: number() }), boolean());
13361
+ object({
13362
+ enabled: boolean(),
13363
+ /** Ms epoch of the last operator-driven change. */
13364
+ lastChangedAt: number()
13365
+ }).extend({
13366
+ /** Ms epoch of the last successful camera fetch (0 = never). */
13367
+ lastFetchedAt: number() });
13368
+ DeviceType.Light, DeviceType.Siren, DeviceType.Switch, method(object({
13369
+ deviceId: number().int().nonnegative(),
13370
+ enabled: boolean()
13371
+ }), _void(), {
13372
+ kind: "mutation",
13373
+ auth: "admin"
13374
+ }), object({
13375
+ deviceId: number(),
13376
+ enabled: boolean(),
13377
+ lastChangedAt: number()
12794
13378
  });
12795
13379
  /**
12796
13380
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
@@ -14716,6 +15300,55 @@ method(object({
14716
15300
  password: string()
14717
15301
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
14718
15302
  /**
15303
+ * A live terminal session hosted by the provider addon. Output and input do
15304
+ * NOT flow through the capability — they use the addon data plane
15305
+ * (`GET /addon/terminal/<id>/out` SSE, `POST /addon/terminal/<id>/in`) because
15306
+ * terminal output must be ordered and lossless. The event bus is telemetry and
15307
+ * may drop chunks ([D8]), and a dropped chunk desynchronises the vt parser
15308
+ * permanently until a full repaint. The capability owns only lifecycle.
15309
+ */
15310
+ var TerminalSessionInfoSchema = object({
15311
+ /** Opaque session id minted by the provider on `openSession`. */
15312
+ sessionId: string(),
15313
+ /** The pre-declared profile this session runs (never a free-form command). */
15314
+ profileId: string(),
15315
+ /** Human-readable profile label for the UI session list. */
15316
+ label: string(),
15317
+ cols: number().int().positive(),
15318
+ rows: number().int().positive(),
15319
+ /** ms-epoch the session's pty was spawned. */
15320
+ startedAt: number()
15321
+ });
15322
+ /**
15323
+ * A profile the operator may open — a pre-declared, allowlisted program
15324
+ * (`monitor` → `btm`). The capability accepts only these ids; a free-form
15325
+ * command string would be remote code execution as the server's user, so it is
15326
+ * deliberately not part of the contract.
15327
+ */
15328
+ var TerminalProfileInfoSchema = object({
15329
+ profileId: string(),
15330
+ label: string(),
15331
+ description: string().optional()
15332
+ });
15333
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
15334
+ profileId: string(),
15335
+ cols: number().int().positive(),
15336
+ rows: number().int().positive()
15337
+ }), TerminalSessionInfoSchema, {
15338
+ kind: "mutation",
15339
+ auth: "admin"
15340
+ }), method(object({
15341
+ sessionId: string(),
15342
+ cols: number().int().positive(),
15343
+ rows: number().int().positive()
15344
+ }), _void(), {
15345
+ kind: "mutation",
15346
+ auth: "admin"
15347
+ }), method(object({ sessionId: string() }), _void(), {
15348
+ kind: "mutation",
15349
+ auth: "admin"
15350
+ });
15351
+ /**
14719
15352
  * Orchestrator-side destination metadata. The orchestrator computes
14720
15353
  * `id = <addonId>:<subId>` from its provider lookup so consumers
14721
15354
  * (admin UI, restore flow) see one canonical key.
@@ -14816,11 +15449,53 @@ var LocationStatSchema = object({
14816
15449
  fileCount: number(),
14817
15450
  present: boolean()
14818
15451
  });
15452
+ /**
15453
+ * A backup schedule — the N:M "entry" that binds one cron cadence to a
15454
+ * SET of destination locations. Supersedes the per-location cron on
15455
+ * `BackupDestinationPolicy`: an operator creates a schedule, picks the
15456
+ * `backups` locations it should write to, and the orchestrator fans a
15457
+ * single archive out to all of them when the cron fires.
15458
+ *
15459
+ * `retentionCount` is per-schedule (D-decision 2026-07-28): every
15460
+ * location targeted by this schedule keeps this many archives from
15461
+ * this schedule's runs.
15462
+ *
15463
+ * `dataSources` optionally narrows which top-level state locations
15464
+ * (db, addons, tls, …) are archived; omitted = the orchestrator's
15465
+ * default full set.
15466
+ */
15467
+ var BackupScheduleSchema = object({
15468
+ /** Stable id. Generated by the orchestrator on first upsert if absent. */
15469
+ id: string(),
15470
+ /** Operator-facing display name. */
15471
+ label: string(),
15472
+ /** 5-field POSIX cron. Empty = disabled cadence (kept for editing). */
15473
+ cron: string(),
15474
+ /** Master on/off toggle for the whole schedule. */
15475
+ enabled: boolean(),
15476
+ /** `backups`-location ids this schedule writes to (fan-out set). */
15477
+ locationIds: array(string()).readonly(),
15478
+ /** Archives kept per targeted location for this schedule. */
15479
+ retentionCount: number().int().min(1).max(1e3),
15480
+ /** Optional subset of source locations to include; omitted = all. */
15481
+ dataSources: array(string()).readonly().optional(),
15482
+ /** ms-epoch of last successful run. */
15483
+ lastRunAt: number().optional(),
15484
+ /** ms-epoch of next computed firing (read-only, filled on list). */
15485
+ nextRunAt: number().optional()
15486
+ });
14819
15487
  method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }), method(object({
14820
15488
  /** Subset of registered `backup-destination` addon ids to write to. */
14821
15489
  destinations: array(string()).optional(),
14822
15490
  locations: array(string()).optional(),
14823
- label: string().optional()
15491
+ label: string().optional(),
15492
+ /**
15493
+ * Per-run retention override applied to every targeted
15494
+ * destination. Used by schedule-driven runs (per-entry
15495
+ * retention). Omitted = each destination's own policy
15496
+ * retention (manual runs).
15497
+ */
15498
+ retentionCount: number().int().min(1).max(1e3).optional()
14824
15499
  }).optional(), array(BackupEntrySchema).readonly(), {
14825
15500
  kind: "mutation",
14826
15501
  auth: "admin"
@@ -14869,7 +15544,21 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
14869
15544
  ok: boolean(),
14870
15545
  error: string().optional(),
14871
15546
  nextRuns: array(number()).readonly()
14872
- }));
15547
+ })), method(_void(), array(BackupScheduleSchema).readonly(), { auth: "admin" }), method(object({
15548
+ id: string().optional(),
15549
+ label: string(),
15550
+ cron: string(),
15551
+ enabled: boolean(),
15552
+ locationIds: array(string()).readonly(),
15553
+ retentionCount: number().int().min(1).max(1e3),
15554
+ dataSources: array(string()).readonly().optional()
15555
+ }), BackupScheduleSchema, {
15556
+ kind: "mutation",
15557
+ auth: "admin"
15558
+ }), method(object({ id: string() }), _void(), {
15559
+ kind: "mutation",
15560
+ auth: "admin"
15561
+ });
14873
15562
  /**
14874
15563
  * `broker` — unified pub/sub broker registry, system-scoped collection.
14875
15564
  *
@@ -15945,1596 +16634,1108 @@ method(object({
15945
16634
  active: boolean()
15946
16635
  }), _void(), {
15947
16636
  kind: "mutation",
15948
- auth: "admin"
15949
- }), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
15950
- capName: string(),
15951
- wrappers: array(string())
15952
- }))), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), object({
15953
- settings: SettingsSchemaWithValuesSchema.nullable(),
15954
- live: SettingsSchemaWithValuesSchema.nullable()
15955
- })), method(object({
15956
- deviceId: number().int().nonnegative(),
15957
- action: string().min(1),
15958
- input: unknown()
15959
- }), unknown(), { kind: "mutation" }), method(object({
15960
- deviceId: number(),
15961
- writerCapName: string(),
15962
- writerAddonId: string(),
15963
- key: string(),
15964
- value: unknown()
15965
- }), object({ success: literal(true) }), {
15966
- kind: "mutation",
15967
- auth: "admin"
15968
- }), method(object({
15969
- deviceId: number(),
15970
- changes: array(object({
15971
- writerCapName: string(),
15972
- writerAddonId: string(),
15973
- key: string(),
15974
- value: unknown()
15975
- }))
15976
- }), object({
15977
- success: literal(true),
15978
- failures: array(object({
15979
- writerCapName: string(),
15980
- writerAddonId: string(),
15981
- error: string()
15982
- }))
15983
- }), {
15984
- kind: "mutation",
15985
- auth: "admin"
15986
- }), method(object({ addonId: string() }), array(DiscoveryCandidateSchema), {
15987
- kind: "mutation",
15988
- auth: "admin"
15989
- }), method(object({
15990
- addonId: string(),
15991
- candidate: DiscoveryCandidateSchema,
15992
- /** Owning integration id, stamped onto the new device's meta by the
15993
- * device-manager forwarder so `removeByIntegration` can cascade it.
15994
- * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
15995
- integrationId: string().optional()
15996
- }), DeviceSummarySchema, {
15997
- kind: "mutation",
15998
- auth: "admin"
15999
- }), method(object({
16000
- addonId: string(),
16001
- type: _enum(DeviceType)
16002
- }), unknown().nullable()), method(object({
16003
- addonId: string(),
16004
- type: _enum(DeviceType),
16005
- config: record(string(), unknown()),
16006
- /** Owning integration id, stamped onto the new device's meta by the
16007
- * device-manager forwarder so `removeByIntegration` can cascade it.
16008
- * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
16009
- integrationId: string().optional()
16010
- }), DeviceSummarySchema, {
16011
- kind: "mutation",
16012
- auth: "admin"
16013
- }), method(object({
16014
- addonId: string(),
16015
- type: _enum(DeviceType),
16016
- key: string(),
16017
- value: unknown(),
16018
- formValues: record(string(), unknown()).optional()
16019
- }), FieldProbeResultSchema, {
16020
- kind: "mutation",
16021
- auth: "admin"
16022
- }), method(object({
16023
- addonId: string(),
16024
- integrationId: string()
16025
- }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
16026
- addonId: string(),
16027
- integrationId: string()
16028
- }), AdoptionStatusSchema, {
16029
- kind: "mutation",
16030
- auth: "admin"
16031
- }), method(AdoptInputSchema.extend({ addonId: string() }), AdoptResultSchema, {
16032
- kind: "mutation",
16033
- auth: "admin"
16034
- }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
16035
- kind: "mutation",
16036
- auth: "admin"
16037
- }), method(ResyncInputSchema, ResyncResultSchema, {
16038
- kind: "mutation",
16039
- auth: "admin"
16040
- }), method(object({}), object({ providers: array(object({
16041
- addonId: string(),
16042
- label: string()
16043
- })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
16044
- addonId: string(),
16045
- label: string(),
16046
- candidates: array(DiscoveryCandidateSchema).readonly(),
16047
- error: string().nullable()
16048
- })).readonly() }), {
16049
- kind: "mutation",
16050
- auth: "admin"
16051
- }), method(object({
16052
- addonId: string(),
16053
- params: record(string(), unknown()).optional()
16054
- }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
16055
- kind: "mutation",
16056
- auth: "admin"
16057
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
16058
- deviceId: number(),
16059
- key: string(),
16060
- value: unknown()
16061
- }), FieldProbeResultSchema, {
16062
- kind: "mutation",
16063
- auth: "admin"
16064
- }), method(object({
16065
- deviceId: number(),
16066
- caps: array(string()).readonly().optional()
16067
- }), record(string(), unknown().nullable()));
16068
- method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
16069
- deviceId: number(),
16070
- capName: string()
16071
- }), record(string(), unknown()).nullable()), method(object({}), record(string(), record(string(), record(string(), unknown())))), method(object({
16072
- deviceId: number(),
16073
- capName: string(),
16074
- slice: record(string(), unknown())
16075
- }), _void(), { kind: "mutation" }), object({
16076
- deviceId: number(),
16077
- capName: string(),
16078
- slice: record(string(), unknown())
16079
- });
16080
- /**
16081
- * Embedding output. `embedding` is wire-encoded as `number[]` so the
16082
- * Zod-validated tRPC surface round-trips cleanly; consumers that need a
16083
- * `Float32Array` can wrap it on the way out (in-process, no marshalling
16084
- * is involved). `inferenceMs` mirrors the runtime field used by the
16085
- * post-analysis enrichment-engine.
16086
- */
16087
- var EmbeddingResultSchema = object({
16088
- embedding: array(number()),
16089
- inferenceMs: number()
16090
- });
16091
- var EmbeddingInfoSchema = object({
16092
- modelId: string(),
16093
- embeddingDim: number(),
16094
- ready: boolean()
16095
- });
16096
- method(object({
16097
- crop: _instanceof(Uint8Array),
16098
- width: number(),
16099
- height: number()
16100
- }), EmbeddingResultSchema), method(object({ text: string() }), EmbeddingResultSchema), method(_void(), EmbeddingInfoSchema);
16101
- /**
16102
- * filesystem-browse — per-node capability for browsing the node's local
16103
- * filesystem, sandboxed to operator-configured allowed roots. Used by the
16104
- * admin "Add filesystem location" flow to pick a node + path. `mode:'per-node'`
16105
- * (one provider per node); the hub calls it with `{nodeId}` so the codegen
16106
- * routes to that exact node (default `nodeIdMode:'routing'`).
16107
- */
16108
- var DirEntrySchema = object({
16109
- name: string(),
16110
- path: string()
16111
- });
16112
- var BrowseResultSchema = object({
16113
- path: string(),
16114
- entries: array(DirEntrySchema).readonly(),
16115
- freeBytes: number(),
16116
- totalBytes: number()
16117
- });
16118
- method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({ path: string() }), BrowseResultSchema, { auth: "admin" }), method(object({ path: string() }), object({ path: string() }), {
16637
+ auth: "admin"
16638
+ }), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
16639
+ capName: string(),
16640
+ wrappers: array(string())
16641
+ }))), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), object({
16642
+ settings: SettingsSchemaWithValuesSchema.nullable(),
16643
+ live: SettingsSchemaWithValuesSchema.nullable()
16644
+ })), method(object({
16645
+ deviceId: number().int().nonnegative(),
16646
+ action: string().min(1),
16647
+ input: unknown()
16648
+ }), unknown(), { kind: "mutation" }), method(object({
16649
+ deviceId: number(),
16650
+ writerCapName: string(),
16651
+ writerAddonId: string(),
16652
+ key: string(),
16653
+ value: unknown()
16654
+ }), object({ success: literal(true) }), {
16119
16655
  kind: "mutation",
16120
16656
  auth: "admin"
16121
- });
16122
- /**
16123
- * Shared LLM generate contracts — imported by BOTH `llm.cap.ts` (consumer
16124
- * surface) and `llm-runtime.cap.ts` (node-side managed executor) so the two
16125
- * caps stay wire-compatible without a circular cap→cap import.
16126
- *
16127
- * Errors are a discriminated-union RESULT, never thrown: the shape survives
16128
- * every transport tier structurally, and failed calls still write usage rows.
16129
- * Token counts only in v1 — no costUsd (operator decision, 2026-07-15).
16130
- */
16131
- var LlmUsageSchema = object({
16132
- inputTokens: number(),
16133
- outputTokens: number()
16134
- });
16135
- var LlmErrorCodeSchema = _enum([
16136
- "timeout",
16137
- "rate-limited",
16138
- "auth",
16139
- "refusal",
16140
- "bad-request",
16141
- "unavailable",
16142
- "no-profile",
16143
- "budget-exceeded",
16144
- "adapter-error"
16145
- ]);
16146
- var LlmGenerateResultSchema = discriminatedUnion("ok", [object({
16147
- ok: literal(true),
16148
- text: string(),
16149
- model: string(),
16150
- usage: LlmUsageSchema,
16151
- truncated: boolean(),
16152
- latencyMs: number()
16657
+ }), method(object({
16658
+ deviceId: number(),
16659
+ changes: array(object({
16660
+ writerCapName: string(),
16661
+ writerAddonId: string(),
16662
+ key: string(),
16663
+ value: unknown()
16664
+ }))
16153
16665
  }), object({
16154
- ok: literal(false),
16155
- code: LlmErrorCodeSchema,
16156
- message: string(),
16157
- retryAfterMs: number().optional()
16158
- })]);
16159
- /**
16160
- * `Uint8Array` is the sanctioned binary convention — superjson + the UDS
16161
- * MsgPack channel round-trip typed arrays (embedding-encoder.cap.ts:29,
16162
- * notification-output.cap.ts:27-31 precedents).
16163
- */
16164
- var LlmImageSchema = object({
16165
- bytes: _instanceof(Uint8Array),
16166
- mimeType: string()
16167
- });
16168
- var LlmGenerateBaseInputSchema = object({
16169
- /** Collection routing (the notification-output posture). */
16170
- addonId: string().optional(),
16171
- /** Explicit profile; else the resolution chain (spec §3). */
16172
- profileId: string().optional(),
16173
- /** MANDATORY usage tag: 'ai-summary', 'notifier-rules', 'adhoc-ui', … */
16174
- consumer: string(),
16175
- system: string().optional(),
16176
- /** v1: single-turn. `messages[]` is a v2 additive field. */
16177
- prompt: string(),
16178
- /** Structured output — adapter-mapped (response_format / forced tool / responseSchema). */
16179
- jsonSchema: record(string(), unknown()).optional(),
16180
- /** Per-call override of the profile default. */
16181
- maxTokens: number().int().positive().optional(),
16182
- temperature: number().optional()
16183
- });
16184
- /**
16185
- * `llm-runtime` — node-side managed llama.cpp executor (spec §4). Registered
16186
- * on EVERY node where `addon-ai` is installed; the hub `llm` provider reaches
16187
- * a specific node's runtime with `nodePin(profile.runtime.nodeId)` — normal
16188
- * cap routing, zero bespoke plumbing. `internal: true`: the operator reaches
16189
- * this only through the `llm` cap's methods.
16190
- *
16191
- * One running llama-server child per node in v1 (models are RAM-heavy).
16192
- * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
16193
- * watchdog — operator decision #3).
16194
- */
16195
- var ManagedModelRefSchema = discriminatedUnion("kind", [
16196
- object({
16197
- kind: literal("catalog"),
16198
- catalogId: string()
16199
- }),
16200
- object({
16201
- kind: literal("url"),
16202
- url: string(),
16203
- sha256: string().optional()
16204
- }),
16205
- object({
16206
- kind: literal("path"),
16207
- path: string()
16208
- })
16209
- ]);
16210
- var ManagedRuntimeConfigSchema = object({
16211
- /** WHERE the runtime lives — hub or any agent. */
16212
- nodeId: string(),
16213
- /** Closed for v1; 'ollama' is a v2 candidate. */
16214
- engine: _enum(["llama-cpp"]),
16215
- model: ManagedModelRefSchema,
16216
- contextSize: number().int().default(4096),
16217
- /** 0 = CPU-only. */
16218
- gpuLayers: number().int().default(0),
16219
- /** Default: cpus-2, clamped ≥1 (resolved node-side). */
16220
- threads: number().int().optional(),
16221
- /** Concurrent slots. */
16222
- parallel: number().int().default(1),
16223
- /** Else lazy: first generate boots it. */
16224
- autoStart: boolean().default(false),
16225
- /** 0 = never; frees RAM after quiet periods. */
16226
- idleStopMinutes: number().int().default(30)
16227
- });
16228
- var LlmRuntimeStatusSchema = object({
16229
- /** Status is ALWAYS node-qualified. */
16230
- nodeId: string(),
16231
- state: _enum([
16232
- "stopped",
16233
- "downloading",
16234
- "starting",
16235
- "ready",
16236
- "crashed",
16237
- "failed"
16238
- ]),
16239
- pid: number().optional(),
16240
- port: number().optional(),
16241
- modelPath: string().optional(),
16242
- modelId: string().optional(),
16243
- downloadProgress: number().min(0).max(1).optional(),
16244
- lastError: string().optional(),
16245
- crashesInWindow: number(),
16246
- /** Child RSS (sampled best-effort). */
16247
- memoryBytes: number().optional(),
16248
- vramBytes: number().optional()
16249
- });
16250
- var LlmNodeModelSchema = object({
16251
- file: string(),
16252
- sizeBytes: number(),
16253
- catalogId: string().optional(),
16254
- installedAt: number().optional()
16255
- });
16256
- var LlmRuntimeDiskUsageSchema = object({
16257
- nodeId: string(),
16258
- modelsBytes: number(),
16259
- freeBytes: number().optional()
16260
- });
16261
- method(LlmGenerateBaseInputSchema.extend({
16262
- images: array(LlmImageSchema).optional(),
16263
- runtime: ManagedRuntimeConfigSchema,
16264
- /** The managed profile's timeout, threaded by the hub provider. */
16265
- timeoutMs: number().int().positive().optional()
16266
- }), LlmGenerateResultSchema, { kind: "mutation" }), method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
16666
+ success: literal(true),
16667
+ failures: array(object({
16668
+ writerCapName: string(),
16669
+ writerAddonId: string(),
16670
+ error: string()
16671
+ }))
16672
+ }), {
16267
16673
  kind: "mutation",
16268
16674
  auth: "admin"
16269
- }), method(object({}), _void(), {
16675
+ }), method(object({ addonId: string() }), array(DiscoveryCandidateSchema), {
16270
16676
  kind: "mutation",
16271
16677
  auth: "admin"
16272
- }), method(object({}), LlmRuntimeStatusSchema), method(object({ model: ManagedModelRefSchema }), _void(), {
16678
+ }), method(object({
16679
+ addonId: string(),
16680
+ candidate: DiscoveryCandidateSchema,
16681
+ /** Owning integration id, stamped onto the new device's meta by the
16682
+ * device-manager forwarder so `removeByIntegration` can cascade it.
16683
+ * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
16684
+ integrationId: string().optional()
16685
+ }), DeviceSummarySchema, {
16273
16686
  kind: "mutation",
16274
16687
  auth: "admin"
16275
- }), method(object({ file: string() }), _void(), {
16688
+ }), method(object({
16689
+ addonId: string(),
16690
+ type: _enum(DeviceType)
16691
+ }), unknown().nullable()), method(object({
16692
+ addonId: string(),
16693
+ type: _enum(DeviceType),
16694
+ config: record(string(), unknown()),
16695
+ /** Owning integration id, stamped onto the new device's meta by the
16696
+ * device-manager forwarder so `removeByIntegration` can cascade it.
16697
+ * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
16698
+ integrationId: string().optional()
16699
+ }), DeviceSummarySchema, {
16276
16700
  kind: "mutation",
16277
16701
  auth: "admin"
16278
- }), method(object({}), array(LlmNodeModelSchema)), method(object({}), LlmRuntimeDiskUsageSchema);
16279
- /**
16280
- * `llm` — consumer-facing LLM surface (spec §1-§3). Collection-mode: array
16281
- * methods concat-fan across providers; single-row methods route to ONE
16282
- * provider by the `addonId` in the call input (the notification-output
16283
- * posture, notification-output.cap.ts:215-250). Provided by `addon-ai`
16284
- * (hub-placed); the cap stays open for future providers.
16285
- *
16286
- * Profiles are ROWS (data), not addons: one row = one usable model endpoint.
16287
- * `apiKey` is a password field — providers REDACT it on read and merge on
16288
- * write; a stored key NEVER round-trips to a client.
16289
- */
16290
- var LlmProfileKindSchema = _enum([
16291
- "openai-compatible",
16292
- "openai",
16293
- "anthropic",
16294
- "google",
16295
- "managed-local"
16296
- ]);
16297
- var LlmProfileSchema = object({
16298
- id: string(),
16299
- name: string(),
16300
- kind: LlmProfileKindSchema,
16301
- /** Stamped by the provider — keeps the fanned catalog routable. */
16702
+ }), method(object({
16302
16703
  addonId: string(),
16303
- enabled: boolean(),
16304
- /** Vendor model id, or the managed runtime's loaded model. */
16305
- model: string(),
16306
- /** Required for openai-compatible; override for cloud kinds. */
16307
- baseUrl: string().optional(),
16308
- /** ConfigUISchema type:'password' — never round-trips (spec §5). */
16309
- apiKey: string().optional(),
16310
- supportsVision: boolean(),
16311
- temperature: number().min(0).max(2).optional(),
16312
- maxTokens: number().int().positive().optional(),
16313
- timeoutMs: number().int().positive().default(6e4),
16314
- extraHeaders: record(string(), string()).optional(),
16315
- /** kind === 'managed-local' only (spec §4). */
16316
- runtime: ManagedRuntimeConfigSchema.optional()
16317
- });
16318
- /** ConfigUISchema tree passed through untyped on the wire (the
16319
- * notification-output `ConfigSchemaPassthrough` precedent at
16320
- * notification-output.cap.ts:151); the exported TS type re-tightens it. */
16321
- var ConfigSchemaPassthrough$1 = unknown();
16322
- var LlmProfileKindDescriptorSchema = object({
16323
- kind: LlmProfileKindSchema,
16324
- label: string(),
16325
- icon: string(),
16326
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
16704
+ type: _enum(DeviceType),
16705
+ key: string(),
16706
+ value: unknown(),
16707
+ formValues: record(string(), unknown()).optional()
16708
+ }), FieldProbeResultSchema, {
16709
+ kind: "mutation",
16710
+ auth: "admin"
16711
+ }), method(object({
16327
16712
  addonId: string(),
16328
- configSchema: ConfigSchemaPassthrough$1
16329
- });
16330
- var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
16331
- var LlmDefaultSchema = object({
16332
- selector: LlmDefaultSelectorSchema,
16333
- profileId: string()
16334
- });
16335
- /** Server-side rollup row — getUsage never dumps raw call rows (spec §6). */
16336
- var LlmUsageRollupSchema = object({
16337
- day: string(),
16338
- consumer: string(),
16339
- profileId: string(),
16340
- calls: number(),
16341
- okCalls: number(),
16342
- errorCalls: number(),
16343
- inputTokens: number(),
16344
- outputTokens: number(),
16345
- avgLatencyMs: number()
16346
- });
16347
- /** LLM-facing view over the reused ModelCatalogEntry mechanism (spec §4.2). */
16348
- var ManagedModelCatalogEntrySchema = object({
16349
- id: string(),
16350
- label: string(),
16351
- family: string(),
16352
- purpose: _enum(["text", "vision"]),
16353
- url: string(),
16354
- sha256: string(),
16355
- sizeBytes: number(),
16356
- quantization: string(),
16357
- /** Load-time guidance shown in the picker. */
16358
- minRamBytes: number(),
16359
- contextSizeDefault: number().int(),
16360
- /** Vision models: companion projector file. */
16361
- mmprojUrl: string().optional()
16362
- });
16363
- var LlmRuntimeNodeSchema = object({
16364
- nodeId: string(),
16365
- reachable: boolean(),
16366
- status: LlmRuntimeStatusSchema.optional(),
16367
- disk: LlmRuntimeDiskUsageSchema.optional(),
16368
- error: string().optional()
16369
- });
16370
- var GenerateVisionInputSchema = LlmGenerateBaseInputSchema.extend({ images: array(LlmImageSchema).min(1) });
16371
- var ProfileRefInputSchema = object({
16713
+ integrationId: string()
16714
+ }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
16372
16715
  addonId: string(),
16373
- profileId: string()
16374
- });
16375
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
16716
+ integrationId: string()
16717
+ }), AdoptionStatusSchema, {
16376
16718
  kind: "mutation",
16377
16719
  auth: "admin"
16378
- }), method(ProfileRefInputSchema, _void(), {
16720
+ }), method(AdoptInputSchema.extend({ addonId: string() }), AdoptResultSchema, {
16379
16721
  kind: "mutation",
16380
16722
  auth: "admin"
16381
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
16723
+ }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
16382
16724
  kind: "mutation",
16383
16725
  auth: "admin"
16384
- }), method(ProfileRefInputSchema, array(string())), method(object({}), array(LlmDefaultSchema)), method(object({
16385
- selector: LlmDefaultSelectorSchema,
16386
- profileId: string().nullable()
16387
- }), _void(), {
16726
+ }), method(ResyncInputSchema, ResyncResultSchema, {
16388
16727
  kind: "mutation",
16389
16728
  auth: "admin"
16390
- }), method(object({
16391
- since: number().optional(),
16392
- until: number().optional(),
16393
- consumer: string().optional(),
16394
- profileId: string().optional()
16395
- }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
16396
- nodeId: string(),
16397
- model: ManagedModelRefSchema
16398
- }), _void(), {
16729
+ }), method(object({}), object({ providers: array(object({
16730
+ addonId: string(),
16731
+ label: string()
16732
+ })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
16733
+ addonId: string(),
16734
+ label: string(),
16735
+ candidates: array(DiscoveryCandidateSchema).readonly(),
16736
+ error: string().nullable()
16737
+ })).readonly() }), {
16399
16738
  kind: "mutation",
16400
16739
  auth: "admin"
16401
16740
  }), method(object({
16402
- nodeId: string(),
16403
- file: string()
16404
- }), _void(), {
16741
+ addonId: string(),
16742
+ params: record(string(), unknown()).optional()
16743
+ }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
16405
16744
  kind: "mutation",
16406
16745
  auth: "admin"
16407
- }), method(ProfileRefInputSchema, LlmRuntimeStatusSchema), method(ProfileRefInputSchema, LlmRuntimeStatusSchema, {
16746
+ }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
16747
+ deviceId: number(),
16748
+ key: string(),
16749
+ value: unknown()
16750
+ }), FieldProbeResultSchema, {
16408
16751
  kind: "mutation",
16409
16752
  auth: "admin"
16410
- }), method(ProfileRefInputSchema, _void(), {
16753
+ }), method(object({
16754
+ deviceId: number(),
16755
+ caps: array(string()).readonly().optional()
16756
+ }), record(string(), unknown().nullable()));
16757
+ method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
16758
+ deviceId: number(),
16759
+ capName: string()
16760
+ }), record(string(), unknown()).nullable()), method(object({}), record(string(), record(string(), record(string(), unknown())))), method(object({
16761
+ deviceId: number(),
16762
+ capName: string(),
16763
+ slice: record(string(), unknown())
16764
+ }), _void(), { kind: "mutation" }), object({
16765
+ deviceId: number(),
16766
+ capName: string(),
16767
+ slice: record(string(), unknown())
16768
+ });
16769
+ /**
16770
+ * Embedding output. `embedding` is wire-encoded as `number[]` so the
16771
+ * Zod-validated tRPC surface round-trips cleanly; consumers that need a
16772
+ * `Float32Array` can wrap it on the way out (in-process, no marshalling
16773
+ * is involved). `inferenceMs` mirrors the runtime field used by the
16774
+ * post-analysis enrichment-engine.
16775
+ */
16776
+ var EmbeddingResultSchema = object({
16777
+ embedding: array(number()),
16778
+ inferenceMs: number()
16779
+ });
16780
+ var EmbeddingInfoSchema = object({
16781
+ modelId: string(),
16782
+ embeddingDim: number(),
16783
+ ready: boolean()
16784
+ });
16785
+ method(object({
16786
+ crop: _instanceof(Uint8Array),
16787
+ width: number(),
16788
+ height: number()
16789
+ }), EmbeddingResultSchema), method(object({ text: string() }), EmbeddingResultSchema), method(_void(), EmbeddingInfoSchema);
16790
+ /**
16791
+ * filesystem-browse — per-node capability for browsing the node's local
16792
+ * filesystem, sandboxed to operator-configured allowed roots. Used by the
16793
+ * admin "Add filesystem location" flow to pick a node + path. `mode:'per-node'`
16794
+ * (one provider per node); the hub calls it with `{nodeId}` so the codegen
16795
+ * routes to that exact node (default `nodeIdMode:'routing'`).
16796
+ */
16797
+ var DirEntrySchema = object({
16798
+ name: string(),
16799
+ path: string()
16800
+ });
16801
+ var BrowseResultSchema = object({
16802
+ path: string(),
16803
+ entries: array(DirEntrySchema).readonly(),
16804
+ freeBytes: number(),
16805
+ totalBytes: number()
16806
+ });
16807
+ method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({ path: string() }), BrowseResultSchema, { auth: "admin" }), method(object({ path: string() }), object({ path: string() }), {
16411
16808
  kind: "mutation",
16412
16809
  auth: "admin"
16413
16810
  });
16414
- var LogLevelSchema = _enum([
16415
- "debug",
16416
- "info",
16417
- "warn",
16418
- "error"
16811
+ /**
16812
+ * Shared LLM generate contracts — imported by BOTH `llm.cap.ts` (consumer
16813
+ * surface) and `llm-runtime.cap.ts` (node-side managed executor) so the two
16814
+ * caps stay wire-compatible without a circular cap→cap import.
16815
+ *
16816
+ * Errors are a discriminated-union RESULT, never thrown: the shape survives
16817
+ * every transport tier structurally, and failed calls still write usage rows.
16818
+ * Token counts only in v1 — no costUsd (operator decision, 2026-07-15).
16819
+ */
16820
+ var LlmUsageSchema = object({
16821
+ inputTokens: number(),
16822
+ outputTokens: number()
16823
+ });
16824
+ var LlmErrorCodeSchema = _enum([
16825
+ "timeout",
16826
+ "rate-limited",
16827
+ "auth",
16828
+ "refusal",
16829
+ "bad-request",
16830
+ "unavailable",
16831
+ "no-profile",
16832
+ "budget-exceeded",
16833
+ "adapter-error"
16419
16834
  ]);
16420
- var LogEntrySchema = object({
16421
- timestamp: date(),
16422
- level: LogLevelSchema,
16423
- scope: array(string()),
16835
+ var LlmGenerateResultSchema = discriminatedUnion("ok", [object({
16836
+ ok: literal(true),
16837
+ text: string(),
16838
+ model: string(),
16839
+ usage: LlmUsageSchema,
16840
+ truncated: boolean(),
16841
+ latencyMs: number()
16842
+ }), object({
16843
+ ok: literal(false),
16844
+ code: LlmErrorCodeSchema,
16424
16845
  message: string(),
16425
- meta: record(string(), unknown()).optional(),
16426
- tags: record(string(), string()).optional()
16427
- });
16428
- method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
16429
- scope: array(string()).optional(),
16430
- level: LogLevelSchema.optional(),
16431
- since: date().optional(),
16432
- until: date().optional(),
16433
- limit: number().optional(),
16434
- tags: record(string(), string()).optional()
16435
- }), array(LogEntrySchema).readonly());
16846
+ retryAfterMs: number().optional()
16847
+ })]);
16436
16848
  /**
16437
- * `login-method` collection cap through which auth addons contribute
16438
- * their pre-auth login surfaces to the login page. This is the SINGLE,
16439
- * generic mechanism that supersedes the dead `auth.listProviders` reader:
16440
- * every auth addon (OIDC, magic-link, WebAuthn/passkey) registers a
16441
- * `login-method` provider and the PUBLIC `auth.listLoginMethods`
16442
- * procedure aggregates them for the unauthenticated login page.
16443
- *
16444
- * A contribution is a discriminated union on `kind`:
16445
- *
16446
- * - `redirect` a declarative button. The login page renders a generic
16447
- * button that navigates to `startUrl` (an addon-owned HTTP route).
16448
- * Covers OIDC (`/addon/auth-oidc/<id>/start`) and magic-link with
16449
- * ZERO shell-side JS. A future SSO addon plugs in the same way — the
16450
- * login page needs NO change.
16451
- *
16452
- * - `widget` — a Module-Federation widget the login page mounts (via
16453
- * `loadRemoteBundle`) for an in-page ceremony. `auth.listLoginMethods`
16454
- * stamps a public `bundleUrl` from `addonId` + `bundle`. Generic
16455
- * mechanism kept for future use; no shipped addon uses it on the login
16456
- * page (the passkey ceremony below runs natively in the shell instead).
16457
- *
16458
- * - `passkey` — a declarative WebAuthn ceremony the shell renders
16459
- * natively (`@simplewebauthn/browser` lives in `addon-admin-ui`, not in
16460
- * a remotely-loaded bundle). Carries the addon's effective `rpId` /
16461
- * `origin` (from its `resolveRpID()` / `resolveOrigin()`) so the shell
16462
- * can gate visibility (IP-literal origin, hostname/rpId mismatch) WITHOUT
16463
- * fetching any remote code pre-auth. Contribution stays unconditional
16464
- * enrollment state is never leaked pre-auth; visibility is a shell
16465
- * decision.
16466
- *
16467
- * Every contribution carries a `stage`:
16468
- * - `primary` — shown on the first credentials screen (OIDC /
16469
- * magic-link buttons; a future usernameless passkey).
16470
- * - `second-factor` — shown AFTER the password leg, gated on the
16471
- * returned `factors` (passkey-as-2FA today).
16849
+ * `Uint8Array` is the sanctioned binary convention superjson + the UDS
16850
+ * MsgPack channel round-trip typed arrays (embedding-encoder.cap.ts:29,
16851
+ * notification-output.cap.ts:27-31 precedents).
16852
+ */
16853
+ var LlmImageSchema = object({
16854
+ bytes: _instanceof(Uint8Array),
16855
+ mimeType: string()
16856
+ });
16857
+ var LlmGenerateBaseInputSchema = object({
16858
+ /** Collection routing (the notification-output posture). */
16859
+ addonId: string().optional(),
16860
+ /** Explicit profile; else the resolution chain (spec §3). */
16861
+ profileId: string().optional(),
16862
+ /** MANDATORY usage tag: 'ai-summary', 'notifier-rules', 'adhoc-ui', … */
16863
+ consumer: string(),
16864
+ system: string().optional(),
16865
+ /** v1: single-turn. `messages[]` is a v2 additive field. */
16866
+ prompt: string(),
16867
+ /** Structured output adapter-mapped (response_format / forced tool / responseSchema). */
16868
+ jsonSchema: record(string(), unknown()).optional(),
16869
+ /** Per-call override of the profile default. */
16870
+ maxTokens: number().int().positive().optional(),
16871
+ temperature: number().optional()
16872
+ });
16873
+ /**
16874
+ * `llm-runtime` node-side managed llama.cpp executor (spec §4). Registered
16875
+ * on EVERY node where `addon-ai` is installed; the hub `llm` provider reaches
16876
+ * a specific node's runtime with `nodePin(profile.runtime.nodeId)` normal
16877
+ * cap routing, zero bespoke plumbing. `internal: true`: the operator reaches
16878
+ * this only through the `llm` cap's methods.
16472
16879
  *
16473
- * `mount: skip` the cap is read server-side by the core auth router
16474
- * (`registry.getCollection('login-method')`), never mounted as its own
16475
- * tRPC router.
16880
+ * One running llama-server child per node in v1 (models are RAM-heavy).
16881
+ * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
16882
+ * watchdog — operator decision #3).
16476
16883
  */
16477
- /** When a login method renders in the two-phase login flow. */
16478
- var LoginStageEnum = _enum(["primary", "second-factor"]);
16479
- /** One login-method contribution — redirect button, pre-auth widget, or native passkey ceremony. */
16480
- var LoginMethodContributionSchema = discriminatedUnion("kind", [
16884
+ var ManagedModelRefSchema = discriminatedUnion("kind", [
16481
16885
  object({
16482
- kind: literal("redirect"),
16483
- /** Stable id within the login-method set (e.g. `auth-oidc/google`). */
16484
- id: string(),
16485
- /** Operator-facing button label. */
16486
- label: string(),
16487
- /** lucide-react icon name. */
16488
- icon: string().optional(),
16489
- /** Addon-owned HTTP route the button navigates to (GET). */
16490
- startUrl: string(),
16491
- stage: LoginStageEnum
16886
+ kind: literal("catalog"),
16887
+ catalogId: string()
16492
16888
  }),
16493
16889
  object({
16494
- kind: literal("widget"),
16495
- /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-login`). */
16496
- id: string(),
16497
- /** Owning addon id — drives the public bundle URL + the MF namespace. */
16498
- addonId: string(),
16499
- /** Bundle filename inside the addon's dist dir (`remoteEntry.js`). */
16500
- bundle: string(),
16501
- /** MF remote descriptor — `{ remoteName, exposedModule, componentKey }`. */
16502
- remote: WidgetRemoteSchema,
16503
- stage: LoginStageEnum
16890
+ kind: literal("url"),
16891
+ url: string(),
16892
+ sha256: string().optional()
16504
16893
  }),
16505
16894
  object({
16506
- kind: literal("passkey"),
16507
- /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-direct-login`). */
16508
- id: string(),
16509
- /** Operator-facing button label. */
16510
- label: string(),
16511
- stage: LoginStageEnum,
16512
- /** Effective WebAuthn RP ID (`resolveRpID()`) — the shell gates visibility on it. */
16513
- rpId: string(),
16514
- /** Effective expected origin (`resolveOrigin()`), null when unconfigured. */
16515
- origin: string().nullable()
16895
+ kind: literal("path"),
16896
+ path: string()
16516
16897
  })
16517
16898
  ]);
16518
- method(_void(), array(LoginMethodContributionSchema).readonly());
16519
- var CpuBreakdownSchema = object({
16520
- total: number(),
16521
- user: number(),
16522
- system: number(),
16523
- irq: number(),
16524
- nice: number(),
16525
- loadAvg: tuple([
16526
- number(),
16527
- number(),
16528
- number()
16529
- ]),
16530
- cores: number()
16531
- });
16532
- var MemoryInfoSchema = object({
16533
- percent: number(),
16534
- totalBytes: number(),
16535
- usedBytes: number(),
16536
- availableBytes: number(),
16537
- swapUsedBytes: number(),
16538
- swapTotalBytes: number()
16539
- });
16540
- var DiskIoSnapshotSchema = object({
16541
- readBytes: number(),
16542
- writeBytes: number(),
16543
- readOps: number(),
16544
- writeOps: number(),
16545
- timestampMs: number()
16546
- });
16547
- var NetworkIoSnapshotSchema = object({
16548
- rxBytes: number(),
16549
- txBytes: number(),
16550
- rxPackets: number(),
16551
- txPackets: number(),
16552
- rxErrors: number(),
16553
- txErrors: number(),
16554
- timestampMs: number()
16555
- });
16556
- var MetricsGpuInfoSchema = object({
16557
- utilization: number(),
16558
- model: string(),
16559
- memoryUsedBytes: number(),
16560
- memoryTotalBytes: number(),
16561
- temperature: number().nullable()
16562
- });
16563
- var ProcessResourceInfoSchema = object({
16564
- openFds: number(),
16565
- threadCount: number(),
16566
- activeHandles: number(),
16567
- activeRequests: number()
16568
- });
16569
- var PressureAvgsSchema = object({
16570
- avg10: number(),
16571
- avg60: number(),
16572
- avg300: number()
16573
- });
16574
- var PressureInfoSchema = object({
16575
- some: PressureAvgsSchema,
16576
- full: PressureAvgsSchema.nullable()
16577
- });
16578
- var SystemResourceSnapshotSchema = object({
16579
- cpu: CpuBreakdownSchema,
16580
- memory: MemoryInfoSchema,
16581
- gpu: MetricsGpuInfoSchema.nullable(),
16582
- network: NetworkIoSnapshotSchema,
16583
- disk: DiskIoSnapshotSchema,
16584
- pressure: object({
16585
- cpu: PressureInfoSchema.nullable(),
16586
- memory: PressureInfoSchema.nullable(),
16587
- io: PressureInfoSchema.nullable()
16588
- }),
16589
- process: ProcessResourceInfoSchema,
16590
- cpuTemperature: number().nullable(),
16591
- timestampMs: number()
16592
- });
16593
- var DiskSpaceInfoSchema = object({
16594
- path: string(),
16595
- totalBytes: number(),
16596
- usedBytes: number(),
16597
- availableBytes: number(),
16598
- percent: number()
16599
- });
16600
- var PidResourceStatsSchema = object({
16601
- pid: number(),
16602
- cpu: number(),
16603
- memory: number(),
16604
- /**
16605
- * Private (anonymous) resident bytes — the per-process V8 heap + native
16606
- * allocations NOT shared with other processes (Linux RssAnon). This is the
16607
- * "real" per-runner cost; summing it across runners is meaningful, unlike
16608
- * `memory` (RSS), which double-counts the shared mmap'd framework code.
16609
- * Undefined where /proc is unavailable (e.g. macOS).
16610
- */
16611
- privateBytes: number().optional(),
16612
- /**
16613
- * Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
16614
- * code shared copy-on-write across runners. Undefined on macOS.
16615
- */
16616
- sharedBytes: number().optional()
16899
+ var ManagedRuntimeConfigSchema = object({
16900
+ /** WHERE the runtime lives — hub or any agent. */
16901
+ nodeId: string(),
16902
+ /** Closed for v1; 'ollama' is a v2 candidate. */
16903
+ engine: _enum(["llama-cpp"]),
16904
+ model: ManagedModelRefSchema,
16905
+ contextSize: number().int().default(4096),
16906
+ /** 0 = CPU-only. */
16907
+ gpuLayers: number().int().default(0),
16908
+ /** Default: cpus-2, clamped ≥1 (resolved node-side). */
16909
+ threads: number().int().optional(),
16910
+ /** Concurrent slots. */
16911
+ parallel: number().int().default(1),
16912
+ /** Else lazy: first generate boots it. */
16913
+ autoStart: boolean().default(false),
16914
+ /** 0 = never; frees RAM after quiet periods. */
16915
+ idleStopMinutes: number().int().default(30)
16617
16916
  });
16618
- var AddonInstanceSchema = object({
16619
- addonId: string(),
16917
+ var LlmRuntimeStatusSchema = object({
16918
+ /** Status is ALWAYS node-qualified. */
16620
16919
  nodeId: string(),
16621
- role: _enum(["hub", "worker"]),
16622
- pid: number(),
16623
16920
  state: _enum([
16624
- "starting",
16625
- "running",
16626
- "stopping",
16627
16921
  "stopped",
16628
- "crashed"
16629
- ]),
16630
- uptimeSec: number()
16631
- });
16632
- var NodeProcessSchema = object({
16633
- pid: number(),
16634
- ppid: number(),
16635
- pgid: number(),
16636
- classification: _enum([
16637
- "root",
16638
- "managed",
16639
- "system",
16640
- "ghost"
16922
+ "downloading",
16923
+ "starting",
16924
+ "ready",
16925
+ "crashed",
16926
+ "failed"
16641
16927
  ]),
16642
- /** `$process` addon binding when `managed`, else null. */
16643
- addonId: string().nullable(),
16644
- /** Kernel-reported nodeId when the process is a known agent/worker. */
16645
- nodeId: string().nullable(),
16646
- /** Truncated command line. */
16647
- command: string(),
16648
- cpuPercent: number(),
16649
- memoryRssBytes: number(),
16650
- /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
16651
- uptimeSec: number(),
16652
- /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
16653
- orphaned: boolean()
16654
- });
16655
- var KillProcessInputSchema = object({
16656
- pid: number(),
16657
- /** Force = SIGKILL. Default is SIGTERM. */
16658
- force: boolean().optional()
16659
- });
16660
- var KillProcessResultSchema = object({
16661
- success: boolean(),
16662
- reason: string().optional(),
16663
- signal: _enum(["SIGTERM", "SIGKILL"]).optional()
16664
- });
16665
- var DumpHeapSnapshotInputSchema = object({
16666
- /** The addon whose runner should dump a heap snapshot. */
16667
- addonId: string() });
16668
- var DumpHeapSnapshotResultSchema = object({
16669
- success: boolean(),
16670
- /** Path of the written .heapsnapshot inside the runner's container/host. */
16671
- path: string().optional(),
16672
- /** Process pid that was signalled. */
16673
16928
  pid: number().optional(),
16674
- reason: string().optional()
16929
+ port: number().optional(),
16930
+ modelPath: string().optional(),
16931
+ modelId: string().optional(),
16932
+ downloadProgress: number().min(0).max(1).optional(),
16933
+ lastError: string().optional(),
16934
+ crashesInWindow: number(),
16935
+ /** Child RSS (sampled best-effort). */
16936
+ memoryBytes: number().optional(),
16937
+ vramBytes: number().optional()
16675
16938
  });
16676
- var SystemMetricsSchema = object({
16677
- cpuPercent: number(),
16678
- memoryPercent: number(),
16679
- memoryUsedMB: number(),
16680
- memoryTotalMB: number(),
16681
- diskPercent: number().optional(),
16682
- temperature: number().optional(),
16683
- gpuPercent: number().optional(),
16684
- gpuMemoryPercent: number().optional()
16939
+ var LlmNodeModelSchema = object({
16940
+ file: string(),
16941
+ sizeBytes: number(),
16942
+ catalogId: string().optional(),
16943
+ installedAt: number().optional()
16685
16944
  });
16686
- 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, {
16945
+ var LlmRuntimeDiskUsageSchema = object({
16946
+ nodeId: string(),
16947
+ modelsBytes: number(),
16948
+ freeBytes: number().optional()
16949
+ });
16950
+ method(LlmGenerateBaseInputSchema.extend({
16951
+ images: array(LlmImageSchema).optional(),
16952
+ runtime: ManagedRuntimeConfigSchema,
16953
+ /** The managed profile's timeout, threaded by the hub provider. */
16954
+ timeoutMs: number().int().positive().optional()
16955
+ }), LlmGenerateResultSchema, { kind: "mutation" }), method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
16687
16956
  kind: "mutation",
16688
16957
  auth: "admin"
16689
- }), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
16958
+ }), method(object({}), _void(), {
16690
16959
  kind: "mutation",
16691
16960
  auth: "admin"
16692
- });
16693
- method(object({
16694
- sourceUrl: string(),
16695
- metadata: ModelConvertMetadataSchema,
16696
- targets: array(ConvertTargetSchema).min(1).readonly(),
16697
- calibrationRef: string().optional(),
16698
- sessionId: string().optional()
16699
- }), ConvertResultSchema, {
16961
+ }), method(object({}), LlmRuntimeStatusSchema), method(object({ model: ManagedModelRefSchema }), _void(), {
16700
16962
  kind: "mutation",
16701
- auth: "admin",
16702
- timeoutMs: 6e5
16703
- });
16704
- method(object({
16705
- nodeId: string(),
16706
- modelId: string(),
16707
- format: _enum(MODEL_FORMATS),
16708
- entry: ModelCatalogEntrySchema
16709
- }), object({
16710
- ok: boolean(),
16711
- /** sha256 of the staged tarball (empty for a hub-local no-op). */
16712
- sha256: string(),
16713
- bytes: number(),
16714
- /** The target node's modelsDir the artifact landed in. */
16715
- path: string()
16716
- }), {
16963
+ auth: "admin"
16964
+ }), method(object({ file: string() }), _void(), {
16717
16965
  kind: "mutation",
16718
16966
  auth: "admin"
16719
- });
16720
- /**
16721
- * `mqtt-broker` — broker-registry cap.
16722
- *
16723
- * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
16724
- * MQTT brokers (external + optionally an embedded `aedes`-backed one)
16725
- * and (b) the connection details a consumer addon needs to spin up
16726
- * its OWN `mqtt.js` client.
16727
- *
16728
- * Why: pub/sub routing over the system event-bus loses fidelity
16729
- * (callback shape, QoS guarantees, will/retain semantics) and adds
16730
- * refcount bookkeeping that addons would rather own themselves. The
16731
- * canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
16732
- * features anyway — give it the connection config, get out of the way.
16733
- *
16734
- * Consumer flow:
16735
- * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
16736
- * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
16737
- * client.subscribe('zigbee2mqtt/+')
16738
- *
16739
- * Collection mode: multiple brokers (e.g. one local mosquitto + one
16740
- * cloud bridge). The "embedded" entry (when present) is just another
16741
- * broker in the registry — its lifecycle is owned by the addon that
16742
- * spawned it.
16743
- */
16744
- var BrokerKindSchema = _enum(["external", "embedded"]);
16967
+ }), method(object({}), array(LlmNodeModelSchema)), method(object({}), LlmRuntimeDiskUsageSchema);
16745
16968
  /**
16746
- * Broker live-probe status.
16969
+ * `llm` — consumer-facing LLM surface (spec §1-§3). Collection-mode: array
16970
+ * methods concat-fan across providers; single-row methods route to ONE
16971
+ * provider by the `addonId` in the call input (the notification-output
16972
+ * posture, notification-output.cap.ts:215-250). Provided by `addon-ai`
16973
+ * (hub-placed); the cap stays open for future providers.
16747
16974
  *
16748
- * - `connected` last probe completed a clean CONNACK
16749
- * - `disconnected` — no probe has run yet (cold cache)
16750
- * - `auth-failed` CONNACK refused with auth error (RC 4 / 5)
16751
- * - `unreachable` — TCP connect timed out / refused
16752
- * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
16975
+ * Profiles are ROWS (data), not addons: one row = one usable model endpoint.
16976
+ * `apiKey` is a password field providers REDACT it on read and merge on
16977
+ * write; a stored key NEVER round-trips to a client.
16753
16978
  */
16754
- var BrokerStatusSchema$1 = _enum([
16755
- "connected",
16756
- "disconnected",
16757
- "auth-failed",
16758
- "unreachable",
16759
- "tls-error"
16979
+ var LlmProfileKindSchema = _enum([
16980
+ "openai-compatible",
16981
+ "openai",
16982
+ "anthropic",
16983
+ "google",
16984
+ "managed-local"
16760
16985
  ]);
16761
- var BrokerInfoSchema = object({
16986
+ var LlmProfileSchema = object({
16762
16987
  id: string(),
16763
16988
  name: string(),
16764
- url: string(),
16765
- kind: BrokerKindSchema,
16766
- status: BrokerStatusSchema$1,
16767
- latencyMs: number().nullable(),
16768
- error: string().optional(),
16769
- /** Embedded brokers only: number of MQTT clients currently connected. */
16770
- connectedClients: number().int().nonnegative().optional(),
16771
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
16772
- lastCheckedAt: number().optional()
16989
+ kind: LlmProfileKindSchema,
16990
+ /** Stamped by the provider — keeps the fanned catalog routable. */
16991
+ addonId: string(),
16992
+ enabled: boolean(),
16993
+ /** Vendor model id, or the managed runtime's loaded model. */
16994
+ model: string(),
16995
+ /** Required for openai-compatible; override for cloud kinds. */
16996
+ baseUrl: string().optional(),
16997
+ /** ConfigUISchema type:'password' — never round-trips (spec §5). */
16998
+ apiKey: string().optional(),
16999
+ supportsVision: boolean(),
17000
+ temperature: number().min(0).max(2).optional(),
17001
+ maxTokens: number().int().positive().optional(),
17002
+ timeoutMs: number().int().positive().default(6e4),
17003
+ extraHeaders: record(string(), string()).optional(),
17004
+ /** kind === 'managed-local' only (spec §4). */
17005
+ runtime: ManagedRuntimeConfigSchema.optional()
16773
17006
  });
16774
- /**
16775
- * Connection details — what a consumer needs to call
16776
- * `mqtt.connect(url, options)`. We split URL + credentials so the
16777
- * consumer can pass them as `mqtt.connect(url, { username, password })`
16778
- * instead of stuffing creds into the URL (which leaks them into logs).
16779
- */
16780
- var BrokerConnectionDetailsSchema = object({
16781
- url: string(),
16782
- username: string().optional(),
16783
- password: string().optional(),
16784
- /**
16785
- * Suggested prefix for `clientId`. Each consumer should suffix this
16786
- * with its own discriminator (addon id, instance id) so reconnects
16787
- * don't kick each other off (MQTT spec: clientId must be unique per
16788
- * broker).
16789
- */
16790
- clientIdPrefix: string().optional()
17007
+ /** ConfigUISchema tree passed through untyped on the wire (the
17008
+ * notification-output `ConfigSchemaPassthrough` precedent at
17009
+ * notification-output.cap.ts:151); the exported TS type re-tightens it. */
17010
+ var ConfigSchemaPassthrough$1 = unknown();
17011
+ var LlmProfileKindDescriptorSchema = object({
17012
+ kind: LlmProfileKindSchema,
17013
+ label: string(),
17014
+ icon: string(),
17015
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
17016
+ addonId: string(),
17017
+ configSchema: ConfigSchemaPassthrough$1
16791
17018
  });
16792
- var AddBrokerInputSchema = object({
16793
- name: string().min(1),
16794
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
16795
- username: string().optional(),
16796
- password: string().optional(),
16797
- clientIdPrefix: string().optional()
17019
+ var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
17020
+ var LlmDefaultSchema = object({
17021
+ selector: LlmDefaultSelectorSchema,
17022
+ profileId: string()
16798
17023
  });
16799
- var AddBrokerResultSchema = object({ id: string() });
16800
- var IdInputSchema = object({ id: string() });
16801
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
16802
- ok: literal(true),
16803
- latencyMs: number()
16804
- }), object({
16805
- ok: literal(false),
16806
- error: string()
16807
- })]);
16808
- var StartEmbeddedInputSchema = object({
16809
- port: number().int().min(1).max(65535).default(1883),
16810
- /** Allow anonymous connect (no username/password). Default: false. */
16811
- allowAnonymous: boolean().default(false),
16812
- /** Optional shared username/password for clients. */
16813
- username: string().optional(),
16814
- password: string().optional()
17024
+ /** Server-side rollup row getUsage never dumps raw call rows (spec §6). */
17025
+ var LlmUsageRollupSchema = object({
17026
+ day: string(),
17027
+ consumer: string(),
17028
+ profileId: string(),
17029
+ calls: number(),
17030
+ okCalls: number(),
17031
+ errorCalls: number(),
17032
+ inputTokens: number(),
17033
+ outputTokens: number(),
17034
+ avgLatencyMs: number()
16815
17035
  });
16816
- var StartEmbeddedResultSchema = object({
17036
+ /** LLM-facing view over the reused ModelCatalogEntry mechanism (spec §4.2). */
17037
+ var ManagedModelCatalogEntrySchema = object({
16817
17038
  id: string(),
16818
- url: string()
16819
- });
16820
- var StatusSchema = object({
16821
- brokerCount: number(),
16822
- embeddedRunning: boolean()
16823
- });
16824
- 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);
16825
- var NetworkEndpointSchema = object({
17039
+ label: string(),
17040
+ family: string(),
17041
+ purpose: _enum(["text", "vision"]),
16826
17042
  url: string(),
16827
- hostname: string(),
16828
- port: number(),
16829
- protocol: _enum(["http", "https"])
17043
+ sha256: string(),
17044
+ sizeBytes: number(),
17045
+ quantization: string(),
17046
+ /** Load-time guidance shown in the picker. */
17047
+ minRamBytes: number(),
17048
+ contextSizeDefault: number().int(),
17049
+ /** Vision models: companion projector file. */
17050
+ mmprojUrl: string().optional()
16830
17051
  });
16831
- var NetworkAccessStatusSchema = object({
16832
- connected: boolean(),
16833
- endpoint: NetworkEndpointSchema.nullable(),
17052
+ var LlmRuntimeNodeSchema = object({
17053
+ nodeId: string(),
17054
+ reachable: boolean(),
17055
+ status: LlmRuntimeStatusSchema.optional(),
17056
+ disk: LlmRuntimeDiskUsageSchema.optional(),
16834
17057
  error: string().optional()
16835
17058
  });
16836
- /**
16837
- * Optional, richer endpoint shape returned by providers that expose
16838
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
16839
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
16840
- * the originating provider config (mode + sourcePort) so the
16841
- * orchestrator UI can label rows distinctly. Providers that expose only
16842
- * one endpoint just omit `listEndpoints` from their provider impl.
16843
- */
16844
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16845
- /**
16846
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
16847
- * the orchestrator can dedupe across `listEndpoints` polls.
16848
- */
16849
- id: string(),
16850
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
16851
- label: string(),
16852
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
16853
- mode: string().optional(),
16854
- /** Originating local port the ingress fronts (informational). */
16855
- sourcePort: number().optional()
17059
+ var GenerateVisionInputSchema = LlmGenerateBaseInputSchema.extend({ images: array(LlmImageSchema).min(1) });
17060
+ var ProfileRefInputSchema = object({
17061
+ addonId: string(),
17062
+ profileId: string()
17063
+ });
17064
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
17065
+ kind: "mutation",
17066
+ auth: "admin"
17067
+ }), method(ProfileRefInputSchema, _void(), {
17068
+ kind: "mutation",
17069
+ auth: "admin"
17070
+ }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
17071
+ kind: "mutation",
17072
+ auth: "admin"
17073
+ }), method(ProfileRefInputSchema, array(string())), method(object({}), array(LlmDefaultSchema)), method(object({
17074
+ selector: LlmDefaultSelectorSchema,
17075
+ profileId: string().nullable()
17076
+ }), _void(), {
17077
+ kind: "mutation",
17078
+ auth: "admin"
17079
+ }), method(object({
17080
+ since: number().optional(),
17081
+ until: number().optional(),
17082
+ consumer: string().optional(),
17083
+ profileId: string().optional()
17084
+ }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
17085
+ nodeId: string(),
17086
+ model: ManagedModelRefSchema
17087
+ }), _void(), {
17088
+ kind: "mutation",
17089
+ auth: "admin"
17090
+ }), method(object({
17091
+ nodeId: string(),
17092
+ file: string()
17093
+ }), _void(), {
17094
+ kind: "mutation",
17095
+ auth: "admin"
17096
+ }), method(ProfileRefInputSchema, LlmRuntimeStatusSchema), method(ProfileRefInputSchema, LlmRuntimeStatusSchema, {
17097
+ kind: "mutation",
17098
+ auth: "admin"
17099
+ }), method(ProfileRefInputSchema, _void(), {
17100
+ kind: "mutation",
17101
+ auth: "admin"
17102
+ });
17103
+ var LogLevelSchema = _enum([
17104
+ "debug",
17105
+ "info",
17106
+ "warn",
17107
+ "error"
17108
+ ]);
17109
+ var LogEntrySchema = object({
17110
+ timestamp: date(),
17111
+ level: LogLevelSchema,
17112
+ scope: array(string()),
17113
+ message: string(),
17114
+ meta: record(string(), unknown()).optional(),
17115
+ tags: record(string(), string()).optional()
16856
17116
  });
16857
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
17117
+ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
17118
+ scope: array(string()).optional(),
17119
+ level: LogLevelSchema.optional(),
17120
+ since: date().optional(),
17121
+ until: date().optional(),
17122
+ limit: number().optional(),
17123
+ tags: record(string(), string()).optional()
17124
+ }), array(LogEntrySchema).readonly());
16858
17125
  /**
16859
- * notification-outputcanonical, capability-gated notification delivery.
17126
+ * `login-method`collection cap through which auth addons contribute
17127
+ * their pre-auth login surfaces to the login page. This is the SINGLE,
17128
+ * generic mechanism that supersedes the dead `auth.listProviders` reader:
17129
+ * every auth addon (OIDC, magic-link, WebAuthn/passkey) registers a
17130
+ * `login-method` provider and the PUBLIC `auth.listLoginMethods`
17131
+ * procedure aggregates them for the unauthenticated login page.
16860
17132
  *
16861
- * Apprise-derived model (see
16862
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16863
- * callers emit ONE canonical `Notification`; each provider declares a
16864
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
16865
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16866
- * message to what the kind supports — callers never special-case a service.
17133
+ * A contribution is a discriminated union on `kind`:
16867
17134
  *
16868
- * DESIGN DECISIONS (locked):
16869
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16870
- * `setTargetEnabled`), each provider persisting via the `settings-store`
16871
- * cap. Rationale: the admin UI needs one uniform surface across the
16872
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16873
- * alternative would fork the UI per addon and cannot host the
16874
- * discovery→adopt flow.
16875
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16876
- * the generated cap-mount auto-`concatCollection`-fans them across every
16877
- * registered provider (notifiers addon + HA addon) so one catalog is
16878
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16879
- * `addonId` the generated collection router extracts from the call input.
16880
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16881
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16882
- * `storage` / `storage-provider` / `recording` caps over the same path. No
16883
- * base64 fallback needed.
17135
+ * - `redirect` a declarative button. The login page renders a generic
17136
+ * button that navigates to `startUrl` (an addon-owned HTTP route).
17137
+ * Covers OIDC (`/addon/auth-oidc/<id>/start`) and magic-link with
17138
+ * ZERO shell-side JS. A future SSO addon plugs in the same way the
17139
+ * login page needs NO change.
16884
17140
  *
16885
- * TODO (deferred, closed-set change separate decision): add
16886
- * `providerKind: 'notify'` so notification providers surface on the unified
16887
- * admin "Integrations" page.
16888
- */
16889
- /**
16890
- * Zentik-derived typed-media enum — the superset across every kind. Each
16891
- * adapter picks what it supports and the degrade engine filters the rest.
16892
- */
16893
- var AttachmentMediaTypeSchema = _enum([
16894
- "image",
16895
- "video",
16896
- "gif",
16897
- "audio",
16898
- "icon"
16899
- ]);
16900
- /**
16901
- * A single attachment. Exactly one of `url` (remote source, most adapters
16902
- * prefer this) or `bytes` (inline source; required for Pushover-style
16903
- * bytes-only kinds) MUST be present the degrade engine expresses a
16904
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
17141
+ * - `widget` a Module-Federation widget the login page mounts (via
17142
+ * `loadRemoteBundle`) for an in-page ceremony. `auth.listLoginMethods`
17143
+ * stamps a public `bundleUrl` from `addonId` + `bundle`. Generic
17144
+ * mechanism kept for future use; no shipped addon uses it on the login
17145
+ * page (the passkey ceremony below runs natively in the shell instead).
17146
+ *
17147
+ * - `passkey` a declarative WebAuthn ceremony the shell renders
17148
+ * natively (`@simplewebauthn/browser` lives in `addon-admin-ui`, not in
17149
+ * a remotely-loaded bundle). Carries the addon's effective `rpId` /
17150
+ * `origin` (from its `resolveRpID()` / `resolveOrigin()`) so the shell
17151
+ * can gate visibility (IP-literal origin, hostname/rpId mismatch) WITHOUT
17152
+ * fetching any remote code pre-auth. Contribution stays unconditional —
17153
+ * enrollment state is never leaked pre-auth; visibility is a shell
17154
+ * decision.
17155
+ *
17156
+ * Every contribution carries a `stage`:
17157
+ * - `primary` — shown on the first credentials screen (OIDC /
17158
+ * magic-link buttons; a future usernameless passkey).
17159
+ * - `second-factor` — shown AFTER the password leg, gated on the
17160
+ * returned `factors` (passkey-as-2FA today).
17161
+ *
17162
+ * `mount: skip` — the cap is read server-side by the core auth router
17163
+ * (`registry.getCollection('login-method')`), never mounted as its own
17164
+ * tRPC router.
16905
17165
  */
16906
- var AttachmentSchema = object({
16907
- mediaType: AttachmentMediaTypeSchema,
16908
- url: string().optional(),
16909
- bytes: _instanceof(Uint8Array).optional(),
16910
- mime: string().optional(),
16911
- name: string().optional()
16912
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16913
- var NotificationFormatSchema = _enum([
16914
- "text",
16915
- "markdown",
16916
- "html"
17166
+ /** When a login method renders in the two-phase login flow. */
17167
+ var LoginStageEnum = _enum(["primary", "second-factor"]);
17168
+ /** One login-method contribution — redirect button, pre-auth widget, or native passkey ceremony. */
17169
+ var LoginMethodContributionSchema = discriminatedUnion("kind", [
17170
+ object({
17171
+ kind: literal("redirect"),
17172
+ /** Stable id within the login-method set (e.g. `auth-oidc/google`). */
17173
+ id: string(),
17174
+ /** Operator-facing button label. */
17175
+ label: string(),
17176
+ /** lucide-react icon name. */
17177
+ icon: string().optional(),
17178
+ /** Addon-owned HTTP route the button navigates to (GET). */
17179
+ startUrl: string(),
17180
+ stage: LoginStageEnum
17181
+ }),
17182
+ object({
17183
+ kind: literal("widget"),
17184
+ /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-login`). */
17185
+ id: string(),
17186
+ /** Owning addon id — drives the public bundle URL + the MF namespace. */
17187
+ addonId: string(),
17188
+ /** Bundle filename inside the addon's dist dir (`remoteEntry.js`). */
17189
+ bundle: string(),
17190
+ /** MF remote descriptor — `{ remoteName, exposedModule, componentKey }`. */
17191
+ remote: WidgetRemoteSchema,
17192
+ stage: LoginStageEnum
17193
+ }),
17194
+ object({
17195
+ kind: literal("passkey"),
17196
+ /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-direct-login`). */
17197
+ id: string(),
17198
+ /** Operator-facing button label. */
17199
+ label: string(),
17200
+ stage: LoginStageEnum,
17201
+ /** Effective WebAuthn RP ID (`resolveRpID()`) — the shell gates visibility on it. */
17202
+ rpId: string(),
17203
+ /** Effective expected origin (`resolveOrigin()`), null when unconfigured. */
17204
+ origin: string().nullable()
17205
+ })
16917
17206
  ]);
16918
- /** A single tap-through action button. */
16919
- var NotificationActionSchema = object({
16920
- id: string(),
16921
- label: string(),
16922
- url: string().optional()
17207
+ method(_void(), array(LoginMethodContributionSchema).readonly());
17208
+ var CpuBreakdownSchema = object({
17209
+ total: number(),
17210
+ user: number(),
17211
+ system: number(),
17212
+ irq: number(),
17213
+ nice: number(),
17214
+ loadAvg: tuple([
17215
+ number(),
17216
+ number(),
17217
+ number()
17218
+ ]),
17219
+ cores: number()
16923
17220
  });
16924
- /**
16925
- * The canonical notification. `body` is the only hard field (Apprise model).
16926
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16927
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16928
- * the adapter maps this ordinal onto its native level. `level?` is an
16929
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
16930
- * `priority` for that one target.
16931
- */
16932
- var NotificationSchema = object({
16933
- body: string(),
16934
- title: string().optional(),
16935
- format: NotificationFormatSchema.default("text"),
16936
- priority: number().int().min(1).max(5).default(3),
16937
- level: string().optional(),
16938
- attachments: array(AttachmentSchema).optional(),
16939
- clickUrl: string().optional(),
16940
- actions: array(NotificationActionSchema).optional(),
16941
- sound: string().optional(),
16942
- ttl: number().optional(),
16943
- tag: string().optional(),
16944
- deviceId: number().optional(),
16945
- eventId: string().optional(),
16946
- metadata: record(string(), unknown()).optional()
17221
+ var MemoryInfoSchema = object({
17222
+ percent: number(),
17223
+ totalBytes: number(),
17224
+ usedBytes: number(),
17225
+ availableBytes: number(),
17226
+ swapUsedBytes: number(),
17227
+ swapTotalBytes: number()
17228
+ });
17229
+ var DiskIoSnapshotSchema = object({
17230
+ readBytes: number(),
17231
+ writeBytes: number(),
17232
+ readOps: number(),
17233
+ writeOps: number(),
17234
+ timestampMs: number()
17235
+ });
17236
+ var NetworkIoSnapshotSchema = object({
17237
+ rxBytes: number(),
17238
+ txBytes: number(),
17239
+ rxPackets: number(),
17240
+ txPackets: number(),
17241
+ rxErrors: number(),
17242
+ txErrors: number(),
17243
+ timestampMs: number()
17244
+ });
17245
+ var MetricsGpuInfoSchema = object({
17246
+ utilization: number(),
17247
+ model: string(),
17248
+ memoryUsedBytes: number(),
17249
+ memoryTotalBytes: number(),
17250
+ temperature: number().nullable()
17251
+ });
17252
+ var ProcessResourceInfoSchema = object({
17253
+ openFds: number(),
17254
+ threadCount: number(),
17255
+ activeHandles: number(),
17256
+ activeRequests: number()
16947
17257
  });
16948
- /** One declared native severity/priority level for a kind. */
16949
- var TargetKindLevelSchema = object({
16950
- id: string(),
16951
- label: string(),
16952
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16953
- ordinal: number().int().min(1).max(5).nullable(),
16954
- flags: object({
16955
- critical: boolean().optional(),
16956
- silent: boolean().optional(),
16957
- noPush: boolean().optional()
16958
- }).optional(),
16959
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16960
- requires: array(string()).optional(),
16961
- description: string().optional()
17258
+ var PressureAvgsSchema = object({
17259
+ avg10: number(),
17260
+ avg60: number(),
17261
+ avg300: number()
16962
17262
  });
16963
- /** The full capability block consulted before dispatch. */
16964
- var TargetKindCapsSchema = object({
16965
- attachments: object({
16966
- mediaTypes: array(AttachmentMediaTypeSchema),
16967
- mode: _enum([
16968
- "url",
16969
- "bytes",
16970
- "both"
16971
- ]),
16972
- max: number().int().nonnegative(),
16973
- maxBytes: number().int().positive().optional()
17263
+ var PressureInfoSchema = object({
17264
+ some: PressureAvgsSchema,
17265
+ full: PressureAvgsSchema.nullable()
17266
+ });
17267
+ var SystemResourceSnapshotSchema = object({
17268
+ cpu: CpuBreakdownSchema,
17269
+ memory: MemoryInfoSchema,
17270
+ gpu: MetricsGpuInfoSchema.nullable(),
17271
+ network: NetworkIoSnapshotSchema,
17272
+ disk: DiskIoSnapshotSchema,
17273
+ pressure: object({
17274
+ cpu: PressureInfoSchema.nullable(),
17275
+ memory: PressureInfoSchema.nullable(),
17276
+ io: PressureInfoSchema.nullable()
16974
17277
  }),
16975
- /** Max action buttons (0 = none). */
16976
- actions: number().int().nonnegative(),
16977
- levels: array(TargetKindLevelSchema),
16978
- format: array(NotificationFormatSchema),
16979
- clickUrl: boolean(),
16980
- sound: boolean(),
16981
- ttl: boolean(),
16982
- bodyMaxLen: number().int().positive()
17278
+ process: ProcessResourceInfoSchema,
17279
+ cpuTemperature: number().nullable(),
17280
+ timestampMs: number()
16983
17281
  });
16984
- /**
16985
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16986
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16987
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`)
16988
- * the union is large and not meant for runtime validation here; the exported
16989
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16990
- */
16991
- var ConfigSchemaPassthrough = unknown();
16992
- var TargetKindSchema = object({
16993
- kind: string(),
16994
- label: string(),
16995
- icon: string(),
16996
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
16997
- addonId: string(),
16998
- configSchema: ConfigSchemaPassthrough,
16999
- supportsDiscovery: boolean(),
17000
- caps: TargetKindCapsSchema
17282
+ var DiskSpaceInfoSchema = object({
17283
+ path: string(),
17284
+ totalBytes: number(),
17285
+ usedBytes: number(),
17286
+ availableBytes: number(),
17287
+ percent: number()
17001
17288
  });
17002
- /**
17003
- * A persisted target. `config` holds secrets; providers REDACT secret fields
17004
- * (return a presence marker only) when serving `listTargets` — never
17005
- * round-trip a stored secret to the UI.
17006
- */
17007
- var TargetSchema = object({
17008
- id: string(),
17009
- name: string(),
17010
- kind: string(),
17289
+ var PidResourceStatsSchema = object({
17290
+ pid: number(),
17291
+ cpu: number(),
17292
+ memory: number(),
17293
+ /**
17294
+ * Private (anonymous) resident bytes — the per-process V8 heap + native
17295
+ * allocations NOT shared with other processes (Linux RssAnon). This is the
17296
+ * "real" per-runner cost; summing it across runners is meaningful, unlike
17297
+ * `memory` (RSS), which double-counts the shared mmap'd framework code.
17298
+ * Undefined where /proc is unavailable (e.g. macOS).
17299
+ */
17300
+ privateBytes: number().optional(),
17301
+ /**
17302
+ * Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
17303
+ * code shared copy-on-write across runners. Undefined on macOS.
17304
+ */
17305
+ sharedBytes: number().optional()
17306
+ });
17307
+ var AddonInstanceSchema = object({
17011
17308
  addonId: string(),
17012
- enabled: boolean(),
17013
- config: record(string(), unknown())
17309
+ nodeId: string(),
17310
+ role: _enum(["hub", "worker"]),
17311
+ pid: number(),
17312
+ state: _enum([
17313
+ "starting",
17314
+ "running",
17315
+ "stopping",
17316
+ "stopped",
17317
+ "crashed"
17318
+ ]),
17319
+ uptimeSec: number()
17014
17320
  });
17015
- /** A discovery-surfaced candidate (config is partial + non-secret). */
17016
- var DiscoveredTargetSchema = object({
17017
- kind: string(),
17018
- suggestedName: string(),
17019
- config: record(string(), unknown())
17321
+ var NodeProcessSchema = object({
17322
+ pid: number(),
17323
+ ppid: number(),
17324
+ pgid: number(),
17325
+ classification: _enum([
17326
+ "root",
17327
+ "managed",
17328
+ "system",
17329
+ "ghost"
17330
+ ]),
17331
+ /** `$process` addon binding when `managed`, else null. */
17332
+ addonId: string().nullable(),
17333
+ /** Kernel-reported nodeId when the process is a known agent/worker. */
17334
+ nodeId: string().nullable(),
17335
+ /** Truncated command line. */
17336
+ command: string(),
17337
+ cpuPercent: number(),
17338
+ memoryRssBytes: number(),
17339
+ /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
17340
+ uptimeSec: number(),
17341
+ /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
17342
+ orphaned: boolean()
17020
17343
  });
17021
- /** The degrade engine's report — what was resolved / dropped / degraded. */
17022
- var RenderedAsSchema = object({
17023
- level: string(),
17024
- format: NotificationFormatSchema,
17025
- attachmentsSent: number().int().nonnegative(),
17026
- actionsSent: number().int().nonnegative(),
17027
- truncated: boolean(),
17028
- dropped: array(string())
17344
+ var KillProcessInputSchema = object({
17345
+ pid: number(),
17346
+ /** Force = SIGKILL. Default is SIGTERM. */
17347
+ force: boolean().optional()
17029
17348
  });
17030
- var SendResultSchema = object({
17349
+ var KillProcessResultSchema = object({
17031
17350
  success: boolean(),
17032
- error: string().optional(),
17033
- renderedAs: RenderedAsSchema.optional()
17351
+ reason: string().optional(),
17352
+ signal: _enum(["SIGTERM", "SIGKILL"]).optional()
17353
+ });
17354
+ var DumpHeapSnapshotInputSchema = object({
17355
+ /** The addon whose runner should dump a heap snapshot. */
17356
+ addonId: string() });
17357
+ var DumpHeapSnapshotResultSchema = object({
17358
+ success: boolean(),
17359
+ /** Path of the written .heapsnapshot inside the runner's container/host. */
17360
+ path: string().optional(),
17361
+ /** Process pid that was signalled. */
17362
+ pid: number().optional(),
17363
+ reason: string().optional()
17364
+ });
17365
+ var SystemMetricsSchema = object({
17366
+ cpuPercent: number(),
17367
+ memoryPercent: number(),
17368
+ memoryUsedMB: number(),
17369
+ memoryTotalMB: number(),
17370
+ diskPercent: number().optional(),
17371
+ temperature: number().optional(),
17372
+ gpuPercent: number().optional(),
17373
+ gpuMemoryPercent: number().optional()
17374
+ });
17375
+ 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, {
17376
+ kind: "mutation",
17377
+ auth: "admin"
17378
+ }), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
17379
+ kind: "mutation",
17380
+ auth: "admin"
17381
+ });
17382
+ method(object({
17383
+ sourceUrl: string(),
17384
+ metadata: ModelConvertMetadataSchema,
17385
+ targets: array(ConvertTargetSchema).min(1).readonly(),
17386
+ calibrationRef: string().optional(),
17387
+ sessionId: string().optional()
17388
+ }), ConvertResultSchema, {
17389
+ kind: "mutation",
17390
+ auth: "admin",
17391
+ timeoutMs: 6e5
17392
+ });
17393
+ method(object({
17394
+ nodeId: string(),
17395
+ modelId: string(),
17396
+ format: _enum(MODEL_FORMATS),
17397
+ entry: ModelCatalogEntrySchema
17398
+ }), object({
17399
+ ok: boolean(),
17400
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
17401
+ sha256: string(),
17402
+ bytes: number(),
17403
+ /** The target node's modelsDir the artifact landed in. */
17404
+ path: string()
17405
+ }), {
17406
+ kind: "mutation",
17407
+ auth: "admin"
17034
17408
  });
17035
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
17036
- var TestResultSchema = SendResultSchema;
17037
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
17038
- kind: string(),
17039
- config: record(string(), unknown()).optional()
17040
- }), array(DiscoveredTargetSchema)), method(object({
17041
- targetId: string(),
17042
- notification: NotificationSchema
17043
- }), SendResultSchema, { kind: "mutation" }), method(object({
17044
- targetId: string(),
17045
- sample: NotificationSchema.optional()
17046
- }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
17047
- targetId: string(),
17048
- enabled: boolean()
17049
- }), _void(), { kind: "mutation" });
17050
17409
  /**
17051
- * notification-rulesthe Notification Center rule surface (P1 core).
17052
- *
17053
- * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
17054
- * (operator decisions D-1/D-2/D-3 are binding):
17410
+ * `mqtt-broker`broker-registry cap.
17055
17411
  *
17056
- * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
17057
- * `notification-center` module), hooked on the durable persistence
17058
- * moments (object-event insert, TrackCloser.closeExpired) with a
17059
- * persisted outbox + retry — never the lossy telemetry bus (D8).
17060
- * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
17061
- * FIRST persisted detection matching the conditions (per-track dedup,
17062
- * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
17063
- * `delivery: 'track-end'` evaluates the finalized track record at close.
17064
- * - DISPATCH stays behind `notification-output` (rules reference targets
17065
- * by id; per-backend params are a passthrough blob capped by the
17066
- * target kind's own caps/degrade engine).
17412
+ * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
17413
+ * MQTT brokers (external + optionally an embedded `aedes`-backed one)
17414
+ * and (b) the connection details a consumer addon needs to spin up
17415
+ * its OWN `mqtt.js` client.
17067
17416
  *
17068
- * P1 scope: admin-authored rules only (`createdBy` stamped from the
17069
- * server-injected caller identity the first `caller: 'required'`
17070
- * adopter). The P1 condition subset is: devices, classes(+exclude),
17071
- * minConfidence, admin zones (any/all + exclude), weekly schedule
17072
- * windows, and the optional label/identity/plate matchers. User rules,
17073
- * private zones, per-recipient fan-out and the wider condition table are
17074
- * P2+ (see spec §7).
17417
+ * Why: pub/sub routing over the system event-bus loses fidelity
17418
+ * (callback shape, QoS guarantees, will/retain semantics) and adds
17419
+ * refcount bookkeeping that addons would rather own themselves. The
17420
+ * canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
17421
+ * features anyway — give it the connection config, get out of the way.
17075
17422
  *
17076
- * All schemas here are the single source of truth — `NcRule` etc. are
17077
- * `z.infer` exports; no duplicate interfaces (the advanced-notifier
17078
- * schema/interface drift is explicitly not repeated).
17423
+ * Consumer flow:
17424
+ * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
17425
+ * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
17426
+ * client.subscribe('zigbee2mqtt/+')
17427
+ *
17428
+ * Collection mode: multiple brokers (e.g. one local mosquitto + one
17429
+ * cloud bridge). The "embedded" entry (when present) is just another
17430
+ * broker in the registry — its lifecycle is owned by the addon that
17431
+ * spawned it.
17079
17432
  */
17433
+ var BrokerKindSchema = _enum(["external", "embedded"]);
17080
17434
  /**
17081
- * D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
17082
- * The value maps 1:1 onto the evaluated record kind:
17083
- * - `immediate` ↔ object-event persist (lowest-latency detection burst)
17084
- * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
17085
- * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
17086
- * change of a LINKED device, one row per linked camera)
17087
- * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
17088
- * delivery / pick-up)
17435
+ * Broker live-probe status.
17089
17436
  *
17090
- * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
17091
- * `package-event` are pure trigger kinds (no urgency dimension). Extending
17092
- * this one field keeps the schema additive a rule still declares exactly
17093
- * one trigger.
17437
+ * - `connected` last probe completed a clean CONNACK
17438
+ * - `disconnected` no probe has run yet (cold cache)
17439
+ * - `auth-failed` CONNACK refused with auth error (RC 4 / 5)
17440
+ * - `unreachable` — TCP connect timed out / refused
17441
+ * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
17094
17442
  */
17095
- var NcDeliverySchema = _enum([
17096
- "immediate",
17097
- "track-end",
17098
- "device-event",
17099
- "package-event"
17443
+ var BrokerStatusSchema$1 = _enum([
17444
+ "connected",
17445
+ "disconnected",
17446
+ "auth-failed",
17447
+ "unreachable",
17448
+ "tls-error"
17100
17449
  ]);
17101
- /** Weekly schedule — OR of windows; absence on the rule = always active. */
17102
- var NcScheduleSchema = object({
17103
- windows: array(object({
17104
- /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
17105
- days: array(number().int().min(0).max(6)).min(1),
17106
- startMinute: number().int().min(0).max(1439),
17107
- endMinute: number().int().min(0).max(1439)
17108
- })).min(1),
17109
- /** IANA timezone; default = hub host timezone. */
17110
- timezone: string().optional(),
17111
- /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
17112
- invert: boolean().optional()
17113
- });
17114
- /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
17115
- var NcPlateMatcherSchema = object({
17116
- values: array(string().min(1)).min(1),
17117
- /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
17118
- maxDistance: number().int().min(0).max(3).default(1)
17119
- });
17120
- /**
17121
- * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
17122
- * occupancy edge for a device — optionally narrowed to a single admin
17123
- * `zoneId` and/or object `className`. `op` selects the edge/threshold:
17124
- * - `became-occupied` (default) — count crossed 0 → ≥ `count`
17125
- * - `became-free` — count crossed ≥ `count` → below it
17126
- * - `>=` / `<=` — count is at/over or at/under `count`
17127
- * `sustainSeconds` requires the condition hold continuously that long
17128
- * before firing (debounces flicker; 0 = fire on the first matching edge).
17129
- * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
17130
- * the condition never matches. Confirmed edge-state survives addon restarts
17131
- * (declared SQLite collection, reseeded on boot).
17132
- */
17133
- var NcOccupancyConditionSchema = object({
17134
- /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
17135
- zoneId: string().optional(),
17136
- /** Object class to count; absent = any class. */
17137
- className: string().optional(),
17138
- op: _enum([
17139
- "became-occupied",
17140
- "became-free",
17141
- ">=",
17142
- "<="
17143
- ]).default("became-occupied"),
17144
- count: number().int().min(0).default(1),
17145
- sustainSeconds: number().int().min(0).max(3600).default(15)
17146
- });
17147
- /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
17148
- var NcZoneConditionSchema = object({
17149
- ids: array(string().min(1)).min(1),
17150
- /** Quantifier over `ids` — at least one / every one visited. */
17151
- match: _enum(["any", "all"]).default("any")
17152
- });
17153
- /**
17154
- * The P1 condition set — a flat AND of groups; absent group = pass;
17155
- * membership lists are OR within the list (spec §2.3).
17156
- */
17157
- var NcConditionsSchema = object({
17158
- /** Device scope — absent = all devices. */
17159
- devices: array(number()).optional(),
17160
- /** Detector class names (any overlap with the record's class set). */
17161
- classes: array(string().min(1)).optional(),
17162
- /** Veto classes — any overlap fails the rule. */
17163
- classesExclude: array(string().min(1)).optional(),
17164
- /** Minimum detection confidence 0–1 (fails when the record has none). */
17165
- minConfidence: number().min(0).max(1).optional(),
17166
- /** Admin zone membership over event `zones` / track `zonesVisited`. */
17167
- zones: NcZoneConditionSchema.optional(),
17168
- /** Veto zones — any hit fails the rule. */
17169
- zonesExclude: array(string().min(1)).optional(),
17170
- /**
17171
- * Exact (case-insensitive) match on the record's collapsed `label`
17172
- * (identity name / plate text / subclass).
17173
- */
17174
- labelEquals: array(string().min(1)).optional(),
17175
- /**
17176
- * Identity matcher. P1 boundary: matched against the record's collapsed
17177
- * `label` (the identity display name propagated by the face pipeline) —
17178
- * identity-ID matching rides in P2 when identity ids reach the record.
17179
- */
17180
- identities: array(string().min(1)).optional(),
17181
- /** Fuzzy plate matcher against the record's `label` (plate text). */
17182
- plates: NcPlateMatcherSchema.optional(),
17183
- /**
17184
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
17185
- * Same P1 boundary: matched against the record's collapsed `label` (the
17186
- * identity display name). A record with NO label passes (nothing to
17187
- * exclude), unlike the include variant which fails on an absent label.
17188
- */
17189
- identitiesExclude: array(string().min(1)).optional(),
17190
- /**
17191
- * Minimum server-computed key-event importance in [0,1] (`Track.importance`).
17192
- * TRACK-END only: importance is scored at track close, so it does not exist
17193
- * at immediate / object-event evaluation time (see catalog `appliesTo`). At
17194
- * close the value is threaded via the close-time info (the `Track` clone is
17195
- * captured before the DB row is updated, so it would otherwise read stale).
17196
- * Fails when the record carries no importance (never guess quality — the
17197
- * `minConfidence` precedent). MVP cut: a single scalar threshold.
17198
- */
17199
- minImportance: number().min(0).max(1).optional(),
17200
- /**
17201
- * Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
17202
- * TRACK-END only: an `immediate` / object-event subject has no closed
17203
- * lifespan, so a dwell condition never matches immediate delivery
17204
- * (documented choice — the object-event record carries no `firstSeen`,
17205
- * so dwell cannot be computed from what the subject actually carries).
17206
- */
17207
- minDwellSeconds: number().min(0).optional(),
17208
- /**
17209
- * Detection provenance filter. `any` (default / absent) matches every
17210
- * source; otherwise the subject's source must equal it. Legacy records
17211
- * with no stamped source are treated as `pipeline`. The union spans both
17212
- * record kinds — object events carry `pipeline` | `onboard`, synthetic
17213
- * tracks carry `sensor`.
17214
- */
17215
- source: _enum([
17216
- "pipeline",
17217
- "onboard",
17218
- "sensor",
17219
- "any"
17220
- ]).optional(),
17221
- /**
17222
- * Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
17223
- * detector `minConfidence` (that gates the object-detection score; this
17224
- * gates the recognition/OCR match score). Fails when the subject carries
17225
- * no label-match confidence (never guess). TRACK-END only: the confidence
17226
- * lives on the recognition result and reaches the subject at track close.
17227
- *
17228
- * What it measures precisely (plumbed at track close — the closer threads
17229
- * the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
17230
- * `importance`): the BEST recognition match confidence observed for the
17231
- * label the track carries at close — for a face, the peak cosine similarity
17232
- * of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
17233
- * for a plate, the peak OCR read score of the best-held plate
17234
- * (`plateText.confidence`). When BOTH a face and a plate were recognized on
17235
- * one track the higher of the two is used. A track that ended with no
17236
- * confident identity/plate match carries no value, so the condition fails
17237
- * closed for it (an un-recognized subject).
17238
- */
17239
- minLabelConfidence: number().min(0).max(1).optional(),
17240
- /**
17241
- * DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
17242
- * e.g. a doorbell `press` / `press_long`) — matched case-insensitively
17243
- * against the token carried on the device-event subject (extracted from the
17244
- * event-emitter runtime slice's `lastEvent.eventType`). Fails when the
17245
- * subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
17246
- * eventType, so gate those with {@link sensorKinds} instead.
17247
- */
17248
- eventTypeTokens: array(string().min(1)).optional(),
17249
- /**
17250
- * DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
17251
- * `contact`, `button`, `device-event`) — matched against the persisted
17252
- * `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
17253
- */
17254
- sensorKinds: array(string().min(1)).optional(),
17255
- /**
17256
- * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
17257
- * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
17258
- * when the subject's phase does not match (a subject always carries a phase
17259
- * on the package-event trigger).
17260
- */
17261
- packagePhase: _enum([
17262
- "delivered",
17263
- "picked-up",
17264
- "both"
17265
- ]).optional(),
17266
- /**
17267
- * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
17268
- * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
17269
- * listed polygon (ZoneEngine membership semantics). Evaluated only when
17270
- * the subject carries a bbox; absent bbox ⇒ the condition FAILS.
17271
- */
17272
- customZones: array(MaskPolygonShapeSchema).optional(),
17273
- /**
17274
- * DEVICE-EVENT only. ZoneAnalytics occupancy edge — fires when a device's
17275
- * (optionally zone/class-scoped) occupancy count crosses the configured
17276
- * threshold and holds for `sustainSeconds`. Fail-closed on missing
17277
- * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
17278
- */
17279
- occupancy: NcOccupancyConditionSchema.optional()
17280
- });
17281
- /** One delivery target: a `notification-output` Target ref + passthrough params. */
17282
- var NcRuleTargetSchema = object({
17283
- /** `notification-output` Target id. */
17284
- targetId: string().min(1),
17285
- /**
17286
- * Per-backend passthrough. Recognized keys are mapped onto the canonical
17287
- * Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
17288
- * degrade engine drops what the backend can't render.
17289
- */
17290
- params: record(string(), unknown()).optional()
17450
+ var BrokerInfoSchema = object({
17451
+ id: string(),
17452
+ name: string(),
17453
+ url: string(),
17454
+ kind: BrokerKindSchema,
17455
+ status: BrokerStatusSchema$1,
17456
+ latencyMs: number().nullable(),
17457
+ error: string().optional(),
17458
+ /** Embedded brokers only: number of MQTT clients currently connected. */
17459
+ connectedClients: number().int().nonnegative().optional(),
17460
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
17461
+ lastCheckedAt: number().optional()
17291
17462
  });
17292
17463
  /**
17293
- * Media attachment policy (P1 still-image subset).
17294
- * - `best` the best AVAILABLE subject image at dispatch time (D-3).
17295
- * - `best-matching` the media that explains WHY the rule fired: a rule
17296
- * matched on identities attaches the subject's `faceCrop`, one matched on
17297
- * plates attaches the `plateCrop`; a rule with no identity/plate condition
17298
- * (or when the specific crop is missing) degrades to `best`, then
17299
- * `keyFrame`, then no attachment — never delaying the send. The matched
17300
- * condition summary is frozen on the outbox row at enqueue (like the rule
17301
- * name), so the choice never drifts from the record that fired it.
17302
- * - `keyFrame` — the clean scene frame (no subject box).
17303
- * - `none` — no attachment.
17464
+ * Connection details what a consumer needs to call
17465
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
17466
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
17467
+ * instead of stuffing creds into the URL (which leaks them into logs).
17304
17468
  */
17305
- var NcMediaPolicySchema = object({ attach: _enum([
17306
- "best",
17307
- "best-matching",
17308
- "keyFrame",
17309
- "none"
17310
- ]).default("best") });
17311
- /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
17312
- var NcThrottleSchema = object({
17313
- cooldownSec: number().int().min(0).max(86400).default(60),
17314
- /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
17315
- scope: _enum(["rule", "rule-device"]).default("rule-device")
17316
- });
17317
- /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
17318
- var NcRuleInputSchema = object({
17319
- name: string().min(1).max(200),
17320
- enabled: boolean().default(true),
17321
- delivery: NcDeliverySchema,
17322
- conditions: NcConditionsSchema.default({}),
17323
- schedule: NcScheduleSchema.optional(),
17324
- targets: array(NcRuleTargetSchema).min(1),
17325
- media: NcMediaPolicySchema.default({ attach: "best" }),
17326
- throttle: NcThrottleSchema.default({
17327
- cooldownSec: 60,
17328
- scope: "rule-device"
17329
- }),
17330
- /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
17331
- template: object({
17332
- title: string().max(500).optional(),
17333
- body: string().max(2e3).optional()
17334
- }).optional(),
17335
- /** Canonical notification priority ordinal (1..5); per-target overridable. */
17336
- priority: number().int().min(1).max(5).default(3),
17469
+ var BrokerConnectionDetailsSchema = object({
17470
+ url: string(),
17471
+ username: string().optional(),
17472
+ password: string().optional(),
17337
17473
  /**
17338
- * Ownership/visibility key. Absent = admin/global rule (unchanged legacy
17339
- * behaviour, visible to all, read-only in the viewer). Present = personal
17340
- * rule owned by this userId. Server-stamped; never trusted from a client.
17474
+ * Suggested prefix for `clientId`. Each consumer should suffix this
17475
+ * with its own discriminator (addon id, instance id) so reconnects
17476
+ * don't kick each other off (MQTT spec: clientId must be unique per
17477
+ * broker).
17341
17478
  */
17342
- ownerUserId: string().optional()
17479
+ clientIdPrefix: string().optional()
17480
+ });
17481
+ var AddBrokerInputSchema = object({
17482
+ name: string().min(1),
17483
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
17484
+ username: string().optional(),
17485
+ password: string().optional(),
17486
+ clientIdPrefix: string().optional()
17487
+ });
17488
+ var AddBrokerResultSchema = object({ id: string() });
17489
+ var IdInputSchema = object({ id: string() });
17490
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
17491
+ ok: literal(true),
17492
+ latencyMs: number()
17493
+ }), object({
17494
+ ok: literal(false),
17495
+ error: string()
17496
+ })]);
17497
+ var StartEmbeddedInputSchema = object({
17498
+ port: number().int().min(1).max(65535).default(1883),
17499
+ /** Allow anonymous connect (no username/password). Default: false. */
17500
+ allowAnonymous: boolean().default(false),
17501
+ /** Optional shared username/password for clients. */
17502
+ username: string().optional(),
17503
+ password: string().optional()
17504
+ });
17505
+ var StartEmbeddedResultSchema = object({
17506
+ id: string(),
17507
+ url: string()
17508
+ });
17509
+ var StatusSchema = object({
17510
+ brokerCount: number(),
17511
+ embeddedRunning: boolean()
17512
+ });
17513
+ 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);
17514
+ var NetworkEndpointSchema = object({
17515
+ url: string(),
17516
+ hostname: string(),
17517
+ port: number(),
17518
+ protocol: _enum(["http", "https"])
17519
+ });
17520
+ var NetworkAccessStatusSchema = object({
17521
+ connected: boolean(),
17522
+ endpoint: NetworkEndpointSchema.nullable(),
17523
+ error: string().optional()
17343
17524
  });
17344
17525
  /**
17345
- * Partial patch for `updateRule` any subset of the input fields, plus the
17346
- * persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
17347
- * NOT a client-authored input field (it lives on the persisted rule, not the
17348
- * input), so it is added here explicitly to let the store's per-target opt-out
17349
- * toggle round-trip through the shared `update` path. Viewer opt-out mutations
17350
- * still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
17351
- * `updateRule` patch.
17526
+ * Optional, richer endpoint shape returned by providers that expose
17527
+ * MORE than one ingress concurrently (Tailscale Ingress with mixed
17528
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
17529
+ * the originating provider config (mode + sourcePort) so the
17530
+ * orchestrator UI can label rows distinctly. Providers that expose only
17531
+ * one endpoint just omit `listEndpoints` from their provider impl.
17352
17532
  */
17353
- var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
17354
- /** A persisted rule. */
17355
- var NcRuleSchema = NcRuleInputSchema.extend({
17356
- id: string(),
17357
- /** userId of the admin who created the rule (server-stamped caller). */
17358
- createdBy: string(),
17359
- createdAt: number(),
17360
- updatedAt: number(),
17533
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
17361
17534
  /**
17362
- * Per-target opt-out set. A targetId here is suppressed for THIS rule at
17363
- * send time. Only a target's OWNER may add/remove its id (server-checked
17364
- * in `nc.setRuleTargetEnabled`). Defaults to empty.
17535
+ * Stable id within the provider typically `<mode>-<sourcePort>` so
17536
+ * the orchestrator can dedupe across `listEndpoints` polls.
17365
17537
  */
17366
- disabledTargetIds: array(string()).default([])
17367
- });
17368
- var NcTestResultSchema = object({
17369
- recordId: string(),
17370
- recordKind: _enum([
17371
- "object-event",
17372
- "track",
17373
- "device-event",
17374
- "package-event"
17375
- ]),
17376
- deviceId: number(),
17377
- timestamp: number(),
17378
- wouldFire: boolean(),
17379
- /** Condition id that failed (first failing group), when `wouldFire` is false. */
17380
- failedCondition: string().optional(),
17381
- className: string().optional(),
17382
- label: string().optional()
17538
+ id: string(),
17539
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
17540
+ label: string(),
17541
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
17542
+ mode: string().optional(),
17543
+ /** Originating local port the ingress fronts (informational). */
17544
+ sourcePort: number().optional()
17383
17545
  });
17384
- var NcConditionDescriptorSchema = object({
17385
- /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
17546
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
17547
+ /**
17548
+ * notification-output — canonical, capability-gated notification delivery.
17549
+ *
17550
+ * Apprise-derived model (see
17551
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
17552
+ * callers emit ONE canonical `Notification`; each provider declares a
17553
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
17554
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
17555
+ * message to what the kind supports — callers never special-case a service.
17556
+ *
17557
+ * DESIGN DECISIONS (locked):
17558
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
17559
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
17560
+ * cap. Rationale: the admin UI needs one uniform surface across the
17561
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
17562
+ * alternative would fork the UI per addon and cannot host the
17563
+ * discovery→adopt flow.
17564
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
17565
+ * the generated cap-mount auto-`concatCollection`-fans them across every
17566
+ * registered provider (notifiers addon + HA addon) so one catalog is
17567
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
17568
+ * `addonId` the generated collection router extracts from the call input.
17569
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
17570
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
17571
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
17572
+ * base64 fallback needed.
17573
+ *
17574
+ * TODO (deferred, closed-set change — separate decision): add
17575
+ * `providerKind: 'notify'` so notification providers surface on the unified
17576
+ * admin "Integrations" page.
17577
+ */
17578
+ /**
17579
+ * Zentik-derived typed-media enum — the superset across every kind. Each
17580
+ * adapter picks what it supports and the degrade engine filters the rest.
17581
+ */
17582
+ var AttachmentMediaTypeSchema = _enum([
17583
+ "image",
17584
+ "video",
17585
+ "gif",
17586
+ "audio",
17587
+ "icon"
17588
+ ]);
17589
+ /**
17590
+ * A single attachment. Exactly one of `url` (remote source, most adapters
17591
+ * prefer this) or `bytes` (inline source; required for Pushover-style
17592
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
17593
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
17594
+ */
17595
+ var AttachmentSchema = object({
17596
+ mediaType: AttachmentMediaTypeSchema,
17597
+ url: string().optional(),
17598
+ bytes: _instanceof(Uint8Array).optional(),
17599
+ mime: string().optional(),
17600
+ name: string().optional()
17601
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
17602
+ var NotificationFormatSchema = _enum([
17603
+ "text",
17604
+ "markdown",
17605
+ "html"
17606
+ ]);
17607
+ /** A single tap-through action button. */
17608
+ var NotificationActionSchema = object({
17609
+ id: string(),
17610
+ label: string(),
17611
+ url: string().optional()
17612
+ });
17613
+ /**
17614
+ * The canonical notification. `body` is the only hard field (Apprise model).
17615
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
17616
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
17617
+ * the adapter maps this ordinal onto its native level. `level?` is an
17618
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
17619
+ * `priority` for that one target.
17620
+ */
17621
+ var NotificationSchema = object({
17622
+ body: string(),
17623
+ title: string().optional(),
17624
+ format: NotificationFormatSchema.default("text"),
17625
+ priority: number().int().min(1).max(5).default(3),
17626
+ level: string().optional(),
17627
+ attachments: array(AttachmentSchema).optional(),
17628
+ clickUrl: string().optional(),
17629
+ actions: array(NotificationActionSchema).optional(),
17630
+ sound: string().optional(),
17631
+ ttl: number().optional(),
17632
+ tag: string().optional(),
17633
+ deviceId: number().optional(),
17634
+ eventId: string().optional(),
17635
+ metadata: record(string(), unknown()).optional()
17636
+ });
17637
+ /** One declared native severity/priority level for a kind. */
17638
+ var TargetKindLevelSchema = object({
17386
17639
  id: string(),
17387
- group: _enum([
17388
- "scope",
17389
- "class",
17390
- "zones",
17391
- "quality",
17392
- "label",
17393
- "schedule",
17394
- "device",
17395
- "package",
17396
- "occupancy"
17397
- ]),
17398
17640
  label: string(),
17399
- /** Editor widget the UI renders never hardcode per-condition forms. */
17400
- valueType: _enum([
17401
- "deviceIdList",
17402
- "stringList",
17403
- "number01",
17404
- "number",
17405
- "sourceSelect",
17406
- "zoneSelection",
17407
- "zoneIdList",
17408
- "schedule",
17409
- "plateMatcher",
17410
- "packagePhase",
17411
- "polygonDraw",
17412
- "occupancy"
17413
- ]),
17414
- operator: _enum([
17415
- "in",
17416
- "notIn",
17417
- "anyOf",
17418
- "allOf",
17419
- "gte",
17420
- "fuzzyIn",
17421
- "withinSchedule"
17422
- ]),
17423
- /** Which delivery kinds the condition applies to. */
17424
- appliesTo: array(NcDeliverySchema),
17425
- phase: string(),
17641
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
17642
+ ordinal: number().int().min(1).max(5).nullable(),
17643
+ flags: object({
17644
+ critical: boolean().optional(),
17645
+ silent: boolean().optional(),
17646
+ noPush: boolean().optional()
17647
+ }).optional(),
17648
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
17649
+ requires: array(string()).optional(),
17426
17650
  description: string().optional()
17427
17651
  });
17652
+ /** The full capability block consulted before dispatch. */
17653
+ var TargetKindCapsSchema = object({
17654
+ attachments: object({
17655
+ mediaTypes: array(AttachmentMediaTypeSchema),
17656
+ mode: _enum([
17657
+ "url",
17658
+ "bytes",
17659
+ "both"
17660
+ ]),
17661
+ max: number().int().nonnegative(),
17662
+ maxBytes: number().int().positive().optional()
17663
+ }),
17664
+ /** Max action buttons (0 = none). */
17665
+ actions: number().int().nonnegative(),
17666
+ levels: array(TargetKindLevelSchema),
17667
+ format: array(NotificationFormatSchema),
17668
+ clickUrl: boolean(),
17669
+ sound: boolean(),
17670
+ ttl: boolean(),
17671
+ bodyMaxLen: number().int().positive()
17672
+ });
17428
17673
  /**
17429
- * The delivery lifecycle status of a history row a straight read of the
17430
- * durable outbox row's own status (single source of truth):
17431
- * - `pending` — enqueued, in-flight or retrying with backoff
17432
- * - `sent` — delivered (terminal)
17433
- * - `dead` dead-lettered after exhausting retries / a permanent
17434
- * backend rejection / a deleted target (terminal; carries
17435
- * the failure `error`)
17436
- *
17437
- * P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
17438
- * user dimension (quiet hours / snooze) and are additive when they land.
17674
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
17675
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
17676
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`)
17677
+ * the union is large and not meant for runtime validation here; the exported
17678
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
17439
17679
  */
17440
- var NcHistoryStatusSchema = _enum([
17441
- "pending",
17442
- "sent",
17443
- "dead"
17444
- ]);
17445
- /** The evaluated record kind a history row descends from (one per trigger). */
17446
- var NcHistoryRecordKindSchema = _enum([
17447
- "object-event",
17448
- "track-end",
17449
- "device-event",
17450
- "package-event"
17451
- ]);
17452
- /** Subject summary frozen on the row at fire time (survives rule/record edits). */
17453
- var NcHistorySubjectSchema = object({
17454
- className: string(),
17455
- label: string().optional(),
17456
- confidence: number().optional(),
17457
- zones: array(string()),
17458
- timestamp: number()
17680
+ var ConfigSchemaPassthrough = unknown();
17681
+ var TargetKindSchema = object({
17682
+ kind: string(),
17683
+ label: string(),
17684
+ icon: string(),
17685
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
17686
+ addonId: string(),
17687
+ configSchema: ConfigSchemaPassthrough,
17688
+ supportsDiscovery: boolean(),
17689
+ caps: TargetKindCapsSchema
17459
17690
  });
17460
17691
  /**
17461
- * One delivery-history row. This is a read-only VIEW over the durable
17462
- * outbox row (single source of truth the same row the drain loop drives;
17463
- * NO second write path, so history can never drift from delivery state).
17464
- * The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
17465
- * `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
17466
- * (fire) / `updatedAt` (last transition), `status` + `error` are the
17467
- * lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
17468
- * enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
17469
- * P1 (admin scope only).
17692
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
17693
+ * (return a presence marker only) when serving `listTargets` never
17694
+ * round-trip a stored secret to the UI.
17470
17695
  */
17471
- var NcHistoryEntrySchema = object({
17472
- /** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
17696
+ var TargetSchema = object({
17473
17697
  id: string(),
17474
- ruleId: string(),
17475
- /** Rule name frozen at fire time (outlives a later rename / delete). */
17476
- ruleName: string(),
17477
- /** The rule urgency/trigger that produced this delivery. */
17478
- delivery: NcDeliverySchema,
17479
- targetId: string(),
17480
- deviceId: number(),
17481
- recordKind: NcHistoryRecordKindSchema,
17482
- /** Event / track ref of the evaluated record (§3.2 `eventRef`). */
17483
- recordId: string(),
17484
- /** Present for track-scoped deliveries (object-event / track-end). */
17485
- trackId: string().optional(),
17486
- status: NcHistoryStatusSchema,
17487
- /** Delivery attempts made so far. */
17488
- attempts: number().int(),
17489
- /** Fire time (outbox enqueue). */
17490
- createdAt: number(),
17491
- /** Last transition time (terminal for sent / dead). */
17492
- updatedAt: number(),
17493
- /** Failure detail — present on a `dead` row. */
17494
- error: string().optional(),
17495
- subject: NcHistorySubjectSchema
17698
+ name: string(),
17699
+ kind: string(),
17700
+ addonId: string(),
17701
+ enabled: boolean(),
17702
+ config: record(string(), unknown())
17496
17703
  });
17497
- /**
17498
- * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
17499
- * AND; absent = unbounded on that axis. `since`/`until` bound the fire time
17500
- * (`createdAt`, epoch ms, inclusive). `limit` is clamped to
17501
- * {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
17502
- */
17503
- var NcHistoryFilterSchema = object({
17504
- ruleId: string().optional(),
17505
- deviceId: number().optional(),
17506
- status: NcHistoryStatusSchema.optional(),
17507
- since: number().optional(),
17508
- until: number().optional(),
17509
- limit: number().int().min(1).max(500).default(100)
17704
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
17705
+ var DiscoveredTargetSchema = object({
17706
+ kind: string(),
17707
+ suggestedName: string(),
17708
+ config: record(string(), unknown())
17510
17709
  });
17511
- 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 }), {
17512
- kind: "mutation",
17513
- auth: "admin",
17514
- caller: "required"
17515
- }), method(object({
17516
- ruleId: string(),
17517
- patch: NcRulePatchSchema
17518
- }), object({ rule: NcRuleSchema }), {
17519
- kind: "mutation",
17520
- auth: "admin",
17521
- caller: "required"
17522
- }), method(object({ ruleId: string() }), object({ success: literal(true) }), {
17523
- kind: "mutation",
17524
- auth: "admin"
17525
- }), method(object({
17526
- ruleId: string(),
17710
+ /** The degrade engine's report what was resolved / dropped / degraded. */
17711
+ var RenderedAsSchema = object({
17712
+ level: string(),
17713
+ format: NotificationFormatSchema,
17714
+ attachmentsSent: number().int().nonnegative(),
17715
+ actionsSent: number().int().nonnegative(),
17716
+ truncated: boolean(),
17717
+ dropped: array(string())
17718
+ });
17719
+ var SendResultSchema = object({
17720
+ success: boolean(),
17721
+ error: string().optional(),
17722
+ renderedAs: RenderedAsSchema.optional()
17723
+ });
17724
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
17725
+ var TestResultSchema = SendResultSchema;
17726
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
17727
+ kind: string(),
17728
+ config: record(string(), unknown()).optional()
17729
+ }), array(DiscoveredTargetSchema)), method(object({
17730
+ targetId: string(),
17731
+ notification: NotificationSchema
17732
+ }), SendResultSchema, { kind: "mutation" }), method(object({
17733
+ targetId: string(),
17734
+ sample: NotificationSchema.optional()
17735
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
17736
+ targetId: string(),
17527
17737
  enabled: boolean()
17528
- }), object({ success: literal(true) }), {
17529
- kind: "mutation",
17530
- auth: "admin"
17531
- }), method(object({
17532
- rule: NcRuleInputSchema,
17533
- lookbackMinutes: number().int().min(1).max(1440).default(60)
17534
- }), object({ results: array(NcTestResultSchema) }), {
17535
- kind: "mutation",
17536
- auth: "admin"
17537
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
17738
+ }), _void(), { kind: "mutation" });
17538
17739
  /**
17539
17740
  * Zod schemas for persisted record types.
17540
17741
  *
@@ -22801,6 +23002,12 @@ Object.freeze({
22801
23002
  addonId: null,
22802
23003
  access: "delete"
22803
23004
  },
23005
+ "backup.deleteSchedule": {
23006
+ capName: "backup",
23007
+ capScope: "system",
23008
+ addonId: null,
23009
+ access: "delete"
23010
+ },
22804
23011
  "backup.getEntries": {
22805
23012
  capName: "backup",
22806
23013
  capScope: "system",
@@ -22831,6 +23038,12 @@ Object.freeze({
22831
23038
  addonId: null,
22832
23039
  access: "view"
22833
23040
  },
23041
+ "backup.listSchedules": {
23042
+ capName: "backup",
23043
+ capScope: "system",
23044
+ addonId: null,
23045
+ access: "view"
23046
+ },
22834
23047
  "backup.previewSchedule": {
22835
23048
  capName: "backup",
22836
23049
  capScope: "system",
@@ -22855,6 +23068,12 @@ Object.freeze({
22855
23068
  addonId: null,
22856
23069
  access: "create"
22857
23070
  },
23071
+ "backup.upsertSchedule": {
23072
+ capName: "backup",
23073
+ capScope: "system",
23074
+ addonId: null,
23075
+ access: "create"
23076
+ },
22858
23077
  "battery.wakeForStream": {
22859
23078
  capName: "battery",
22860
23079
  capScope: "device",
@@ -26689,6 +26908,36 @@ Object.freeze({
26689
26908
  addonId: null,
26690
26909
  access: "create"
26691
26910
  },
26911
+ "terminalSession.close": {
26912
+ capName: "terminal-session",
26913
+ capScope: "system",
26914
+ addonId: null,
26915
+ access: "create"
26916
+ },
26917
+ "terminalSession.listProfiles": {
26918
+ capName: "terminal-session",
26919
+ capScope: "system",
26920
+ addonId: null,
26921
+ access: "view"
26922
+ },
26923
+ "terminalSession.listSessions": {
26924
+ capName: "terminal-session",
26925
+ capScope: "system",
26926
+ addonId: null,
26927
+ access: "view"
26928
+ },
26929
+ "terminalSession.openSession": {
26930
+ capName: "terminal-session",
26931
+ capScope: "system",
26932
+ addonId: null,
26933
+ access: "create"
26934
+ },
26935
+ "terminalSession.resize": {
26936
+ capName: "terminal-session",
26937
+ capScope: "system",
26938
+ addonId: null,
26939
+ access: "create"
26940
+ },
26692
26941
  "toast.onToast": {
26693
26942
  capName: "toast",
26694
26943
  capScope: "system",