@camstack/system 1.2.84 → 1.2.86

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 (60) 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 +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/system-config/system-config.addon.js +1 -1
  46. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-BAwlOEUy.mjs → dist-B90N7AWO.mjs} +575 -34
  50. package/dist/{dist-CQy9KjsL.js → dist-BV5Bug9t.js} +575 -34
  51. package/dist/index.js +73 -16
  52. package/dist/index.mjs +73 -16
  53. package/dist/kernel/addon-installer.d.ts +49 -5
  54. package/dist/kernel/moleculer/event-bus-core.d.ts +47 -1
  55. package/dist/kernel/transport/child-cap-protocol.d.ts +32 -0
  56. package/dist/kernel/transport/hub-cap-forward.d.ts +8 -0
  57. package/dist/kernel/transport/parent-unowned-call.d.ts +19 -0
  58. package/dist/{manifest-python-deps-DG_LLcZF.js → manifest-python-deps-C0zkl1Ae.js} +133 -53
  59. package/dist/{manifest-python-deps-C1DMs1J9.mjs → manifest-python-deps-fWzdEasI.mjs} +133 -53
  60. 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-CQy9KjsL.js");
3
+ const require_dist = require("./dist-BV5Bug9t.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");
@@ -4227,6 +4227,12 @@ var AddonInstaller = class AddonInstaller {
4227
4227
  * installs from the admin UI go through `install()` instead, which
4228
4228
  * defaults to npm in production.
4229
4229
  *
4230
+ * There is no version argument and there is deliberately no pin: the system is
4231
+ * addons-agnostic and `latest` is the whole contract at first boot (operator,
4232
+ * 2026-08-13). A copy already under the addon root is never replaced either —
4233
+ * bootstrap is seed-only, and overwriting a deployed bundle would be an
4234
+ * un-deploy ([D90](../../../../docs/decisions/adr-0090.md)).
4235
+ *
4230
4236
  * @param packages — optional custom package list (default: REQUIRED_PACKAGES)
4231
4237
  */
4232
4238
  async ensureRequiredPackages(packages) {
@@ -4651,6 +4657,61 @@ var AddonInstaller = class AddonInstaller {
4651
4657
  throw new Error(`${packageName} — runtime dependency install failed: ${require_dist.errMsg(err)}`, { cause: err });
4652
4658
  }
4653
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 = node_path.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 require_manifest_python_deps.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: require_dist.errMsg(nativeErr)
4707
+ } });
4708
+ throw new Error(`${packageName} — native deps install failed: ${require_dist.errMsg(nativeErr)}`, { cause: nativeErr });
4709
+ }
4710
+ await ensureAddonNativePrebuilds(pkgDir, {
4711
+ addonName: packageName,
4712
+ logger: this.logger
4713
+ });
4714
+ }
4654
4715
  /** Install addon from a tgz file (uploaded or downloaded) */
4655
4716
  async installFromTgz(tgzPath) {
4656
4717
  const stagingRoot = node_path.join(this.addonsDir, ".staging");
@@ -4675,15 +4736,7 @@ var AddonInstaller = class AddonInstaller {
4675
4736
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
4676
4737
  const targetDir = node_path.join(this.addonsDir, pkgView.name);
4677
4738
  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
- });
4739
+ await this.installNativeDependencies(pkgDir, pkgView.name);
4687
4740
  await this.evictInstallDir(targetDir);
4688
4741
  ensureDir(node_path.dirname(targetDir));
4689
4742
  await node_fs.promises.rename(pkgDir, targetDir);
@@ -4788,9 +4841,12 @@ var AddonInstaller = class AddonInstaller {
4788
4841
  *
4789
4842
  * "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
4790
4843
  * 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}.
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}.
4794
4850
  *
4795
4851
  * Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
4796
4852
  * the live install is only then backed up and replaced. So the addon keeps
@@ -4803,8 +4859,8 @@ var AddonInstaller = class AddonInstaller {
4803
4859
  * restored and the error is rethrown — identical recovery to `applyUpdate`.
4804
4860
  *
4805
4861
  * 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.
4862
+ * is missing (same preconditions as `applyUpdate`), or if the runtime-dep /
4863
+ * native-dep install fails.
4808
4864
  */
4809
4865
  async applyUpdateFromStaged(packageName, version, stagedPath) {
4810
4866
  const current = this.manifest.get(packageName);
@@ -4812,6 +4868,7 @@ var AddonInstaller = class AddonInstaller {
4812
4868
  const addonDir = node_path.join(this.addonsDir, packageName);
4813
4869
  if (!node_fs.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
4814
4870
  await this.installRuntimeDependencies(stagedPath, packageName);
4871
+ await this.installNativeDependencies(stagedPath, packageName);
4815
4872
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4816
4873
  const backupRoot = node_path.join(this.addonsDir, ".backups", packageName);
4817
4874
  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-BAwlOEUy.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-B90N7AWO.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";
@@ -24,7 +24,7 @@ import { SystemConfigAddon } from "./builtins/system-config/system-config.addon.
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";
@@ -4219,6 +4219,12 @@ var AddonInstaller = class AddonInstaller {
4219
4219
  * installs from the admin UI go through `install()` instead, which
4220
4220
  * defaults to npm in production.
4221
4221
  *
4222
+ * There is no version argument and there is deliberately no pin: the system is
4223
+ * addons-agnostic and `latest` is the whole contract at first boot (operator,
4224
+ * 2026-08-13). A copy already under the addon root is never replaced either —
4225
+ * bootstrap is seed-only, and overwriting a deployed bundle would be an
4226
+ * un-deploy ([D90](../../../../docs/decisions/adr-0090.md)).
4227
+ *
4222
4228
  * @param packages — optional custom package list (default: REQUIRED_PACKAGES)
4223
4229
  */
4224
4230
  async ensureRequiredPackages(packages) {
@@ -4643,6 +4649,61 @@ var AddonInstaller = class AddonInstaller {
4643
4649
  throw new Error(`${packageName} — runtime dependency install failed: ${errMsg$1(err)}`, { cause: err });
4644
4650
  }
4645
4651
  }
4652
+ /**
4653
+ * Make `pkgDir`'s NATIVE modules loadable on THIS host, before it is swapped
4654
+ * in: install whatever `camstack.nativeDependencies` declares, then ensure
4655
+ * every traditional native present in the tree actually carries a compiled
4656
+ * `.node` for the running ABI.
4657
+ *
4658
+ * ONE implementation, for the same reason as {@link installRuntimeDependencies}
4659
+ * — every path that materialises an addon tree calls it, so no call site can
4660
+ * forget it. `installFromTgz` (deploy/upload) always ran these two steps; the
4661
+ * OTA staged-swap path ran NEITHER even after the runtime-dep fix (2026-08-12,
4662
+ * task #15, which named this gap explicitly). Same outage class one layer
4663
+ * down: an addon with native modules was broken by its FIRST OTA update, its
4664
+ * dist landing without an ABI-matched binary, and every load dying at the
4665
+ * binding (`Could not locate the bindings file`) — from the outside
4666
+ * indistinguishable from a bad bundle.
4667
+ *
4668
+ * **Two distinct steps, both load-bearing.**
4669
+ * `installManifestNativeDeps` fetches the DECLARED natives (prebuilt host copy
4670
+ * first — offline — then npm + an ABI rebuild), and is idempotent through a
4671
+ * content-hash marker. `ensureAddonNativePrebuilds` is the post-install pass:
4672
+ * npm can leave a traditional native (`better-sqlite3`) SOURCE-ONLY on a
4673
+ * toolchain-less host, so it re-fetches the prebuild for the running ABI and
4674
+ * THROWS naming module + addon if the binary is still absent. No bundled
4675
+ * source is available on this path (unlike `installCopy`) — fetch only.
4676
+ *
4677
+ * **Failure is FATAL**, mirroring the tgz path: callers complete the tree
4678
+ * BEFORE they swap, so an abort simply means the swap never happens and the
4679
+ * previous version keeps serving. Shipping a native-less copy instead would
4680
+ * trade one broken version for a crash-loop.
4681
+ *
4682
+ * A missing/unreadable manifest is logged and skipped — `installRuntimeDependencies`
4683
+ * has already said so loudly, and a tree with no readable package.json
4684
+ * declares no natives.
4685
+ */
4686
+ async installNativeDependencies(pkgDir, packageName) {
4687
+ const pkgJsonPath = path$39.join(pkgDir, "package.json");
4688
+ const pkgView = readPackageJson(pkgJsonPath);
4689
+ if (pkgView == null) {
4690
+ this.logger.error(`${packageName} — no readable package.json in the prepared tree; native dependencies NOT installed`, { meta: { pkgJsonPath } });
4691
+ return;
4692
+ }
4693
+ try {
4694
+ await installManifestNativeDeps(pkgDir, pkgView.raw, this.logger, this.registry, this.npmCacheDir);
4695
+ } catch (nativeErr) {
4696
+ this.logger.error(`${packageName} — native dependency install FAILED; install ABORTED (a swapped-in copy would fail every load at the binding)`, { meta: {
4697
+ pkgDir,
4698
+ error: errMsg$1(nativeErr)
4699
+ } });
4700
+ throw new Error(`${packageName} — native deps install failed: ${errMsg$1(nativeErr)}`, { cause: nativeErr });
4701
+ }
4702
+ await ensureAddonNativePrebuilds(pkgDir, {
4703
+ addonName: packageName,
4704
+ logger: this.logger
4705
+ });
4706
+ }
4646
4707
  /** Install addon from a tgz file (uploaded or downloaded) */
4647
4708
  async installFromTgz(tgzPath) {
4648
4709
  const stagingRoot = path$39.join(this.addonsDir, ".staging");
@@ -4667,15 +4728,7 @@ var AddonInstaller = class AddonInstaller {
4667
4728
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
4668
4729
  const targetDir = path$39.join(this.addonsDir, pkgView.name);
4669
4730
  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
- });
4731
+ await this.installNativeDependencies(pkgDir, pkgView.name);
4679
4732
  await this.evictInstallDir(targetDir);
4680
4733
  ensureDir(path$39.dirname(targetDir));
4681
4734
  await fs$17.promises.rename(pkgDir, targetDir);
@@ -4780,9 +4833,12 @@ var AddonInstaller = class AddonInstaller {
4780
4833
  *
4781
4834
  * "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
4782
4835
  * 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}.
4836
+ * `camstack.runtimeDependencies` AND `camstack.nativeDependencies` still have
4837
+ * to be installed into it, exactly as `installFromTgz` does. Skipping the
4838
+ * first broke every runtime-dep addon on its first OTA update; skipping the
4839
+ * second broke every addon with a native module the same way, one layer down
4840
+ * (`Could not locate the bindings file` instead of `Cannot find module`) —
4841
+ * see {@link installRuntimeDependencies} and {@link installNativeDependencies}.
4786
4842
  *
4787
4843
  * Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
4788
4844
  * the live install is only then backed up and replaced. So the addon keeps
@@ -4795,8 +4851,8 @@ var AddonInstaller = class AddonInstaller {
4795
4851
  * restored and the error is rethrown — identical recovery to `applyUpdate`.
4796
4852
  *
4797
4853
  * 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.
4854
+ * is missing (same preconditions as `applyUpdate`), or if the runtime-dep /
4855
+ * native-dep install fails.
4800
4856
  */
4801
4857
  async applyUpdateFromStaged(packageName, version, stagedPath) {
4802
4858
  const current = this.manifest.get(packageName);
@@ -4804,6 +4860,7 @@ var AddonInstaller = class AddonInstaller {
4804
4860
  const addonDir = path$39.join(this.addonsDir, packageName);
4805
4861
  if (!fs$17.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
4806
4862
  await this.installRuntimeDependencies(stagedPath, packageName);
4863
+ await this.installNativeDependencies(stagedPath, packageName);
4807
4864
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4808
4865
  const backupRoot = path$39.join(this.addonsDir, ".backups", packageName);
4809
4866
  ensureDir(backupRoot);
@@ -186,6 +186,12 @@ export declare class AddonInstaller {
186
186
  * installs from the admin UI go through `install()` instead, which
187
187
  * defaults to npm in production.
188
188
  *
189
+ * There is no version argument and there is deliberately no pin: the system is
190
+ * addons-agnostic and `latest` is the whole contract at first boot (operator,
191
+ * 2026-08-13). A copy already under the addon root is never replaced either —
192
+ * bootstrap is seed-only, and overwriting a deployed bundle would be an
193
+ * un-deploy ([D90](../../../../docs/decisions/adr-0090.md)).
194
+ *
189
195
  * @param packages — optional custom package list (default: REQUIRED_PACKAGES)
190
196
  */
191
197
  ensureRequiredPackages(packages?: readonly string[]): Promise<void>;
@@ -341,6 +347,41 @@ export declare class AddonInstaller {
341
347
  * package entirely).
342
348
  */
343
349
  private installRuntimeDependencies;
350
+ /**
351
+ * Make `pkgDir`'s NATIVE modules loadable on THIS host, before it is swapped
352
+ * in: install whatever `camstack.nativeDependencies` declares, then ensure
353
+ * every traditional native present in the tree actually carries a compiled
354
+ * `.node` for the running ABI.
355
+ *
356
+ * ONE implementation, for the same reason as {@link installRuntimeDependencies}
357
+ * — every path that materialises an addon tree calls it, so no call site can
358
+ * forget it. `installFromTgz` (deploy/upload) always ran these two steps; the
359
+ * OTA staged-swap path ran NEITHER even after the runtime-dep fix (2026-08-12,
360
+ * task #15, which named this gap explicitly). Same outage class one layer
361
+ * down: an addon with native modules was broken by its FIRST OTA update, its
362
+ * dist landing without an ABI-matched binary, and every load dying at the
363
+ * binding (`Could not locate the bindings file`) — from the outside
364
+ * indistinguishable from a bad bundle.
365
+ *
366
+ * **Two distinct steps, both load-bearing.**
367
+ * `installManifestNativeDeps` fetches the DECLARED natives (prebuilt host copy
368
+ * first — offline — then npm + an ABI rebuild), and is idempotent through a
369
+ * content-hash marker. `ensureAddonNativePrebuilds` is the post-install pass:
370
+ * npm can leave a traditional native (`better-sqlite3`) SOURCE-ONLY on a
371
+ * toolchain-less host, so it re-fetches the prebuild for the running ABI and
372
+ * THROWS naming module + addon if the binary is still absent. No bundled
373
+ * source is available on this path (unlike `installCopy`) — fetch only.
374
+ *
375
+ * **Failure is FATAL**, mirroring the tgz path: callers complete the tree
376
+ * BEFORE they swap, so an abort simply means the swap never happens and the
377
+ * previous version keeps serving. Shipping a native-less copy instead would
378
+ * trade one broken version for a crash-loop.
379
+ *
380
+ * A missing/unreadable manifest is logged and skipped — `installRuntimeDependencies`
381
+ * has already said so loudly, and a tree with no readable package.json
382
+ * declares no natives.
383
+ */
384
+ private installNativeDependencies;
344
385
  /** Install addon from a tgz file (uploaded or downloaded) */
345
386
  installFromTgz(tgzPath: string): Promise<{
346
387
  name: string;
@@ -381,9 +422,12 @@ export declare class AddonInstaller {
381
422
  *
382
423
  * "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
383
424
  * 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}.
425
+ * `camstack.runtimeDependencies` AND `camstack.nativeDependencies` still have
426
+ * to be installed into it, exactly as `installFromTgz` does. Skipping the
427
+ * first broke every runtime-dep addon on its first OTA update; skipping the
428
+ * second broke every addon with a native module the same way, one layer down
429
+ * (`Could not locate the bindings file` instead of `Cannot find module`) —
430
+ * see {@link installRuntimeDependencies} and {@link installNativeDependencies}.
387
431
  *
388
432
  * Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
389
433
  * the live install is only then backed up and replaced. So the addon keeps
@@ -396,8 +440,8 @@ export declare class AddonInstaller {
396
440
  * restored and the error is rethrown — identical recovery to `applyUpdate`.
397
441
  *
398
442
  * 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.
443
+ * is missing (same preconditions as `applyUpdate`), or if the runtime-dep /
444
+ * native-dep install fails.
401
445
  */
402
446
  applyUpdateFromStaged(packageName: string, version: string, stagedPath: string): Promise<{
403
447
  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
  }