@camstack/system 1.2.83 → 1.2.85

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 (66) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +32 -2
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +32 -2
  17. package/dist/builtins/device-manager/device-meta-actions.d.ts +12 -0
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  20. package/dist/builtins/hub-forwarder/index.js +1 -1
  21. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  26. package/dist/builtins/local-network/local-network.addon.js +1 -1
  27. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  28. package/dist/builtins/loki-logging/index.js +1 -1
  29. package/dist/builtins/loki-logging/index.mjs +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  32. package/dist/builtins/platform-probe/index.js +1 -1
  33. package/dist/builtins/platform-probe/index.mjs +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  36. package/dist/builtins/snapshot/index.js +66 -11
  37. package/dist/builtins/snapshot/index.mjs +66 -11
  38. package/dist/builtins/snapshot/snapshot.addon.d.ts +39 -3
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +30 -10
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +31 -11
  41. package/dist/builtins/sqlite-storage/path-guard.d.ts +8 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  44. package/dist/builtins/storage-orchestrator/index.d.ts +5 -6
  45. package/dist/builtins/storage-orchestrator/index.mjs +1 -1
  46. package/dist/builtins/storage-orchestrator/provider-discovery.d.ts +1 -1
  47. package/dist/builtins/storage-orchestrator/storage-migration-coordinator.d.ts +1 -1
  48. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +1 -1
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1252 -1191
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1253 -1192
  51. package/dist/builtins/system-config/system-config.addon.js +1 -1
  52. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  53. package/dist/builtins/winston-logging/index.js +1 -1
  54. package/dist/builtins/winston-logging/index.mjs +1 -1
  55. package/dist/{dist-EAvNJD24.mjs → dist-DWuNENzg.mjs} +527 -11
  56. package/dist/{dist-CqiMPPs0.js → dist-DiScsb8j.js} +527 -11
  57. package/dist/index.js +83 -18
  58. package/dist/index.mjs +84 -19
  59. package/dist/kernel/addon-installer.d.ts +58 -6
  60. package/dist/kernel/moleculer/event-bus-core.d.ts +47 -1
  61. package/dist/kernel/transport/child-cap-protocol.d.ts +32 -0
  62. package/dist/kernel/transport/hub-cap-forward.d.ts +8 -0
  63. package/dist/kernel/transport/parent-unowned-call.d.ts +19 -0
  64. package/dist/{manifest-python-deps-DG_LLcZF.js → manifest-python-deps-C0zkl1Ae.js} +133 -53
  65. package/dist/{manifest-python-deps-C1DMs1J9.mjs → manifest-python-deps-fWzdEasI.mjs} +133 -53
  66. package/package.json +6 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-CqiMPPs0.js");
3
+ const require_dist = require("./dist-DiScsb8j.js");
4
4
  const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
5
5
  require("./builtins/alerts/index.js");
6
6
  const require_formatter = require("./formatter-DqAKDlvN.js");
@@ -25,7 +25,7 @@ const require_builtins_system_config_system_config_addon = require("./builtins/s
25
25
  require("./builtins/system-config/index.js");
26
26
  const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
27
27
  const require_file_data_plane = require("./file-data-plane-DUHPHa-Y.js");
28
- const require_manifest_python_deps = require("./manifest-python-deps-DG_LLcZF.js");
28
+ const require_manifest_python_deps = require("./manifest-python-deps-C0zkl1Ae.js");
29
29
  const require_resource_monitor = require("./resource-monitor-CdnzxBLP.js");
30
30
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
31
31
  let zod = require("zod");
@@ -4228,8 +4228,22 @@ var AddonInstaller = class AddonInstaller {
4228
4228
  * defaults to npm in production.
4229
4229
  *
4230
4230
  * @param packages — optional custom package list (default: REQUIRED_PACKAGES)
4231
+ * @param pins — optional `name → exact version` map. A named package is
4232
+ * fetched at THAT version instead of the registry's `latest`.
4233
+ *
4234
+ * Preparation for [D45](../../../../docs/decisions/adr-0045.md) task 32: the
4235
+ * image seed answers first today, so `latest` has never been the version a
4236
+ * node actually got. Once the image stops baking an addon tree under `/opt`
4237
+ * this call IS the answer, and it has to be the version the running closure was
4238
+ * published with — otherwise a node fetches latest-of-everything and its
4239
+ * addon set depends on the minute it first booted. Passing nothing keeps
4240
+ * exactly today's behaviour, and `launcher.ts` passes nothing yet.
4241
+ *
4242
+ * A pin never triggers a re-install: bootstrap stays seed-only, so a copy
4243
+ * already under the addon root is left alone whatever its version. Replacing
4244
+ * it would be an un-deploy ([D90](../../../../docs/decisions/adr-0090.md)).
4231
4245
  */
4232
- async ensureRequiredPackages(packages) {
4246
+ async ensureRequiredPackages(packages, pins) {
4233
4247
  const pkgList = packages ?? AddonInstaller.REQUIRED_PACKAGES;
4234
4248
  ensureDir(this.addonsDir);
4235
4249
  const isLocal = this.installSource !== "npm" && this.workspaceDir != null;
@@ -4257,7 +4271,7 @@ var AddonInstaller = class AddonInstaller {
4257
4271
  }
4258
4272
  this.logger.info(`${packageName} — not found locally, trying npm`);
4259
4273
  }
4260
- await this.installFromNpm(packageName);
4274
+ await this.installFromNpm(packageName, pins?.[packageName]);
4261
4275
  } catch (err) {
4262
4276
  const msg = require_dist.errMsg(err);
4263
4277
  if (packageName === "@camstack/system") throw new Error(`Required package ${packageName} failed to install: ${msg}`, { cause: err });
@@ -4651,6 +4665,61 @@ var AddonInstaller = class AddonInstaller {
4651
4665
  throw new Error(`${packageName} — runtime dependency install failed: ${require_dist.errMsg(err)}`, { cause: err });
4652
4666
  }
4653
4667
  }
4668
+ /**
4669
+ * Make `pkgDir`'s NATIVE modules loadable on THIS host, before it is swapped
4670
+ * in: install whatever `camstack.nativeDependencies` declares, then ensure
4671
+ * every traditional native present in the tree actually carries a compiled
4672
+ * `.node` for the running ABI.
4673
+ *
4674
+ * ONE implementation, for the same reason as {@link installRuntimeDependencies}
4675
+ * — every path that materialises an addon tree calls it, so no call site can
4676
+ * forget it. `installFromTgz` (deploy/upload) always ran these two steps; the
4677
+ * OTA staged-swap path ran NEITHER even after the runtime-dep fix (2026-08-12,
4678
+ * task #15, which named this gap explicitly). Same outage class one layer
4679
+ * down: an addon with native modules was broken by its FIRST OTA update, its
4680
+ * dist landing without an ABI-matched binary, and every load dying at the
4681
+ * binding (`Could not locate the bindings file`) — from the outside
4682
+ * indistinguishable from a bad bundle.
4683
+ *
4684
+ * **Two distinct steps, both load-bearing.**
4685
+ * `installManifestNativeDeps` fetches the DECLARED natives (prebuilt host copy
4686
+ * first — offline — then npm + an ABI rebuild), and is idempotent through a
4687
+ * content-hash marker. `ensureAddonNativePrebuilds` is the post-install pass:
4688
+ * npm can leave a traditional native (`better-sqlite3`) SOURCE-ONLY on a
4689
+ * toolchain-less host, so it re-fetches the prebuild for the running ABI and
4690
+ * THROWS naming module + addon if the binary is still absent. No bundled
4691
+ * source is available on this path (unlike `installCopy`) — fetch only.
4692
+ *
4693
+ * **Failure is FATAL**, mirroring the tgz path: callers complete the tree
4694
+ * BEFORE they swap, so an abort simply means the swap never happens and the
4695
+ * previous version keeps serving. Shipping a native-less copy instead would
4696
+ * trade one broken version for a crash-loop.
4697
+ *
4698
+ * A missing/unreadable manifest is logged and skipped — `installRuntimeDependencies`
4699
+ * has already said so loudly, and a tree with no readable package.json
4700
+ * declares no natives.
4701
+ */
4702
+ async installNativeDependencies(pkgDir, packageName) {
4703
+ const pkgJsonPath = node_path.join(pkgDir, "package.json");
4704
+ const pkgView = readPackageJson(pkgJsonPath);
4705
+ if (pkgView == null) {
4706
+ this.logger.error(`${packageName} — no readable package.json in the prepared tree; native dependencies NOT installed`, { meta: { pkgJsonPath } });
4707
+ return;
4708
+ }
4709
+ try {
4710
+ await require_manifest_python_deps.installManifestNativeDeps(pkgDir, pkgView.raw, this.logger, this.registry, this.npmCacheDir);
4711
+ } catch (nativeErr) {
4712
+ this.logger.error(`${packageName} — native dependency install FAILED; install ABORTED (a swapped-in copy would fail every load at the binding)`, { meta: {
4713
+ pkgDir,
4714
+ error: require_dist.errMsg(nativeErr)
4715
+ } });
4716
+ throw new Error(`${packageName} — native deps install failed: ${require_dist.errMsg(nativeErr)}`, { cause: nativeErr });
4717
+ }
4718
+ await ensureAddonNativePrebuilds(pkgDir, {
4719
+ addonName: packageName,
4720
+ logger: this.logger
4721
+ });
4722
+ }
4654
4723
  /** Install addon from a tgz file (uploaded or downloaded) */
4655
4724
  async installFromTgz(tgzPath) {
4656
4725
  const stagingRoot = node_path.join(this.addonsDir, ".staging");
@@ -4675,15 +4744,7 @@ var AddonInstaller = class AddonInstaller {
4675
4744
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
4676
4745
  const targetDir = node_path.join(this.addonsDir, pkgView.name);
4677
4746
  await this.installRuntimeDependencies(pkgDir, pkgView.name);
4678
- try {
4679
- await require_manifest_python_deps.installManifestNativeDeps(pkgDir, pkgView.raw, this.logger, this.registry, this.npmCacheDir);
4680
- } catch (nativeErr) {
4681
- throw new Error(`${pkgView.name} — native deps install failed: ${require_dist.errMsg(nativeErr)}`, { cause: nativeErr });
4682
- }
4683
- await ensureAddonNativePrebuilds(pkgDir, {
4684
- addonName: pkgView.name,
4685
- logger: this.logger
4686
- });
4747
+ await this.installNativeDependencies(pkgDir, pkgView.name);
4687
4748
  await this.evictInstallDir(targetDir);
4688
4749
  ensureDir(node_path.dirname(targetDir));
4689
4750
  await node_fs.promises.rename(pkgDir, targetDir);
@@ -4788,9 +4849,12 @@ var AddonInstaller = class AddonInstaller {
4788
4849
  *
4789
4850
  * "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
4790
4851
  * tree is the raw unpacked tarball, so the addon's own
4791
- * `camstack.runtimeDependencies` still have to be installed into it, exactly
4792
- * as `installFromTgz` does. Skipping that is what broke every runtime-dep
4793
- * addon on its first OTA update see {@link installRuntimeDependencies}.
4852
+ * `camstack.runtimeDependencies` AND `camstack.nativeDependencies` still have
4853
+ * to be installed into it, exactly as `installFromTgz` does. Skipping the
4854
+ * first broke every runtime-dep addon on its first OTA update; skipping the
4855
+ * second broke every addon with a native module the same way, one layer down
4856
+ * (`Could not locate the bindings file` instead of `Cannot find module`) —
4857
+ * see {@link installRuntimeDependencies} and {@link installNativeDependencies}.
4794
4858
  *
4795
4859
  * Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
4796
4860
  * the live install is only then backed up and replaced. So the addon keeps
@@ -4803,8 +4867,8 @@ var AddonInstaller = class AddonInstaller {
4803
4867
  * restored and the error is rethrown — identical recovery to `applyUpdate`.
4804
4868
  *
4805
4869
  * Throws if the package is not tracked in the manifest, if `addonDir`
4806
- * is missing (same preconditions as `applyUpdate`), or if the runtime-dep
4807
- * install fails.
4870
+ * is missing (same preconditions as `applyUpdate`), or if the runtime-dep /
4871
+ * native-dep install fails.
4808
4872
  */
4809
4873
  async applyUpdateFromStaged(packageName, version, stagedPath) {
4810
4874
  const current = this.manifest.get(packageName);
@@ -4812,6 +4876,7 @@ var AddonInstaller = class AddonInstaller {
4812
4876
  const addonDir = node_path.join(this.addonsDir, packageName);
4813
4877
  if (!node_fs.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
4814
4878
  await this.installRuntimeDependencies(stagedPath, packageName);
4879
+ await this.installNativeDependencies(stagedPath, packageName);
4815
4880
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4816
4881
  const backupRoot = node_path.join(this.addonsDir, ".backups", packageName);
4817
4882
  ensureDir(backupRoot);
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
2
- import { $ as objectInputDeclaresAddonId, At as emitDownForOwnedCaps, B as isArrayOutputSchema, Bt as scopeKey, Ct as ReadinessRegistry, Dt as asNumber, Et as asJsonObject$1, G as kebabToCamel, H as isObjectInput, Ht as EventCategory$1, It as parseJsonObject, K as lifecycleJobSchema, Lt as parseJsonUnknown$1, Mt as expandCapMethods, Ot as asString$1, R as extractNestedAddonId, Rt as readinessKey, V as isCollectionArrayMethod, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, _t as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, ht as errMsg$1, it as scopesAllowDeviceCap, kt as createEvent, nt as procedureAuthKey, rt as scopesAllowAddon, t as ALL_CAPABILITY_DEFINITIONS, vt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, wt as ReadinessTimeoutError, y as addonSettingsCapability, yt as DEVICE_STATUS_METHOD, zt as resolveCapMount } from "./dist-EAvNJD24.mjs";
2
+ import { $ as objectInputDeclaresAddonId, At as emitDownForOwnedCaps, B as isArrayOutputSchema, Bt as scopeKey, Ct as ReadinessRegistry, Dt as asNumber, Et as asJsonObject$1, G as kebabToCamel, H as isObjectInput, Ht as EventCategory$1, It as parseJsonObject, K as lifecycleJobSchema, Lt as parseJsonUnknown$1, Mt as expandCapMethods, Ot as asString$1, R as extractNestedAddonId, Rt as readinessKey, V as isCollectionArrayMethod, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, _t as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, ht as errMsg$1, it as scopesAllowDeviceCap, kt as createEvent, nt as procedureAuthKey, rt as scopesAllowAddon, t as ALL_CAPABILITY_DEFINITIONS, vt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, wt as ReadinessTimeoutError, y as addonSettingsCapability, yt as DEVICE_STATUS_METHOD, zt as resolveCapMount } from "./dist-DWuNENzg.mjs";
3
3
  import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
4
4
  import "./builtins/alerts/index.mjs";
5
5
  import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
@@ -18,13 +18,13 @@ import NativeMetricsAddon, { t as NativeMetricsProvider } from "./builtins/nativ
18
18
  import { FilesystemStorageAddon, t as FilesystemStorageProvider } from "./builtins/sqlite-storage/filesystem-storage.addon.mjs";
19
19
  import { SqliteSettingsAddon, o as SqliteSettingsBackend } from "./builtins/sqlite-storage/sqlite-settings.addon.mjs";
20
20
  import { ConfigStore, DeviceStore } from "./builtins/sqlite-storage/index.mjs";
21
- import { StorageOrchestratorAddon, r as StorageOrchestratorService } from "./builtins/storage-orchestrator/storage-orchestrator.addon.mjs";
21
+ import { StorageOrchestratorAddon, t as StorageOrchestratorService } from "./builtins/storage-orchestrator/storage-orchestrator.addon.mjs";
22
22
  import "./builtins/storage-orchestrator/index.mjs";
23
23
  import { SystemConfigAddon } from "./builtins/system-config/system-config.addon.mjs";
24
24
  import "./builtins/system-config/index.mjs";
25
25
  import { WinstonDestination, WinstonLoggingAddon } from "./builtins/winston-logging/index.mjs";
26
26
  import { a as parseTokenizedUrl, c as collectModelFiles, d as downloadModel, f as ensureModel, h as isModelDownloaded, i as parseRangeHeader, l as deleteModelFromDisk, m as getModelFilePath, n as contentTypeFor, o as resolveFilePath, p as fetchJson, r as createAuthenticatedFileServer, s as ModelDownloadService, t as createFileDataPlaneHandler, u as downloadFile } from "./file-data-plane-CuE_hBli.mjs";
27
- import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-C1DMs1J9.mjs";
27
+ import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-fWzdEasI.mjs";
28
28
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BWmQ5i-o.mjs";
29
29
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
30
30
  import { z } from "zod";
@@ -4220,8 +4220,22 @@ var AddonInstaller = class AddonInstaller {
4220
4220
  * defaults to npm in production.
4221
4221
  *
4222
4222
  * @param packages — optional custom package list (default: REQUIRED_PACKAGES)
4223
+ * @param pins — optional `name → exact version` map. A named package is
4224
+ * fetched at THAT version instead of the registry's `latest`.
4225
+ *
4226
+ * Preparation for [D45](../../../../docs/decisions/adr-0045.md) task 32: the
4227
+ * image seed answers first today, so `latest` has never been the version a
4228
+ * node actually got. Once the image stops baking an addon tree under `/opt`
4229
+ * this call IS the answer, and it has to be the version the running closure was
4230
+ * published with — otherwise a node fetches latest-of-everything and its
4231
+ * addon set depends on the minute it first booted. Passing nothing keeps
4232
+ * exactly today's behaviour, and `launcher.ts` passes nothing yet.
4233
+ *
4234
+ * A pin never triggers a re-install: bootstrap stays seed-only, so a copy
4235
+ * already under the addon root is left alone whatever its version. Replacing
4236
+ * it would be an un-deploy ([D90](../../../../docs/decisions/adr-0090.md)).
4223
4237
  */
4224
- async ensureRequiredPackages(packages) {
4238
+ async ensureRequiredPackages(packages, pins) {
4225
4239
  const pkgList = packages ?? AddonInstaller.REQUIRED_PACKAGES;
4226
4240
  ensureDir(this.addonsDir);
4227
4241
  const isLocal = this.installSource !== "npm" && this.workspaceDir != null;
@@ -4249,7 +4263,7 @@ var AddonInstaller = class AddonInstaller {
4249
4263
  }
4250
4264
  this.logger.info(`${packageName} — not found locally, trying npm`);
4251
4265
  }
4252
- await this.installFromNpm(packageName);
4266
+ await this.installFromNpm(packageName, pins?.[packageName]);
4253
4267
  } catch (err) {
4254
4268
  const msg = errMsg$1(err);
4255
4269
  if (packageName === "@camstack/system") throw new Error(`Required package ${packageName} failed to install: ${msg}`, { cause: err });
@@ -4643,6 +4657,61 @@ var AddonInstaller = class AddonInstaller {
4643
4657
  throw new Error(`${packageName} — runtime dependency install failed: ${errMsg$1(err)}`, { cause: err });
4644
4658
  }
4645
4659
  }
4660
+ /**
4661
+ * Make `pkgDir`'s NATIVE modules loadable on THIS host, before it is swapped
4662
+ * in: install whatever `camstack.nativeDependencies` declares, then ensure
4663
+ * every traditional native present in the tree actually carries a compiled
4664
+ * `.node` for the running ABI.
4665
+ *
4666
+ * ONE implementation, for the same reason as {@link installRuntimeDependencies}
4667
+ * — every path that materialises an addon tree calls it, so no call site can
4668
+ * forget it. `installFromTgz` (deploy/upload) always ran these two steps; the
4669
+ * OTA staged-swap path ran NEITHER even after the runtime-dep fix (2026-08-12,
4670
+ * task #15, which named this gap explicitly). Same outage class one layer
4671
+ * down: an addon with native modules was broken by its FIRST OTA update, its
4672
+ * dist landing without an ABI-matched binary, and every load dying at the
4673
+ * binding (`Could not locate the bindings file`) — from the outside
4674
+ * indistinguishable from a bad bundle.
4675
+ *
4676
+ * **Two distinct steps, both load-bearing.**
4677
+ * `installManifestNativeDeps` fetches the DECLARED natives (prebuilt host copy
4678
+ * first — offline — then npm + an ABI rebuild), and is idempotent through a
4679
+ * content-hash marker. `ensureAddonNativePrebuilds` is the post-install pass:
4680
+ * npm can leave a traditional native (`better-sqlite3`) SOURCE-ONLY on a
4681
+ * toolchain-less host, so it re-fetches the prebuild for the running ABI and
4682
+ * THROWS naming module + addon if the binary is still absent. No bundled
4683
+ * source is available on this path (unlike `installCopy`) — fetch only.
4684
+ *
4685
+ * **Failure is FATAL**, mirroring the tgz path: callers complete the tree
4686
+ * BEFORE they swap, so an abort simply means the swap never happens and the
4687
+ * previous version keeps serving. Shipping a native-less copy instead would
4688
+ * trade one broken version for a crash-loop.
4689
+ *
4690
+ * A missing/unreadable manifest is logged and skipped — `installRuntimeDependencies`
4691
+ * has already said so loudly, and a tree with no readable package.json
4692
+ * declares no natives.
4693
+ */
4694
+ async installNativeDependencies(pkgDir, packageName) {
4695
+ const pkgJsonPath = path$39.join(pkgDir, "package.json");
4696
+ const pkgView = readPackageJson(pkgJsonPath);
4697
+ if (pkgView == null) {
4698
+ this.logger.error(`${packageName} — no readable package.json in the prepared tree; native dependencies NOT installed`, { meta: { pkgJsonPath } });
4699
+ return;
4700
+ }
4701
+ try {
4702
+ await installManifestNativeDeps(pkgDir, pkgView.raw, this.logger, this.registry, this.npmCacheDir);
4703
+ } catch (nativeErr) {
4704
+ this.logger.error(`${packageName} — native dependency install FAILED; install ABORTED (a swapped-in copy would fail every load at the binding)`, { meta: {
4705
+ pkgDir,
4706
+ error: errMsg$1(nativeErr)
4707
+ } });
4708
+ throw new Error(`${packageName} — native deps install failed: ${errMsg$1(nativeErr)}`, { cause: nativeErr });
4709
+ }
4710
+ await ensureAddonNativePrebuilds(pkgDir, {
4711
+ addonName: packageName,
4712
+ logger: this.logger
4713
+ });
4714
+ }
4646
4715
  /** Install addon from a tgz file (uploaded or downloaded) */
4647
4716
  async installFromTgz(tgzPath) {
4648
4717
  const stagingRoot = path$39.join(this.addonsDir, ".staging");
@@ -4667,15 +4736,7 @@ var AddonInstaller = class AddonInstaller {
4667
4736
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
4668
4737
  const targetDir = path$39.join(this.addonsDir, pkgView.name);
4669
4738
  await this.installRuntimeDependencies(pkgDir, pkgView.name);
4670
- try {
4671
- await installManifestNativeDeps(pkgDir, pkgView.raw, this.logger, this.registry, this.npmCacheDir);
4672
- } catch (nativeErr) {
4673
- throw new Error(`${pkgView.name} — native deps install failed: ${errMsg$1(nativeErr)}`, { cause: nativeErr });
4674
- }
4675
- await ensureAddonNativePrebuilds(pkgDir, {
4676
- addonName: pkgView.name,
4677
- logger: this.logger
4678
- });
4739
+ await this.installNativeDependencies(pkgDir, pkgView.name);
4679
4740
  await this.evictInstallDir(targetDir);
4680
4741
  ensureDir(path$39.dirname(targetDir));
4681
4742
  await fs$17.promises.rename(pkgDir, targetDir);
@@ -4780,9 +4841,12 @@ var AddonInstaller = class AddonInstaller {
4780
4841
  *
4781
4842
  * "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
4782
4843
  * tree is the raw unpacked tarball, so the addon's own
4783
- * `camstack.runtimeDependencies` still have to be installed into it, exactly
4784
- * as `installFromTgz` does. Skipping that is what broke every runtime-dep
4785
- * addon on its first OTA update see {@link installRuntimeDependencies}.
4844
+ * `camstack.runtimeDependencies` AND `camstack.nativeDependencies` still have
4845
+ * to be installed into it, exactly as `installFromTgz` does. Skipping the
4846
+ * first broke every runtime-dep addon on its first OTA update; skipping the
4847
+ * second broke every addon with a native module the same way, one layer down
4848
+ * (`Could not locate the bindings file` instead of `Cannot find module`) —
4849
+ * see {@link installRuntimeDependencies} and {@link installNativeDependencies}.
4786
4850
  *
4787
4851
  * Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
4788
4852
  * the live install is only then backed up and replaced. So the addon keeps
@@ -4795,8 +4859,8 @@ var AddonInstaller = class AddonInstaller {
4795
4859
  * restored and the error is rethrown — identical recovery to `applyUpdate`.
4796
4860
  *
4797
4861
  * Throws if the package is not tracked in the manifest, if `addonDir`
4798
- * is missing (same preconditions as `applyUpdate`), or if the runtime-dep
4799
- * install fails.
4862
+ * is missing (same preconditions as `applyUpdate`), or if the runtime-dep /
4863
+ * native-dep install fails.
4800
4864
  */
4801
4865
  async applyUpdateFromStaged(packageName, version, stagedPath) {
4802
4866
  const current = this.manifest.get(packageName);
@@ -4804,6 +4868,7 @@ var AddonInstaller = class AddonInstaller {
4804
4868
  const addonDir = path$39.join(this.addonsDir, packageName);
4805
4869
  if (!fs$17.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
4806
4870
  await this.installRuntimeDependencies(stagedPath, packageName);
4871
+ await this.installNativeDependencies(stagedPath, packageName);
4807
4872
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4808
4873
  const backupRoot = path$39.join(this.addonsDir, ".backups", packageName);
4809
4874
  ensureDir(backupRoot);
@@ -187,8 +187,22 @@ export declare class AddonInstaller {
187
187
  * defaults to npm in production.
188
188
  *
189
189
  * @param packages — optional custom package list (default: REQUIRED_PACKAGES)
190
+ * @param pins — optional `name → exact version` map. A named package is
191
+ * fetched at THAT version instead of the registry's `latest`.
192
+ *
193
+ * Preparation for [D45](../../../../docs/decisions/adr-0045.md) task 32: the
194
+ * image seed answers first today, so `latest` has never been the version a
195
+ * node actually got. Once the image stops baking an addon tree under `/opt`
196
+ * this call IS the answer, and it has to be the version the running closure was
197
+ * published with — otherwise a node fetches latest-of-everything and its
198
+ * addon set depends on the minute it first booted. Passing nothing keeps
199
+ * exactly today's behaviour, and `launcher.ts` passes nothing yet.
200
+ *
201
+ * A pin never triggers a re-install: bootstrap stays seed-only, so a copy
202
+ * already under the addon root is left alone whatever its version. Replacing
203
+ * it would be an un-deploy ([D90](../../../../docs/decisions/adr-0090.md)).
190
204
  */
191
- ensureRequiredPackages(packages?: readonly string[]): Promise<void>;
205
+ ensureRequiredPackages(packages?: readonly string[], pins?: Readonly<Record<string, string>>): Promise<void>;
192
206
  /**
193
207
  * Reconcile the install manifest against the addons actually present on
194
208
  * disk: register seeded addons, correct a version that disagrees with the
@@ -341,6 +355,41 @@ export declare class AddonInstaller {
341
355
  * package entirely).
342
356
  */
343
357
  private installRuntimeDependencies;
358
+ /**
359
+ * Make `pkgDir`'s NATIVE modules loadable on THIS host, before it is swapped
360
+ * in: install whatever `camstack.nativeDependencies` declares, then ensure
361
+ * every traditional native present in the tree actually carries a compiled
362
+ * `.node` for the running ABI.
363
+ *
364
+ * ONE implementation, for the same reason as {@link installRuntimeDependencies}
365
+ * — every path that materialises an addon tree calls it, so no call site can
366
+ * forget it. `installFromTgz` (deploy/upload) always ran these two steps; the
367
+ * OTA staged-swap path ran NEITHER even after the runtime-dep fix (2026-08-12,
368
+ * task #15, which named this gap explicitly). Same outage class one layer
369
+ * down: an addon with native modules was broken by its FIRST OTA update, its
370
+ * dist landing without an ABI-matched binary, and every load dying at the
371
+ * binding (`Could not locate the bindings file`) — from the outside
372
+ * indistinguishable from a bad bundle.
373
+ *
374
+ * **Two distinct steps, both load-bearing.**
375
+ * `installManifestNativeDeps` fetches the DECLARED natives (prebuilt host copy
376
+ * first — offline — then npm + an ABI rebuild), and is idempotent through a
377
+ * content-hash marker. `ensureAddonNativePrebuilds` is the post-install pass:
378
+ * npm can leave a traditional native (`better-sqlite3`) SOURCE-ONLY on a
379
+ * toolchain-less host, so it re-fetches the prebuild for the running ABI and
380
+ * THROWS naming module + addon if the binary is still absent. No bundled
381
+ * source is available on this path (unlike `installCopy`) — fetch only.
382
+ *
383
+ * **Failure is FATAL**, mirroring the tgz path: callers complete the tree
384
+ * BEFORE they swap, so an abort simply means the swap never happens and the
385
+ * previous version keeps serving. Shipping a native-less copy instead would
386
+ * trade one broken version for a crash-loop.
387
+ *
388
+ * A missing/unreadable manifest is logged and skipped — `installRuntimeDependencies`
389
+ * has already said so loudly, and a tree with no readable package.json
390
+ * declares no natives.
391
+ */
392
+ private installNativeDependencies;
344
393
  /** Install addon from a tgz file (uploaded or downloaded) */
345
394
  installFromTgz(tgzPath: string): Promise<{
346
395
  name: string;
@@ -381,9 +430,12 @@ export declare class AddonInstaller {
381
430
  *
382
431
  * "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
383
432
  * tree is the raw unpacked tarball, so the addon's own
384
- * `camstack.runtimeDependencies` still have to be installed into it, exactly
385
- * as `installFromTgz` does. Skipping that is what broke every runtime-dep
386
- * addon on its first OTA update see {@link installRuntimeDependencies}.
433
+ * `camstack.runtimeDependencies` AND `camstack.nativeDependencies` still have
434
+ * to be installed into it, exactly as `installFromTgz` does. Skipping the
435
+ * first broke every runtime-dep addon on its first OTA update; skipping the
436
+ * second broke every addon with a native module the same way, one layer down
437
+ * (`Could not locate the bindings file` instead of `Cannot find module`) —
438
+ * see {@link installRuntimeDependencies} and {@link installNativeDependencies}.
387
439
  *
388
440
  * Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
389
441
  * the live install is only then backed up and replaced. So the addon keeps
@@ -396,8 +448,8 @@ export declare class AddonInstaller {
396
448
  * restored and the error is rethrown — identical recovery to `applyUpdate`.
397
449
  *
398
450
  * Throws if the package is not tracked in the manifest, if `addonDir`
399
- * is missing (same preconditions as `applyUpdate`), or if the runtime-dep
400
- * install fails.
451
+ * is missing (same preconditions as `applyUpdate`), or if the runtime-dep /
452
+ * native-dep install fails.
401
453
  */
402
454
  applyUpdateFromStaged(packageName: string, version: string, stagedPath: string): Promise<{
403
455
  name: string;
@@ -2,7 +2,12 @@ import { EventFilter, SystemEvent } from '@camstack/types';
2
2
  export interface SubscriberEntry {
3
3
  /** Original filter — kept whole so non-category dimensions are honoured per-event. */
4
4
  readonly filter: EventFilter | string | undefined;
5
- /** Pre-computed category pattern for the cheap pre-filter. */
5
+ /**
6
+ * Pre-computed category pattern for the cheap pre-filter — the FIRST of
7
+ * {@link extractCategoryPatterns} when the filter names several. Diagnostic
8
+ * only: the authority on which keys an entry occupies is the handler map
9
+ * itself, written by {@link registerSubscriber}.
10
+ */
6
11
  readonly categoryPattern: string;
7
12
  readonly handler: (event: SystemEvent) => void;
8
13
  }
@@ -47,7 +52,48 @@ export interface SharedBusState {
47
52
  */
48
53
  export declare function createSharedBusState(retainRecent?: boolean): SharedBusState;
49
54
  export declare function matchesPattern(pattern: string, category: string): boolean;
55
+ /**
56
+ * The FIRST category pattern of a filter.
57
+ *
58
+ * Kept for the diagnostic `SubscriberEntry.categoryPattern` field and for
59
+ * callers that genuinely want one string. It is NOT the registration path:
60
+ * a filter naming several categories must occupy several map keys, which is
61
+ * what {@link extractCategoryPatterns} + {@link registerSubscriber} do. Using
62
+ * this function to key a subscriber is the bug described on
63
+ * `extractCategoryPatterns`.
64
+ */
50
65
  export declare function extractCategoryPattern(filter: EventFilter | string): string;
66
+ /**
67
+ * EVERY category pattern a filter asks for — the registration key set.
68
+ *
69
+ * `EventFilter.category` has always accepted an array, and until 2026-08-12
70
+ * the delivery path kept only `cat[0]`. Inside a forked runner that was not
71
+ * merely a local filter: `createUdsEventBus` reports its handler-map keys to
72
+ * the parent as this child's forwarding interest, so the categories that never
73
+ * became keys were never forwarded across the process boundary at all. A
74
+ * ten-category subscribe received one category, silently.
75
+ *
76
+ * Duplicates are collapsed so a filter that names the same category twice does
77
+ * not put the entry in one key twice.
78
+ */
79
+ export declare function extractCategoryPatterns(filter: EventFilter | string): readonly string[];
80
+ /**
81
+ * Register one handler under every category pattern its filter names, and
82
+ * return the unsubscribe that removes it from all of them.
83
+ *
84
+ * The ONE place a subscriber enters `state.handlers` — both bus
85
+ * implementations (broker and UDS) call it, so "which keys does this
86
+ * subscriber occupy" has a single answer. An emptied key is DELETED rather
87
+ * than left as an empty `Set`: on a child bus the key set is what the parent
88
+ * is told to forward, and an empty set left behind pins the parent to forward
89
+ * a category nobody wants.
90
+ *
91
+ * @param onKeysChanged - Called after the key set may have changed (both on
92
+ * registration and on unsubscribe), for buses that mirror their key set
93
+ * somewhere — the UDS child's parent report, the broker's local-interest
94
+ * surface. Receives the patterns this call touched.
95
+ */
96
+ export declare function registerSubscriber(state: SharedBusState, filter: EventFilter | string, handler: (event: SystemEvent) => void, onKeysChanged?: (patterns: readonly string[], added: boolean) => void): () => void;
51
97
  /**
52
98
  * Match an event against the full `EventFilter`. The Map-keyed subscribe path
53
99
  * only handles the category pattern (deciding which handler set to fan out to);
@@ -62,6 +62,32 @@ export interface CapCallOutMessage {
62
62
  * `platform-probe` hardware). Set via `ctx.api`'s `onNode(nodeId)` modifier.
63
63
  */
64
64
  readonly nodeId?: string;
65
+ /**
66
+ * "Resolve the per-device NATIVE provider for this cap, never a wrapper."
67
+ * Set by `ctx.getNativeProvider(...)` in a forked runner.
68
+ *
69
+ * ## Why a flag and not just the cap name
70
+ *
71
+ * A wrapper and its natives share ONE cap name. As long as the wrapper lived
72
+ * in hub-main that was harmless — the wrapper resolved its native through the
73
+ * hub's `CapabilityRegistry`, which knows the difference. A wrapper in a
74
+ * runner has no such registry, and every generic route back out of the
75
+ * process resolves `capName` the way every OTHER caller does: to the wrapper.
76
+ * `localProviderLink` answers it from the runner's own provider before the
77
+ * call ever reaches the socket, and `resolveChildId` would hand it back to
78
+ * the wrapper's own child if it did. Either way the wrapper calls itself:
79
+ * the second entry joins the first's single flight and waits on a promise
80
+ * only it can settle.
81
+ *
82
+ * So the intent has to travel. `native: true` makes the parent skip
83
+ * capName-based resolution entirely and ask the ONE native authority
84
+ * (`CapabilityRegistry.getNativeProvider`, which covers in-process natives,
85
+ * hub-local UDS vendor children and remote agents alike) for `(cap, device)`.
86
+ * With no native for the pair the call FAILS — it never degrades into the
87
+ * wrapper — with a message a wrapper can read as "absent" and fall through to
88
+ * its own strategy.
89
+ */
90
+ readonly native?: boolean;
65
91
  }
66
92
  /** Child → parent: a system event produced by the child to be forwarded to the hub event bus. */
67
93
  export interface ChildEventMessage {
@@ -110,6 +136,12 @@ export interface CapCallMessage {
110
136
  * the child dispatch falls back to its sole provider for the cap.
111
137
  */
112
138
  readonly addonId?: string;
139
+ /**
140
+ * Native-only routing hint — see {@link CapCallOutMessage.native}. Carried
141
+ * here because `CapCallInput` (the type every parent-side router takes) is
142
+ * this message minus its discriminant; a child DISPATCH ignores it.
143
+ */
144
+ readonly native?: boolean;
113
145
  }
114
146
  /** Parent → child: a system event delivered by the hub for the child to handle locally. */
115
147
  export interface ParentEventMessage {
@@ -16,6 +16,14 @@ export interface HubCapForwardParams {
16
16
  readonly args: unknown;
17
17
  readonly deviceId?: number;
18
18
  readonly nodeId?: string;
19
+ /**
20
+ * Native-only routing hint (see `CapCallOutMessage.native`). An agent has no
21
+ * native authority of its own, so a forked wrapper on an agent asking for a
22
+ * per-device native forwards the question here. It MUST survive the envelope:
23
+ * without it the hub resolves the cap NAME, i.e. the wrapper the caller is
24
+ * trying to skip.
25
+ */
26
+ readonly native?: boolean;
19
27
  }
20
28
  /**
21
29
  * Handler that routes an agent-originated cap call through the hub's own
@@ -183,6 +183,25 @@ export interface ParentUnownedCallDeps {
183
183
  * exactly: the miss falls straight to the broker fallback with no retry.
184
184
  */
185
185
  readonly isCoreServiceCap?: (capName: string) => boolean;
186
+ /**
187
+ * Resolve the per-device NATIVE provider for `(capName, deviceId)` — the
188
+ * `native: true` route (see {@link CapCallInput.native}).
189
+ *
190
+ * Wired to this node's `CapabilityRegistry.getNativeProvider`, which is the
191
+ * SINGLE authority on the question: it already covers in-process natives,
192
+ * hub-local UDS vendor children (through the hub's `setNativeFallback`
193
+ * resolver) and remote agents. Nothing here re-derives that — a rule
194
+ * implemented twice is a rule that drifts, and this one previously drifted
195
+ * into a wrapper answering on behalf of the native it was supposed to call.
196
+ *
197
+ * Returns null when no addon publishes the cap for that device. The handler
198
+ * then FAILS the call rather than falling back to capName routing: falling
199
+ * back is how a wrapper ends up calling itself.
200
+ *
201
+ * Omitted ⇒ a `native: true` call is forwarded to the hub (agents) or
202
+ * rejected (no forwarder) — never silently downgraded to wrapper routing.
203
+ */
204
+ readonly resolveNative?: (capName: string, deviceId: number) => Record<string, (input: unknown) => Promise<unknown>> | null;
186
205
  /** Optional logger for the broker-fallback diagnostic line. */
187
206
  readonly logger?: ParentUnownedCallLogger;
188
207
  }