@camstack/server 1.2.407 → 1.2.409

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.
@@ -47,26 +47,52 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
47
47
  }));
48
48
  }),
49
49
  // ─── Get current preference for a capability ──────────────────────
50
+ //
51
+ // `state` separates "the operator set nothing" (`known`, `preference`
52
+ // null / no disabled ids) from "the persisted preference cannot be read"
53
+ // (`unknown` — the boot read of the settings store is pending or failed,
54
+ // D675). The latter used to be reported as `null`: an unset preference.
50
55
  getPreference: trpc_middleware_1.adminProcedure.input(zod_1.z.object({ capability: zod_1.z.string() })).query(({ input }) => {
51
56
  const registry = addonRegistry.getCapabilityRegistry();
52
57
  const mode = registry.getMode(input.capability);
53
58
  if (!mode)
54
59
  return null;
60
+ let raw;
61
+ try {
62
+ raw = configService.get(mode === 'singleton'
63
+ ? `capabilities.singleton.${input.capability}`
64
+ : (0, collection_preference_1.collectionPreferenceKey)(input.capability));
65
+ }
66
+ catch (err) {
67
+ if (!(0, system_1.isSettingsReadUnavailable)(err))
68
+ throw err;
69
+ return {
70
+ capability: input.capability,
71
+ mode,
72
+ state: 'unknown',
73
+ reason: err.reason,
74
+ preference: null,
75
+ };
76
+ }
55
77
  if (mode === 'singleton') {
56
- const addonId = configService.get(`capabilities.singleton.${input.capability}`);
57
78
  return {
58
79
  capability: input.capability,
59
- mode: mode,
60
- preference: addonId ? { addonId } : null,
80
+ mode: 'singleton',
81
+ state: 'known',
82
+ preference: raw ? { addonId: raw } : null,
61
83
  };
62
84
  }
63
85
  // collection
64
- const raw = configService.get((0, collection_preference_1.collectionPreferenceKey)(input.capability));
65
86
  let disabled = [];
66
87
  if (raw) {
67
88
  try {
68
89
  const parsed = JSON.parse(raw);
69
- disabled = Array.isArray(parsed.disabled) ? parsed.disabled : [];
90
+ if (typeof parsed === 'object' && parsed !== null && 'disabled' in parsed) {
91
+ const list = parsed.disabled;
92
+ disabled = Array.isArray(list)
93
+ ? list.filter((id) => typeof id === 'string')
94
+ : [];
95
+ }
70
96
  }
71
97
  catch {
72
98
  /* ignore malformed */
@@ -74,7 +100,8 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
74
100
  }
75
101
  return {
76
102
  capability: input.capability,
77
- mode: mode,
103
+ mode: 'collection',
104
+ state: 'known',
78
105
  preference: { disabled },
79
106
  };
80
107
  }),
@@ -104,7 +131,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
104
131
  await registry.setActiveSingleton(capability, addonId);
105
132
  }
106
133
  // Persist preference
107
- configService.set(`capabilities.singleton.${capability}`, addonId);
134
+ await configService.set(`capabilities.singleton.${capability}`, addonId);
108
135
  logger.info('Singleton preference set', {
109
136
  tags: { addonId },
110
137
  meta: { capability, requiresRestart },
@@ -133,7 +160,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
133
160
  }
134
161
  // Persist disabled list via the shared canonical writer.
135
162
  const updatedCap = registry.listCapabilities().find((c) => c.name === capability);
136
- (0, collection_preference_1.persistCollectionDisabled)(configService, capability, updatedCap?.disabledProviders ?? []);
163
+ await (0, collection_preference_1.persistCollectionDisabled)(configService, capability, updatedCap?.disabledProviders ?? []);
137
164
  logger.info('Collection provider toggled', {
138
165
  tags: { addonId },
139
166
  meta: { capability, enabled },
@@ -143,7 +170,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
143
170
  // ─── Reset preference to default ──────────────────────────────────
144
171
  resetPreference: trpc_middleware_1.adminProcedure
145
172
  .input(zod_1.z.object({ capability: zod_1.z.string() }))
146
- .mutation(({ input }) => {
173
+ .mutation(async ({ input }) => {
147
174
  const registry = addonRegistry.getCapabilityRegistry();
148
175
  const mode = registry.getMode(input.capability);
149
176
  if (!mode)
@@ -152,7 +179,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
152
179
  message: `Unknown capability: ${input.capability}`,
153
180
  });
154
181
  if (mode === 'singleton') {
155
- configService.set(`capabilities.singleton.${input.capability}`, null);
182
+ await configService.set(`capabilities.singleton.${input.capability}`, null);
156
183
  logger.info('Singleton preference reset (takes effect on restart)', {
157
184
  meta: { capability: input.capability },
158
185
  });
@@ -166,7 +193,7 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
166
193
  registry.enableCollectionProvider(input.capability, addonId);
167
194
  }
168
195
  }
169
- configService.set(`capabilities.collection.${input.capability}`, null);
196
+ await configService.set(`capabilities.collection.${input.capability}`, null);
170
197
  logger.info('Collection preference reset (all providers re-enabled)', {
171
198
  meta: { capability: input.capability },
172
199
  });
@@ -2,153 +2,107 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createAddonSettingsRouter = createAddonSettingsRouter;
4
4
  /**
5
- * Addon settings router — raw DB proxy for the common settings API.
5
+ * `addonSettingsRaw` — admin read of an addon's RAW stored settings.
6
6
  *
7
- * ## ⚠️ This is NOT the store `ctx.settings.readAddonStore()` reads
7
+ * ## It reads the same rows `ctx.settings` does — through the same door
8
8
  *
9
- * Two different physical stores wear the name "addon-settings", and mistaking
10
- * one for the other costs a whole debugging session — it did on 2026-08-13,
11
- * where a per-node inference override was written here, read back here
12
- * (looking perfectly correct), and was invisible to the addon forever:
9
+ * There is ONE store: the `addon-settings` collection, one row per key,
10
+ * `id = "<addonId>.<key>"`, `data = {addonId, key, value}` (and
11
+ * `addon-device-settings`, `id = "<addonId>:<deviceId>.<key>"`, for the
12
+ * per-device overrides). An in-process addon's `ctx.settings` reads exactly
13
+ * those rows.
13
14
  *
14
- * | Surface | Physical shape | Read by |
15
- * | --- | --- | --- |
16
- * | THIS router (`ConfigService.getAddonConfig` → `getAllAddon`) | the `addon-settings` table, **one row per key**, `id = "<addonId>.<key>"`, `data = {addonId, key, value}` | `buildAddonConfig` → `ctx.addonConfig`, and `virtual-doorbell` |
17
- * | `settings-store` cap (`ctx.settings.readAddonStore()`) | **ONE blob row**, namespace `<addonId>`, collection `addon-settings`, key `root` | `BaseAddon.resolveConfig` / every addon's global settings |
15
+ * Until D675 this router read them through `ConfigManager.getAddonConfig`, a
16
+ * SYNCHRONOUS reader that needs an in-process `ISettingsStore`. Since D231
17
+ * isolated `sqlite-settings`, hub-main never has one — only the async door — so
18
+ * `getAddonConfig` fell back to the bootstrap file, which holds no addon key,
19
+ * and every `getGlobal` answered `{}` as a success. That `{}` was once
20
+ * explained here as "two different physical stores wear the name
21
+ * addon-settings". That diagnosis was wrong: the rows were exactly where this
22
+ * router looked; the reader simply had no backend. It sent at least one
23
+ * session hunting for a key mismatch that did not exist.
18
24
  *
19
- * Nothing bridges them. To write an addon's global setting — including a
20
- * `perNode: true` key — go through the `addonSettings` CAP
21
- * (`updateGlobalSettings`, which applies the schema's per-node scoping), or
22
- * read it back with
23
- * `settingsStore.get {namespace:<addonId>, collection:'addon-settings', key:'root'}`.
24
- * `addonSettingsRaw.getGlobal` answering `{}` for an addon that plainly has
25
- * settings is the tell.
25
+ * Now both reads go through the settings door, THREE-WAY:
26
+ * - the stored record, when there is one;
27
+ * - `{}` when the store ANSWERED and holds nothing for this addon/device;
28
+ * - a `SERVICE_UNAVAILABLE` error when the store could not be read — never
29
+ * `{}`, which would be "never written" to a caller that asked (D315, D393).
26
30
  *
27
- * `setAllAddon` is also a **bulk replace**: `updateGlobal` read-modify-writes
28
- * the whole per-key set, so a write here deletes every other row of that addon
29
- * in this table.
31
+ * A forked addon's settings live in its own namespaced blob (`namespace
32
+ * <addonId>`, collection `addon-settings`, key `root`), which this router does
33
+ * not read — use the `addonSettings` cap (`getGlobalSettings`) for an addon's
34
+ * hydrated settings, whichever process it runs in.
35
+ *
36
+ * The write methods (`updateGlobal` / `updateDevice` / `replaceGlobal`) are
37
+ * GONE (D675): they had no callers, threw `SettingsStore not initialized` on
38
+ * every isolated-engine hub, and bypassed the schema gate. Write an addon's
39
+ * settings through the `addonSettings` cap (`updateGlobalSettings`, which also
40
+ * applies per-node scoping).
30
41
  *
31
42
  * ## ⚠️ ADMIN-ONLY — every method here is `adminProcedure`
32
43
  *
33
- * Reads (`getGlobal` / `getDeviceOverrides`) return RAW, un-redacted addon
34
- * config — the same blob that holds notifier tokens, webhook secrets and API
35
- * keys; writes (`updateGlobal` / `updateDevice` / `replaceGlobal`) overwrite it
36
- * with no schema gate. Before 2026-08-13 these were `protectedProcedure`, so
37
- * any authenticated non-admin token could read and rewrite every addon's
38
- * config. The `:139` comment claimed "only workers with valid hub tokens can
39
- * call this" but nothing enforced it.
44
+ * Both reads return RAW, un-redacted addon config — the same rows that hold
45
+ * notifier tokens, webhook secrets and API keys. Before 2026-08-13 they were
46
+ * `protectedProcedure`, so any authenticated non-admin token could read every
47
+ * addon's config.
40
48
  *
41
49
  * Gating with `adminProcedure` costs the legitimate callers NOTHING: this
42
50
  * router is in `CORE_NAMESPACES` (`core-cap-bridge.ts`), so a forked addon's
43
51
  * `ctx.api.addonSettingsRaw.*` arrives over the trusted core-cap/UDS mesh whose
44
52
  * context is `createMeshTrpcContext` (`isAdmin: true`), and the admin UI is an
45
- * admin session. The two consumers:
46
- * 1. Forked addons (via `ctx.api` over the mesh) reading/writing their
47
- * 3-level settings chain from the worker process.
48
- * 2. Admin UI flows inspecting/mutating addon settings.
49
- *
50
- * The router is deliberately thin — it does NOT perform schema-based
51
- * resolver merging (defaults → global → per-device). That happens on the
52
- * consumer side where the addon's `ConfigUISchema` is available:
53
- * - In-process addons: handled by `SettingsResolverService.createView()`
54
- * wired into `AddonContext.settings` during `createAddonContext()`.
55
- * - Forked addons: handled by the `AddonSettingsView` constructed inside
56
- * `WorkerBootstrapService`, which has access to the worker's local
57
- * addon schema.
53
+ * admin session.
58
54
  *
59
- * Introduced in session 5 Sprint 3a (worker-bootstrap cap-aware wiring).
55
+ * The router is deliberately thin — it does NOT perform schema-based resolver
56
+ * merging (defaults → global → per-device). That happens on the consumer side
57
+ * where the addon's `ConfigUISchema` is available.
60
58
  */
59
+ const server_1 = require("@trpc/server");
60
+ const system_1 = require("@camstack/system");
61
61
  const zod_1 = require("zod");
62
62
  const trpc_middleware_js_1 = require("../trpc/trpc.middleware.js");
63
63
  const AddonSettingsRecordSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.unknown());
64
64
  const AddonIdInputSchema = zod_1.z.object({
65
- addonId: zod_1.z.string(),
65
+ addonId: zod_1.z.string().min(1),
66
66
  });
67
+ /**
68
+ * `deviceId` stays a STRING on the wire (the shape callers already send) but
69
+ * must be a numeric device id: the per-device rows are keyed by the numeric id.
70
+ */
67
71
  const AddonDeviceInputSchema = zod_1.z.object({
68
- addonId: zod_1.z.string(),
69
- deviceId: zod_1.z.string(),
70
- });
71
- const UpdateGlobalInputSchema = zod_1.z.object({
72
- addonId: zod_1.z.string(),
73
- field: zod_1.z.string(),
74
- value: zod_1.z.unknown(),
75
- });
76
- const UpdateDeviceInputSchema = zod_1.z.object({
77
- addonId: zod_1.z.string(),
78
- deviceId: zod_1.z.string(),
79
- field: zod_1.z.string(),
80
- value: zod_1.z.unknown(),
81
- });
82
- const SuccessSchema = zod_1.z.object({ success: zod_1.z.literal(true) });
83
- const ReplaceGlobalInputSchema = zod_1.z.object({
84
- addonId: zod_1.z.string(),
85
- config: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
72
+ addonId: zod_1.z.string().min(1),
73
+ deviceId: zod_1.z.string().regex(/^\d+$/, 'deviceId must be a numeric device id'),
86
74
  });
75
+ /** A settings read that could not be answered is an error, never `{}`. */
76
+ async function answerOrRefuse(read) {
77
+ try {
78
+ return await read();
79
+ }
80
+ catch (err) {
81
+ if ((0, system_1.isSettingsReadUnavailable)(err)) {
82
+ throw new server_1.TRPCError({ code: 'SERVICE_UNAVAILABLE', message: err.message, cause: err });
83
+ }
84
+ throw err;
85
+ }
86
+ }
87
87
  function createAddonSettingsRouter(cfg) {
88
88
  return (0, trpc_middleware_js_1.trpcRouter)({
89
89
  /**
90
- * Read the addon-global settings record for the given addon.
91
- * Returns the raw stored values (no defaults, no device overrides).
90
+ * The addon-global stored record for the given addon — raw values, no
91
+ * defaults, no device overrides. `{}` only when the store answered with no
92
+ * row for this addon; a failed read is `SERVICE_UNAVAILABLE`.
92
93
  */
93
94
  getGlobal: trpc_middleware_js_1.adminProcedure
94
95
  .input(AddonIdInputSchema)
95
96
  .output(AddonSettingsRecordSchema)
96
- .query(({ input }) => cfg.getAddonConfig(input.addonId)),
97
+ .query(({ input }) => answerOrRefuse(() => cfg.readAddonGlobalStrict(input.addonId))),
97
98
  /**
98
- * Read the per-device override record for the given addon × device.
99
- * Returns the raw stored values (schema filtering happens on the
100
- * consumer side at merge time).
99
+ * The per-device override record for the given addon × device — raw
100
+ * values. `{}` only when the store answered with none; a failed read is
101
+ * `SERVICE_UNAVAILABLE`.
101
102
  */
102
103
  getDeviceOverrides: trpc_middleware_js_1.adminProcedure
103
104
  .input(AddonDeviceInputSchema)
104
105
  .output(AddonSettingsRecordSchema)
105
- .query(({ input }) => cfg.getAddonDevice(input.addonId, input.deviceId)),
106
- /**
107
- * Update a single field in the addon-global settings record.
108
- * Reads the current record, merges the new value, and writes back
109
- * via `setAddonConfig` (bulk replace). Intended for small per-field
110
- * writes from addon code; bulk updates should use a dedicated admin
111
- * endpoint (not exposed here).
112
- */
113
- updateGlobal: trpc_middleware_js_1.adminProcedure
114
- .input(UpdateGlobalInputSchema)
115
- .output(SuccessSchema)
116
- .mutation(({ input }) => {
117
- const current = cfg.getAddonConfig(input.addonId);
118
- cfg.setAddonConfig(input.addonId, { ...current, [input.field]: input.value });
119
- return { success: true };
120
- }),
121
- /**
122
- * Update a single field in the per-device override record for the
123
- * given addon × device. Merges with the existing overrides and
124
- * writes back via `setAddonDevice`. Scope enforcement (dropping
125
- * fields not declared as `scope: 'device'`) is the consumer's
126
- * responsibility — we preserve the raw shape at this layer so the
127
- * resolver contract remains symmetric with `getDeviceOverrides`.
128
- */
129
- updateDevice: trpc_middleware_js_1.adminProcedure
130
- .input(UpdateDeviceInputSchema)
131
- .output(SuccessSchema)
132
- .mutation(({ input }) => {
133
- const current = cfg.getAddonDevice(input.addonId, input.deviceId);
134
- cfg.setAddonDevice(input.addonId, input.deviceId, {
135
- ...current,
136
- [input.field]: input.value,
137
- });
138
- return { success: true };
139
- }),
140
- /**
141
- * Replace the entire addon-global settings record in one call.
142
- * Used by forked workers for `context.config.setAll()`. Unlike
143
- * `updateGlobal` (single-field merge), this overwrites the full record.
144
- * Admin-level write: only workers with valid hub tokens can call this.
145
- */
146
- replaceGlobal: trpc_middleware_js_1.adminProcedure
147
- .input(ReplaceGlobalInputSchema)
148
- .output(SuccessSchema)
149
- .mutation(({ input }) => {
150
- cfg.setAddonConfig(input.addonId, input.config);
151
- return { success: true };
152
- }),
106
+ .query(({ input }) => answerOrRefuse(() => cfg.readAddonDeviceStrict(input.addonId, Number(input.deviceId)))),
153
107
  });
154
108
  }
@@ -1475,7 +1475,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx) {
1475
1475
  // Reuses the same `capabilities.collection.<cap>` key/format the
1476
1476
  // `capabilities` core router writes — via the shared canonical writer.
1477
1477
  const updated = registry.listCapabilities().find((c) => c.name === input.capName);
1478
- (0, collection_preference_js_1.persistCollectionDisabled)(configService, input.capName, updated?.disabledProviders ?? []);
1478
+ await (0, collection_preference_js_1.persistCollectionDisabled)(configService, input.capName, updated?.disabledProviders ?? []);
1479
1479
  return { success: true };
1480
1480
  },
1481
1481
  getVersions: async (input) => ps.getPackageVersions(input.name),
@@ -33,24 +33,24 @@ function createCapabilitiesRouter(registry, config) {
33
33
  if (input.nodeId !== undefined) {
34
34
  // Per-node override: persist under the node-qualified key so the boot
35
35
  // restorer (`setNodeConfigReader`) replays it on restart.
36
- config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, input.addonId);
36
+ await config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, input.addonId);
37
37
  }
38
38
  else {
39
39
  // Cluster-global default: persist to the SAME key the boot restorer reads
40
40
  // (addon-registry `setConfigReader` → `capabilities.singleton.<cap>`).
41
- config.set(`capabilities.singleton.${input.capability}`, input.addonId);
41
+ await config.set(`capabilities.singleton.${input.capability}`, input.addonId);
42
42
  }
43
43
  }),
44
44
  clearSingletonNodeOverride: trpc_middleware_js_1.adminProcedure
45
45
  .input(zod_1.z.object({ capability: zod_1.z.string(), nodeId: zod_1.z.string() }))
46
46
  .output(zod_1.z.void())
47
- .mutation(({ input }) => {
47
+ .mutation(async ({ input }) => {
48
48
  if (!registry)
49
49
  throw new Error('Capability registry unavailable');
50
50
  registry.clearSingletonNodeOverride(input.capability, input.nodeId);
51
51
  // Persist the cleared state as null so the boot restorer doesn't re-apply
52
52
  // a stale override on restart.
53
- config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, null);
53
+ await config.set(`capabilities.singletonNode.${input.capability}.${input.nodeId}`, null);
54
54
  }),
55
55
  /**
56
56
  * Set the enabled set of providers for a collection capability in
@@ -12,6 +12,6 @@ function collectionPreferenceKey(capability) {
12
12
  * enable/disable mutation so the stored value always mirrors the live
13
13
  * registry state.
14
14
  */
15
- function persistCollectionDisabled(configService, capability, disabled) {
16
- configService.set(collectionPreferenceKey(capability), JSON.stringify({ disabled: [...disabled] }));
15
+ async function persistCollectionDisabled(configService, capability, disabled) {
16
+ await configService.set(collectionPreferenceKey(capability), JSON.stringify({ disabled: [...disabled] }));
17
17
  }
@@ -3247,6 +3247,15 @@ function createCapRouter_deviceManager(getProvider, createRemoteProxy) {
3247
3247
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3248
3248
  return p.setWrapperActive(methodInput);
3249
3249
  }),
3250
+ getWrapperActivation: trpc_middleware_js_1.protectedProcedure
3251
+ .input(types_45.deviceManagerCapability.methods.getWrapperActivation.input.loose())
3252
+ .output(types_45.deviceManagerCapability.methods.getWrapperActivation.output)
3253
+ .query(async ({ input, ctx }) => {
3254
+ const { nodeId, ...methodInput } = input;
3255
+ const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
3256
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
3257
+ return p.getWrapperActivation(methodInput);
3258
+ }),
3250
3259
  listWrappersForCap: trpc_middleware_js_1.protectedProcedure
3251
3260
  .input(types_45.deviceManagerCapability.methods.listWrappersForCap.input.loose())
3252
3261
  .output(types_45.deviceManagerCapability.methods.listWrappersForCap.output)
@@ -48,6 +48,7 @@ const types_1 = require("@camstack/types");
48
48
  const client_1 = require("@trpc/client");
49
49
  const addon_call_gateway_js_1 = require("./addon-call-gateway.js");
50
50
  const addon_row_manifest_1 = require("./addon-row-manifest");
51
+ const settings_door_wiring_js_1 = require("./settings-door-wiring.js");
51
52
  const addon_settings_provider_js_1 = require("./addon-settings-provider.js");
52
53
  const device_meta_mirror_js_1 = require("./device-meta-mirror.js");
53
54
  const integration_visibility_js_1 = require("./integration-visibility.js");
@@ -409,12 +410,18 @@ class AddonRegistryService {
409
410
  });
410
411
  // Create capability registry with config reader for singleton preferences
411
412
  this.capabilityRegistry = new system_1.CapabilityRegistry(this.loggingService.createLogger('CapabilityRegistry'), this.eventBusService);
413
+ // The preference readers answer "no preference" while the persisted
414
+ // preferences cannot be read yet — before the settings door exists, or
415
+ // while its one boot read is in flight (D675: `get` then THROWS rather
416
+ // than answering "unset"). The default wins meanwhile, and
417
+ // `wireSettingsDoor` re-applies the persisted choice
418
+ // the moment the read settles.
412
419
  this.capabilityRegistry.setConfigReader((capability) => {
413
420
  try {
414
421
  return this.configService.get(`capabilities.singleton.${capability}`) ?? undefined;
415
422
  }
416
423
  catch (err) {
417
- this.logger.debug('settings-store not wired yet during early boot', {
424
+ this.logger.debug('capability preference not readable yet — default applies until replay', {
418
425
  meta: { capability, error: (0, types_1.errMsg)(err) },
419
426
  });
420
427
  return undefined;
@@ -426,8 +433,9 @@ class AddonRegistryService {
426
433
  undefined);
427
434
  }
428
435
  catch (err) {
429
- this.logger.debug('settings-store not wired yet during early boot', {
430
- meta: { capability, nodeId, error: (0, types_1.errMsg)(err) },
436
+ this.logger.debug('capability preference not readable yet — default applies', {
437
+ tags: { nodeId },
438
+ meta: { capability, error: (0, types_1.errMsg)(err) },
431
439
  });
432
440
  return undefined;
433
441
  }
@@ -452,7 +460,7 @@ class AddonRegistryService {
452
460
  return undefined;
453
461
  }
454
462
  catch (err) {
455
- this.logger.debug('settings-store not wired yet or malformed collection preference', {
463
+ this.logger.debug('collection preference not readable yet, or malformed', {
456
464
  meta: { capability, error: (0, types_1.errMsg)(err) },
457
465
  });
458
466
  return undefined;
@@ -2498,7 +2506,11 @@ class AddonRegistryService {
2498
2506
  const store = this.capabilityRegistry.getProviderByAddon('settings-store', addonId);
2499
2507
  if (!store)
2500
2508
  return;
2501
- this.configService.setSettingsDoor(store);
2509
+ void (0, settings_door_wiring_js_1.wireSettingsDoor)({
2510
+ configService: this.configService,
2511
+ registry: this.capabilityRegistry,
2512
+ logger: this.logger,
2513
+ }, store);
2502
2514
  this.integrationRegistry = new system_1.IntegrationRegistry(store);
2503
2515
  void this.integrationRegistry.initialize().then(() => {
2504
2516
  this.logger.info('IntegrationRegistry initialized', { meta: { phase: 'v2' } });
@@ -2770,42 +2782,6 @@ class AddonRegistryService {
2770
2782
  }
2771
2783
  return null;
2772
2784
  }
2773
- /**
2774
- * Build the addonConfig for a given addon — strictly per-addon now.
2775
- *
2776
- * The legacy cross-addon merge (hardcoded `ADDON_SYSTEM_SETTINGS` map
2777
- * copying `system_settings.<section>.*` into the bootstrap config) has
2778
- * been removed. Addons that need to read shared system settings
2779
- * sections (`ffmpeg`, `logging`, `recording`, …) do so at runtime via
2780
- * `ctx.settings.getGlobal({ section: '<name>' })`, which delegates to
2781
- * `ConfigManager.getSection()` and covers the full SQL → YAML →
2782
- * RUNTIME_DEFAULTS fallback chain.
2783
- *
2784
- * This keeps the kernel agnostic: every addon declares its own needs
2785
- * in its own `initialize(ctx)` code, with zero kernel-side addon id
2786
- * special-casing. The only remaining exception is `sqlite-settings`,
2787
- * which receives `_runtimeDefaults` for first-boot seeding (tracked
2788
- * separately under the kernel-cleanup roadmap — Point 3d).
2789
- */
2790
- buildAddonConfig(addonId) {
2791
- // Start with per-addon SQL settings (may be empty for most addons)
2792
- let addonSpecific = {};
2793
- try {
2794
- addonSpecific = this.configService.getAddonConfig(addonId);
2795
- }
2796
- catch (err) {
2797
- this.logger.debug('ConfigManager not ready for addon', {
2798
- tags: { addonId },
2799
- meta: { error: (0, types_1.errMsg)(err) },
2800
- });
2801
- }
2802
- // No addon id special-casing here anymore. Bootstrap-level
2803
- // concerns (e.g. first-boot seeding from RUNTIME_DEFAULTS) are
2804
- // owned by the addon itself — sqlite-settings imports the
2805
- // constant directly from `@camstack/types` instead of relying on
2806
- // a kernel-injected bootstrap field.
2807
- return addonSpecific;
2808
- }
2809
2785
  async createAddonContext(addon) {
2810
2786
  const addonId = addon.manifest.id;
2811
2787
  const brokerNodeId = this.broker.nodeID;
@@ -2816,7 +2792,6 @@ class AddonRegistryService {
2816
2792
  const logger = this.loggingService
2817
2793
  .createLogger()
2818
2794
  .withTags({ addonId, nodeId: brokerNodeId, agentId });
2819
- const bootstrapConfig = this.buildAddonConfig(addonId);
2820
2795
  // Per-addon private data directory — resolved from active storage
2821
2796
  // provider if available, otherwise falls back to a deterministic
2822
2797
  // hardcoded path. Addons that need the full storage provider now
@@ -2920,7 +2895,12 @@ class AddonRegistryService {
2920
2895
  id: `addon:${addonId}`,
2921
2896
  logger,
2922
2897
  eventBus: this.eventBusService,
2923
- addonConfig: bootstrapConfig,
2898
+ // No store data (D675). An in-process addon reads its persisted settings
2899
+ // through `ctx.settings`; `addonConfig` used to be filled from the sync
2900
+ // `getAddonConfig`, which has no backend on an isolated engine and handed
2901
+ // every addon `{}`. Nothing in production reads it — the same empty
2902
+ // record a forked runner's context carries (`addon-context-factory`).
2903
+ addonConfig: {},
2924
2904
  dataDir,
2925
2905
  nodeDataDir,
2926
2906
  dataPlane: dataPlaneFacility.dataPlane,
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.wireSettingsDoor = wireSettingsDoor;
4
+ /**
5
+ * Wiring the async `settings-store` door into `ConfigService` — and replaying
6
+ * the operator's persisted capability preferences once its boot read settles
7
+ * (D675).
8
+ *
9
+ * One function, so the replay cannot be dropped from the wiring without a
10
+ * spec noticing: `setSettingsDoor` starts the ONE boot read of the preloaded
11
+ * `system-settings` keys, and that read's own promise drives a single
12
+ * `applyPersistedPreferences` pass over the providers that registered before
13
+ * it. No timer, no retry (D3). A failed read is logged at error and leaves
14
+ * every preference UNKNOWN until the next boot.
15
+ */
16
+ const system_1 = require("@camstack/system");
17
+ const types_1 = require("@camstack/types");
18
+ /**
19
+ * Wire `door` and schedule the one preference replay. Returns the replay's
20
+ * promise (it never rejects) so a caller — or a spec — can await it.
21
+ */
22
+ function wireSettingsDoor(deps, door) {
23
+ deps.configService.setSettingsDoor(door);
24
+ const preload = deps.configService.whenSystemSettingsPreloaded();
25
+ if (preload === null)
26
+ return Promise.resolve();
27
+ return preload
28
+ .then((result) => {
29
+ if (result.kind === 'failed') {
30
+ deps.logger.error('capability preferences could not be read at boot — every persisted preference is UNKNOWN until the next boot, and the defaults apply', { meta: { error: result.error } });
31
+ return;
32
+ }
33
+ const replay = deps.registry.applyPersistedPreferences((capability) => (0, system_1.isInfraCapability)(capability));
34
+ deps.logger.info('capability preferences loaded from the settings store', {
35
+ meta: {
36
+ keyCount: result.keyCount,
37
+ singletonsApplied: replay.singletons.map((s) => `${s.capability}=${s.addonId}`),
38
+ collectionProvidersDisabled: replay.disabled.map((d) => `${d.capability}/${d.addonId}`),
39
+ },
40
+ });
41
+ })
42
+ .catch((err) => {
43
+ deps.logger.error('capability preference replay failed', { meta: { error: (0, types_1.errMsg)(err) } });
44
+ });
45
+ }
@@ -205,9 +205,14 @@ class MoleculerService {
205
205
  async onModuleInit() {
206
206
  const logger = this.logging.createLogger('moleculer');
207
207
  const hubDeps = {
208
- getAddonConfig: (addonId) => {
209
- return this.config.getAddonConfig(addonId);
210
- },
208
+ // Through the settings door, three-way (D675): the stored record, `{}`
209
+ // when the store answered with none, a THROW when it could not be read.
210
+ // The sync `getAddonConfig` has no backend on an isolated engine and
211
+ // answered `{}` for every addon.
212
+ getAddonConfig: (addonId) => this.config.readAddonGlobalStrict(addonId),
213
+ // `ConfigManager.get` REFUSES (throws) a key the isolated engine may hold
214
+ // but cannot read synchronously (D675) — the caller gets that error over
215
+ // Moleculer rather than a default dressed as the stored value.
211
216
  getSettings: (scope, key) => {
212
217
  return this.config.get(key ? `${scope}.${key}` : scope);
213
218
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.407",
3
+ "version": "1.2.409",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -37,18 +37,18 @@
37
37
  ]
38
38
  },
39
39
  "dependencies": {
40
- "@camstack/addon-admin-ui": "1.2.284",
41
- "@camstack/addon-agent-ui": "1.2.154",
40
+ "@camstack/addon-admin-ui": "1.2.286",
41
+ "@camstack/addon-agent-ui": "1.2.155",
42
42
  "@camstack/addon-auth": "1.2.123",
43
43
  "@camstack/addon-decoder-nodeav": "1.2.115",
44
44
  "@camstack/addon-notifiers": "1.2.121",
45
45
  "@camstack/addon-pipeline": "1.2.305",
46
- "@camstack/addon-pipeline-orchestrator": "1.2.261",
47
- "@camstack/addon-post-analysis": "1.2.294",
46
+ "@camstack/addon-pipeline-orchestrator": "1.2.262",
47
+ "@camstack/addon-post-analysis": "1.2.296",
48
48
  "@camstack/sdk": "1.2.118",
49
- "@camstack/system": "1.2.316",
50
- "@camstack/types": "1.2.270",
51
- "@camstack/ui-library": "1.2.212",
49
+ "@camstack/system": "1.2.318",
50
+ "@camstack/types": "1.2.272",
51
+ "@camstack/ui-library": "1.2.213",
52
52
  "@fastify/compress": "^9.0.0",
53
53
  "@fastify/cookie": "^11.0.2",
54
54
  "@fastify/cors": "^11.2.0",