@camstack/addon-provider-onvif 1.2.5 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +1881 -1632
  2. package/dist/addon.mjs +1881 -1632
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -7517,16 +7517,23 @@ var StorageLocationDeclarationSchema = object({
7517
7517
  * Which node root the seeded `<id>:default` instance is placed under on a
7518
7518
  * FRESH install:
7519
7519
  * - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
7520
- * the appData volume. Right for small/durable data (backups, logs, models).
7520
+ * the appData volume. Right for small/durable data (logs, models).
7521
7521
  * - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
7522
7522
  * env is set, else falls back to the data root. Right for bulky, hot media
7523
7523
  * (recordings, event media) that should stay off the appData disk.
7524
+ * - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
7525
+ * `/backups` in the image) so archives live on their own mount rather than
7526
+ * filling the appData disk. Falls back to the data root when unset.
7524
7527
  *
7525
7528
  * Only affects the seeded default's `basePath`; operators can repoint any
7526
7529
  * location afterwards, and a `defaultsTo` slot inherits its parent's root
7527
7530
  * regardless of this field. Absent (the common case) is treated as `'data'`.
7528
7531
  */
7529
- defaultRoot: _enum(["data", "media"]).optional()
7532
+ defaultRoot: _enum([
7533
+ "data",
7534
+ "media",
7535
+ "backup"
7536
+ ]).optional()
7530
7537
  });
7531
7538
  var DecoderStatsSchema = object({
7532
7539
  inputFps: number(),
@@ -9047,92 +9054,730 @@ function startReachabilityPoll(options) {
9047
9054
  } };
9048
9055
  }
9049
9056
  /**
9050
- * Generic device-level status snapshot. Auto-registered by `BaseDevice`
9051
- * for every device, regardless of provider the kernel needs a uniform
9052
- * cap-keyed slice for the basic device flags every consumer expects to
9053
- * read across processes (the `online` flag in particular). Driver-specific
9054
- * caps (`battery`, `doorbell`, …) carry their domain-specific state on
9055
- * their own slices.
9057
+ * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9058
+ * motion-zones, and the detection zones/lines editor all speak this one
9059
+ * language so a single drawing-plane editor and the providers stay
9060
+ * decoupled from each cap's storage.
9056
9061
  *
9057
- * Pattern is identical to `battery`: schema-bearing `runtimeState`,
9058
- * empty `methods`, single change event. Reads land at
9059
- * `runtimeState.getCapState('device-status')`; writes at
9060
- * `runtimeState.setCapState('device-status', …)`. Cross-process
9061
- * consumers reach the same data via the `device-state` cap router
9062
- * (`getCapSlice({deviceId, capName: 'device-status'})`).
9062
+ * All coordinates are normalized 0..1 of the camera frame (top-left
9063
+ * origin). Each cap composes the SUBSET of shape kinds it supports and
9064
+ * advertises it via `supportedShapes` in its `getOptions`.
9063
9065
  */
9064
- var DeviceStatusSchema = object({
9065
- /**
9066
- * Device-level liveness. Drivers flip via `markOnline(boolean)` on
9067
- * `BaseDevice`. Provider semantics vary — RTSP aggregates broker
9068
- * stream-health, Reolink reads firmware push events, ONVIF tracks
9069
- * ping responses. This cap intentionally does NOT prescribe which
9070
- * signal drives the flag.
9071
- */
9072
- online: boolean(),
9073
- /** Ms epoch of the last `online` transition. Lets consumers tell
9074
- * apart "just came online" from "still online". */
9075
- lastChangedAt: number()
9066
+ /** A normalized 0..1 point (top-left origin). */
9067
+ var MaskPointSchema = object({
9068
+ x: number(),
9069
+ y: number()
9076
9070
  });
9077
- object({
9078
- deviceId: number(),
9079
- status: DeviceStatusSchema
9071
+ /** Axis-aligned rectangle (normalized 0..1). */
9072
+ var MaskRectShapeSchema = object({
9073
+ kind: literal("rect"),
9074
+ x: number(),
9075
+ y: number(),
9076
+ width: number(),
9077
+ height: number()
9078
+ });
9079
+ /** Free polygon — an ordered list of normalized vertices (≥3). */
9080
+ var MaskPolygonShapeSchema = object({
9081
+ kind: literal("polygon"),
9082
+ points: array(MaskPointSchema)
9083
+ });
9084
+ /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
9085
+ var MaskGridShapeSchema = object({
9086
+ kind: literal("grid"),
9087
+ gridWidth: number(),
9088
+ gridHeight: number(),
9089
+ cells: array(boolean())
9090
+ });
9091
+ discriminatedUnion("kind", [
9092
+ MaskRectShapeSchema,
9093
+ MaskPolygonShapeSchema,
9094
+ MaskGridShapeSchema,
9095
+ object({
9096
+ kind: literal("line"),
9097
+ points: array(MaskPointSchema)
9098
+ })
9099
+ ]);
9100
+ /** Every shape-kind discriminant, for `supportedShapes` advertisement. */
9101
+ var MaskShapeKindSchema = _enum([
9102
+ "rect",
9103
+ "polygon",
9104
+ "grid",
9105
+ "line"
9106
+ ]);
9107
+ /** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
9108
+ var MaskPolygonVerticesSchema = object({
9109
+ min: number(),
9110
+ max: number()
9111
+ });
9112
+ /** Grid dimensions when a cap supports 'grid'. */
9113
+ var MaskGridDimsSchema = object({
9114
+ width: number(),
9115
+ height: number()
9080
9116
  });
9081
9117
  /**
9082
- * Per-device feature/identity probe slice. Holds the runtime-resolved
9083
- * truth about what a device CAN do — which the kernel uses to:
9084
- * 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
9085
- * based on what the firmware actually advertises).
9086
- * 2. Compute the public `features: DeviceFeature[]` array surfaced via
9087
- * `device-manager.listAll`.
9088
- * 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
9089
- * to register on the device's capability surface.
9118
+ * notification-rules the Notification Center rule surface (P1 core).
9090
9119
  *
9091
- * Auto-registered by `BaseDevice` for every device. Drivers populate the
9092
- * slice from `onProbe()` (kernel calls it once after register, before
9093
- * accessory reconciliation). Consumers read via:
9094
- * `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
9120
+ * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
9121
+ * (operator decisions D-1/D-2/D-3 are binding):
9095
9122
  *
9096
- * `flags` is an open record so each driver carries its own keys without
9097
- * a centralized schema bottleneck Reolink writes `hasPtz/hasIntercom`,
9098
- * Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
9123
+ * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
9124
+ * `notification-center` module), hooked on the durable persistence
9125
+ * moments (object-event insert, TrackCloser.closeExpired) with a
9126
+ * persisted outbox + retry — never the lossy telemetry bus (D8).
9127
+ * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
9128
+ * FIRST persisted detection matching the conditions (per-track dedup,
9129
+ * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
9130
+ * `delivery: 'track-end'` evaluates the finalized track record at close.
9131
+ * - DISPATCH stays behind `notification-output` (rules reference targets
9132
+ * by id; per-backend params are a passthrough blob capped by the
9133
+ * target kind's own caps/degrade engine).
9099
9134
  *
9100
- * Replaces the older driver-local `deviceCache.has*` blob: the per-device
9101
- * config is for operator-edited overrides + UI snapshots; runtime probe
9102
- * results belong in runtime-state where the kernel handles persistence,
9103
- * cross-process mirroring, and reactive updates.
9135
+ * P1 scope: admin-authored rules only (`createdBy` stamped from the
9136
+ * server-injected caller identity the first `caller: 'required'`
9137
+ * adopter). The P1 condition subset is: devices, classes(+exclude),
9138
+ * minConfidence, admin zones (any/all + exclude), weekly schedule
9139
+ * windows, and the optional label/identity/plate matchers. User rules,
9140
+ * private zones, per-recipient fan-out and the wider condition table are
9141
+ * P2+ (see spec §7).
9142
+ *
9143
+ * All schemas here are the single source of truth — `NcRule` etc. are
9144
+ * `z.infer` exports; no duplicate interfaces (the advanced-notifier
9145
+ * schema/interface drift is explicitly not repeated).
9104
9146
  */
9105
- var FeatureProbeStatusSchema = object({
9147
+ /**
9148
+ * D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
9149
+ * The value maps 1:1 onto the evaluated record kind:
9150
+ * - `immediate` ↔ object-event persist (lowest-latency detection burst)
9151
+ * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
9152
+ * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
9153
+ * change of a LINKED device, one row per linked camera)
9154
+ * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
9155
+ * delivery / pick-up)
9156
+ *
9157
+ * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
9158
+ * `package-event` are pure trigger kinds (no urgency dimension). Extending
9159
+ * this one field keeps the schema additive — a rule still declares exactly
9160
+ * one trigger.
9161
+ */
9162
+ var NcDeliverySchema = _enum([
9163
+ "immediate",
9164
+ "track-end",
9165
+ "device-event",
9166
+ "package-event"
9167
+ ]);
9168
+ /** Weekly schedule — OR of windows; absence on the rule = always active. */
9169
+ var NcScheduleSchema = object({
9170
+ windows: array(object({
9171
+ /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
9172
+ days: array(number().int().min(0).max(6)).min(1),
9173
+ startMinute: number().int().min(0).max(1439),
9174
+ endMinute: number().int().min(0).max(1439)
9175
+ })).min(1),
9176
+ /** IANA timezone; default = hub host timezone. */
9177
+ timezone: string().optional(),
9178
+ /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
9179
+ invert: boolean().optional()
9180
+ });
9181
+ /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
9182
+ var NcPlateMatcherSchema = object({
9183
+ values: array(string().min(1)).min(1),
9184
+ /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
9185
+ maxDistance: number().int().min(0).max(3).default(1)
9186
+ });
9187
+ /**
9188
+ * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
9189
+ * occupancy edge for a device — optionally narrowed to a single admin
9190
+ * `zoneId` and/or object `className`. `op` selects the edge/threshold:
9191
+ * - `became-occupied` (default) — count crossed 0 → ≥ `count`
9192
+ * - `became-free` — count crossed ≥ `count` → below it
9193
+ * - `>=` / `<=` — count is at/over or at/under `count`
9194
+ * `sustainSeconds` requires the condition hold continuously that long
9195
+ * before firing (debounces flicker; 0 = fire on the first matching edge).
9196
+ * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
9197
+ * the condition never matches. Confirmed edge-state survives addon restarts
9198
+ * (declared SQLite collection, reseeded on boot).
9199
+ */
9200
+ var NcOccupancyConditionSchema = object({
9201
+ /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
9202
+ zoneId: string().optional(),
9203
+ /** Object class to count; absent = any class. */
9204
+ className: string().optional(),
9205
+ op: _enum([
9206
+ "became-occupied",
9207
+ "became-free",
9208
+ ">=",
9209
+ "<="
9210
+ ]).default("became-occupied"),
9211
+ count: number().int().min(0).default(1),
9212
+ sustainSeconds: number().int().min(0).max(3600).default(15)
9213
+ });
9214
+ /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9215
+ var NcZoneConditionSchema = object({
9216
+ ids: array(string().min(1)).min(1),
9217
+ /** Quantifier over `ids` — at least one / every one visited. */
9218
+ match: _enum(["any", "all"]).default("any")
9219
+ });
9220
+ /**
9221
+ * The P1 condition set — a flat AND of groups; absent group = pass;
9222
+ * membership lists are OR within the list (spec §2.3).
9223
+ */
9224
+ var NcConditionsSchema = object({
9225
+ /** Device scope — absent = all devices. */
9226
+ devices: array(number()).optional(),
9227
+ /** Detector class names (any overlap with the record's class set). */
9228
+ classes: array(string().min(1)).optional(),
9229
+ /** Veto classes — any overlap fails the rule. */
9230
+ classesExclude: array(string().min(1)).optional(),
9231
+ /** Minimum detection confidence 0–1 (fails when the record has none). */
9232
+ minConfidence: number().min(0).max(1).optional(),
9233
+ /** Admin zone membership over event `zones` / track `zonesVisited`. */
9234
+ zones: NcZoneConditionSchema.optional(),
9235
+ /** Veto zones — any hit fails the rule. */
9236
+ zonesExclude: array(string().min(1)).optional(),
9106
9237
  /**
9107
- * Driver-specific flag bag. Each driver picks its own key names — the
9108
- * cap deliberately does NOT enforce a closed enum here. Reolink keys:
9109
- * `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
9110
- * `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
9111
- * `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
9238
+ * Exact (case-insensitive) match on the record's collapsed `label`
9239
+ * (identity name / plate text / subclass).
9112
9240
  */
9113
- flags: record(string(), unknown()),
9241
+ labelEquals: array(string().min(1)).optional(),
9114
9242
  /**
9115
- * Coarse driver-classification lets cross-process consumers tell apart
9116
- * cameras / battery-cams / NVRs without re-running the probe. `null`
9117
- * before the first probe completes.
9243
+ * Identity matcher. P1 boundary: matched against the record's collapsed
9244
+ * `label` (the identity display name propagated by the face pipeline) —
9245
+ * identity-ID matching rides in P2 when identity ids reach the record.
9118
9246
  */
9119
- deviceType: string().nullable(),
9120
- /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
9121
- model: string().nullable(),
9122
- /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
9123
- channelCount: number().nullable(),
9247
+ identities: array(string().min(1)).optional(),
9248
+ /** Fuzzy plate matcher against the record's `label` (plate text). */
9249
+ plates: NcPlateMatcherSchema.optional(),
9124
9250
  /**
9125
- * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
9126
- * completes drivers' `getAccessoryChildren()` should treat zero as
9127
- * "probe not done yet, return empty" so accessories aren't spawned
9128
- * before the firmware is queried.
9251
+ * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
9252
+ * Same P1 boundary: matched against the record's collapsed `label` (the
9253
+ * identity display name). A record with NO label passes (nothing to
9254
+ * exclude), unlike the include variant which fails on an absent label.
9129
9255
  */
9130
- lastProbedAt: number(),
9256
+ identitiesExclude: array(string().min(1)).optional(),
9131
9257
  /**
9132
- * Framework convention: every runtime-state slice carries this for the
9133
- * createRuntimeStateBridge stale-check helper. We keep it in sync with
9134
- * `lastProbedAt` on every write.
9135
- */
9258
+ * Minimum server-computed key-event importance in [0,1] (`Track.importance`).
9259
+ * TRACK-END only: importance is scored at track close, so it does not exist
9260
+ * at immediate / object-event evaluation time (see catalog `appliesTo`). At
9261
+ * close the value is threaded via the close-time info (the `Track` clone is
9262
+ * captured before the DB row is updated, so it would otherwise read stale).
9263
+ * Fails when the record carries no importance (never guess quality — the
9264
+ * `minConfidence` precedent). MVP cut: a single scalar threshold.
9265
+ */
9266
+ minImportance: number().min(0).max(1).optional(),
9267
+ /**
9268
+ * Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
9269
+ * TRACK-END only: an `immediate` / object-event subject has no closed
9270
+ * lifespan, so a dwell condition never matches immediate delivery
9271
+ * (documented choice — the object-event record carries no `firstSeen`,
9272
+ * so dwell cannot be computed from what the subject actually carries).
9273
+ */
9274
+ minDwellSeconds: number().min(0).optional(),
9275
+ /**
9276
+ * Detection provenance filter. `any` (default / absent) matches every
9277
+ * source; otherwise the subject's source must equal it. Legacy records
9278
+ * with no stamped source are treated as `pipeline`. The union spans both
9279
+ * record kinds — object events carry `pipeline` | `onboard`, synthetic
9280
+ * tracks carry `sensor`.
9281
+ */
9282
+ source: _enum([
9283
+ "pipeline",
9284
+ "onboard",
9285
+ "sensor",
9286
+ "any"
9287
+ ]).optional(),
9288
+ /**
9289
+ * Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
9290
+ * detector `minConfidence` (that gates the object-detection score; this
9291
+ * gates the recognition/OCR match score). Fails when the subject carries
9292
+ * no label-match confidence (never guess). TRACK-END only: the confidence
9293
+ * lives on the recognition result and reaches the subject at track close.
9294
+ *
9295
+ * What it measures precisely (plumbed at track close — the closer threads
9296
+ * the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
9297
+ * `importance`): the BEST recognition match confidence observed for the
9298
+ * label the track carries at close — for a face, the peak cosine similarity
9299
+ * of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
9300
+ * for a plate, the peak OCR read score of the best-held plate
9301
+ * (`plateText.confidence`). When BOTH a face and a plate were recognized on
9302
+ * one track the higher of the two is used. A track that ended with no
9303
+ * confident identity/plate match carries no value, so the condition fails
9304
+ * closed for it (an un-recognized subject).
9305
+ */
9306
+ minLabelConfidence: number().min(0).max(1).optional(),
9307
+ /**
9308
+ * DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
9309
+ * e.g. a doorbell `press` / `press_long`) — matched case-insensitively
9310
+ * against the token carried on the device-event subject (extracted from the
9311
+ * event-emitter runtime slice's `lastEvent.eventType`). Fails when the
9312
+ * subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
9313
+ * eventType, so gate those with {@link sensorKinds} instead.
9314
+ */
9315
+ eventTypeTokens: array(string().min(1)).optional(),
9316
+ /**
9317
+ * DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
9318
+ * `contact`, `button`, `device-event`) — matched against the persisted
9319
+ * `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
9320
+ */
9321
+ sensorKinds: array(string().min(1)).optional(),
9322
+ /**
9323
+ * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
9324
+ * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
9325
+ * when the subject's phase does not match (a subject always carries a phase
9326
+ * on the package-event trigger).
9327
+ */
9328
+ packagePhase: _enum([
9329
+ "delivered",
9330
+ "picked-up",
9331
+ "both"
9332
+ ]).optional(),
9333
+ /**
9334
+ * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
9335
+ * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
9336
+ * listed polygon (ZoneEngine membership semantics). Evaluated only when
9337
+ * the subject carries a bbox; absent bbox ⇒ the condition FAILS.
9338
+ */
9339
+ customZones: array(MaskPolygonShapeSchema).optional(),
9340
+ /**
9341
+ * DEVICE-EVENT only. ZoneAnalytics occupancy edge — fires when a device's
9342
+ * (optionally zone/class-scoped) occupancy count crosses the configured
9343
+ * threshold and holds for `sustainSeconds`. Fail-closed on missing
9344
+ * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
9345
+ */
9346
+ occupancy: NcOccupancyConditionSchema.optional()
9347
+ });
9348
+ /** One delivery target: a `notification-output` Target ref + passthrough params. */
9349
+ var NcRuleTargetSchema = object({
9350
+ /** `notification-output` Target id. */
9351
+ targetId: string().min(1),
9352
+ /**
9353
+ * Per-backend passthrough. Recognized keys are mapped onto the canonical
9354
+ * Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
9355
+ * degrade engine drops what the backend can't render.
9356
+ */
9357
+ params: record(string(), unknown()).optional()
9358
+ });
9359
+ /**
9360
+ * Media attachment policy (P1 still-image subset).
9361
+ * - `best` — the best AVAILABLE subject image at dispatch time (D-3).
9362
+ * - `best-matching` — the media that explains WHY the rule fired: a rule
9363
+ * matched on identities attaches the subject's `faceCrop`, one matched on
9364
+ * plates attaches the `plateCrop`; a rule with no identity/plate condition
9365
+ * (or when the specific crop is missing) degrades to `best`, then
9366
+ * `keyFrame`, then no attachment — never delaying the send. The matched
9367
+ * condition summary is frozen on the outbox row at enqueue (like the rule
9368
+ * name), so the choice never drifts from the record that fired it.
9369
+ * - `keyFrame` — the clean scene frame (no subject box).
9370
+ * - `none` — no attachment.
9371
+ */
9372
+ var NcMediaPolicySchema = object({ attach: _enum([
9373
+ "best",
9374
+ "best-matching",
9375
+ "keyFrame",
9376
+ "none"
9377
+ ]).default("best") });
9378
+ /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9379
+ var NcThrottleSchema = object({
9380
+ cooldownSec: number().int().min(0).max(86400).default(60),
9381
+ /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
9382
+ scope: _enum(["rule", "rule-device"]).default("rule-device")
9383
+ });
9384
+ /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
9385
+ var NcRuleInputSchema = object({
9386
+ name: string().min(1).max(200),
9387
+ enabled: boolean().default(true),
9388
+ delivery: NcDeliverySchema,
9389
+ conditions: NcConditionsSchema.default({}),
9390
+ schedule: NcScheduleSchema.optional(),
9391
+ targets: array(NcRuleTargetSchema).min(1),
9392
+ media: NcMediaPolicySchema.default({ attach: "best" }),
9393
+ throttle: NcThrottleSchema.default({
9394
+ cooldownSec: 60,
9395
+ scope: "rule-device"
9396
+ }),
9397
+ /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
9398
+ template: object({
9399
+ title: string().max(500).optional(),
9400
+ body: string().max(2e3).optional()
9401
+ }).optional(),
9402
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
9403
+ priority: number().int().min(1).max(5).default(3),
9404
+ /**
9405
+ * Ownership/visibility key. Absent = admin/global rule (unchanged legacy
9406
+ * behaviour, visible to all, read-only in the viewer). Present = personal
9407
+ * rule owned by this userId. Server-stamped; never trusted from a client.
9408
+ */
9409
+ ownerUserId: string().optional()
9410
+ });
9411
+ /**
9412
+ * Partial patch for `updateRule` — any subset of the input fields, plus the
9413
+ * persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
9414
+ * NOT a client-authored input field (it lives on the persisted rule, not the
9415
+ * input), so it is added here explicitly to let the store's per-target opt-out
9416
+ * toggle round-trip through the shared `update` path. Viewer opt-out mutations
9417
+ * still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
9418
+ * `updateRule` patch.
9419
+ */
9420
+ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
9421
+ /** A persisted rule. */
9422
+ var NcRuleSchema = NcRuleInputSchema.extend({
9423
+ id: string(),
9424
+ /** userId of the admin who created the rule (server-stamped caller). */
9425
+ createdBy: string(),
9426
+ createdAt: number(),
9427
+ updatedAt: number(),
9428
+ /**
9429
+ * Per-target opt-out set. A targetId here is suppressed for THIS rule at
9430
+ * send time. Only a target's OWNER may add/remove its id (server-checked
9431
+ * in `nc.setRuleTargetEnabled`). Defaults to empty.
9432
+ */
9433
+ disabledTargetIds: array(string()).default([])
9434
+ });
9435
+ var NcTestResultSchema = object({
9436
+ recordId: string(),
9437
+ recordKind: _enum([
9438
+ "object-event",
9439
+ "track",
9440
+ "device-event",
9441
+ "package-event"
9442
+ ]),
9443
+ deviceId: number(),
9444
+ timestamp: number(),
9445
+ wouldFire: boolean(),
9446
+ /** Condition id that failed (first failing group), when `wouldFire` is false. */
9447
+ failedCondition: string().optional(),
9448
+ className: string().optional(),
9449
+ label: string().optional()
9450
+ });
9451
+ var NcConditionDescriptorSchema = object({
9452
+ /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
9453
+ id: string(),
9454
+ group: _enum([
9455
+ "scope",
9456
+ "class",
9457
+ "zones",
9458
+ "quality",
9459
+ "label",
9460
+ "schedule",
9461
+ "device",
9462
+ "package",
9463
+ "occupancy"
9464
+ ]),
9465
+ label: string(),
9466
+ /** Editor widget the UI renders — never hardcode per-condition forms. */
9467
+ valueType: _enum([
9468
+ "deviceIdList",
9469
+ "stringList",
9470
+ "number01",
9471
+ "number",
9472
+ "sourceSelect",
9473
+ "zoneSelection",
9474
+ "zoneIdList",
9475
+ "schedule",
9476
+ "plateMatcher",
9477
+ "packagePhase",
9478
+ "polygonDraw",
9479
+ "occupancy"
9480
+ ]),
9481
+ operator: _enum([
9482
+ "in",
9483
+ "notIn",
9484
+ "anyOf",
9485
+ "allOf",
9486
+ "gte",
9487
+ "fuzzyIn",
9488
+ "withinSchedule"
9489
+ ]),
9490
+ /** Which delivery kinds the condition applies to. */
9491
+ appliesTo: array(NcDeliverySchema),
9492
+ phase: string(),
9493
+ description: string().optional()
9494
+ });
9495
+ /**
9496
+ * The delivery lifecycle status of a history row — a straight read of the
9497
+ * durable outbox row's own status (single source of truth):
9498
+ * - `pending` — enqueued, in-flight or retrying with backoff
9499
+ * - `sent` — delivered (terminal)
9500
+ * - `dead` — dead-lettered after exhausting retries / a permanent
9501
+ * backend rejection / a deleted target (terminal; carries
9502
+ * the failure `error`)
9503
+ *
9504
+ * P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
9505
+ * user dimension (quiet hours / snooze) and are additive when they land.
9506
+ */
9507
+ var NcHistoryStatusSchema = _enum([
9508
+ "pending",
9509
+ "sent",
9510
+ "dead"
9511
+ ]);
9512
+ /** The evaluated record kind a history row descends from (one per trigger). */
9513
+ var NcHistoryRecordKindSchema = _enum([
9514
+ "object-event",
9515
+ "track-end",
9516
+ "device-event",
9517
+ "package-event"
9518
+ ]);
9519
+ /** Subject summary frozen on the row at fire time (survives rule/record edits). */
9520
+ var NcHistorySubjectSchema = object({
9521
+ className: string(),
9522
+ label: string().optional(),
9523
+ confidence: number().optional(),
9524
+ zones: array(string()),
9525
+ timestamp: number()
9526
+ });
9527
+ /**
9528
+ * One delivery-history row. This is a read-only VIEW over the durable
9529
+ * outbox row (single source of truth — the same row the drain loop drives;
9530
+ * NO second write path, so history can never drift from delivery state).
9531
+ * The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
9532
+ * `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
9533
+ * (fire) / `updatedAt` (last transition), `status` + `error` are the
9534
+ * lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
9535
+ * enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
9536
+ * P1 (admin scope only).
9537
+ */
9538
+ var NcHistoryEntrySchema = object({
9539
+ /** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
9540
+ id: string(),
9541
+ ruleId: string(),
9542
+ /** Rule name frozen at fire time (outlives a later rename / delete). */
9543
+ ruleName: string(),
9544
+ /** The rule urgency/trigger that produced this delivery. */
9545
+ delivery: NcDeliverySchema,
9546
+ targetId: string(),
9547
+ deviceId: number(),
9548
+ recordKind: NcHistoryRecordKindSchema,
9549
+ /** Event / track ref of the evaluated record (§3.2 `eventRef`). */
9550
+ recordId: string(),
9551
+ /** Present for track-scoped deliveries (object-event / track-end). */
9552
+ trackId: string().optional(),
9553
+ status: NcHistoryStatusSchema,
9554
+ /** Delivery attempts made so far. */
9555
+ attempts: number().int(),
9556
+ /** Fire time (outbox enqueue). */
9557
+ createdAt: number(),
9558
+ /** Last transition time (terminal for sent / dead). */
9559
+ updatedAt: number(),
9560
+ /** Failure detail — present on a `dead` row. */
9561
+ error: string().optional(),
9562
+ subject: NcHistorySubjectSchema
9563
+ });
9564
+ /**
9565
+ * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
9566
+ * AND; absent = unbounded on that axis. `since`/`until` bound the fire time
9567
+ * (`createdAt`, epoch ms, inclusive). `limit` is clamped to
9568
+ * {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
9569
+ */
9570
+ var NcHistoryFilterSchema = object({
9571
+ ruleId: string().optional(),
9572
+ deviceId: number().optional(),
9573
+ status: NcHistoryStatusSchema.optional(),
9574
+ since: number().optional(),
9575
+ until: number().optional(),
9576
+ limit: number().int().min(1).max(500).default(100)
9577
+ });
9578
+ 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 }), {
9579
+ kind: "mutation",
9580
+ auth: "admin",
9581
+ caller: "required"
9582
+ }), method(object({
9583
+ ruleId: string(),
9584
+ patch: NcRulePatchSchema
9585
+ }), object({ rule: NcRuleSchema }), {
9586
+ kind: "mutation",
9587
+ auth: "admin",
9588
+ caller: "required"
9589
+ }), method(object({ ruleId: string() }), object({ success: literal(true) }), {
9590
+ kind: "mutation",
9591
+ auth: "admin"
9592
+ }), method(object({
9593
+ ruleId: string(),
9594
+ enabled: boolean()
9595
+ }), object({ success: literal(true) }), {
9596
+ kind: "mutation",
9597
+ auth: "admin"
9598
+ }), method(object({
9599
+ rule: NcRuleInputSchema,
9600
+ lookbackMinutes: number().int().min(1).max(1440).default(60)
9601
+ }), object({ results: array(NcTestResultSchema) }), {
9602
+ kind: "mutation",
9603
+ auth: "admin"
9604
+ }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
9605
+ /**
9606
+ * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9607
+ *
9608
+ * Spec: `docs/superpowers/specs/2026-07-24-nc-occupancy-timelapse-design.md`
9609
+ * §3.2/§3.3.
9610
+ *
9611
+ * Deliberately NOT a capability definition and NOT an `NcRule`:
9612
+ * - Every `NcDelivery` member is a *persisted-pipeline-record* trigger. A
9613
+ * timelapse fires on a SCHEDULE WINDOW BOUNDARY, evaluates no pipeline
9614
+ * record, and produces a video it assembled itself — so it rides no
9615
+ * delivery-enum member (the enum is frozen) and no cap method. This file is
9616
+ * a plain typed schema; it does NOT go through `npm run codegen`.
9617
+ * - It shares only the delivery leg (`notification-output.send`) and the
9618
+ * persistence/ownership patterns with the Notification Center, reusing
9619
+ * {@link NcScheduleSchema} (weekly windows, midnight-crossing, invertible)
9620
+ * and {@link NcRuleTargetSchema} (target ref + passthrough params).
9621
+ *
9622
+ * Ownership is SERVER-DERIVED. `ownerUserId` / `createdBy` / `createdAt` /
9623
+ * `updatedAt` / `id` / `lastGeneratedAt` live on the PERSISTED rule only —
9624
+ * {@link TimelapseRuleInputSchema} and {@link TimelapseRulePatchSchema} do not
9625
+ * carry them, so a forged client payload can never claim or re-own a rule
9626
+ * (Zod strips unknown keys). The store stamps them from the resolved caller.
9627
+ */
9628
+ /** `{{var}}` templating over camera/rule/time — same vocabulary as `NcRule`. */
9629
+ var TimelapseTemplateSchema = object({
9630
+ title: string().max(500).optional(),
9631
+ body: string().max(2e3).optional()
9632
+ });
9633
+ var NameField = string().min(1).max(200);
9634
+ var DeviceIdsField = array(number()).min(1);
9635
+ var CadenceSecField = number().int().min(2).max(3600);
9636
+ var FramerateField = number().int().min(1).max(60);
9637
+ var TargetsField = array(NcRuleTargetSchema).min(1);
9638
+ var PriorityField = number().int().min(1).max(5);
9639
+ /**
9640
+ * Client-supplied timelapse-rule fields. The server stamps id / createdBy /
9641
+ * createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
9642
+ * here (see the ownership note above).
9643
+ */
9644
+ var TimelapseRuleInputSchema = object({
9645
+ name: NameField,
9646
+ enabled: boolean().default(true),
9647
+ /** Cameras sampled by this rule — one scratch dir + one artifact per device. */
9648
+ deviceIds: DeviceIdsField,
9649
+ /**
9650
+ * Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
9651
+ * means "always active"): a timelapse is defined by its window boundaries —
9652
+ * open clears the scratch, close assembles and delivers.
9653
+ */
9654
+ schedule: NcScheduleSchema,
9655
+ /** Force-snapshot cadence inside the window, seconds (predecessor parity). */
9656
+ cadenceSec: CadenceSecField.default(15),
9657
+ /** Output frames per second of the assembled mp4 (predecessor parity). */
9658
+ framerate: FramerateField.default(10),
9659
+ /** `notification-output` targets the finished video/thumbnail is sent to. */
9660
+ targets: TargetsField,
9661
+ template: TimelapseTemplateSchema.optional(),
9662
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
9663
+ priority: PriorityField.default(3)
9664
+ });
9665
+ object({
9666
+ name: NameField.optional(),
9667
+ enabled: boolean().optional(),
9668
+ deviceIds: DeviceIdsField.optional(),
9669
+ schedule: NcScheduleSchema.optional(),
9670
+ cadenceSec: CadenceSecField.optional(),
9671
+ framerate: FramerateField.optional(),
9672
+ targets: TargetsField.optional(),
9673
+ template: TimelapseTemplateSchema.nullable().optional(),
9674
+ priority: PriorityField.optional()
9675
+ });
9676
+ TimelapseRuleInputSchema.extend({
9677
+ id: string(),
9678
+ /**
9679
+ * Ownership/visibility key. Absent = admin/global rule (visible to all).
9680
+ * Present = personal rule owned by this userId. Server-stamped from the
9681
+ * resolved caller; never trusted from a client payload.
9682
+ */
9683
+ ownerUserId: string().optional(),
9684
+ /**
9685
+ * Epoch-ms of the last successful generation — the 1-hour re-generation
9686
+ * guard's durable state (predecessor parity). Absent = never generated.
9687
+ */
9688
+ lastGeneratedAt: number().optional(),
9689
+ /** userId of the caller who created the rule (server-stamped). */
9690
+ createdBy: string(),
9691
+ createdAt: number(),
9692
+ updatedAt: number()
9693
+ });
9694
+ /**
9695
+ * Generic device-level status snapshot. Auto-registered by `BaseDevice`
9696
+ * for every device, regardless of provider — the kernel needs a uniform
9697
+ * cap-keyed slice for the basic device flags every consumer expects to
9698
+ * read across processes (the `online` flag in particular). Driver-specific
9699
+ * caps (`battery`, `doorbell`, …) carry their domain-specific state on
9700
+ * their own slices.
9701
+ *
9702
+ * Pattern is identical to `battery`: schema-bearing `runtimeState`,
9703
+ * empty `methods`, single change event. Reads land at
9704
+ * `runtimeState.getCapState('device-status')`; writes at
9705
+ * `runtimeState.setCapState('device-status', …)`. Cross-process
9706
+ * consumers reach the same data via the `device-state` cap router
9707
+ * (`getCapSlice({deviceId, capName: 'device-status'})`).
9708
+ */
9709
+ var DeviceStatusSchema = object({
9710
+ /**
9711
+ * Device-level liveness. Drivers flip via `markOnline(boolean)` on
9712
+ * `BaseDevice`. Provider semantics vary — RTSP aggregates broker
9713
+ * stream-health, Reolink reads firmware push events, ONVIF tracks
9714
+ * ping responses. This cap intentionally does NOT prescribe which
9715
+ * signal drives the flag.
9716
+ */
9717
+ online: boolean(),
9718
+ /** Ms epoch of the last `online` transition. Lets consumers tell
9719
+ * apart "just came online" from "still online". */
9720
+ lastChangedAt: number()
9721
+ });
9722
+ object({
9723
+ deviceId: number(),
9724
+ status: DeviceStatusSchema
9725
+ });
9726
+ /**
9727
+ * Per-device feature/identity probe slice. Holds the runtime-resolved
9728
+ * truth about what a device CAN do — which the kernel uses to:
9729
+ * 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
9730
+ * based on what the firmware actually advertises).
9731
+ * 2. Compute the public `features: DeviceFeature[]` array surfaced via
9732
+ * `device-manager.listAll`.
9733
+ * 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
9734
+ * to register on the device's capability surface.
9735
+ *
9736
+ * Auto-registered by `BaseDevice` for every device. Drivers populate the
9737
+ * slice from `onProbe()` (kernel calls it once after register, before
9738
+ * accessory reconciliation). Consumers read via:
9739
+ * `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
9740
+ *
9741
+ * `flags` is an open record so each driver carries its own keys without
9742
+ * a centralized schema bottleneck — Reolink writes `hasPtz/hasIntercom`,
9743
+ * Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
9744
+ *
9745
+ * Replaces the older driver-local `deviceCache.has*` blob: the per-device
9746
+ * config is for operator-edited overrides + UI snapshots; runtime probe
9747
+ * results belong in runtime-state where the kernel handles persistence,
9748
+ * cross-process mirroring, and reactive updates.
9749
+ */
9750
+ var FeatureProbeStatusSchema = object({
9751
+ /**
9752
+ * Driver-specific flag bag. Each driver picks its own key names — the
9753
+ * cap deliberately does NOT enforce a closed enum here. Reolink keys:
9754
+ * `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
9755
+ * `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
9756
+ * `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
9757
+ */
9758
+ flags: record(string(), unknown()),
9759
+ /**
9760
+ * Coarse driver-classification — lets cross-process consumers tell apart
9761
+ * cameras / battery-cams / NVRs without re-running the probe. `null`
9762
+ * before the first probe completes.
9763
+ */
9764
+ deviceType: string().nullable(),
9765
+ /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
9766
+ model: string().nullable(),
9767
+ /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
9768
+ channelCount: number().nullable(),
9769
+ /**
9770
+ * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
9771
+ * completes — drivers' `getAccessoryChildren()` should treat zero as
9772
+ * "probe not done yet, return empty" so accessories aren't spawned
9773
+ * before the firmware is queried.
9774
+ */
9775
+ lastProbedAt: number(),
9776
+ /**
9777
+ * Framework convention: every runtime-state slice carries this for the
9778
+ * createRuntimeStateBridge stale-check helper. We keep it in sync with
9779
+ * `lastProbedAt` on every write.
9780
+ */
9136
9781
  lastFetchedAt: number()
9137
9782
  });
9138
9783
  object({
@@ -11990,101 +12635,40 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
11990
12635
  }), object({ details: array(DetailResultSchema) }).nullable(), { kind: "mutation" });
11991
12636
  object({
11992
12637
  detected: boolean(),
11993
- /** Ms epoch of the last detected-true observation. Null if never detected. */
11994
- lastDetectedAt: number().nullable(),
11995
- /**
11996
- * Ms after which `detected` auto-reverts to false if no fresh push
11997
- * arrives. Null means the provider leaves detected state until a
11998
- * native "clear" event.
11999
- */
12000
- autoClearAfterMs: number().nullable()
12001
- });
12002
- object({
12003
- deviceId: number(),
12004
- detected: boolean(),
12005
- timestamp: number(),
12006
- source: MotionSourceEnum,
12007
- regions: array(MotionRegionSchema).readonly().optional()
12008
- });
12009
- DeviceType.Camera, DeviceType.Sensor, method(object({ deviceId: number() }), boolean());
12010
- object({
12011
- enabled: boolean(),
12012
- /** Ms epoch of the last operator-driven change. */
12013
- lastChangedAt: number()
12014
- }).extend({
12015
- /** Ms epoch of the last successful camera fetch (0 = never). */
12016
- lastFetchedAt: number() });
12017
- DeviceType.Light, DeviceType.Siren, DeviceType.Switch, method(object({
12018
- deviceId: number().int().nonnegative(),
12019
- enabled: boolean()
12020
- }), _void(), {
12021
- kind: "mutation",
12022
- auth: "admin"
12023
- }), object({
12024
- deviceId: number(),
12025
- enabled: boolean(),
12026
- lastChangedAt: number()
12027
- });
12028
- /**
12029
- * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
12030
- * motion-zones, and the detection zones/lines editor all speak this one
12031
- * language so a single drawing-plane editor and the providers stay
12032
- * decoupled from each cap's storage.
12033
- *
12034
- * All coordinates are normalized 0..1 of the camera frame (top-left
12035
- * origin). Each cap composes the SUBSET of shape kinds it supports and
12036
- * advertises it via `supportedShapes` in its `getOptions`.
12037
- */
12038
- /** A normalized 0..1 point (top-left origin). */
12039
- var MaskPointSchema = object({
12040
- x: number(),
12041
- y: number()
12042
- });
12043
- /** Axis-aligned rectangle (normalized 0..1). */
12044
- var MaskRectShapeSchema = object({
12045
- kind: literal("rect"),
12046
- x: number(),
12047
- y: number(),
12048
- width: number(),
12049
- height: number()
12050
- });
12051
- /** Free polygon — an ordered list of normalized vertices (≥3). */
12052
- var MaskPolygonShapeSchema = object({
12053
- kind: literal("polygon"),
12054
- points: array(MaskPointSchema)
12055
- });
12056
- /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
12057
- var MaskGridShapeSchema = object({
12058
- kind: literal("grid"),
12059
- gridWidth: number(),
12060
- gridHeight: number(),
12061
- cells: array(boolean())
12062
- });
12063
- discriminatedUnion("kind", [
12064
- MaskRectShapeSchema,
12065
- MaskPolygonShapeSchema,
12066
- MaskGridShapeSchema,
12067
- object({
12068
- kind: literal("line"),
12069
- points: array(MaskPointSchema)
12070
- })
12071
- ]);
12072
- /** Every shape-kind discriminant, for `supportedShapes` advertisement. */
12073
- var MaskShapeKindSchema = _enum([
12074
- "rect",
12075
- "polygon",
12076
- "grid",
12077
- "line"
12078
- ]);
12079
- /** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
12080
- var MaskPolygonVerticesSchema = object({
12081
- min: number(),
12082
- max: number()
12638
+ /** Ms epoch of the last detected-true observation. Null if never detected. */
12639
+ lastDetectedAt: number().nullable(),
12640
+ /**
12641
+ * Ms after which `detected` auto-reverts to false if no fresh push
12642
+ * arrives. Null means the provider leaves detected state until a
12643
+ * native "clear" event.
12644
+ */
12645
+ autoClearAfterMs: number().nullable()
12083
12646
  });
12084
- /** Grid dimensions when a cap supports 'grid'. */
12085
- var MaskGridDimsSchema = object({
12086
- width: number(),
12087
- height: number()
12647
+ object({
12648
+ deviceId: number(),
12649
+ detected: boolean(),
12650
+ timestamp: number(),
12651
+ source: MotionSourceEnum,
12652
+ regions: array(MotionRegionSchema).readonly().optional()
12653
+ });
12654
+ DeviceType.Camera, DeviceType.Sensor, method(object({ deviceId: number() }), boolean());
12655
+ object({
12656
+ enabled: boolean(),
12657
+ /** Ms epoch of the last operator-driven change. */
12658
+ lastChangedAt: number()
12659
+ }).extend({
12660
+ /** Ms epoch of the last successful camera fetch (0 = never). */
12661
+ lastFetchedAt: number() });
12662
+ DeviceType.Light, DeviceType.Siren, DeviceType.Switch, method(object({
12663
+ deviceId: number().int().nonnegative(),
12664
+ enabled: boolean()
12665
+ }), _void(), {
12666
+ kind: "mutation",
12667
+ auth: "admin"
12668
+ }), object({
12669
+ deviceId: number(),
12670
+ enabled: boolean(),
12671
+ lastChangedAt: number()
12088
12672
  });
12089
12673
  /**
12090
12674
  * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
@@ -14249,6 +14833,55 @@ method(object({
14249
14833
  password: string()
14250
14834
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
14251
14835
  /**
14836
+ * A live terminal session hosted by the provider addon. Output and input do
14837
+ * NOT flow through the capability — they use the addon data plane
14838
+ * (`GET /addon/terminal/<id>/out` SSE, `POST /addon/terminal/<id>/in`) because
14839
+ * terminal output must be ordered and lossless. The event bus is telemetry and
14840
+ * may drop chunks ([D8]), and a dropped chunk desynchronises the vt parser
14841
+ * permanently until a full repaint. The capability owns only lifecycle.
14842
+ */
14843
+ var TerminalSessionInfoSchema = object({
14844
+ /** Opaque session id minted by the provider on `openSession`. */
14845
+ sessionId: string(),
14846
+ /** The pre-declared profile this session runs (never a free-form command). */
14847
+ profileId: string(),
14848
+ /** Human-readable profile label for the UI session list. */
14849
+ label: string(),
14850
+ cols: number().int().positive(),
14851
+ rows: number().int().positive(),
14852
+ /** ms-epoch the session's pty was spawned. */
14853
+ startedAt: number()
14854
+ });
14855
+ /**
14856
+ * A profile the operator may open — a pre-declared, allowlisted program
14857
+ * (`monitor` → `btm`). The capability accepts only these ids; a free-form
14858
+ * command string would be remote code execution as the server's user, so it is
14859
+ * deliberately not part of the contract.
14860
+ */
14861
+ var TerminalProfileInfoSchema = object({
14862
+ profileId: string(),
14863
+ label: string(),
14864
+ description: string().optional()
14865
+ });
14866
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
14867
+ profileId: string(),
14868
+ cols: number().int().positive(),
14869
+ rows: number().int().positive()
14870
+ }), TerminalSessionInfoSchema, {
14871
+ kind: "mutation",
14872
+ auth: "admin"
14873
+ }), method(object({
14874
+ sessionId: string(),
14875
+ cols: number().int().positive(),
14876
+ rows: number().int().positive()
14877
+ }), _void(), {
14878
+ kind: "mutation",
14879
+ auth: "admin"
14880
+ }), method(object({ sessionId: string() }), _void(), {
14881
+ kind: "mutation",
14882
+ auth: "admin"
14883
+ });
14884
+ /**
14252
14885
  * Orchestrator-side destination metadata. The orchestrator computes
14253
14886
  * `id = <addonId>:<subId>` from its provider lookup so consumers
14254
14887
  * (admin UI, restore flow) see one canonical key.
@@ -14349,11 +14982,53 @@ var LocationStatSchema = object({
14349
14982
  fileCount: number(),
14350
14983
  present: boolean()
14351
14984
  });
14985
+ /**
14986
+ * A backup schedule — the N:M "entry" that binds one cron cadence to a
14987
+ * SET of destination locations. Supersedes the per-location cron on
14988
+ * `BackupDestinationPolicy`: an operator creates a schedule, picks the
14989
+ * `backups` locations it should write to, and the orchestrator fans a
14990
+ * single archive out to all of them when the cron fires.
14991
+ *
14992
+ * `retentionCount` is per-schedule (D-decision 2026-07-28): every
14993
+ * location targeted by this schedule keeps this many archives from
14994
+ * this schedule's runs.
14995
+ *
14996
+ * `dataSources` optionally narrows which top-level state locations
14997
+ * (db, addons, tls, …) are archived; omitted = the orchestrator's
14998
+ * default full set.
14999
+ */
15000
+ var BackupScheduleSchema = object({
15001
+ /** Stable id. Generated by the orchestrator on first upsert if absent. */
15002
+ id: string(),
15003
+ /** Operator-facing display name. */
15004
+ label: string(),
15005
+ /** 5-field POSIX cron. Empty = disabled cadence (kept for editing). */
15006
+ cron: string(),
15007
+ /** Master on/off toggle for the whole schedule. */
15008
+ enabled: boolean(),
15009
+ /** `backups`-location ids this schedule writes to (fan-out set). */
15010
+ locationIds: array(string()).readonly(),
15011
+ /** Archives kept per targeted location for this schedule. */
15012
+ retentionCount: number().int().min(1).max(1e3),
15013
+ /** Optional subset of source locations to include; omitted = all. */
15014
+ dataSources: array(string()).readonly().optional(),
15015
+ /** ms-epoch of last successful run. */
15016
+ lastRunAt: number().optional(),
15017
+ /** ms-epoch of next computed firing (read-only, filled on list). */
15018
+ nextRunAt: number().optional()
15019
+ });
14352
15020
  method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }), method(object({
14353
15021
  /** Subset of registered `backup-destination` addon ids to write to. */
14354
15022
  destinations: array(string()).optional(),
14355
15023
  locations: array(string()).optional(),
14356
- label: string().optional()
15024
+ label: string().optional(),
15025
+ /**
15026
+ * Per-run retention override applied to every targeted
15027
+ * destination. Used by schedule-driven runs (per-entry
15028
+ * retention). Omitted = each destination's own policy
15029
+ * retention (manual runs).
15030
+ */
15031
+ retentionCount: number().int().min(1).max(1e3).optional()
14357
15032
  }).optional(), array(BackupEntrySchema).readonly(), {
14358
15033
  kind: "mutation",
14359
15034
  auth: "admin"
@@ -14402,7 +15077,21 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
14402
15077
  ok: boolean(),
14403
15078
  error: string().optional(),
14404
15079
  nextRuns: array(number()).readonly()
14405
- }));
15080
+ })), method(_void(), array(BackupScheduleSchema).readonly(), { auth: "admin" }), method(object({
15081
+ id: string().optional(),
15082
+ label: string(),
15083
+ cron: string(),
15084
+ enabled: boolean(),
15085
+ locationIds: array(string()).readonly(),
15086
+ retentionCount: number().int().min(1).max(1e3),
15087
+ dataSources: array(string()).readonly().optional()
15088
+ }), BackupScheduleSchema, {
15089
+ kind: "mutation",
15090
+ auth: "admin"
15091
+ }), method(object({ id: string() }), _void(), {
15092
+ kind: "mutation",
15093
+ auth: "admin"
15094
+ });
14406
15095
  /**
14407
15096
  * `broker` — unified pub/sub broker registry, system-scoped collection.
14408
15097
  *
@@ -15418,1596 +16107,1108 @@ method(object({
15418
16107
  active: boolean()
15419
16108
  }), _void(), {
15420
16109
  kind: "mutation",
15421
- auth: "admin"
15422
- }), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
15423
- capName: string(),
15424
- wrappers: array(string())
15425
- }))), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), object({
15426
- settings: SettingsSchemaWithValuesSchema.nullable(),
15427
- live: SettingsSchemaWithValuesSchema.nullable()
15428
- })), method(object({
15429
- deviceId: number().int().nonnegative(),
15430
- action: string().min(1),
15431
- input: unknown()
15432
- }), unknown(), { kind: "mutation" }), method(object({
15433
- deviceId: number(),
15434
- writerCapName: string(),
15435
- writerAddonId: string(),
15436
- key: string(),
15437
- value: unknown()
15438
- }), object({ success: literal(true) }), {
15439
- kind: "mutation",
15440
- auth: "admin"
15441
- }), method(object({
15442
- deviceId: number(),
15443
- changes: array(object({
15444
- writerCapName: string(),
15445
- writerAddonId: string(),
15446
- key: string(),
15447
- value: unknown()
15448
- }))
15449
- }), object({
15450
- success: literal(true),
15451
- failures: array(object({
15452
- writerCapName: string(),
15453
- writerAddonId: string(),
15454
- error: string()
15455
- }))
15456
- }), {
15457
- kind: "mutation",
15458
- auth: "admin"
15459
- }), method(object({ addonId: string() }), array(DiscoveryCandidateSchema), {
15460
- kind: "mutation",
15461
- auth: "admin"
15462
- }), method(object({
15463
- addonId: string(),
15464
- candidate: DiscoveryCandidateSchema,
15465
- /** Owning integration id, stamped onto the new device's meta by the
15466
- * device-manager forwarder so `removeByIntegration` can cascade it.
15467
- * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
15468
- integrationId: string().optional()
15469
- }), DeviceSummarySchema, {
15470
- kind: "mutation",
15471
- auth: "admin"
15472
- }), method(object({
15473
- addonId: string(),
15474
- type: _enum(DeviceType)
15475
- }), unknown().nullable()), method(object({
15476
- addonId: string(),
15477
- type: _enum(DeviceType),
15478
- config: record(string(), unknown()),
15479
- /** Owning integration id, stamped onto the new device's meta by the
15480
- * device-manager forwarder so `removeByIntegration` can cascade it.
15481
- * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
15482
- integrationId: string().optional()
15483
- }), DeviceSummarySchema, {
15484
- kind: "mutation",
15485
- auth: "admin"
15486
- }), method(object({
15487
- addonId: string(),
15488
- type: _enum(DeviceType),
15489
- key: string(),
15490
- value: unknown(),
15491
- formValues: record(string(), unknown()).optional()
15492
- }), FieldProbeResultSchema, {
15493
- kind: "mutation",
15494
- auth: "admin"
15495
- }), method(object({
15496
- addonId: string(),
15497
- integrationId: string()
15498
- }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15499
- addonId: string(),
15500
- integrationId: string()
15501
- }), AdoptionStatusSchema, {
15502
- kind: "mutation",
15503
- auth: "admin"
15504
- }), method(AdoptInputSchema.extend({ addonId: string() }), AdoptResultSchema, {
15505
- kind: "mutation",
15506
- auth: "admin"
15507
- }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
15508
- kind: "mutation",
15509
- auth: "admin"
15510
- }), method(ResyncInputSchema, ResyncResultSchema, {
15511
- kind: "mutation",
15512
- auth: "admin"
15513
- }), method(object({}), object({ providers: array(object({
15514
- addonId: string(),
15515
- label: string()
15516
- })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
15517
- addonId: string(),
15518
- label: string(),
15519
- candidates: array(DiscoveryCandidateSchema).readonly(),
15520
- error: string().nullable()
15521
- })).readonly() }), {
15522
- kind: "mutation",
15523
- auth: "admin"
15524
- }), method(object({
15525
- addonId: string(),
15526
- params: record(string(), unknown()).optional()
15527
- }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
15528
- kind: "mutation",
15529
- auth: "admin"
15530
- }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
15531
- deviceId: number(),
15532
- key: string(),
15533
- value: unknown()
15534
- }), FieldProbeResultSchema, {
15535
- kind: "mutation",
15536
- auth: "admin"
15537
- }), method(object({
15538
- deviceId: number(),
15539
- caps: array(string()).readonly().optional()
15540
- }), record(string(), unknown().nullable()));
15541
- method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
15542
- deviceId: number(),
15543
- capName: string()
15544
- }), record(string(), unknown()).nullable()), method(object({}), record(string(), record(string(), record(string(), unknown())))), method(object({
15545
- deviceId: number(),
15546
- capName: string(),
15547
- slice: record(string(), unknown())
15548
- }), _void(), { kind: "mutation" }), object({
15549
- deviceId: number(),
15550
- capName: string(),
15551
- slice: record(string(), unknown())
15552
- });
15553
- /**
15554
- * Embedding output. `embedding` is wire-encoded as `number[]` so the
15555
- * Zod-validated tRPC surface round-trips cleanly; consumers that need a
15556
- * `Float32Array` can wrap it on the way out (in-process, no marshalling
15557
- * is involved). `inferenceMs` mirrors the runtime field used by the
15558
- * post-analysis enrichment-engine.
15559
- */
15560
- var EmbeddingResultSchema = object({
15561
- embedding: array(number()),
15562
- inferenceMs: number()
15563
- });
15564
- var EmbeddingInfoSchema = object({
15565
- modelId: string(),
15566
- embeddingDim: number(),
15567
- ready: boolean()
15568
- });
15569
- method(object({
15570
- crop: _instanceof(Uint8Array),
15571
- width: number(),
15572
- height: number()
15573
- }), EmbeddingResultSchema), method(object({ text: string() }), EmbeddingResultSchema), method(_void(), EmbeddingInfoSchema);
15574
- /**
15575
- * filesystem-browse — per-node capability for browsing the node's local
15576
- * filesystem, sandboxed to operator-configured allowed roots. Used by the
15577
- * admin "Add filesystem location" flow to pick a node + path. `mode:'per-node'`
15578
- * (one provider per node); the hub calls it with `{nodeId}` so the codegen
15579
- * routes to that exact node (default `nodeIdMode:'routing'`).
15580
- */
15581
- var DirEntrySchema = object({
15582
- name: string(),
15583
- path: string()
15584
- });
15585
- var BrowseResultSchema = object({
15586
- path: string(),
15587
- entries: array(DirEntrySchema).readonly(),
15588
- freeBytes: number(),
15589
- totalBytes: number()
15590
- });
15591
- method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({ path: string() }), BrowseResultSchema, { auth: "admin" }), method(object({ path: string() }), object({ path: string() }), {
16110
+ auth: "admin"
16111
+ }), method(object({ capName: string() }), array(string())), method(object({ deviceType: string() }), array(object({
16112
+ capName: string(),
16113
+ wrappers: array(string())
16114
+ }))), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), SettingsSchemaWithValuesSchema.nullable()), method(object({ deviceId: number() }), object({
16115
+ settings: SettingsSchemaWithValuesSchema.nullable(),
16116
+ live: SettingsSchemaWithValuesSchema.nullable()
16117
+ })), method(object({
16118
+ deviceId: number().int().nonnegative(),
16119
+ action: string().min(1),
16120
+ input: unknown()
16121
+ }), unknown(), { kind: "mutation" }), method(object({
16122
+ deviceId: number(),
16123
+ writerCapName: string(),
16124
+ writerAddonId: string(),
16125
+ key: string(),
16126
+ value: unknown()
16127
+ }), object({ success: literal(true) }), {
15592
16128
  kind: "mutation",
15593
16129
  auth: "admin"
15594
- });
15595
- /**
15596
- * Shared LLM generate contracts — imported by BOTH `llm.cap.ts` (consumer
15597
- * surface) and `llm-runtime.cap.ts` (node-side managed executor) so the two
15598
- * caps stay wire-compatible without a circular cap→cap import.
15599
- *
15600
- * Errors are a discriminated-union RESULT, never thrown: the shape survives
15601
- * every transport tier structurally, and failed calls still write usage rows.
15602
- * Token counts only in v1 — no costUsd (operator decision, 2026-07-15).
15603
- */
15604
- var LlmUsageSchema = object({
15605
- inputTokens: number(),
15606
- outputTokens: number()
15607
- });
15608
- var LlmErrorCodeSchema = _enum([
15609
- "timeout",
15610
- "rate-limited",
15611
- "auth",
15612
- "refusal",
15613
- "bad-request",
15614
- "unavailable",
15615
- "no-profile",
15616
- "budget-exceeded",
15617
- "adapter-error"
15618
- ]);
15619
- var LlmGenerateResultSchema = discriminatedUnion("ok", [object({
15620
- ok: literal(true),
15621
- text: string(),
15622
- model: string(),
15623
- usage: LlmUsageSchema,
15624
- truncated: boolean(),
15625
- latencyMs: number()
16130
+ }), method(object({
16131
+ deviceId: number(),
16132
+ changes: array(object({
16133
+ writerCapName: string(),
16134
+ writerAddonId: string(),
16135
+ key: string(),
16136
+ value: unknown()
16137
+ }))
15626
16138
  }), object({
15627
- ok: literal(false),
15628
- code: LlmErrorCodeSchema,
15629
- message: string(),
15630
- retryAfterMs: number().optional()
15631
- })]);
15632
- /**
15633
- * `Uint8Array` is the sanctioned binary convention — superjson + the UDS
15634
- * MsgPack channel round-trip typed arrays (embedding-encoder.cap.ts:29,
15635
- * notification-output.cap.ts:27-31 precedents).
15636
- */
15637
- var LlmImageSchema = object({
15638
- bytes: _instanceof(Uint8Array),
15639
- mimeType: string()
15640
- });
15641
- var LlmGenerateBaseInputSchema = object({
15642
- /** Collection routing (the notification-output posture). */
15643
- addonId: string().optional(),
15644
- /** Explicit profile; else the resolution chain (spec §3). */
15645
- profileId: string().optional(),
15646
- /** MANDATORY usage tag: 'ai-summary', 'notifier-rules', 'adhoc-ui', … */
15647
- consumer: string(),
15648
- system: string().optional(),
15649
- /** v1: single-turn. `messages[]` is a v2 additive field. */
15650
- prompt: string(),
15651
- /** Structured output — adapter-mapped (response_format / forced tool / responseSchema). */
15652
- jsonSchema: record(string(), unknown()).optional(),
15653
- /** Per-call override of the profile default. */
15654
- maxTokens: number().int().positive().optional(),
15655
- temperature: number().optional()
15656
- });
15657
- /**
15658
- * `llm-runtime` — node-side managed llama.cpp executor (spec §4). Registered
15659
- * on EVERY node where `addon-ai` is installed; the hub `llm` provider reaches
15660
- * a specific node's runtime with `nodePin(profile.runtime.nodeId)` — normal
15661
- * cap routing, zero bespoke plumbing. `internal: true`: the operator reaches
15662
- * this only through the `llm` cap's methods.
15663
- *
15664
- * One running llama-server child per node in v1 (models are RAM-heavy).
15665
- * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
15666
- * watchdog — operator decision #3).
15667
- */
15668
- var ManagedModelRefSchema = discriminatedUnion("kind", [
15669
- object({
15670
- kind: literal("catalog"),
15671
- catalogId: string()
15672
- }),
15673
- object({
15674
- kind: literal("url"),
15675
- url: string(),
15676
- sha256: string().optional()
15677
- }),
15678
- object({
15679
- kind: literal("path"),
15680
- path: string()
15681
- })
15682
- ]);
15683
- var ManagedRuntimeConfigSchema = object({
15684
- /** WHERE the runtime lives — hub or any agent. */
15685
- nodeId: string(),
15686
- /** Closed for v1; 'ollama' is a v2 candidate. */
15687
- engine: _enum(["llama-cpp"]),
15688
- model: ManagedModelRefSchema,
15689
- contextSize: number().int().default(4096),
15690
- /** 0 = CPU-only. */
15691
- gpuLayers: number().int().default(0),
15692
- /** Default: cpus-2, clamped ≥1 (resolved node-side). */
15693
- threads: number().int().optional(),
15694
- /** Concurrent slots. */
15695
- parallel: number().int().default(1),
15696
- /** Else lazy: first generate boots it. */
15697
- autoStart: boolean().default(false),
15698
- /** 0 = never; frees RAM after quiet periods. */
15699
- idleStopMinutes: number().int().default(30)
15700
- });
15701
- var LlmRuntimeStatusSchema = object({
15702
- /** Status is ALWAYS node-qualified. */
15703
- nodeId: string(),
15704
- state: _enum([
15705
- "stopped",
15706
- "downloading",
15707
- "starting",
15708
- "ready",
15709
- "crashed",
15710
- "failed"
15711
- ]),
15712
- pid: number().optional(),
15713
- port: number().optional(),
15714
- modelPath: string().optional(),
15715
- modelId: string().optional(),
15716
- downloadProgress: number().min(0).max(1).optional(),
15717
- lastError: string().optional(),
15718
- crashesInWindow: number(),
15719
- /** Child RSS (sampled best-effort). */
15720
- memoryBytes: number().optional(),
15721
- vramBytes: number().optional()
15722
- });
15723
- var LlmNodeModelSchema = object({
15724
- file: string(),
15725
- sizeBytes: number(),
15726
- catalogId: string().optional(),
15727
- installedAt: number().optional()
15728
- });
15729
- var LlmRuntimeDiskUsageSchema = object({
15730
- nodeId: string(),
15731
- modelsBytes: number(),
15732
- freeBytes: number().optional()
15733
- });
15734
- method(LlmGenerateBaseInputSchema.extend({
15735
- images: array(LlmImageSchema).optional(),
15736
- runtime: ManagedRuntimeConfigSchema,
15737
- /** The managed profile's timeout, threaded by the hub provider. */
15738
- timeoutMs: number().int().positive().optional()
15739
- }), LlmGenerateResultSchema, { kind: "mutation" }), method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
16139
+ success: literal(true),
16140
+ failures: array(object({
16141
+ writerCapName: string(),
16142
+ writerAddonId: string(),
16143
+ error: string()
16144
+ }))
16145
+ }), {
15740
16146
  kind: "mutation",
15741
16147
  auth: "admin"
15742
- }), method(object({}), _void(), {
16148
+ }), method(object({ addonId: string() }), array(DiscoveryCandidateSchema), {
15743
16149
  kind: "mutation",
15744
16150
  auth: "admin"
15745
- }), method(object({}), LlmRuntimeStatusSchema), method(object({ model: ManagedModelRefSchema }), _void(), {
16151
+ }), method(object({
16152
+ addonId: string(),
16153
+ candidate: DiscoveryCandidateSchema,
16154
+ /** Owning integration id, stamped onto the new device's meta by the
16155
+ * device-manager forwarder so `removeByIntegration` can cascade it.
16156
+ * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
16157
+ integrationId: string().optional()
16158
+ }), DeviceSummarySchema, {
15746
16159
  kind: "mutation",
15747
16160
  auth: "admin"
15748
- }), method(object({ file: string() }), _void(), {
16161
+ }), method(object({
16162
+ addonId: string(),
16163
+ type: _enum(DeviceType)
16164
+ }), unknown().nullable()), method(object({
16165
+ addonId: string(),
16166
+ type: _enum(DeviceType),
16167
+ config: record(string(), unknown()),
16168
+ /** Owning integration id, stamped onto the new device's meta by the
16169
+ * device-manager forwarder so `removeByIntegration` can cascade it.
16170
+ * Optional for back-compat (omitted = no stamp = pre-existing behavior). */
16171
+ integrationId: string().optional()
16172
+ }), DeviceSummarySchema, {
15749
16173
  kind: "mutation",
15750
16174
  auth: "admin"
15751
- }), method(object({}), array(LlmNodeModelSchema)), method(object({}), LlmRuntimeDiskUsageSchema);
15752
- /**
15753
- * `llm` — consumer-facing LLM surface (spec §1-§3). Collection-mode: array
15754
- * methods concat-fan across providers; single-row methods route to ONE
15755
- * provider by the `addonId` in the call input (the notification-output
15756
- * posture, notification-output.cap.ts:215-250). Provided by `addon-ai`
15757
- * (hub-placed); the cap stays open for future providers.
15758
- *
15759
- * Profiles are ROWS (data), not addons: one row = one usable model endpoint.
15760
- * `apiKey` is a password field — providers REDACT it on read and merge on
15761
- * write; a stored key NEVER round-trips to a client.
15762
- */
15763
- var LlmProfileKindSchema = _enum([
15764
- "openai-compatible",
15765
- "openai",
15766
- "anthropic",
15767
- "google",
15768
- "managed-local"
15769
- ]);
15770
- var LlmProfileSchema = object({
15771
- id: string(),
15772
- name: string(),
15773
- kind: LlmProfileKindSchema,
15774
- /** Stamped by the provider — keeps the fanned catalog routable. */
16175
+ }), method(object({
15775
16176
  addonId: string(),
15776
- enabled: boolean(),
15777
- /** Vendor model id, or the managed runtime's loaded model. */
15778
- model: string(),
15779
- /** Required for openai-compatible; override for cloud kinds. */
15780
- baseUrl: string().optional(),
15781
- /** ConfigUISchema type:'password' — never round-trips (spec §5). */
15782
- apiKey: string().optional(),
15783
- supportsVision: boolean(),
15784
- temperature: number().min(0).max(2).optional(),
15785
- maxTokens: number().int().positive().optional(),
15786
- timeoutMs: number().int().positive().default(6e4),
15787
- extraHeaders: record(string(), string()).optional(),
15788
- /** kind === 'managed-local' only (spec §4). */
15789
- runtime: ManagedRuntimeConfigSchema.optional()
15790
- });
15791
- /** ConfigUISchema tree passed through untyped on the wire (the
15792
- * notification-output `ConfigSchemaPassthrough` precedent at
15793
- * notification-output.cap.ts:151); the exported TS type re-tightens it. */
15794
- var ConfigSchemaPassthrough$1 = unknown();
15795
- var LlmProfileKindDescriptorSchema = object({
15796
- kind: LlmProfileKindSchema,
15797
- label: string(),
15798
- icon: string(),
15799
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
16177
+ type: _enum(DeviceType),
16178
+ key: string(),
16179
+ value: unknown(),
16180
+ formValues: record(string(), unknown()).optional()
16181
+ }), FieldProbeResultSchema, {
16182
+ kind: "mutation",
16183
+ auth: "admin"
16184
+ }), method(object({
15800
16185
  addonId: string(),
15801
- configSchema: ConfigSchemaPassthrough$1
15802
- });
15803
- var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
15804
- var LlmDefaultSchema = object({
15805
- selector: LlmDefaultSelectorSchema,
15806
- profileId: string()
15807
- });
15808
- /** Server-side rollup row — getUsage never dumps raw call rows (spec §6). */
15809
- var LlmUsageRollupSchema = object({
15810
- day: string(),
15811
- consumer: string(),
15812
- profileId: string(),
15813
- calls: number(),
15814
- okCalls: number(),
15815
- errorCalls: number(),
15816
- inputTokens: number(),
15817
- outputTokens: number(),
15818
- avgLatencyMs: number()
15819
- });
15820
- /** LLM-facing view over the reused ModelCatalogEntry mechanism (spec §4.2). */
15821
- var ManagedModelCatalogEntrySchema = object({
15822
- id: string(),
15823
- label: string(),
15824
- family: string(),
15825
- purpose: _enum(["text", "vision"]),
15826
- url: string(),
15827
- sha256: string(),
15828
- sizeBytes: number(),
15829
- quantization: string(),
15830
- /** Load-time guidance shown in the picker. */
15831
- minRamBytes: number(),
15832
- contextSizeDefault: number().int(),
15833
- /** Vision models: companion projector file. */
15834
- mmprojUrl: string().optional()
15835
- });
15836
- var LlmRuntimeNodeSchema = object({
15837
- nodeId: string(),
15838
- reachable: boolean(),
15839
- status: LlmRuntimeStatusSchema.optional(),
15840
- disk: LlmRuntimeDiskUsageSchema.optional(),
15841
- error: string().optional()
15842
- });
15843
- var GenerateVisionInputSchema = LlmGenerateBaseInputSchema.extend({ images: array(LlmImageSchema).min(1) });
15844
- var ProfileRefInputSchema = object({
16186
+ integrationId: string()
16187
+ }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15845
16188
  addonId: string(),
15846
- profileId: string()
15847
- });
15848
- method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
16189
+ integrationId: string()
16190
+ }), AdoptionStatusSchema, {
15849
16191
  kind: "mutation",
15850
16192
  auth: "admin"
15851
- }), method(ProfileRefInputSchema, _void(), {
16193
+ }), method(AdoptInputSchema.extend({ addonId: string() }), AdoptResultSchema, {
15852
16194
  kind: "mutation",
15853
16195
  auth: "admin"
15854
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
16196
+ }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
15855
16197
  kind: "mutation",
15856
16198
  auth: "admin"
15857
- }), method(ProfileRefInputSchema, array(string())), method(object({}), array(LlmDefaultSchema)), method(object({
15858
- selector: LlmDefaultSelectorSchema,
15859
- profileId: string().nullable()
15860
- }), _void(), {
16199
+ }), method(ResyncInputSchema, ResyncResultSchema, {
15861
16200
  kind: "mutation",
15862
16201
  auth: "admin"
15863
- }), method(object({
15864
- since: number().optional(),
15865
- until: number().optional(),
15866
- consumer: string().optional(),
15867
- profileId: string().optional()
15868
- }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
15869
- nodeId: string(),
15870
- model: ManagedModelRefSchema
15871
- }), _void(), {
16202
+ }), method(object({}), object({ providers: array(object({
16203
+ addonId: string(),
16204
+ label: string()
16205
+ })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
16206
+ addonId: string(),
16207
+ label: string(),
16208
+ candidates: array(DiscoveryCandidateSchema).readonly(),
16209
+ error: string().nullable()
16210
+ })).readonly() }), {
15872
16211
  kind: "mutation",
15873
16212
  auth: "admin"
15874
16213
  }), method(object({
15875
- nodeId: string(),
15876
- file: string()
15877
- }), _void(), {
16214
+ addonId: string(),
16215
+ params: record(string(), unknown()).optional()
16216
+ }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
15878
16217
  kind: "mutation",
15879
16218
  auth: "admin"
15880
- }), method(ProfileRefInputSchema, LlmRuntimeStatusSchema), method(ProfileRefInputSchema, LlmRuntimeStatusSchema, {
16219
+ }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
16220
+ deviceId: number(),
16221
+ key: string(),
16222
+ value: unknown()
16223
+ }), FieldProbeResultSchema, {
15881
16224
  kind: "mutation",
15882
16225
  auth: "admin"
15883
- }), method(ProfileRefInputSchema, _void(), {
16226
+ }), method(object({
16227
+ deviceId: number(),
16228
+ caps: array(string()).readonly().optional()
16229
+ }), record(string(), unknown().nullable()));
16230
+ method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
16231
+ deviceId: number(),
16232
+ capName: string()
16233
+ }), record(string(), unknown()).nullable()), method(object({}), record(string(), record(string(), record(string(), unknown())))), method(object({
16234
+ deviceId: number(),
16235
+ capName: string(),
16236
+ slice: record(string(), unknown())
16237
+ }), _void(), { kind: "mutation" }), object({
16238
+ deviceId: number(),
16239
+ capName: string(),
16240
+ slice: record(string(), unknown())
16241
+ });
16242
+ /**
16243
+ * Embedding output. `embedding` is wire-encoded as `number[]` so the
16244
+ * Zod-validated tRPC surface round-trips cleanly; consumers that need a
16245
+ * `Float32Array` can wrap it on the way out (in-process, no marshalling
16246
+ * is involved). `inferenceMs` mirrors the runtime field used by the
16247
+ * post-analysis enrichment-engine.
16248
+ */
16249
+ var EmbeddingResultSchema = object({
16250
+ embedding: array(number()),
16251
+ inferenceMs: number()
16252
+ });
16253
+ var EmbeddingInfoSchema = object({
16254
+ modelId: string(),
16255
+ embeddingDim: number(),
16256
+ ready: boolean()
16257
+ });
16258
+ method(object({
16259
+ crop: _instanceof(Uint8Array),
16260
+ width: number(),
16261
+ height: number()
16262
+ }), EmbeddingResultSchema), method(object({ text: string() }), EmbeddingResultSchema), method(_void(), EmbeddingInfoSchema);
16263
+ /**
16264
+ * filesystem-browse — per-node capability for browsing the node's local
16265
+ * filesystem, sandboxed to operator-configured allowed roots. Used by the
16266
+ * admin "Add filesystem location" flow to pick a node + path. `mode:'per-node'`
16267
+ * (one provider per node); the hub calls it with `{nodeId}` so the codegen
16268
+ * routes to that exact node (default `nodeIdMode:'routing'`).
16269
+ */
16270
+ var DirEntrySchema = object({
16271
+ name: string(),
16272
+ path: string()
16273
+ });
16274
+ var BrowseResultSchema = object({
16275
+ path: string(),
16276
+ entries: array(DirEntrySchema).readonly(),
16277
+ freeBytes: number(),
16278
+ totalBytes: number()
16279
+ });
16280
+ method(_void(), array(string()).readonly(), { auth: "admin" }), method(object({ path: string() }), BrowseResultSchema, { auth: "admin" }), method(object({ path: string() }), object({ path: string() }), {
15884
16281
  kind: "mutation",
15885
16282
  auth: "admin"
15886
16283
  });
15887
- var LogLevelSchema = _enum([
15888
- "debug",
15889
- "info",
15890
- "warn",
15891
- "error"
16284
+ /**
16285
+ * Shared LLM generate contracts — imported by BOTH `llm.cap.ts` (consumer
16286
+ * surface) and `llm-runtime.cap.ts` (node-side managed executor) so the two
16287
+ * caps stay wire-compatible without a circular cap→cap import.
16288
+ *
16289
+ * Errors are a discriminated-union RESULT, never thrown: the shape survives
16290
+ * every transport tier structurally, and failed calls still write usage rows.
16291
+ * Token counts only in v1 — no costUsd (operator decision, 2026-07-15).
16292
+ */
16293
+ var LlmUsageSchema = object({
16294
+ inputTokens: number(),
16295
+ outputTokens: number()
16296
+ });
16297
+ var LlmErrorCodeSchema = _enum([
16298
+ "timeout",
16299
+ "rate-limited",
16300
+ "auth",
16301
+ "refusal",
16302
+ "bad-request",
16303
+ "unavailable",
16304
+ "no-profile",
16305
+ "budget-exceeded",
16306
+ "adapter-error"
15892
16307
  ]);
15893
- var LogEntrySchema = object({
15894
- timestamp: date(),
15895
- level: LogLevelSchema,
15896
- scope: array(string()),
16308
+ var LlmGenerateResultSchema = discriminatedUnion("ok", [object({
16309
+ ok: literal(true),
16310
+ text: string(),
16311
+ model: string(),
16312
+ usage: LlmUsageSchema,
16313
+ truncated: boolean(),
16314
+ latencyMs: number()
16315
+ }), object({
16316
+ ok: literal(false),
16317
+ code: LlmErrorCodeSchema,
15897
16318
  message: string(),
15898
- meta: record(string(), unknown()).optional(),
15899
- tags: record(string(), string()).optional()
15900
- });
15901
- method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
15902
- scope: array(string()).optional(),
15903
- level: LogLevelSchema.optional(),
15904
- since: date().optional(),
15905
- until: date().optional(),
15906
- limit: number().optional(),
15907
- tags: record(string(), string()).optional()
15908
- }), array(LogEntrySchema).readonly());
16319
+ retryAfterMs: number().optional()
16320
+ })]);
15909
16321
  /**
15910
- * `login-method` collection cap through which auth addons contribute
15911
- * their pre-auth login surfaces to the login page. This is the SINGLE,
15912
- * generic mechanism that supersedes the dead `auth.listProviders` reader:
15913
- * every auth addon (OIDC, magic-link, WebAuthn/passkey) registers a
15914
- * `login-method` provider and the PUBLIC `auth.listLoginMethods`
15915
- * procedure aggregates them for the unauthenticated login page.
15916
- *
15917
- * A contribution is a discriminated union on `kind`:
15918
- *
15919
- * - `redirect` a declarative button. The login page renders a generic
15920
- * button that navigates to `startUrl` (an addon-owned HTTP route).
15921
- * Covers OIDC (`/addon/auth-oidc/<id>/start`) and magic-link with
15922
- * ZERO shell-side JS. A future SSO addon plugs in the same way — the
15923
- * login page needs NO change.
15924
- *
15925
- * - `widget` — a Module-Federation widget the login page mounts (via
15926
- * `loadRemoteBundle`) for an in-page ceremony. `auth.listLoginMethods`
15927
- * stamps a public `bundleUrl` from `addonId` + `bundle`. Generic
15928
- * mechanism kept for future use; no shipped addon uses it on the login
15929
- * page (the passkey ceremony below runs natively in the shell instead).
15930
- *
15931
- * - `passkey` — a declarative WebAuthn ceremony the shell renders
15932
- * natively (`@simplewebauthn/browser` lives in `addon-admin-ui`, not in
15933
- * a remotely-loaded bundle). Carries the addon's effective `rpId` /
15934
- * `origin` (from its `resolveRpID()` / `resolveOrigin()`) so the shell
15935
- * can gate visibility (IP-literal origin, hostname/rpId mismatch) WITHOUT
15936
- * fetching any remote code pre-auth. Contribution stays unconditional
15937
- * enrollment state is never leaked pre-auth; visibility is a shell
15938
- * decision.
15939
- *
15940
- * Every contribution carries a `stage`:
15941
- * - `primary` — shown on the first credentials screen (OIDC /
15942
- * magic-link buttons; a future usernameless passkey).
15943
- * - `second-factor` — shown AFTER the password leg, gated on the
15944
- * returned `factors` (passkey-as-2FA today).
16322
+ * `Uint8Array` is the sanctioned binary convention superjson + the UDS
16323
+ * MsgPack channel round-trip typed arrays (embedding-encoder.cap.ts:29,
16324
+ * notification-output.cap.ts:27-31 precedents).
16325
+ */
16326
+ var LlmImageSchema = object({
16327
+ bytes: _instanceof(Uint8Array),
16328
+ mimeType: string()
16329
+ });
16330
+ var LlmGenerateBaseInputSchema = object({
16331
+ /** Collection routing (the notification-output posture). */
16332
+ addonId: string().optional(),
16333
+ /** Explicit profile; else the resolution chain (spec §3). */
16334
+ profileId: string().optional(),
16335
+ /** MANDATORY usage tag: 'ai-summary', 'notifier-rules', 'adhoc-ui', … */
16336
+ consumer: string(),
16337
+ system: string().optional(),
16338
+ /** v1: single-turn. `messages[]` is a v2 additive field. */
16339
+ prompt: string(),
16340
+ /** Structured output adapter-mapped (response_format / forced tool / responseSchema). */
16341
+ jsonSchema: record(string(), unknown()).optional(),
16342
+ /** Per-call override of the profile default. */
16343
+ maxTokens: number().int().positive().optional(),
16344
+ temperature: number().optional()
16345
+ });
16346
+ /**
16347
+ * `llm-runtime` node-side managed llama.cpp executor (spec §4). Registered
16348
+ * on EVERY node where `addon-ai` is installed; the hub `llm` provider reaches
16349
+ * a specific node's runtime with `nodePin(profile.runtime.nodeId)` normal
16350
+ * cap routing, zero bespoke plumbing. `internal: true`: the operator reaches
16351
+ * this only through the `llm` cap's methods.
15945
16352
  *
15946
- * `mount: skip` the cap is read server-side by the core auth router
15947
- * (`registry.getCollection('login-method')`), never mounted as its own
15948
- * tRPC router.
16353
+ * One running llama-server child per node in v1 (models are RAM-heavy).
16354
+ * Resource ceiling = llama-server flags + idleStopMinutes ONLY (no RSS
16355
+ * watchdog — operator decision #3).
15949
16356
  */
15950
- /** When a login method renders in the two-phase login flow. */
15951
- var LoginStageEnum = _enum(["primary", "second-factor"]);
15952
- /** One login-method contribution — redirect button, pre-auth widget, or native passkey ceremony. */
15953
- var LoginMethodContributionSchema = discriminatedUnion("kind", [
16357
+ var ManagedModelRefSchema = discriminatedUnion("kind", [
15954
16358
  object({
15955
- kind: literal("redirect"),
15956
- /** Stable id within the login-method set (e.g. `auth-oidc/google`). */
15957
- id: string(),
15958
- /** Operator-facing button label. */
15959
- label: string(),
15960
- /** lucide-react icon name. */
15961
- icon: string().optional(),
15962
- /** Addon-owned HTTP route the button navigates to (GET). */
15963
- startUrl: string(),
15964
- stage: LoginStageEnum
16359
+ kind: literal("catalog"),
16360
+ catalogId: string()
15965
16361
  }),
15966
16362
  object({
15967
- kind: literal("widget"),
15968
- /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-login`). */
15969
- id: string(),
15970
- /** Owning addon id — drives the public bundle URL + the MF namespace. */
15971
- addonId: string(),
15972
- /** Bundle filename inside the addon's dist dir (`remoteEntry.js`). */
15973
- bundle: string(),
15974
- /** MF remote descriptor — `{ remoteName, exposedModule, componentKey }`. */
15975
- remote: WidgetRemoteSchema,
15976
- stage: LoginStageEnum
16363
+ kind: literal("url"),
16364
+ url: string(),
16365
+ sha256: string().optional()
15977
16366
  }),
15978
16367
  object({
15979
- kind: literal("passkey"),
15980
- /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-direct-login`). */
15981
- id: string(),
15982
- /** Operator-facing button label. */
15983
- label: string(),
15984
- stage: LoginStageEnum,
15985
- /** Effective WebAuthn RP ID (`resolveRpID()`) — the shell gates visibility on it. */
15986
- rpId: string(),
15987
- /** Effective expected origin (`resolveOrigin()`), null when unconfigured. */
15988
- origin: string().nullable()
16368
+ kind: literal("path"),
16369
+ path: string()
15989
16370
  })
15990
16371
  ]);
15991
- method(_void(), array(LoginMethodContributionSchema).readonly());
15992
- var CpuBreakdownSchema = object({
15993
- total: number(),
15994
- user: number(),
15995
- system: number(),
15996
- irq: number(),
15997
- nice: number(),
15998
- loadAvg: tuple([
15999
- number(),
16000
- number(),
16001
- number()
16002
- ]),
16003
- cores: number()
16004
- });
16005
- var MemoryInfoSchema = object({
16006
- percent: number(),
16007
- totalBytes: number(),
16008
- usedBytes: number(),
16009
- availableBytes: number(),
16010
- swapUsedBytes: number(),
16011
- swapTotalBytes: number()
16012
- });
16013
- var DiskIoSnapshotSchema = object({
16014
- readBytes: number(),
16015
- writeBytes: number(),
16016
- readOps: number(),
16017
- writeOps: number(),
16018
- timestampMs: number()
16019
- });
16020
- var NetworkIoSnapshotSchema = object({
16021
- rxBytes: number(),
16022
- txBytes: number(),
16023
- rxPackets: number(),
16024
- txPackets: number(),
16025
- rxErrors: number(),
16026
- txErrors: number(),
16027
- timestampMs: number()
16028
- });
16029
- var MetricsGpuInfoSchema = object({
16030
- utilization: number(),
16031
- model: string(),
16032
- memoryUsedBytes: number(),
16033
- memoryTotalBytes: number(),
16034
- temperature: number().nullable()
16035
- });
16036
- var ProcessResourceInfoSchema = object({
16037
- openFds: number(),
16038
- threadCount: number(),
16039
- activeHandles: number(),
16040
- activeRequests: number()
16041
- });
16042
- var PressureAvgsSchema = object({
16043
- avg10: number(),
16044
- avg60: number(),
16045
- avg300: number()
16046
- });
16047
- var PressureInfoSchema = object({
16048
- some: PressureAvgsSchema,
16049
- full: PressureAvgsSchema.nullable()
16050
- });
16051
- var SystemResourceSnapshotSchema = object({
16052
- cpu: CpuBreakdownSchema,
16053
- memory: MemoryInfoSchema,
16054
- gpu: MetricsGpuInfoSchema.nullable(),
16055
- network: NetworkIoSnapshotSchema,
16056
- disk: DiskIoSnapshotSchema,
16057
- pressure: object({
16058
- cpu: PressureInfoSchema.nullable(),
16059
- memory: PressureInfoSchema.nullable(),
16060
- io: PressureInfoSchema.nullable()
16061
- }),
16062
- process: ProcessResourceInfoSchema,
16063
- cpuTemperature: number().nullable(),
16064
- timestampMs: number()
16065
- });
16066
- var DiskSpaceInfoSchema = object({
16067
- path: string(),
16068
- totalBytes: number(),
16069
- usedBytes: number(),
16070
- availableBytes: number(),
16071
- percent: number()
16072
- });
16073
- var PidResourceStatsSchema = object({
16074
- pid: number(),
16075
- cpu: number(),
16076
- memory: number(),
16077
- /**
16078
- * Private (anonymous) resident bytes — the per-process V8 heap + native
16079
- * allocations NOT shared with other processes (Linux RssAnon). This is the
16080
- * "real" per-runner cost; summing it across runners is meaningful, unlike
16081
- * `memory` (RSS), which double-counts the shared mmap'd framework code.
16082
- * Undefined where /proc is unavailable (e.g. macOS).
16083
- */
16084
- privateBytes: number().optional(),
16085
- /**
16086
- * Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
16087
- * code shared copy-on-write across runners. Undefined on macOS.
16088
- */
16089
- sharedBytes: number().optional()
16372
+ var ManagedRuntimeConfigSchema = object({
16373
+ /** WHERE the runtime lives — hub or any agent. */
16374
+ nodeId: string(),
16375
+ /** Closed for v1; 'ollama' is a v2 candidate. */
16376
+ engine: _enum(["llama-cpp"]),
16377
+ model: ManagedModelRefSchema,
16378
+ contextSize: number().int().default(4096),
16379
+ /** 0 = CPU-only. */
16380
+ gpuLayers: number().int().default(0),
16381
+ /** Default: cpus-2, clamped ≥1 (resolved node-side). */
16382
+ threads: number().int().optional(),
16383
+ /** Concurrent slots. */
16384
+ parallel: number().int().default(1),
16385
+ /** Else lazy: first generate boots it. */
16386
+ autoStart: boolean().default(false),
16387
+ /** 0 = never; frees RAM after quiet periods. */
16388
+ idleStopMinutes: number().int().default(30)
16090
16389
  });
16091
- var AddonInstanceSchema = object({
16092
- addonId: string(),
16390
+ var LlmRuntimeStatusSchema = object({
16391
+ /** Status is ALWAYS node-qualified. */
16093
16392
  nodeId: string(),
16094
- role: _enum(["hub", "worker"]),
16095
- pid: number(),
16096
16393
  state: _enum([
16097
- "starting",
16098
- "running",
16099
- "stopping",
16100
16394
  "stopped",
16101
- "crashed"
16102
- ]),
16103
- uptimeSec: number()
16104
- });
16105
- var NodeProcessSchema = object({
16106
- pid: number(),
16107
- ppid: number(),
16108
- pgid: number(),
16109
- classification: _enum([
16110
- "root",
16111
- "managed",
16112
- "system",
16113
- "ghost"
16395
+ "downloading",
16396
+ "starting",
16397
+ "ready",
16398
+ "crashed",
16399
+ "failed"
16114
16400
  ]),
16115
- /** `$process` addon binding when `managed`, else null. */
16116
- addonId: string().nullable(),
16117
- /** Kernel-reported nodeId when the process is a known agent/worker. */
16118
- nodeId: string().nullable(),
16119
- /** Truncated command line. */
16120
- command: string(),
16121
- cpuPercent: number(),
16122
- memoryRssBytes: number(),
16123
- /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
16124
- uptimeSec: number(),
16125
- /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
16126
- orphaned: boolean()
16127
- });
16128
- var KillProcessInputSchema = object({
16129
- pid: number(),
16130
- /** Force = SIGKILL. Default is SIGTERM. */
16131
- force: boolean().optional()
16132
- });
16133
- var KillProcessResultSchema = object({
16134
- success: boolean(),
16135
- reason: string().optional(),
16136
- signal: _enum(["SIGTERM", "SIGKILL"]).optional()
16137
- });
16138
- var DumpHeapSnapshotInputSchema = object({
16139
- /** The addon whose runner should dump a heap snapshot. */
16140
- addonId: string() });
16141
- var DumpHeapSnapshotResultSchema = object({
16142
- success: boolean(),
16143
- /** Path of the written .heapsnapshot inside the runner's container/host. */
16144
- path: string().optional(),
16145
- /** Process pid that was signalled. */
16146
16401
  pid: number().optional(),
16147
- reason: string().optional()
16402
+ port: number().optional(),
16403
+ modelPath: string().optional(),
16404
+ modelId: string().optional(),
16405
+ downloadProgress: number().min(0).max(1).optional(),
16406
+ lastError: string().optional(),
16407
+ crashesInWindow: number(),
16408
+ /** Child RSS (sampled best-effort). */
16409
+ memoryBytes: number().optional(),
16410
+ vramBytes: number().optional()
16148
16411
  });
16149
- var SystemMetricsSchema = object({
16150
- cpuPercent: number(),
16151
- memoryPercent: number(),
16152
- memoryUsedMB: number(),
16153
- memoryTotalMB: number(),
16154
- diskPercent: number().optional(),
16155
- temperature: number().optional(),
16156
- gpuPercent: number().optional(),
16157
- gpuMemoryPercent: number().optional()
16412
+ var LlmNodeModelSchema = object({
16413
+ file: string(),
16414
+ sizeBytes: number(),
16415
+ catalogId: string().optional(),
16416
+ installedAt: number().optional()
16158
16417
  });
16159
- 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, {
16418
+ var LlmRuntimeDiskUsageSchema = object({
16419
+ nodeId: string(),
16420
+ modelsBytes: number(),
16421
+ freeBytes: number().optional()
16422
+ });
16423
+ method(LlmGenerateBaseInputSchema.extend({
16424
+ images: array(LlmImageSchema).optional(),
16425
+ runtime: ManagedRuntimeConfigSchema,
16426
+ /** The managed profile's timeout, threaded by the hub provider. */
16427
+ timeoutMs: number().int().positive().optional()
16428
+ }), LlmGenerateResultSchema, { kind: "mutation" }), method(object({ runtime: ManagedRuntimeConfigSchema }), LlmRuntimeStatusSchema, {
16160
16429
  kind: "mutation",
16161
16430
  auth: "admin"
16162
- }), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
16431
+ }), method(object({}), _void(), {
16163
16432
  kind: "mutation",
16164
16433
  auth: "admin"
16165
- });
16166
- method(object({
16167
- sourceUrl: string(),
16168
- metadata: ModelConvertMetadataSchema,
16169
- targets: array(ConvertTargetSchema).min(1).readonly(),
16170
- calibrationRef: string().optional(),
16171
- sessionId: string().optional()
16172
- }), ConvertResultSchema, {
16434
+ }), method(object({}), LlmRuntimeStatusSchema), method(object({ model: ManagedModelRefSchema }), _void(), {
16173
16435
  kind: "mutation",
16174
- auth: "admin",
16175
- timeoutMs: 6e5
16176
- });
16177
- method(object({
16178
- nodeId: string(),
16179
- modelId: string(),
16180
- format: _enum(MODEL_FORMATS),
16181
- entry: ModelCatalogEntrySchema
16182
- }), object({
16183
- ok: boolean(),
16184
- /** sha256 of the staged tarball (empty for a hub-local no-op). */
16185
- sha256: string(),
16186
- bytes: number(),
16187
- /** The target node's modelsDir the artifact landed in. */
16188
- path: string()
16189
- }), {
16436
+ auth: "admin"
16437
+ }), method(object({ file: string() }), _void(), {
16190
16438
  kind: "mutation",
16191
16439
  auth: "admin"
16192
- });
16193
- /**
16194
- * `mqtt-broker` — broker-registry cap.
16195
- *
16196
- * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
16197
- * MQTT brokers (external + optionally an embedded `aedes`-backed one)
16198
- * and (b) the connection details a consumer addon needs to spin up
16199
- * its OWN `mqtt.js` client.
16200
- *
16201
- * Why: pub/sub routing over the system event-bus loses fidelity
16202
- * (callback shape, QoS guarantees, will/retain semantics) and adds
16203
- * refcount bookkeeping that addons would rather own themselves. The
16204
- * canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
16205
- * features anyway — give it the connection config, get out of the way.
16206
- *
16207
- * Consumer flow:
16208
- * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
16209
- * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
16210
- * client.subscribe('zigbee2mqtt/+')
16211
- *
16212
- * Collection mode: multiple brokers (e.g. one local mosquitto + one
16213
- * cloud bridge). The "embedded" entry (when present) is just another
16214
- * broker in the registry — its lifecycle is owned by the addon that
16215
- * spawned it.
16216
- */
16217
- var BrokerKindSchema = _enum(["external", "embedded"]);
16440
+ }), method(object({}), array(LlmNodeModelSchema)), method(object({}), LlmRuntimeDiskUsageSchema);
16218
16441
  /**
16219
- * Broker live-probe status.
16442
+ * `llm` — consumer-facing LLM surface (spec §1-§3). Collection-mode: array
16443
+ * methods concat-fan across providers; single-row methods route to ONE
16444
+ * provider by the `addonId` in the call input (the notification-output
16445
+ * posture, notification-output.cap.ts:215-250). Provided by `addon-ai`
16446
+ * (hub-placed); the cap stays open for future providers.
16220
16447
  *
16221
- * - `connected` last probe completed a clean CONNACK
16222
- * - `disconnected` — no probe has run yet (cold cache)
16223
- * - `auth-failed` CONNACK refused with auth error (RC 4 / 5)
16224
- * - `unreachable` — TCP connect timed out / refused
16225
- * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
16448
+ * Profiles are ROWS (data), not addons: one row = one usable model endpoint.
16449
+ * `apiKey` is a password field providers REDACT it on read and merge on
16450
+ * write; a stored key NEVER round-trips to a client.
16226
16451
  */
16227
- var BrokerStatusSchema$1 = _enum([
16228
- "connected",
16229
- "disconnected",
16230
- "auth-failed",
16231
- "unreachable",
16232
- "tls-error"
16452
+ var LlmProfileKindSchema = _enum([
16453
+ "openai-compatible",
16454
+ "openai",
16455
+ "anthropic",
16456
+ "google",
16457
+ "managed-local"
16233
16458
  ]);
16234
- var BrokerInfoSchema = object({
16459
+ var LlmProfileSchema = object({
16235
16460
  id: string(),
16236
16461
  name: string(),
16237
- url: string(),
16238
- kind: BrokerKindSchema,
16239
- status: BrokerStatusSchema$1,
16240
- latencyMs: number().nullable(),
16241
- error: string().optional(),
16242
- /** Embedded brokers only: number of MQTT clients currently connected. */
16243
- connectedClients: number().int().nonnegative().optional(),
16244
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
16245
- lastCheckedAt: number().optional()
16462
+ kind: LlmProfileKindSchema,
16463
+ /** Stamped by the provider — keeps the fanned catalog routable. */
16464
+ addonId: string(),
16465
+ enabled: boolean(),
16466
+ /** Vendor model id, or the managed runtime's loaded model. */
16467
+ model: string(),
16468
+ /** Required for openai-compatible; override for cloud kinds. */
16469
+ baseUrl: string().optional(),
16470
+ /** ConfigUISchema type:'password' — never round-trips (spec §5). */
16471
+ apiKey: string().optional(),
16472
+ supportsVision: boolean(),
16473
+ temperature: number().min(0).max(2).optional(),
16474
+ maxTokens: number().int().positive().optional(),
16475
+ timeoutMs: number().int().positive().default(6e4),
16476
+ extraHeaders: record(string(), string()).optional(),
16477
+ /** kind === 'managed-local' only (spec §4). */
16478
+ runtime: ManagedRuntimeConfigSchema.optional()
16246
16479
  });
16247
- /**
16248
- * Connection details — what a consumer needs to call
16249
- * `mqtt.connect(url, options)`. We split URL + credentials so the
16250
- * consumer can pass them as `mqtt.connect(url, { username, password })`
16251
- * instead of stuffing creds into the URL (which leaks them into logs).
16252
- */
16253
- var BrokerConnectionDetailsSchema = object({
16254
- url: string(),
16255
- username: string().optional(),
16256
- password: string().optional(),
16257
- /**
16258
- * Suggested prefix for `clientId`. Each consumer should suffix this
16259
- * with its own discriminator (addon id, instance id) so reconnects
16260
- * don't kick each other off (MQTT spec: clientId must be unique per
16261
- * broker).
16262
- */
16263
- clientIdPrefix: string().optional()
16480
+ /** ConfigUISchema tree passed through untyped on the wire (the
16481
+ * notification-output `ConfigSchemaPassthrough` precedent at
16482
+ * notification-output.cap.ts:151); the exported TS type re-tightens it. */
16483
+ var ConfigSchemaPassthrough$1 = unknown();
16484
+ var LlmProfileKindDescriptorSchema = object({
16485
+ kind: LlmProfileKindSchema,
16486
+ label: string(),
16487
+ icon: string(),
16488
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
16489
+ addonId: string(),
16490
+ configSchema: ConfigSchemaPassthrough$1
16264
16491
  });
16265
- var AddBrokerInputSchema = object({
16266
- name: string().min(1),
16267
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
16268
- username: string().optional(),
16269
- password: string().optional(),
16270
- clientIdPrefix: string().optional()
16492
+ var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
16493
+ var LlmDefaultSchema = object({
16494
+ selector: LlmDefaultSelectorSchema,
16495
+ profileId: string()
16271
16496
  });
16272
- var AddBrokerResultSchema = object({ id: string() });
16273
- var IdInputSchema = object({ id: string() });
16274
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
16275
- ok: literal(true),
16276
- latencyMs: number()
16277
- }), object({
16278
- ok: literal(false),
16279
- error: string()
16280
- })]);
16281
- var StartEmbeddedInputSchema = object({
16282
- port: number().int().min(1).max(65535).default(1883),
16283
- /** Allow anonymous connect (no username/password). Default: false. */
16284
- allowAnonymous: boolean().default(false),
16285
- /** Optional shared username/password for clients. */
16286
- username: string().optional(),
16287
- password: string().optional()
16497
+ /** Server-side rollup row getUsage never dumps raw call rows (spec §6). */
16498
+ var LlmUsageRollupSchema = object({
16499
+ day: string(),
16500
+ consumer: string(),
16501
+ profileId: string(),
16502
+ calls: number(),
16503
+ okCalls: number(),
16504
+ errorCalls: number(),
16505
+ inputTokens: number(),
16506
+ outputTokens: number(),
16507
+ avgLatencyMs: number()
16288
16508
  });
16289
- var StartEmbeddedResultSchema = object({
16509
+ /** LLM-facing view over the reused ModelCatalogEntry mechanism (spec §4.2). */
16510
+ var ManagedModelCatalogEntrySchema = object({
16290
16511
  id: string(),
16291
- url: string()
16292
- });
16293
- var StatusSchema = object({
16294
- brokerCount: number(),
16295
- embeddedRunning: boolean()
16296
- });
16297
- 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);
16298
- var NetworkEndpointSchema = object({
16512
+ label: string(),
16513
+ family: string(),
16514
+ purpose: _enum(["text", "vision"]),
16299
16515
  url: string(),
16300
- hostname: string(),
16301
- port: number(),
16302
- protocol: _enum(["http", "https"])
16516
+ sha256: string(),
16517
+ sizeBytes: number(),
16518
+ quantization: string(),
16519
+ /** Load-time guidance shown in the picker. */
16520
+ minRamBytes: number(),
16521
+ contextSizeDefault: number().int(),
16522
+ /** Vision models: companion projector file. */
16523
+ mmprojUrl: string().optional()
16303
16524
  });
16304
- var NetworkAccessStatusSchema = object({
16305
- connected: boolean(),
16306
- endpoint: NetworkEndpointSchema.nullable(),
16525
+ var LlmRuntimeNodeSchema = object({
16526
+ nodeId: string(),
16527
+ reachable: boolean(),
16528
+ status: LlmRuntimeStatusSchema.optional(),
16529
+ disk: LlmRuntimeDiskUsageSchema.optional(),
16307
16530
  error: string().optional()
16308
16531
  });
16309
- /**
16310
- * Optional, richer endpoint shape returned by providers that expose
16311
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
16312
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
16313
- * the originating provider config (mode + sourcePort) so the
16314
- * orchestrator UI can label rows distinctly. Providers that expose only
16315
- * one endpoint just omit `listEndpoints` from their provider impl.
16316
- */
16317
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16318
- /**
16319
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
16320
- * the orchestrator can dedupe across `listEndpoints` polls.
16321
- */
16322
- id: string(),
16323
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
16324
- label: string(),
16325
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
16326
- mode: string().optional(),
16327
- /** Originating local port the ingress fronts (informational). */
16328
- sourcePort: number().optional()
16532
+ var GenerateVisionInputSchema = LlmGenerateBaseInputSchema.extend({ images: array(LlmImageSchema).min(1) });
16533
+ var ProfileRefInputSchema = object({
16534
+ addonId: string(),
16535
+ profileId: string()
16536
+ });
16537
+ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(GenerateVisionInputSchema, LlmGenerateResultSchema, { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
16538
+ kind: "mutation",
16539
+ auth: "admin"
16540
+ }), method(ProfileRefInputSchema, _void(), {
16541
+ kind: "mutation",
16542
+ auth: "admin"
16543
+ }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
16544
+ kind: "mutation",
16545
+ auth: "admin"
16546
+ }), method(ProfileRefInputSchema, array(string())), method(object({}), array(LlmDefaultSchema)), method(object({
16547
+ selector: LlmDefaultSelectorSchema,
16548
+ profileId: string().nullable()
16549
+ }), _void(), {
16550
+ kind: "mutation",
16551
+ auth: "admin"
16552
+ }), method(object({
16553
+ since: number().optional(),
16554
+ until: number().optional(),
16555
+ consumer: string().optional(),
16556
+ profileId: string().optional()
16557
+ }), array(LlmUsageRollupSchema)), method(object({}), array(ManagedModelCatalogEntrySchema)), method(object({}), array(LlmRuntimeNodeSchema)), method(object({ nodeId: string() }), array(LlmNodeModelSchema)), method(object({
16558
+ nodeId: string(),
16559
+ model: ManagedModelRefSchema
16560
+ }), _void(), {
16561
+ kind: "mutation",
16562
+ auth: "admin"
16563
+ }), method(object({
16564
+ nodeId: string(),
16565
+ file: string()
16566
+ }), _void(), {
16567
+ kind: "mutation",
16568
+ auth: "admin"
16569
+ }), method(ProfileRefInputSchema, LlmRuntimeStatusSchema), method(ProfileRefInputSchema, LlmRuntimeStatusSchema, {
16570
+ kind: "mutation",
16571
+ auth: "admin"
16572
+ }), method(ProfileRefInputSchema, _void(), {
16573
+ kind: "mutation",
16574
+ auth: "admin"
16575
+ });
16576
+ var LogLevelSchema = _enum([
16577
+ "debug",
16578
+ "info",
16579
+ "warn",
16580
+ "error"
16581
+ ]);
16582
+ var LogEntrySchema = object({
16583
+ timestamp: date(),
16584
+ level: LogLevelSchema,
16585
+ scope: array(string()),
16586
+ message: string(),
16587
+ meta: record(string(), unknown()).optional(),
16588
+ tags: record(string(), string()).optional()
16329
16589
  });
16330
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
16590
+ method(LogEntrySchema, _void(), { kind: "mutation" }), method(object({
16591
+ scope: array(string()).optional(),
16592
+ level: LogLevelSchema.optional(),
16593
+ since: date().optional(),
16594
+ until: date().optional(),
16595
+ limit: number().optional(),
16596
+ tags: record(string(), string()).optional()
16597
+ }), array(LogEntrySchema).readonly());
16331
16598
  /**
16332
- * notification-outputcanonical, capability-gated notification delivery.
16599
+ * `login-method`collection cap through which auth addons contribute
16600
+ * their pre-auth login surfaces to the login page. This is the SINGLE,
16601
+ * generic mechanism that supersedes the dead `auth.listProviders` reader:
16602
+ * every auth addon (OIDC, magic-link, WebAuthn/passkey) registers a
16603
+ * `login-method` provider and the PUBLIC `auth.listLoginMethods`
16604
+ * procedure aggregates them for the unauthenticated login page.
16333
16605
  *
16334
- * Apprise-derived model (see
16335
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16336
- * callers emit ONE canonical `Notification`; each provider declares a
16337
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
16338
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16339
- * message to what the kind supports — callers never special-case a service.
16606
+ * A contribution is a discriminated union on `kind`:
16340
16607
  *
16341
- * DESIGN DECISIONS (locked):
16342
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16343
- * `setTargetEnabled`), each provider persisting via the `settings-store`
16344
- * cap. Rationale: the admin UI needs one uniform surface across the
16345
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16346
- * alternative would fork the UI per addon and cannot host the
16347
- * discovery→adopt flow.
16348
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16349
- * the generated cap-mount auto-`concatCollection`-fans them across every
16350
- * registered provider (notifiers addon + HA addon) so one catalog is
16351
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16352
- * `addonId` the generated collection router extracts from the call input.
16353
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16354
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16355
- * `storage` / `storage-provider` / `recording` caps over the same path. No
16356
- * base64 fallback needed.
16608
+ * - `redirect` a declarative button. The login page renders a generic
16609
+ * button that navigates to `startUrl` (an addon-owned HTTP route).
16610
+ * Covers OIDC (`/addon/auth-oidc/<id>/start`) and magic-link with
16611
+ * ZERO shell-side JS. A future SSO addon plugs in the same way the
16612
+ * login page needs NO change.
16357
16613
  *
16358
- * TODO (deferred, closed-set change separate decision): add
16359
- * `providerKind: 'notify'` so notification providers surface on the unified
16360
- * admin "Integrations" page.
16361
- */
16362
- /**
16363
- * Zentik-derived typed-media enum — the superset across every kind. Each
16364
- * adapter picks what it supports and the degrade engine filters the rest.
16365
- */
16366
- var AttachmentMediaTypeSchema = _enum([
16367
- "image",
16368
- "video",
16369
- "gif",
16370
- "audio",
16371
- "icon"
16372
- ]);
16373
- /**
16374
- * A single attachment. Exactly one of `url` (remote source, most adapters
16375
- * prefer this) or `bytes` (inline source; required for Pushover-style
16376
- * bytes-only kinds) MUST be present the degrade engine expresses a
16377
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
16614
+ * - `widget` a Module-Federation widget the login page mounts (via
16615
+ * `loadRemoteBundle`) for an in-page ceremony. `auth.listLoginMethods`
16616
+ * stamps a public `bundleUrl` from `addonId` + `bundle`. Generic
16617
+ * mechanism kept for future use; no shipped addon uses it on the login
16618
+ * page (the passkey ceremony below runs natively in the shell instead).
16619
+ *
16620
+ * - `passkey` a declarative WebAuthn ceremony the shell renders
16621
+ * natively (`@simplewebauthn/browser` lives in `addon-admin-ui`, not in
16622
+ * a remotely-loaded bundle). Carries the addon's effective `rpId` /
16623
+ * `origin` (from its `resolveRpID()` / `resolveOrigin()`) so the shell
16624
+ * can gate visibility (IP-literal origin, hostname/rpId mismatch) WITHOUT
16625
+ * fetching any remote code pre-auth. Contribution stays unconditional —
16626
+ * enrollment state is never leaked pre-auth; visibility is a shell
16627
+ * decision.
16628
+ *
16629
+ * Every contribution carries a `stage`:
16630
+ * - `primary` — shown on the first credentials screen (OIDC /
16631
+ * magic-link buttons; a future usernameless passkey).
16632
+ * - `second-factor` — shown AFTER the password leg, gated on the
16633
+ * returned `factors` (passkey-as-2FA today).
16634
+ *
16635
+ * `mount: skip` — the cap is read server-side by the core auth router
16636
+ * (`registry.getCollection('login-method')`), never mounted as its own
16637
+ * tRPC router.
16378
16638
  */
16379
- var AttachmentSchema = object({
16380
- mediaType: AttachmentMediaTypeSchema,
16381
- url: string().optional(),
16382
- bytes: _instanceof(Uint8Array).optional(),
16383
- mime: string().optional(),
16384
- name: string().optional()
16385
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16386
- var NotificationFormatSchema = _enum([
16387
- "text",
16388
- "markdown",
16389
- "html"
16639
+ /** When a login method renders in the two-phase login flow. */
16640
+ var LoginStageEnum = _enum(["primary", "second-factor"]);
16641
+ /** One login-method contribution — redirect button, pre-auth widget, or native passkey ceremony. */
16642
+ var LoginMethodContributionSchema = discriminatedUnion("kind", [
16643
+ object({
16644
+ kind: literal("redirect"),
16645
+ /** Stable id within the login-method set (e.g. `auth-oidc/google`). */
16646
+ id: string(),
16647
+ /** Operator-facing button label. */
16648
+ label: string(),
16649
+ /** lucide-react icon name. */
16650
+ icon: string().optional(),
16651
+ /** Addon-owned HTTP route the button navigates to (GET). */
16652
+ startUrl: string(),
16653
+ stage: LoginStageEnum
16654
+ }),
16655
+ object({
16656
+ kind: literal("widget"),
16657
+ /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-login`). */
16658
+ id: string(),
16659
+ /** Owning addon id — drives the public bundle URL + the MF namespace. */
16660
+ addonId: string(),
16661
+ /** Bundle filename inside the addon's dist dir (`remoteEntry.js`). */
16662
+ bundle: string(),
16663
+ /** MF remote descriptor — `{ remoteName, exposedModule, componentKey }`. */
16664
+ remote: WidgetRemoteSchema,
16665
+ stage: LoginStageEnum
16666
+ }),
16667
+ object({
16668
+ kind: literal("passkey"),
16669
+ /** Stable id within the login-method set (e.g. `auth-webauthn/passkey-direct-login`). */
16670
+ id: string(),
16671
+ /** Operator-facing button label. */
16672
+ label: string(),
16673
+ stage: LoginStageEnum,
16674
+ /** Effective WebAuthn RP ID (`resolveRpID()`) — the shell gates visibility on it. */
16675
+ rpId: string(),
16676
+ /** Effective expected origin (`resolveOrigin()`), null when unconfigured. */
16677
+ origin: string().nullable()
16678
+ })
16390
16679
  ]);
16391
- /** A single tap-through action button. */
16392
- var NotificationActionSchema = object({
16393
- id: string(),
16394
- label: string(),
16395
- url: string().optional()
16680
+ method(_void(), array(LoginMethodContributionSchema).readonly());
16681
+ var CpuBreakdownSchema = object({
16682
+ total: number(),
16683
+ user: number(),
16684
+ system: number(),
16685
+ irq: number(),
16686
+ nice: number(),
16687
+ loadAvg: tuple([
16688
+ number(),
16689
+ number(),
16690
+ number()
16691
+ ]),
16692
+ cores: number()
16396
16693
  });
16397
- /**
16398
- * The canonical notification. `body` is the only hard field (Apprise model).
16399
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16400
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16401
- * the adapter maps this ordinal onto its native level. `level?` is an
16402
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
16403
- * `priority` for that one target.
16404
- */
16405
- var NotificationSchema = object({
16406
- body: string(),
16407
- title: string().optional(),
16408
- format: NotificationFormatSchema.default("text"),
16409
- priority: number().int().min(1).max(5).default(3),
16410
- level: string().optional(),
16411
- attachments: array(AttachmentSchema).optional(),
16412
- clickUrl: string().optional(),
16413
- actions: array(NotificationActionSchema).optional(),
16414
- sound: string().optional(),
16415
- ttl: number().optional(),
16416
- tag: string().optional(),
16417
- deviceId: number().optional(),
16418
- eventId: string().optional(),
16419
- metadata: record(string(), unknown()).optional()
16694
+ var MemoryInfoSchema = object({
16695
+ percent: number(),
16696
+ totalBytes: number(),
16697
+ usedBytes: number(),
16698
+ availableBytes: number(),
16699
+ swapUsedBytes: number(),
16700
+ swapTotalBytes: number()
16701
+ });
16702
+ var DiskIoSnapshotSchema = object({
16703
+ readBytes: number(),
16704
+ writeBytes: number(),
16705
+ readOps: number(),
16706
+ writeOps: number(),
16707
+ timestampMs: number()
16708
+ });
16709
+ var NetworkIoSnapshotSchema = object({
16710
+ rxBytes: number(),
16711
+ txBytes: number(),
16712
+ rxPackets: number(),
16713
+ txPackets: number(),
16714
+ rxErrors: number(),
16715
+ txErrors: number(),
16716
+ timestampMs: number()
16717
+ });
16718
+ var MetricsGpuInfoSchema = object({
16719
+ utilization: number(),
16720
+ model: string(),
16721
+ memoryUsedBytes: number(),
16722
+ memoryTotalBytes: number(),
16723
+ temperature: number().nullable()
16724
+ });
16725
+ var ProcessResourceInfoSchema = object({
16726
+ openFds: number(),
16727
+ threadCount: number(),
16728
+ activeHandles: number(),
16729
+ activeRequests: number()
16420
16730
  });
16421
- /** One declared native severity/priority level for a kind. */
16422
- var TargetKindLevelSchema = object({
16423
- id: string(),
16424
- label: string(),
16425
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16426
- ordinal: number().int().min(1).max(5).nullable(),
16427
- flags: object({
16428
- critical: boolean().optional(),
16429
- silent: boolean().optional(),
16430
- noPush: boolean().optional()
16431
- }).optional(),
16432
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16433
- requires: array(string()).optional(),
16434
- description: string().optional()
16731
+ var PressureAvgsSchema = object({
16732
+ avg10: number(),
16733
+ avg60: number(),
16734
+ avg300: number()
16435
16735
  });
16436
- /** The full capability block consulted before dispatch. */
16437
- var TargetKindCapsSchema = object({
16438
- attachments: object({
16439
- mediaTypes: array(AttachmentMediaTypeSchema),
16440
- mode: _enum([
16441
- "url",
16442
- "bytes",
16443
- "both"
16444
- ]),
16445
- max: number().int().nonnegative(),
16446
- maxBytes: number().int().positive().optional()
16736
+ var PressureInfoSchema = object({
16737
+ some: PressureAvgsSchema,
16738
+ full: PressureAvgsSchema.nullable()
16739
+ });
16740
+ var SystemResourceSnapshotSchema = object({
16741
+ cpu: CpuBreakdownSchema,
16742
+ memory: MemoryInfoSchema,
16743
+ gpu: MetricsGpuInfoSchema.nullable(),
16744
+ network: NetworkIoSnapshotSchema,
16745
+ disk: DiskIoSnapshotSchema,
16746
+ pressure: object({
16747
+ cpu: PressureInfoSchema.nullable(),
16748
+ memory: PressureInfoSchema.nullable(),
16749
+ io: PressureInfoSchema.nullable()
16447
16750
  }),
16448
- /** Max action buttons (0 = none). */
16449
- actions: number().int().nonnegative(),
16450
- levels: array(TargetKindLevelSchema),
16451
- format: array(NotificationFormatSchema),
16452
- clickUrl: boolean(),
16453
- sound: boolean(),
16454
- ttl: boolean(),
16455
- bodyMaxLen: number().int().positive()
16751
+ process: ProcessResourceInfoSchema,
16752
+ cpuTemperature: number().nullable(),
16753
+ timestampMs: number()
16456
16754
  });
16457
- /**
16458
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16459
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16460
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`)
16461
- * the union is large and not meant for runtime validation here; the exported
16462
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16463
- */
16464
- var ConfigSchemaPassthrough = unknown();
16465
- var TargetKindSchema = object({
16466
- kind: string(),
16467
- label: string(),
16468
- icon: string(),
16469
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
16470
- addonId: string(),
16471
- configSchema: ConfigSchemaPassthrough,
16472
- supportsDiscovery: boolean(),
16473
- caps: TargetKindCapsSchema
16755
+ var DiskSpaceInfoSchema = object({
16756
+ path: string(),
16757
+ totalBytes: number(),
16758
+ usedBytes: number(),
16759
+ availableBytes: number(),
16760
+ percent: number()
16474
16761
  });
16475
- /**
16476
- * A persisted target. `config` holds secrets; providers REDACT secret fields
16477
- * (return a presence marker only) when serving `listTargets` — never
16478
- * round-trip a stored secret to the UI.
16479
- */
16480
- var TargetSchema = object({
16481
- id: string(),
16482
- name: string(),
16483
- kind: string(),
16762
+ var PidResourceStatsSchema = object({
16763
+ pid: number(),
16764
+ cpu: number(),
16765
+ memory: number(),
16766
+ /**
16767
+ * Private (anonymous) resident bytes — the per-process V8 heap + native
16768
+ * allocations NOT shared with other processes (Linux RssAnon). This is the
16769
+ * "real" per-runner cost; summing it across runners is meaningful, unlike
16770
+ * `memory` (RSS), which double-counts the shared mmap'd framework code.
16771
+ * Undefined where /proc is unavailable (e.g. macOS).
16772
+ */
16773
+ privateBytes: number().optional(),
16774
+ /**
16775
+ * Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
16776
+ * code shared copy-on-write across runners. Undefined on macOS.
16777
+ */
16778
+ sharedBytes: number().optional()
16779
+ });
16780
+ var AddonInstanceSchema = object({
16484
16781
  addonId: string(),
16485
- enabled: boolean(),
16486
- config: record(string(), unknown())
16782
+ nodeId: string(),
16783
+ role: _enum(["hub", "worker"]),
16784
+ pid: number(),
16785
+ state: _enum([
16786
+ "starting",
16787
+ "running",
16788
+ "stopping",
16789
+ "stopped",
16790
+ "crashed"
16791
+ ]),
16792
+ uptimeSec: number()
16487
16793
  });
16488
- /** A discovery-surfaced candidate (config is partial + non-secret). */
16489
- var DiscoveredTargetSchema = object({
16490
- kind: string(),
16491
- suggestedName: string(),
16492
- config: record(string(), unknown())
16794
+ var NodeProcessSchema = object({
16795
+ pid: number(),
16796
+ ppid: number(),
16797
+ pgid: number(),
16798
+ classification: _enum([
16799
+ "root",
16800
+ "managed",
16801
+ "system",
16802
+ "ghost"
16803
+ ]),
16804
+ /** `$process` addon binding when `managed`, else null. */
16805
+ addonId: string().nullable(),
16806
+ /** Kernel-reported nodeId when the process is a known agent/worker. */
16807
+ nodeId: string().nullable(),
16808
+ /** Truncated command line. */
16809
+ command: string(),
16810
+ cpuPercent: number(),
16811
+ memoryRssBytes: number(),
16812
+ /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
16813
+ uptimeSec: number(),
16814
+ /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
16815
+ orphaned: boolean()
16493
16816
  });
16494
- /** The degrade engine's report — what was resolved / dropped / degraded. */
16495
- var RenderedAsSchema = object({
16496
- level: string(),
16497
- format: NotificationFormatSchema,
16498
- attachmentsSent: number().int().nonnegative(),
16499
- actionsSent: number().int().nonnegative(),
16500
- truncated: boolean(),
16501
- dropped: array(string())
16817
+ var KillProcessInputSchema = object({
16818
+ pid: number(),
16819
+ /** Force = SIGKILL. Default is SIGTERM. */
16820
+ force: boolean().optional()
16502
16821
  });
16503
- var SendResultSchema = object({
16822
+ var KillProcessResultSchema = object({
16504
16823
  success: boolean(),
16505
- error: string().optional(),
16506
- renderedAs: RenderedAsSchema.optional()
16824
+ reason: string().optional(),
16825
+ signal: _enum(["SIGTERM", "SIGKILL"]).optional()
16826
+ });
16827
+ var DumpHeapSnapshotInputSchema = object({
16828
+ /** The addon whose runner should dump a heap snapshot. */
16829
+ addonId: string() });
16830
+ var DumpHeapSnapshotResultSchema = object({
16831
+ success: boolean(),
16832
+ /** Path of the written .heapsnapshot inside the runner's container/host. */
16833
+ path: string().optional(),
16834
+ /** Process pid that was signalled. */
16835
+ pid: number().optional(),
16836
+ reason: string().optional()
16837
+ });
16838
+ var SystemMetricsSchema = object({
16839
+ cpuPercent: number(),
16840
+ memoryPercent: number(),
16841
+ memoryUsedMB: number(),
16842
+ memoryTotalMB: number(),
16843
+ diskPercent: number().optional(),
16844
+ temperature: number().optional(),
16845
+ gpuPercent: number().optional(),
16846
+ gpuMemoryPercent: number().optional()
16847
+ });
16848
+ 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, {
16849
+ kind: "mutation",
16850
+ auth: "admin"
16851
+ }), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
16852
+ kind: "mutation",
16853
+ auth: "admin"
16854
+ });
16855
+ method(object({
16856
+ sourceUrl: string(),
16857
+ metadata: ModelConvertMetadataSchema,
16858
+ targets: array(ConvertTargetSchema).min(1).readonly(),
16859
+ calibrationRef: string().optional(),
16860
+ sessionId: string().optional()
16861
+ }), ConvertResultSchema, {
16862
+ kind: "mutation",
16863
+ auth: "admin",
16864
+ timeoutMs: 6e5
16865
+ });
16866
+ method(object({
16867
+ nodeId: string(),
16868
+ modelId: string(),
16869
+ format: _enum(MODEL_FORMATS),
16870
+ entry: ModelCatalogEntrySchema
16871
+ }), object({
16872
+ ok: boolean(),
16873
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
16874
+ sha256: string(),
16875
+ bytes: number(),
16876
+ /** The target node's modelsDir the artifact landed in. */
16877
+ path: string()
16878
+ }), {
16879
+ kind: "mutation",
16880
+ auth: "admin"
16507
16881
  });
16508
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
16509
- var TestResultSchema = SendResultSchema;
16510
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
16511
- kind: string(),
16512
- config: record(string(), unknown()).optional()
16513
- }), array(DiscoveredTargetSchema)), method(object({
16514
- targetId: string(),
16515
- notification: NotificationSchema
16516
- }), SendResultSchema, { kind: "mutation" }), method(object({
16517
- targetId: string(),
16518
- sample: NotificationSchema.optional()
16519
- }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
16520
- targetId: string(),
16521
- enabled: boolean()
16522
- }), _void(), { kind: "mutation" });
16523
16882
  /**
16524
- * notification-rulesthe Notification Center rule surface (P1 core).
16525
- *
16526
- * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
16527
- * (operator decisions D-1/D-2/D-3 are binding):
16883
+ * `mqtt-broker`broker-registry cap.
16528
16884
  *
16529
- * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
16530
- * `notification-center` module), hooked on the durable persistence
16531
- * moments (object-event insert, TrackCloser.closeExpired) with a
16532
- * persisted outbox + retry — never the lossy telemetry bus (D8).
16533
- * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
16534
- * FIRST persisted detection matching the conditions (per-track dedup,
16535
- * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
16536
- * `delivery: 'track-end'` evaluates the finalized track record at close.
16537
- * - DISPATCH stays behind `notification-output` (rules reference targets
16538
- * by id; per-backend params are a passthrough blob capped by the
16539
- * target kind's own caps/degrade engine).
16885
+ * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
16886
+ * MQTT brokers (external + optionally an embedded `aedes`-backed one)
16887
+ * and (b) the connection details a consumer addon needs to spin up
16888
+ * its OWN `mqtt.js` client.
16540
16889
  *
16541
- * P1 scope: admin-authored rules only (`createdBy` stamped from the
16542
- * server-injected caller identity the first `caller: 'required'`
16543
- * adopter). The P1 condition subset is: devices, classes(+exclude),
16544
- * minConfidence, admin zones (any/all + exclude), weekly schedule
16545
- * windows, and the optional label/identity/plate matchers. User rules,
16546
- * private zones, per-recipient fan-out and the wider condition table are
16547
- * P2+ (see spec §7).
16890
+ * Why: pub/sub routing over the system event-bus loses fidelity
16891
+ * (callback shape, QoS guarantees, will/retain semantics) and adds
16892
+ * refcount bookkeeping that addons would rather own themselves. The
16893
+ * canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
16894
+ * features anyway — give it the connection config, get out of the way.
16548
16895
  *
16549
- * All schemas here are the single source of truth — `NcRule` etc. are
16550
- * `z.infer` exports; no duplicate interfaces (the advanced-notifier
16551
- * schema/interface drift is explicitly not repeated).
16896
+ * Consumer flow:
16897
+ * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
16898
+ * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
16899
+ * client.subscribe('zigbee2mqtt/+')
16900
+ *
16901
+ * Collection mode: multiple brokers (e.g. one local mosquitto + one
16902
+ * cloud bridge). The "embedded" entry (when present) is just another
16903
+ * broker in the registry — its lifecycle is owned by the addon that
16904
+ * spawned it.
16552
16905
  */
16906
+ var BrokerKindSchema = _enum(["external", "embedded"]);
16553
16907
  /**
16554
- * D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
16555
- * The value maps 1:1 onto the evaluated record kind:
16556
- * - `immediate` ↔ object-event persist (lowest-latency detection burst)
16557
- * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
16558
- * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
16559
- * change of a LINKED device, one row per linked camera)
16560
- * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
16561
- * delivery / pick-up)
16908
+ * Broker live-probe status.
16562
16909
  *
16563
- * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
16564
- * `package-event` are pure trigger kinds (no urgency dimension). Extending
16565
- * this one field keeps the schema additive a rule still declares exactly
16566
- * one trigger.
16910
+ * - `connected` last probe completed a clean CONNACK
16911
+ * - `disconnected` no probe has run yet (cold cache)
16912
+ * - `auth-failed` CONNACK refused with auth error (RC 4 / 5)
16913
+ * - `unreachable` — TCP connect timed out / refused
16914
+ * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
16567
16915
  */
16568
- var NcDeliverySchema = _enum([
16569
- "immediate",
16570
- "track-end",
16571
- "device-event",
16572
- "package-event"
16916
+ var BrokerStatusSchema$1 = _enum([
16917
+ "connected",
16918
+ "disconnected",
16919
+ "auth-failed",
16920
+ "unreachable",
16921
+ "tls-error"
16573
16922
  ]);
16574
- /** Weekly schedule — OR of windows; absence on the rule = always active. */
16575
- var NcScheduleSchema = object({
16576
- windows: array(object({
16577
- /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
16578
- days: array(number().int().min(0).max(6)).min(1),
16579
- startMinute: number().int().min(0).max(1439),
16580
- endMinute: number().int().min(0).max(1439)
16581
- })).min(1),
16582
- /** IANA timezone; default = hub host timezone. */
16583
- timezone: string().optional(),
16584
- /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
16585
- invert: boolean().optional()
16586
- });
16587
- /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
16588
- var NcPlateMatcherSchema = object({
16589
- values: array(string().min(1)).min(1),
16590
- /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
16591
- maxDistance: number().int().min(0).max(3).default(1)
16592
- });
16593
- /**
16594
- * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
16595
- * occupancy edge for a device — optionally narrowed to a single admin
16596
- * `zoneId` and/or object `className`. `op` selects the edge/threshold:
16597
- * - `became-occupied` (default) — count crossed 0 → ≥ `count`
16598
- * - `became-free` — count crossed ≥ `count` → below it
16599
- * - `>=` / `<=` — count is at/over or at/under `count`
16600
- * `sustainSeconds` requires the condition hold continuously that long
16601
- * before firing (debounces flicker; 0 = fire on the first matching edge).
16602
- * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
16603
- * the condition never matches. Confirmed edge-state survives addon restarts
16604
- * (declared SQLite collection, reseeded on boot).
16605
- */
16606
- var NcOccupancyConditionSchema = object({
16607
- /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
16608
- zoneId: string().optional(),
16609
- /** Object class to count; absent = any class. */
16610
- className: string().optional(),
16611
- op: _enum([
16612
- "became-occupied",
16613
- "became-free",
16614
- ">=",
16615
- "<="
16616
- ]).default("became-occupied"),
16617
- count: number().int().min(0).default(1),
16618
- sustainSeconds: number().int().min(0).max(3600).default(15)
16619
- });
16620
- /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
16621
- var NcZoneConditionSchema = object({
16622
- ids: array(string().min(1)).min(1),
16623
- /** Quantifier over `ids` — at least one / every one visited. */
16624
- match: _enum(["any", "all"]).default("any")
16625
- });
16626
- /**
16627
- * The P1 condition set — a flat AND of groups; absent group = pass;
16628
- * membership lists are OR within the list (spec §2.3).
16629
- */
16630
- var NcConditionsSchema = object({
16631
- /** Device scope — absent = all devices. */
16632
- devices: array(number()).optional(),
16633
- /** Detector class names (any overlap with the record's class set). */
16634
- classes: array(string().min(1)).optional(),
16635
- /** Veto classes — any overlap fails the rule. */
16636
- classesExclude: array(string().min(1)).optional(),
16637
- /** Minimum detection confidence 0–1 (fails when the record has none). */
16638
- minConfidence: number().min(0).max(1).optional(),
16639
- /** Admin zone membership over event `zones` / track `zonesVisited`. */
16640
- zones: NcZoneConditionSchema.optional(),
16641
- /** Veto zones — any hit fails the rule. */
16642
- zonesExclude: array(string().min(1)).optional(),
16643
- /**
16644
- * Exact (case-insensitive) match on the record's collapsed `label`
16645
- * (identity name / plate text / subclass).
16646
- */
16647
- labelEquals: array(string().min(1)).optional(),
16648
- /**
16649
- * Identity matcher. P1 boundary: matched against the record's collapsed
16650
- * `label` (the identity display name propagated by the face pipeline) —
16651
- * identity-ID matching rides in P2 when identity ids reach the record.
16652
- */
16653
- identities: array(string().min(1)).optional(),
16654
- /** Fuzzy plate matcher against the record's `label` (plate text). */
16655
- plates: NcPlateMatcherSchema.optional(),
16656
- /**
16657
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
16658
- * Same P1 boundary: matched against the record's collapsed `label` (the
16659
- * identity display name). A record with NO label passes (nothing to
16660
- * exclude), unlike the include variant which fails on an absent label.
16661
- */
16662
- identitiesExclude: array(string().min(1)).optional(),
16663
- /**
16664
- * Minimum server-computed key-event importance in [0,1] (`Track.importance`).
16665
- * TRACK-END only: importance is scored at track close, so it does not exist
16666
- * at immediate / object-event evaluation time (see catalog `appliesTo`). At
16667
- * close the value is threaded via the close-time info (the `Track` clone is
16668
- * captured before the DB row is updated, so it would otherwise read stale).
16669
- * Fails when the record carries no importance (never guess quality — the
16670
- * `minConfidence` precedent). MVP cut: a single scalar threshold.
16671
- */
16672
- minImportance: number().min(0).max(1).optional(),
16673
- /**
16674
- * Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
16675
- * TRACK-END only: an `immediate` / object-event subject has no closed
16676
- * lifespan, so a dwell condition never matches immediate delivery
16677
- * (documented choice — the object-event record carries no `firstSeen`,
16678
- * so dwell cannot be computed from what the subject actually carries).
16679
- */
16680
- minDwellSeconds: number().min(0).optional(),
16681
- /**
16682
- * Detection provenance filter. `any` (default / absent) matches every
16683
- * source; otherwise the subject's source must equal it. Legacy records
16684
- * with no stamped source are treated as `pipeline`. The union spans both
16685
- * record kinds — object events carry `pipeline` | `onboard`, synthetic
16686
- * tracks carry `sensor`.
16687
- */
16688
- source: _enum([
16689
- "pipeline",
16690
- "onboard",
16691
- "sensor",
16692
- "any"
16693
- ]).optional(),
16694
- /**
16695
- * Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
16696
- * detector `minConfidence` (that gates the object-detection score; this
16697
- * gates the recognition/OCR match score). Fails when the subject carries
16698
- * no label-match confidence (never guess). TRACK-END only: the confidence
16699
- * lives on the recognition result and reaches the subject at track close.
16700
- *
16701
- * What it measures precisely (plumbed at track close — the closer threads
16702
- * the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
16703
- * `importance`): the BEST recognition match confidence observed for the
16704
- * label the track carries at close — for a face, the peak cosine similarity
16705
- * of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
16706
- * for a plate, the peak OCR read score of the best-held plate
16707
- * (`plateText.confidence`). When BOTH a face and a plate were recognized on
16708
- * one track the higher of the two is used. A track that ended with no
16709
- * confident identity/plate match carries no value, so the condition fails
16710
- * closed for it (an un-recognized subject).
16711
- */
16712
- minLabelConfidence: number().min(0).max(1).optional(),
16713
- /**
16714
- * DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
16715
- * e.g. a doorbell `press` / `press_long`) — matched case-insensitively
16716
- * against the token carried on the device-event subject (extracted from the
16717
- * event-emitter runtime slice's `lastEvent.eventType`). Fails when the
16718
- * subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
16719
- * eventType, so gate those with {@link sensorKinds} instead.
16720
- */
16721
- eventTypeTokens: array(string().min(1)).optional(),
16722
- /**
16723
- * DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
16724
- * `contact`, `button`, `device-event`) — matched against the persisted
16725
- * `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
16726
- */
16727
- sensorKinds: array(string().min(1)).optional(),
16728
- /**
16729
- * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
16730
- * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
16731
- * when the subject's phase does not match (a subject always carries a phase
16732
- * on the package-event trigger).
16733
- */
16734
- packagePhase: _enum([
16735
- "delivered",
16736
- "picked-up",
16737
- "both"
16738
- ]).optional(),
16739
- /**
16740
- * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
16741
- * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
16742
- * listed polygon (ZoneEngine membership semantics). Evaluated only when
16743
- * the subject carries a bbox; absent bbox ⇒ the condition FAILS.
16744
- */
16745
- customZones: array(MaskPolygonShapeSchema).optional(),
16746
- /**
16747
- * DEVICE-EVENT only. ZoneAnalytics occupancy edge — fires when a device's
16748
- * (optionally zone/class-scoped) occupancy count crosses the configured
16749
- * threshold and holds for `sustainSeconds`. Fail-closed on missing
16750
- * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
16751
- */
16752
- occupancy: NcOccupancyConditionSchema.optional()
16753
- });
16754
- /** One delivery target: a `notification-output` Target ref + passthrough params. */
16755
- var NcRuleTargetSchema = object({
16756
- /** `notification-output` Target id. */
16757
- targetId: string().min(1),
16758
- /**
16759
- * Per-backend passthrough. Recognized keys are mapped onto the canonical
16760
- * Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
16761
- * degrade engine drops what the backend can't render.
16762
- */
16763
- params: record(string(), unknown()).optional()
16923
+ var BrokerInfoSchema = object({
16924
+ id: string(),
16925
+ name: string(),
16926
+ url: string(),
16927
+ kind: BrokerKindSchema,
16928
+ status: BrokerStatusSchema$1,
16929
+ latencyMs: number().nullable(),
16930
+ error: string().optional(),
16931
+ /** Embedded brokers only: number of MQTT clients currently connected. */
16932
+ connectedClients: number().int().nonnegative().optional(),
16933
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
16934
+ lastCheckedAt: number().optional()
16764
16935
  });
16765
16936
  /**
16766
- * Media attachment policy (P1 still-image subset).
16767
- * - `best` the best AVAILABLE subject image at dispatch time (D-3).
16768
- * - `best-matching` the media that explains WHY the rule fired: a rule
16769
- * matched on identities attaches the subject's `faceCrop`, one matched on
16770
- * plates attaches the `plateCrop`; a rule with no identity/plate condition
16771
- * (or when the specific crop is missing) degrades to `best`, then
16772
- * `keyFrame`, then no attachment — never delaying the send. The matched
16773
- * condition summary is frozen on the outbox row at enqueue (like the rule
16774
- * name), so the choice never drifts from the record that fired it.
16775
- * - `keyFrame` — the clean scene frame (no subject box).
16776
- * - `none` — no attachment.
16937
+ * Connection details what a consumer needs to call
16938
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
16939
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
16940
+ * instead of stuffing creds into the URL (which leaks them into logs).
16777
16941
  */
16778
- var NcMediaPolicySchema = object({ attach: _enum([
16779
- "best",
16780
- "best-matching",
16781
- "keyFrame",
16782
- "none"
16783
- ]).default("best") });
16784
- /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
16785
- var NcThrottleSchema = object({
16786
- cooldownSec: number().int().min(0).max(86400).default(60),
16787
- /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
16788
- scope: _enum(["rule", "rule-device"]).default("rule-device")
16789
- });
16790
- /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
16791
- var NcRuleInputSchema = object({
16792
- name: string().min(1).max(200),
16793
- enabled: boolean().default(true),
16794
- delivery: NcDeliverySchema,
16795
- conditions: NcConditionsSchema.default({}),
16796
- schedule: NcScheduleSchema.optional(),
16797
- targets: array(NcRuleTargetSchema).min(1),
16798
- media: NcMediaPolicySchema.default({ attach: "best" }),
16799
- throttle: NcThrottleSchema.default({
16800
- cooldownSec: 60,
16801
- scope: "rule-device"
16802
- }),
16803
- /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
16804
- template: object({
16805
- title: string().max(500).optional(),
16806
- body: string().max(2e3).optional()
16807
- }).optional(),
16808
- /** Canonical notification priority ordinal (1..5); per-target overridable. */
16809
- priority: number().int().min(1).max(5).default(3),
16942
+ var BrokerConnectionDetailsSchema = object({
16943
+ url: string(),
16944
+ username: string().optional(),
16945
+ password: string().optional(),
16810
16946
  /**
16811
- * Ownership/visibility key. Absent = admin/global rule (unchanged legacy
16812
- * behaviour, visible to all, read-only in the viewer). Present = personal
16813
- * rule owned by this userId. Server-stamped; never trusted from a client.
16947
+ * Suggested prefix for `clientId`. Each consumer should suffix this
16948
+ * with its own discriminator (addon id, instance id) so reconnects
16949
+ * don't kick each other off (MQTT spec: clientId must be unique per
16950
+ * broker).
16814
16951
  */
16815
- ownerUserId: string().optional()
16952
+ clientIdPrefix: string().optional()
16953
+ });
16954
+ var AddBrokerInputSchema = object({
16955
+ name: string().min(1),
16956
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
16957
+ username: string().optional(),
16958
+ password: string().optional(),
16959
+ clientIdPrefix: string().optional()
16960
+ });
16961
+ var AddBrokerResultSchema = object({ id: string() });
16962
+ var IdInputSchema = object({ id: string() });
16963
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
16964
+ ok: literal(true),
16965
+ latencyMs: number()
16966
+ }), object({
16967
+ ok: literal(false),
16968
+ error: string()
16969
+ })]);
16970
+ var StartEmbeddedInputSchema = object({
16971
+ port: number().int().min(1).max(65535).default(1883),
16972
+ /** Allow anonymous connect (no username/password). Default: false. */
16973
+ allowAnonymous: boolean().default(false),
16974
+ /** Optional shared username/password for clients. */
16975
+ username: string().optional(),
16976
+ password: string().optional()
16977
+ });
16978
+ var StartEmbeddedResultSchema = object({
16979
+ id: string(),
16980
+ url: string()
16981
+ });
16982
+ var StatusSchema = object({
16983
+ brokerCount: number(),
16984
+ embeddedRunning: boolean()
16985
+ });
16986
+ 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);
16987
+ var NetworkEndpointSchema = object({
16988
+ url: string(),
16989
+ hostname: string(),
16990
+ port: number(),
16991
+ protocol: _enum(["http", "https"])
16992
+ });
16993
+ var NetworkAccessStatusSchema = object({
16994
+ connected: boolean(),
16995
+ endpoint: NetworkEndpointSchema.nullable(),
16996
+ error: string().optional()
16816
16997
  });
16817
16998
  /**
16818
- * Partial patch for `updateRule` any subset of the input fields, plus the
16819
- * persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
16820
- * NOT a client-authored input field (it lives on the persisted rule, not the
16821
- * input), so it is added here explicitly to let the store's per-target opt-out
16822
- * toggle round-trip through the shared `update` path. Viewer opt-out mutations
16823
- * still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
16824
- * `updateRule` patch.
16999
+ * Optional, richer endpoint shape returned by providers that expose
17000
+ * MORE than one ingress concurrently (Tailscale Ingress with mixed
17001
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
17002
+ * the originating provider config (mode + sourcePort) so the
17003
+ * orchestrator UI can label rows distinctly. Providers that expose only
17004
+ * one endpoint just omit `listEndpoints` from their provider impl.
16825
17005
  */
16826
- var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
16827
- /** A persisted rule. */
16828
- var NcRuleSchema = NcRuleInputSchema.extend({
16829
- id: string(),
16830
- /** userId of the admin who created the rule (server-stamped caller). */
16831
- createdBy: string(),
16832
- createdAt: number(),
16833
- updatedAt: number(),
17006
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16834
17007
  /**
16835
- * Per-target opt-out set. A targetId here is suppressed for THIS rule at
16836
- * send time. Only a target's OWNER may add/remove its id (server-checked
16837
- * in `nc.setRuleTargetEnabled`). Defaults to empty.
17008
+ * Stable id within the provider typically `<mode>-<sourcePort>` so
17009
+ * the orchestrator can dedupe across `listEndpoints` polls.
16838
17010
  */
16839
- disabledTargetIds: array(string()).default([])
16840
- });
16841
- var NcTestResultSchema = object({
16842
- recordId: string(),
16843
- recordKind: _enum([
16844
- "object-event",
16845
- "track",
16846
- "device-event",
16847
- "package-event"
16848
- ]),
16849
- deviceId: number(),
16850
- timestamp: number(),
16851
- wouldFire: boolean(),
16852
- /** Condition id that failed (first failing group), when `wouldFire` is false. */
16853
- failedCondition: string().optional(),
16854
- className: string().optional(),
16855
- label: string().optional()
17011
+ id: string(),
17012
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
17013
+ label: string(),
17014
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
17015
+ mode: string().optional(),
17016
+ /** Originating local port the ingress fronts (informational). */
17017
+ sourcePort: number().optional()
16856
17018
  });
16857
- var NcConditionDescriptorSchema = object({
16858
- /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
17019
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
17020
+ /**
17021
+ * notification-output — canonical, capability-gated notification delivery.
17022
+ *
17023
+ * Apprise-derived model (see
17024
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
17025
+ * callers emit ONE canonical `Notification`; each provider declares a
17026
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
17027
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
17028
+ * message to what the kind supports — callers never special-case a service.
17029
+ *
17030
+ * DESIGN DECISIONS (locked):
17031
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
17032
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
17033
+ * cap. Rationale: the admin UI needs one uniform surface across the
17034
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
17035
+ * alternative would fork the UI per addon and cannot host the
17036
+ * discovery→adopt flow.
17037
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
17038
+ * the generated cap-mount auto-`concatCollection`-fans them across every
17039
+ * registered provider (notifiers addon + HA addon) so one catalog is
17040
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
17041
+ * `addonId` the generated collection router extracts from the call input.
17042
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
17043
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
17044
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
17045
+ * base64 fallback needed.
17046
+ *
17047
+ * TODO (deferred, closed-set change — separate decision): add
17048
+ * `providerKind: 'notify'` so notification providers surface on the unified
17049
+ * admin "Integrations" page.
17050
+ */
17051
+ /**
17052
+ * Zentik-derived typed-media enum — the superset across every kind. Each
17053
+ * adapter picks what it supports and the degrade engine filters the rest.
17054
+ */
17055
+ var AttachmentMediaTypeSchema = _enum([
17056
+ "image",
17057
+ "video",
17058
+ "gif",
17059
+ "audio",
17060
+ "icon"
17061
+ ]);
17062
+ /**
17063
+ * A single attachment. Exactly one of `url` (remote source, most adapters
17064
+ * prefer this) or `bytes` (inline source; required for Pushover-style
17065
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
17066
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
17067
+ */
17068
+ var AttachmentSchema = object({
17069
+ mediaType: AttachmentMediaTypeSchema,
17070
+ url: string().optional(),
17071
+ bytes: _instanceof(Uint8Array).optional(),
17072
+ mime: string().optional(),
17073
+ name: string().optional()
17074
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
17075
+ var NotificationFormatSchema = _enum([
17076
+ "text",
17077
+ "markdown",
17078
+ "html"
17079
+ ]);
17080
+ /** A single tap-through action button. */
17081
+ var NotificationActionSchema = object({
17082
+ id: string(),
17083
+ label: string(),
17084
+ url: string().optional()
17085
+ });
17086
+ /**
17087
+ * The canonical notification. `body` is the only hard field (Apprise model).
17088
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
17089
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
17090
+ * the adapter maps this ordinal onto its native level. `level?` is an
17091
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
17092
+ * `priority` for that one target.
17093
+ */
17094
+ var NotificationSchema = object({
17095
+ body: string(),
17096
+ title: string().optional(),
17097
+ format: NotificationFormatSchema.default("text"),
17098
+ priority: number().int().min(1).max(5).default(3),
17099
+ level: string().optional(),
17100
+ attachments: array(AttachmentSchema).optional(),
17101
+ clickUrl: string().optional(),
17102
+ actions: array(NotificationActionSchema).optional(),
17103
+ sound: string().optional(),
17104
+ ttl: number().optional(),
17105
+ tag: string().optional(),
17106
+ deviceId: number().optional(),
17107
+ eventId: string().optional(),
17108
+ metadata: record(string(), unknown()).optional()
17109
+ });
17110
+ /** One declared native severity/priority level for a kind. */
17111
+ var TargetKindLevelSchema = object({
16859
17112
  id: string(),
16860
- group: _enum([
16861
- "scope",
16862
- "class",
16863
- "zones",
16864
- "quality",
16865
- "label",
16866
- "schedule",
16867
- "device",
16868
- "package",
16869
- "occupancy"
16870
- ]),
16871
17113
  label: string(),
16872
- /** Editor widget the UI renders never hardcode per-condition forms. */
16873
- valueType: _enum([
16874
- "deviceIdList",
16875
- "stringList",
16876
- "number01",
16877
- "number",
16878
- "sourceSelect",
16879
- "zoneSelection",
16880
- "zoneIdList",
16881
- "schedule",
16882
- "plateMatcher",
16883
- "packagePhase",
16884
- "polygonDraw",
16885
- "occupancy"
16886
- ]),
16887
- operator: _enum([
16888
- "in",
16889
- "notIn",
16890
- "anyOf",
16891
- "allOf",
16892
- "gte",
16893
- "fuzzyIn",
16894
- "withinSchedule"
16895
- ]),
16896
- /** Which delivery kinds the condition applies to. */
16897
- appliesTo: array(NcDeliverySchema),
16898
- phase: string(),
17114
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
17115
+ ordinal: number().int().min(1).max(5).nullable(),
17116
+ flags: object({
17117
+ critical: boolean().optional(),
17118
+ silent: boolean().optional(),
17119
+ noPush: boolean().optional()
17120
+ }).optional(),
17121
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
17122
+ requires: array(string()).optional(),
16899
17123
  description: string().optional()
16900
17124
  });
17125
+ /** The full capability block consulted before dispatch. */
17126
+ var TargetKindCapsSchema = object({
17127
+ attachments: object({
17128
+ mediaTypes: array(AttachmentMediaTypeSchema),
17129
+ mode: _enum([
17130
+ "url",
17131
+ "bytes",
17132
+ "both"
17133
+ ]),
17134
+ max: number().int().nonnegative(),
17135
+ maxBytes: number().int().positive().optional()
17136
+ }),
17137
+ /** Max action buttons (0 = none). */
17138
+ actions: number().int().nonnegative(),
17139
+ levels: array(TargetKindLevelSchema),
17140
+ format: array(NotificationFormatSchema),
17141
+ clickUrl: boolean(),
17142
+ sound: boolean(),
17143
+ ttl: boolean(),
17144
+ bodyMaxLen: number().int().positive()
17145
+ });
16901
17146
  /**
16902
- * The delivery lifecycle status of a history row a straight read of the
16903
- * durable outbox row's own status (single source of truth):
16904
- * - `pending` — enqueued, in-flight or retrying with backoff
16905
- * - `sent` — delivered (terminal)
16906
- * - `dead` dead-lettered after exhausting retries / a permanent
16907
- * backend rejection / a deleted target (terminal; carries
16908
- * the failure `error`)
16909
- *
16910
- * P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
16911
- * user dimension (quiet hours / snooze) and are additive when they land.
17147
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
17148
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
17149
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`)
17150
+ * the union is large and not meant for runtime validation here; the exported
17151
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16912
17152
  */
16913
- var NcHistoryStatusSchema = _enum([
16914
- "pending",
16915
- "sent",
16916
- "dead"
16917
- ]);
16918
- /** The evaluated record kind a history row descends from (one per trigger). */
16919
- var NcHistoryRecordKindSchema = _enum([
16920
- "object-event",
16921
- "track-end",
16922
- "device-event",
16923
- "package-event"
16924
- ]);
16925
- /** Subject summary frozen on the row at fire time (survives rule/record edits). */
16926
- var NcHistorySubjectSchema = object({
16927
- className: string(),
16928
- label: string().optional(),
16929
- confidence: number().optional(),
16930
- zones: array(string()),
16931
- timestamp: number()
17153
+ var ConfigSchemaPassthrough = unknown();
17154
+ var TargetKindSchema = object({
17155
+ kind: string(),
17156
+ label: string(),
17157
+ icon: string(),
17158
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
17159
+ addonId: string(),
17160
+ configSchema: ConfigSchemaPassthrough,
17161
+ supportsDiscovery: boolean(),
17162
+ caps: TargetKindCapsSchema
16932
17163
  });
16933
17164
  /**
16934
- * One delivery-history row. This is a read-only VIEW over the durable
16935
- * outbox row (single source of truth the same row the drain loop drives;
16936
- * NO second write path, so history can never drift from delivery state).
16937
- * The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
16938
- * `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
16939
- * (fire) / `updatedAt` (last transition), `status` + `error` are the
16940
- * lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
16941
- * enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
16942
- * P1 (admin scope only).
17165
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
17166
+ * (return a presence marker only) when serving `listTargets` never
17167
+ * round-trip a stored secret to the UI.
16943
17168
  */
16944
- var NcHistoryEntrySchema = object({
16945
- /** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
17169
+ var TargetSchema = object({
16946
17170
  id: string(),
16947
- ruleId: string(),
16948
- /** Rule name frozen at fire time (outlives a later rename / delete). */
16949
- ruleName: string(),
16950
- /** The rule urgency/trigger that produced this delivery. */
16951
- delivery: NcDeliverySchema,
16952
- targetId: string(),
16953
- deviceId: number(),
16954
- recordKind: NcHistoryRecordKindSchema,
16955
- /** Event / track ref of the evaluated record (§3.2 `eventRef`). */
16956
- recordId: string(),
16957
- /** Present for track-scoped deliveries (object-event / track-end). */
16958
- trackId: string().optional(),
16959
- status: NcHistoryStatusSchema,
16960
- /** Delivery attempts made so far. */
16961
- attempts: number().int(),
16962
- /** Fire time (outbox enqueue). */
16963
- createdAt: number(),
16964
- /** Last transition time (terminal for sent / dead). */
16965
- updatedAt: number(),
16966
- /** Failure detail — present on a `dead` row. */
16967
- error: string().optional(),
16968
- subject: NcHistorySubjectSchema
17171
+ name: string(),
17172
+ kind: string(),
17173
+ addonId: string(),
17174
+ enabled: boolean(),
17175
+ config: record(string(), unknown())
16969
17176
  });
16970
- /**
16971
- * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
16972
- * AND; absent = unbounded on that axis. `since`/`until` bound the fire time
16973
- * (`createdAt`, epoch ms, inclusive). `limit` is clamped to
16974
- * {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
16975
- */
16976
- var NcHistoryFilterSchema = object({
16977
- ruleId: string().optional(),
16978
- deviceId: number().optional(),
16979
- status: NcHistoryStatusSchema.optional(),
16980
- since: number().optional(),
16981
- until: number().optional(),
16982
- limit: number().int().min(1).max(500).default(100)
17177
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
17178
+ var DiscoveredTargetSchema = object({
17179
+ kind: string(),
17180
+ suggestedName: string(),
17181
+ config: record(string(), unknown())
16983
17182
  });
16984
- 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 }), {
16985
- kind: "mutation",
16986
- auth: "admin",
16987
- caller: "required"
16988
- }), method(object({
16989
- ruleId: string(),
16990
- patch: NcRulePatchSchema
16991
- }), object({ rule: NcRuleSchema }), {
16992
- kind: "mutation",
16993
- auth: "admin",
16994
- caller: "required"
16995
- }), method(object({ ruleId: string() }), object({ success: literal(true) }), {
16996
- kind: "mutation",
16997
- auth: "admin"
16998
- }), method(object({
16999
- ruleId: string(),
17183
+ /** The degrade engine's report what was resolved / dropped / degraded. */
17184
+ var RenderedAsSchema = object({
17185
+ level: string(),
17186
+ format: NotificationFormatSchema,
17187
+ attachmentsSent: number().int().nonnegative(),
17188
+ actionsSent: number().int().nonnegative(),
17189
+ truncated: boolean(),
17190
+ dropped: array(string())
17191
+ });
17192
+ var SendResultSchema = object({
17193
+ success: boolean(),
17194
+ error: string().optional(),
17195
+ renderedAs: RenderedAsSchema.optional()
17196
+ });
17197
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
17198
+ var TestResultSchema = SendResultSchema;
17199
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
17200
+ kind: string(),
17201
+ config: record(string(), unknown()).optional()
17202
+ }), array(DiscoveredTargetSchema)), method(object({
17203
+ targetId: string(),
17204
+ notification: NotificationSchema
17205
+ }), SendResultSchema, { kind: "mutation" }), method(object({
17206
+ targetId: string(),
17207
+ sample: NotificationSchema.optional()
17208
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
17209
+ targetId: string(),
17000
17210
  enabled: boolean()
17001
- }), object({ success: literal(true) }), {
17002
- kind: "mutation",
17003
- auth: "admin"
17004
- }), method(object({
17005
- rule: NcRuleInputSchema,
17006
- lookbackMinutes: number().int().min(1).max(1440).default(60)
17007
- }), object({ results: array(NcTestResultSchema) }), {
17008
- kind: "mutation",
17009
- auth: "admin"
17010
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
17211
+ }), _void(), { kind: "mutation" });
17011
17212
  /**
17012
17213
  * Zod schemas for persisted record types.
17013
17214
  *
@@ -22105,6 +22306,12 @@ Object.freeze({
22105
22306
  addonId: null,
22106
22307
  access: "delete"
22107
22308
  },
22309
+ "backup.deleteSchedule": {
22310
+ capName: "backup",
22311
+ capScope: "system",
22312
+ addonId: null,
22313
+ access: "delete"
22314
+ },
22108
22315
  "backup.getEntries": {
22109
22316
  capName: "backup",
22110
22317
  capScope: "system",
@@ -22135,6 +22342,12 @@ Object.freeze({
22135
22342
  addonId: null,
22136
22343
  access: "view"
22137
22344
  },
22345
+ "backup.listSchedules": {
22346
+ capName: "backup",
22347
+ capScope: "system",
22348
+ addonId: null,
22349
+ access: "view"
22350
+ },
22138
22351
  "backup.previewSchedule": {
22139
22352
  capName: "backup",
22140
22353
  capScope: "system",
@@ -22159,6 +22372,12 @@ Object.freeze({
22159
22372
  addonId: null,
22160
22373
  access: "create"
22161
22374
  },
22375
+ "backup.upsertSchedule": {
22376
+ capName: "backup",
22377
+ capScope: "system",
22378
+ addonId: null,
22379
+ access: "create"
22380
+ },
22162
22381
  "battery.wakeForStream": {
22163
22382
  capName: "battery",
22164
22383
  capScope: "device",
@@ -25993,6 +26212,36 @@ Object.freeze({
25993
26212
  addonId: null,
25994
26213
  access: "create"
25995
26214
  },
26215
+ "terminalSession.close": {
26216
+ capName: "terminal-session",
26217
+ capScope: "system",
26218
+ addonId: null,
26219
+ access: "create"
26220
+ },
26221
+ "terminalSession.listProfiles": {
26222
+ capName: "terminal-session",
26223
+ capScope: "system",
26224
+ addonId: null,
26225
+ access: "view"
26226
+ },
26227
+ "terminalSession.listSessions": {
26228
+ capName: "terminal-session",
26229
+ capScope: "system",
26230
+ addonId: null,
26231
+ access: "view"
26232
+ },
26233
+ "terminalSession.openSession": {
26234
+ capName: "terminal-session",
26235
+ capScope: "system",
26236
+ addonId: null,
26237
+ access: "create"
26238
+ },
26239
+ "terminalSession.resize": {
26240
+ capName: "terminal-session",
26241
+ capScope: "system",
26242
+ addonId: null,
26243
+ access: "create"
26244
+ },
25996
26245
  "toast.onToast": {
25997
26246
  capName: "toast",
25998
26247
  capScope: "system",