@camstack/system 1.2.110 → 1.2.111

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.
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-GejnH--L.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-MA_BZGBz.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { I as createUdsLoggerWithControl, L as LocalChildClient, Pt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-5ei2xVUh.mjs";
1
+ import { I as createUdsLoggerWithControl, L as LocalChildClient, Pt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-Cb1ANt3i.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as path$1 from "node:path";
package/dist/index.js CHANGED
@@ -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-GejnH--L.js");
28
+ const require_manifest_python_deps = require("./manifest-python-deps-MA_BZGBz.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");
@@ -5367,6 +5367,7 @@ var CapabilityRegistry = class CapabilityRegistry {
5367
5367
  const state = {
5368
5368
  definition,
5369
5369
  providers: /* @__PURE__ */ new Map(),
5370
+ providerOwners: /* @__PURE__ */ new Map(),
5370
5371
  activeAddonId: null,
5371
5372
  router: null,
5372
5373
  disabledProviders: /* @__PURE__ */ new Set()
@@ -5424,8 +5425,15 @@ var CapabilityRegistry = class CapabilityRegistry {
5424
5425
  * Double-register of the same pair is always a programmer error —
5425
5426
  * two legitimately different addons implementing the same cap keep
5426
5427
  * using different `addonId`s and the user picks one via `configReader`.
5427
- */
5428
- registerProvider(capabilityName, addonId, provider) {
5428
+ *
5429
+ * `owner` (optional) stamps the registration with the incarnation that made
5430
+ * it — see {@link ProviderOwnerToken}. Once stamped, only an unregister
5431
+ * presenting the SAME token can delete it. Registration paths whose key is
5432
+ * reused across process generations (the UDS child manifest, the D3
5433
+ * `registerNode` handshake) MUST pass one; ownerless registrations keep the
5434
+ * historical unconditional behaviour.
5435
+ */
5436
+ registerProvider(capabilityName, addonId, provider, owner) {
5429
5437
  let state = this.capabilities.get(capabilityName);
5430
5438
  if (!state) {
5431
5439
  const colonIdx = capabilityName.indexOf(":");
@@ -5457,6 +5465,7 @@ var CapabilityRegistry = class CapabilityRegistry {
5457
5465
  if (missing.length > 0) throw new Error(`CapabilityRegistry: provider "${addonId}" for capability "${capabilityName}" declares exposesDeviceSettings: true but is missing DeviceSettingsContribution method(s): ${missing.join(", ")}. Implement them on the provider (or drop the flag from the cap def).`);
5458
5466
  }
5459
5467
  state.providers.set(addonId, provider);
5468
+ if (owner !== void 0) state.providerOwners.set(addonId, owner);
5460
5469
  if (state.definition.mode === "singleton") {
5461
5470
  const userChoice = this.configReader?.(capabilityName);
5462
5471
  const preferred = state.definition.preferredProvider;
@@ -5487,7 +5496,8 @@ var CapabilityRegistry = class CapabilityRegistry {
5487
5496
  tags: { addonId },
5488
5497
  meta: {
5489
5498
  capability: capabilityName,
5490
- mode: state.definition.mode
5499
+ mode: state.definition.mode,
5500
+ ...owner === void 0 ? {} : { owner }
5491
5501
  }
5492
5502
  });
5493
5503
  this.emitEvent("capability:provider-registered", {
@@ -5534,11 +5544,70 @@ var CapabilityRegistry = class CapabilityRegistry {
5534
5544
  const declarers = this.manifestDeclarers.get(capabilityName);
5535
5545
  return declarers ? [...declarers] : [];
5536
5546
  }
5537
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
5538
- unregisterProvider(capabilityName, addonId) {
5547
+ /**
5548
+ * The owner token a registration declared, or `null` when the pair is not
5549
+ * registered or was registered without one.
5550
+ */
5551
+ getProviderOwner(capabilityName, addonId) {
5552
+ return this.capabilities.get(capabilityName)?.providerOwners.get(addonId) ?? null;
5553
+ }
5554
+ /**
5555
+ * Hand an EXISTING registration to a new owner, without touching the provider
5556
+ * instance, the active-singleton selection or any event.
5557
+ *
5558
+ * The takeover case: a replacement runner handshakes with an UNCHANGED cap
5559
+ * set, so the manifest diff correctly leaves the registration alone — but the
5560
+ * registration is still stamped with the dead generation's token, and nothing
5561
+ * the LIVE generation can present would ever match it. Re-stamping keeps the
5562
+ * capability cleanable when the successor eventually dies.
5563
+ *
5564
+ * Returns `false` when the pair is not registered (nothing to hand over) —
5565
+ * the caller decides whether that is a register-instead or a diagnosis.
5566
+ */
5567
+ reassignProviderOwner(capabilityName, addonId, owner) {
5568
+ const state = this.capabilities.get(capabilityName);
5569
+ if (!state || !state.providers.has(addonId)) return false;
5570
+ const previous = state.providerOwners.get(addonId);
5571
+ if (previous === owner) return true;
5572
+ state.providerOwners.set(addonId, owner);
5573
+ this.logger.info("Capability registration ownership reassigned", {
5574
+ tags: { addonId },
5575
+ meta: {
5576
+ capability: capabilityName,
5577
+ owner,
5578
+ previousOwner: previous ?? null
5579
+ }
5580
+ });
5581
+ return true;
5582
+ }
5583
+ /**
5584
+ * Unregister a provider. For singleton: clears active. For collection:
5585
+ * removes from list.
5586
+ *
5587
+ * `owner` is the caller's PROOF that it owns the registration it is deleting.
5588
+ * When the registration declared an owner (see {@link ProviderOwnerToken})
5589
+ * and the caller presents a different one — or none at all — this is a
5590
+ * logged no-op: the caller belongs to a generation that no longer holds the
5591
+ * key. This is what stops a dead runner's teardown from erasing its
5592
+ * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
5593
+ */
5594
+ unregisterProvider(capabilityName, addonId, owner) {
5539
5595
  const state = this.capabilities.get(capabilityName);
5540
5596
  if (!state) return;
5597
+ const registeredOwner = state.providerOwners.get(addonId);
5598
+ if (registeredOwner !== void 0 && registeredOwner !== owner) {
5599
+ this.logger.info("Unregister ignored — registration is owned by another incarnation", {
5600
+ tags: { addonId },
5601
+ meta: {
5602
+ capability: capabilityName,
5603
+ registeredOwner,
5604
+ presentedOwner: owner ?? null
5605
+ }
5606
+ });
5607
+ return;
5608
+ }
5541
5609
  state.providers.delete(addonId);
5610
+ state.providerOwners.delete(addonId);
5542
5611
  if (state.definition.mode === "singleton" && state.activeAddonId === addonId) {
5543
5612
  const userChoice = this.configReader?.(capabilityName);
5544
5613
  const preferred = state.definition.preferredProvider;
@@ -94555,6 +94624,11 @@ var JobJournal = class {
94555
94624
  function safeSegment(name) {
94556
94625
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
94557
94626
  }
94627
+ /** An exact semver — the only shape the staged package.json can be compared
94628
+ * against literally. Anything else ('latest', 'beta', a range) is a dist-tag
94629
+ * the FETCHER resolves; comparing the resolved version to the literal tag
94630
+ * failed every tag-based update (30/30 on 2026-08-20). */
94631
+ var EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
94558
94632
  var StagingArea = class {
94559
94633
  stagingDir;
94560
94634
  deps;
@@ -94572,8 +94646,12 @@ var StagingArea = class {
94572
94646
  if (input.signal.aborted) throw new Error("staging aborted after fetch");
94573
94647
  await this.deps.extract(tgz, destDir);
94574
94648
  const raw = JSON.parse(node_fs.readFileSync(node_path.join(destDir, "package.json"), "utf-8"));
94575
- if (raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${String(raw.version)}`);
94576
- return { stagedPath: destDir };
94649
+ if (typeof raw.version !== "string" || raw.version.length === 0) throw new Error(`staging validation failed: ${input.name} staged a package.json with no version`);
94650
+ if (EXACT_SEMVER.test(input.version) && raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${raw.version}`);
94651
+ return {
94652
+ stagedPath: destDir,
94653
+ resolvedVersion: raw.version
94654
+ };
94577
94655
  }
94578
94656
  cleanup(jobId) {
94579
94657
  node_fs.rmSync(node_path.join(this.stagingDir, jobId), {
package/dist/index.mjs CHANGED
@@ -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, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, 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, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, 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-5ei2xVUh.mjs";
27
+ import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, 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, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, 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-Cb1ANt3i.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";
@@ -5360,6 +5360,7 @@ var CapabilityRegistry = class CapabilityRegistry {
5360
5360
  const state = {
5361
5361
  definition,
5362
5362
  providers: /* @__PURE__ */ new Map(),
5363
+ providerOwners: /* @__PURE__ */ new Map(),
5363
5364
  activeAddonId: null,
5364
5365
  router: null,
5365
5366
  disabledProviders: /* @__PURE__ */ new Set()
@@ -5417,8 +5418,15 @@ var CapabilityRegistry = class CapabilityRegistry {
5417
5418
  * Double-register of the same pair is always a programmer error —
5418
5419
  * two legitimately different addons implementing the same cap keep
5419
5420
  * using different `addonId`s and the user picks one via `configReader`.
5420
- */
5421
- registerProvider(capabilityName, addonId, provider) {
5421
+ *
5422
+ * `owner` (optional) stamps the registration with the incarnation that made
5423
+ * it — see {@link ProviderOwnerToken}. Once stamped, only an unregister
5424
+ * presenting the SAME token can delete it. Registration paths whose key is
5425
+ * reused across process generations (the UDS child manifest, the D3
5426
+ * `registerNode` handshake) MUST pass one; ownerless registrations keep the
5427
+ * historical unconditional behaviour.
5428
+ */
5429
+ registerProvider(capabilityName, addonId, provider, owner) {
5422
5430
  let state = this.capabilities.get(capabilityName);
5423
5431
  if (!state) {
5424
5432
  const colonIdx = capabilityName.indexOf(":");
@@ -5450,6 +5458,7 @@ var CapabilityRegistry = class CapabilityRegistry {
5450
5458
  if (missing.length > 0) throw new Error(`CapabilityRegistry: provider "${addonId}" for capability "${capabilityName}" declares exposesDeviceSettings: true but is missing DeviceSettingsContribution method(s): ${missing.join(", ")}. Implement them on the provider (or drop the flag from the cap def).`);
5451
5459
  }
5452
5460
  state.providers.set(addonId, provider);
5461
+ if (owner !== void 0) state.providerOwners.set(addonId, owner);
5453
5462
  if (state.definition.mode === "singleton") {
5454
5463
  const userChoice = this.configReader?.(capabilityName);
5455
5464
  const preferred = state.definition.preferredProvider;
@@ -5480,7 +5489,8 @@ var CapabilityRegistry = class CapabilityRegistry {
5480
5489
  tags: { addonId },
5481
5490
  meta: {
5482
5491
  capability: capabilityName,
5483
- mode: state.definition.mode
5492
+ mode: state.definition.mode,
5493
+ ...owner === void 0 ? {} : { owner }
5484
5494
  }
5485
5495
  });
5486
5496
  this.emitEvent("capability:provider-registered", {
@@ -5527,11 +5537,70 @@ var CapabilityRegistry = class CapabilityRegistry {
5527
5537
  const declarers = this.manifestDeclarers.get(capabilityName);
5528
5538
  return declarers ? [...declarers] : [];
5529
5539
  }
5530
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
5531
- unregisterProvider(capabilityName, addonId) {
5540
+ /**
5541
+ * The owner token a registration declared, or `null` when the pair is not
5542
+ * registered or was registered without one.
5543
+ */
5544
+ getProviderOwner(capabilityName, addonId) {
5545
+ return this.capabilities.get(capabilityName)?.providerOwners.get(addonId) ?? null;
5546
+ }
5547
+ /**
5548
+ * Hand an EXISTING registration to a new owner, without touching the provider
5549
+ * instance, the active-singleton selection or any event.
5550
+ *
5551
+ * The takeover case: a replacement runner handshakes with an UNCHANGED cap
5552
+ * set, so the manifest diff correctly leaves the registration alone — but the
5553
+ * registration is still stamped with the dead generation's token, and nothing
5554
+ * the LIVE generation can present would ever match it. Re-stamping keeps the
5555
+ * capability cleanable when the successor eventually dies.
5556
+ *
5557
+ * Returns `false` when the pair is not registered (nothing to hand over) —
5558
+ * the caller decides whether that is a register-instead or a diagnosis.
5559
+ */
5560
+ reassignProviderOwner(capabilityName, addonId, owner) {
5561
+ const state = this.capabilities.get(capabilityName);
5562
+ if (!state || !state.providers.has(addonId)) return false;
5563
+ const previous = state.providerOwners.get(addonId);
5564
+ if (previous === owner) return true;
5565
+ state.providerOwners.set(addonId, owner);
5566
+ this.logger.info("Capability registration ownership reassigned", {
5567
+ tags: { addonId },
5568
+ meta: {
5569
+ capability: capabilityName,
5570
+ owner,
5571
+ previousOwner: previous ?? null
5572
+ }
5573
+ });
5574
+ return true;
5575
+ }
5576
+ /**
5577
+ * Unregister a provider. For singleton: clears active. For collection:
5578
+ * removes from list.
5579
+ *
5580
+ * `owner` is the caller's PROOF that it owns the registration it is deleting.
5581
+ * When the registration declared an owner (see {@link ProviderOwnerToken})
5582
+ * and the caller presents a different one — or none at all — this is a
5583
+ * logged no-op: the caller belongs to a generation that no longer holds the
5584
+ * key. This is what stops a dead runner's teardown from erasing its
5585
+ * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
5586
+ */
5587
+ unregisterProvider(capabilityName, addonId, owner) {
5532
5588
  const state = this.capabilities.get(capabilityName);
5533
5589
  if (!state) return;
5590
+ const registeredOwner = state.providerOwners.get(addonId);
5591
+ if (registeredOwner !== void 0 && registeredOwner !== owner) {
5592
+ this.logger.info("Unregister ignored — registration is owned by another incarnation", {
5593
+ tags: { addonId },
5594
+ meta: {
5595
+ capability: capabilityName,
5596
+ registeredOwner,
5597
+ presentedOwner: owner ?? null
5598
+ }
5599
+ });
5600
+ return;
5601
+ }
5534
5602
  state.providers.delete(addonId);
5603
+ state.providerOwners.delete(addonId);
5535
5604
  if (state.definition.mode === "singleton" && state.activeAddonId === addonId) {
5536
5605
  const userChoice = this.configReader?.(capabilityName);
5537
5606
  const preferred = state.definition.preferredProvider;
@@ -94548,6 +94617,11 @@ var JobJournal = class {
94548
94617
  function safeSegment(name) {
94549
94618
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
94550
94619
  }
94620
+ /** An exact semver — the only shape the staged package.json can be compared
94621
+ * against literally. Anything else ('latest', 'beta', a range) is a dist-tag
94622
+ * the FETCHER resolves; comparing the resolved version to the literal tag
94623
+ * failed every tag-based update (30/30 on 2026-08-20). */
94624
+ var EXACT_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
94551
94625
  var StagingArea = class {
94552
94626
  stagingDir;
94553
94627
  deps;
@@ -94565,8 +94639,12 @@ var StagingArea = class {
94565
94639
  if (input.signal.aborted) throw new Error("staging aborted after fetch");
94566
94640
  await this.deps.extract(tgz, destDir);
94567
94641
  const raw = JSON.parse(fs$17.readFileSync(path$39.join(destDir, "package.json"), "utf-8"));
94568
- if (raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${String(raw.version)}`);
94569
- return { stagedPath: destDir };
94642
+ if (typeof raw.version !== "string" || raw.version.length === 0) throw new Error(`staging validation failed: ${input.name} staged a package.json with no version`);
94643
+ if (EXACT_SEMVER.test(input.version) && raw.version !== input.version) throw new Error(`staging validation failed: ${input.name} expected version ${input.version}, got ${raw.version}`);
94644
+ return {
94645
+ stagedPath: destDir,
94646
+ resolvedVersion: raw.version
94647
+ };
94570
94648
  }
94571
94649
  cleanup(jobId) {
94572
94650
  fs$17.rmSync(path$39.join(this.stagingDir, jobId), {
@@ -19,6 +19,18 @@ export type ConfigReader = (capability: string) => string | undefined;
19
19
  * {@link ConfigReader} does for singletons.
20
20
  */
21
21
  export type CollectionConfigReader = (capability: string) => readonly string[] | undefined;
22
+ /**
23
+ * Proof that an unregister owns the registration it is about to delete.
24
+ *
25
+ * A `(capability, addonId)` key is NOT unique over time: a runner replaced by
26
+ * an update re-registers under the exact same key (`resolveRunnerId` is
27
+ * deterministic), so the dead generation's teardown and the live generation's
28
+ * registration are indistinguishable by key alone. The owner token names the
29
+ * INCARNATION that made a registration — e.g. `hub/recorder#7` — and
30
+ * {@link CapabilityRegistry.unregisterProvider} refuses any unregister that
31
+ * cannot present it. See docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md.
32
+ */
33
+ export type ProviderOwnerToken = string;
22
34
  export declare class CapabilityRegistry {
23
35
  private readonly capabilities;
24
36
  private readonly logger;
@@ -148,8 +160,15 @@ export declare class CapabilityRegistry {
148
160
  * Double-register of the same pair is always a programmer error —
149
161
  * two legitimately different addons implementing the same cap keep
150
162
  * using different `addonId`s and the user picks one via `configReader`.
163
+ *
164
+ * `owner` (optional) stamps the registration with the incarnation that made
165
+ * it — see {@link ProviderOwnerToken}. Once stamped, only an unregister
166
+ * presenting the SAME token can delete it. Registration paths whose key is
167
+ * reused across process generations (the UDS child manifest, the D3
168
+ * `registerNode` handshake) MUST pass one; ownerless registrations keep the
169
+ * historical unconditional behaviour.
151
170
  */
152
- registerProvider(capabilityName: string, addonId: string, provider: unknown): void;
171
+ registerProvider(capabilityName: string, addonId: string, provider: unknown, owner?: ProviderOwnerToken): void;
153
172
  /**
154
173
  * Whether a provider for the exact `(capabilityName, addonId)` pair is
155
174
  * currently registered. Lets a re-registration path (e.g. the agent's
@@ -173,8 +192,37 @@ export declare class CapabilityRegistry {
173
192
  * it is the diagnosis).
174
193
  */
175
194
  getManifestDeclarers(capabilityName: string): readonly string[];
176
- /** Unregister a provider. For singleton: clears active. For collection: removes from list. */
177
- unregisterProvider(capabilityName: string, addonId: string): void;
195
+ /**
196
+ * The owner token a registration declared, or `null` when the pair is not
197
+ * registered or was registered without one.
198
+ */
199
+ getProviderOwner(capabilityName: string, addonId: string): ProviderOwnerToken | null;
200
+ /**
201
+ * Hand an EXISTING registration to a new owner, without touching the provider
202
+ * instance, the active-singleton selection or any event.
203
+ *
204
+ * The takeover case: a replacement runner handshakes with an UNCHANGED cap
205
+ * set, so the manifest diff correctly leaves the registration alone — but the
206
+ * registration is still stamped with the dead generation's token, and nothing
207
+ * the LIVE generation can present would ever match it. Re-stamping keeps the
208
+ * capability cleanable when the successor eventually dies.
209
+ *
210
+ * Returns `false` when the pair is not registered (nothing to hand over) —
211
+ * the caller decides whether that is a register-instead or a diagnosis.
212
+ */
213
+ reassignProviderOwner(capabilityName: string, addonId: string, owner: ProviderOwnerToken): boolean;
214
+ /**
215
+ * Unregister a provider. For singleton: clears active. For collection:
216
+ * removes from list.
217
+ *
218
+ * `owner` is the caller's PROOF that it owns the registration it is deleting.
219
+ * When the registration declared an owner (see {@link ProviderOwnerToken})
220
+ * and the caller presents a different one — or none at all — this is a
221
+ * logged no-op: the caller belongs to a generation that no longer holds the
222
+ * key. This is what stops a dead runner's teardown from erasing its
223
+ * successor's live registration ([D188](../../../../docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md)).
224
+ */
225
+ unregisterProvider(capabilityName: string, addonId: string, owner?: ProviderOwnerToken): void;
178
226
  /** Enable a previously disabled collection provider. */
179
227
  enableCollectionProvider(capability: string, addonId: string): void;
180
228
  /** Disable a collection provider (keeps it registered but excluded from active list). */
@@ -17,7 +17,7 @@ export { buildCapRouters, builderMountedCapNames } from './cap-router-builder.js
17
17
  export type { CapRouterPrimitives, CapRouterServices, CapProviderGetter, RemoteProxyFactory, ProcedureBuilder, ProcedureResolverArgs, } from './cap-router-builder.js';
18
18
  export { describeProviderKindDrift } from './provider-kind-drift.js';
19
19
  export type { ProviderKindHint } from './provider-kind-drift.js';
20
- export type { CapabilityRouter, CapabilityRouterFactory, ConfigReader, } from './capability-registry.js';
20
+ export type { CapabilityRouter, CapabilityRouterFactory, ConfigReader, ProviderOwnerToken, } from './capability-registry.js';
21
21
  export { CustomActionRegistry, type CustomActionEntry } from './custom-action-registry.js';
22
22
  export { INFRA_CAPABILITIES, isInfraCapability } from './infra-capabilities.js';
23
23
  export type { InfraCapability } from './infra-capabilities.js';
@@ -16,6 +16,7 @@ export declare class StagingArea {
16
16
  constructor(stagingDir: string, deps: StagingAreaDeps);
17
17
  fetchAndStage(input: FetchAndStageInput): Promise<{
18
18
  stagedPath: string;
19
+ resolvedVersion: string;
19
20
  }>;
20
21
  cleanup(jobId: string): void;
21
22
  /**
@@ -222,6 +222,18 @@ export type ParentToChildRequest = CapCallMessage | ParentEventMessage | Readine
222
222
  export interface RegisteredChild {
223
223
  readonly childId: string;
224
224
  readonly caps: readonly ChildCapDescriptor[];
225
+ /**
226
+ * Monotonic id of the CONNECTION this registration arrived on, unique within
227
+ * the parent process.
228
+ *
229
+ * `childId` is deterministic (`resolveRunnerId`), so a runner replaced by an
230
+ * update reconnects under the exact same id — the two generations are
231
+ * indistinguishable without this. Consumers stamp it onto whatever they
232
+ * derive from the child (capability registrations, subtree manifests) so a
233
+ * teardown arriving from the DEAD generation can be told apart from the live
234
+ * one. See docs/decisions/adr-0188-an-unregister-carries-proof-of-ownership.md.
235
+ */
236
+ readonly incarnation: number;
225
237
  }
226
238
  /** Arguments to route a cap method call (the `cap-call` message minus its discriminant). */
227
239
  export type CapCallInput = Omit<CapCallMessage, 'kind'>;
@@ -139,6 +139,8 @@ export declare class LocalChildRegistry {
139
139
  private readonly children;
140
140
  private registeredHandler;
141
141
  private goneHandler;
142
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
143
+ private nextIncarnation;
142
144
  private eventHandler;
143
145
  private logHandler;
144
146
  private readinessHandler;
@@ -291,8 +293,13 @@ export declare class LocalChildRegistry {
291
293
  listChildren(): readonly RegisteredChild[];
292
294
  /** Register the (single) child-registered handler. Only one handler is active at a time. */
293
295
  onChildRegistered(handler: (child: RegisteredChild) => void): void;
294
- /** Register the (single) child-gone handler. Only one handler is active at a time. */
295
- onChildGone(handler: (childId: string) => void): void;
296
+ /**
297
+ * Register the (single) child-gone handler. Only one handler is active at a
298
+ * time. The handler receives the {@link RegisteredChild.incarnation} of the
299
+ * connection that closed — carry it into whatever teardown it triggers, so a
300
+ * dead generation cannot tear down its successor's work.
301
+ */
302
+ onChildGone(handler: (childId: string, incarnation: number) => void): void;
296
303
  /**
297
304
  * Register the (single) child-event handler. Invoked when a child sends an
298
305
  * event via `LocalChildClient.emitEvent`. Only one handler is active at a
@@ -4832,6 +4832,8 @@ var LocalChildRegistry = class {
4832
4832
  children = /* @__PURE__ */ new Map();
4833
4833
  registeredHandler = () => {};
4834
4834
  goneHandler = () => {};
4835
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
4836
+ nextIncarnation = 1;
4835
4837
  eventHandler = null;
4836
4838
  logHandler = null;
4837
4839
  readinessHandler = null;
@@ -5106,14 +5108,20 @@ var LocalChildRegistry = class {
5106
5108
  listChildren() {
5107
5109
  return [...this.children.values()].map((e) => ({
5108
5110
  childId: e.childId,
5109
- caps: e.caps
5111
+ caps: e.caps,
5112
+ incarnation: e.incarnation
5110
5113
  }));
5111
5114
  }
5112
5115
  /** Register the (single) child-registered handler. Only one handler is active at a time. */
5113
5116
  onChildRegistered(handler) {
5114
5117
  this.registeredHandler = handler;
5115
5118
  }
5116
- /** Register the (single) child-gone handler. Only one handler is active at a time. */
5119
+ /**
5120
+ * Register the (single) child-gone handler. Only one handler is active at a
5121
+ * time. The handler receives the {@link RegisteredChild.incarnation} of the
5122
+ * connection that closed — carry it into whatever teardown it triggers, so a
5123
+ * dead generation cannot tear down its successor's work.
5124
+ */
5117
5125
  onChildGone(handler) {
5118
5126
  this.goneHandler = handler;
5119
5127
  }
@@ -5198,6 +5206,7 @@ var LocalChildRegistry = class {
5198
5206
  }
5199
5207
  onConnection(channel) {
5200
5208
  let childId = null;
5209
+ const incarnation = this.nextIncarnation++;
5201
5210
  channel.onEvent((body) => {
5202
5211
  const msg = body;
5203
5212
  if (msg.kind === "event") {
@@ -5226,16 +5235,24 @@ var LocalChildRegistry = class {
5226
5235
  if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
5227
5236
  childId = msg.childId;
5228
5237
  const eventPatterns = msg.eventPatterns ?? null;
5238
+ const superseded = this.children.get(msg.childId);
5239
+ if (superseded !== void 0 && superseded.channel !== channel) this.logger?.info("local child superseded by a new connection", {
5240
+ childId: msg.childId,
5241
+ supersededIncarnation: superseded.incarnation,
5242
+ incarnation
5243
+ });
5229
5244
  this.children.set(msg.childId, {
5230
5245
  childId: msg.childId,
5231
5246
  channel,
5232
5247
  caps: msg.caps,
5233
- eventPatterns
5248
+ eventPatterns,
5249
+ incarnation
5234
5250
  });
5235
5251
  this.logPatternSet(msg.childId, eventPatterns);
5236
5252
  this.registeredHandler({
5237
5253
  childId: msg.childId,
5238
- caps: msg.caps
5254
+ caps: msg.caps,
5255
+ incarnation
5239
5256
  });
5240
5257
  return { ok: true };
5241
5258
  }
@@ -5270,11 +5287,21 @@ var LocalChildRegistry = class {
5270
5287
  throw new Error(`unknown child request kind: ${msg.kind}`);
5271
5288
  });
5272
5289
  channel.onClose(() => {
5273
- if (childId !== null && this.children.delete(childId)) {
5274
- this.childEventStats.delete(childId);
5275
- this.loggedPatternSet.delete(childId);
5276
- this.goneHandler(childId);
5290
+ if (childId === null) return;
5291
+ const entry = this.children.get(childId);
5292
+ if (entry === void 0) return;
5293
+ if (entry.channel !== channel) {
5294
+ this.logger?.info("ignoring close of a superseded local child connection", {
5295
+ childId,
5296
+ incarnation,
5297
+ liveIncarnation: entry.incarnation
5298
+ });
5299
+ return;
5277
5300
  }
5301
+ this.children.delete(childId);
5302
+ this.childEventStats.delete(childId);
5303
+ this.loggedPatternSet.delete(childId);
5304
+ this.goneHandler(childId, incarnation);
5278
5305
  });
5279
5306
  }
5280
5307
  };
@@ -4836,6 +4836,8 @@ var LocalChildRegistry = class {
4836
4836
  children = /* @__PURE__ */ new Map();
4837
4837
  registeredHandler = () => {};
4838
4838
  goneHandler = () => {};
4839
+ /** Source of {@link RegisteredChild.incarnation}; one bump per accepted connection. */
4840
+ nextIncarnation = 1;
4839
4841
  eventHandler = null;
4840
4842
  logHandler = null;
4841
4843
  readinessHandler = null;
@@ -5110,14 +5112,20 @@ var LocalChildRegistry = class {
5110
5112
  listChildren() {
5111
5113
  return [...this.children.values()].map((e) => ({
5112
5114
  childId: e.childId,
5113
- caps: e.caps
5115
+ caps: e.caps,
5116
+ incarnation: e.incarnation
5114
5117
  }));
5115
5118
  }
5116
5119
  /** Register the (single) child-registered handler. Only one handler is active at a time. */
5117
5120
  onChildRegistered(handler) {
5118
5121
  this.registeredHandler = handler;
5119
5122
  }
5120
- /** Register the (single) child-gone handler. Only one handler is active at a time. */
5123
+ /**
5124
+ * Register the (single) child-gone handler. Only one handler is active at a
5125
+ * time. The handler receives the {@link RegisteredChild.incarnation} of the
5126
+ * connection that closed — carry it into whatever teardown it triggers, so a
5127
+ * dead generation cannot tear down its successor's work.
5128
+ */
5121
5129
  onChildGone(handler) {
5122
5130
  this.goneHandler = handler;
5123
5131
  }
@@ -5202,6 +5210,7 @@ var LocalChildRegistry = class {
5202
5210
  }
5203
5211
  onConnection(channel) {
5204
5212
  let childId = null;
5213
+ const incarnation = this.nextIncarnation++;
5205
5214
  channel.onEvent((body) => {
5206
5215
  const msg = body;
5207
5216
  if (msg.kind === "event") {
@@ -5230,16 +5239,24 @@ var LocalChildRegistry = class {
5230
5239
  if (childId !== null && childId !== msg.childId) throw new Error(`child attempted to change identity from "${childId}" to "${msg.childId}"`);
5231
5240
  childId = msg.childId;
5232
5241
  const eventPatterns = msg.eventPatterns ?? null;
5242
+ const superseded = this.children.get(msg.childId);
5243
+ if (superseded !== void 0 && superseded.channel !== channel) this.logger?.info("local child superseded by a new connection", {
5244
+ childId: msg.childId,
5245
+ supersededIncarnation: superseded.incarnation,
5246
+ incarnation
5247
+ });
5233
5248
  this.children.set(msg.childId, {
5234
5249
  childId: msg.childId,
5235
5250
  channel,
5236
5251
  caps: msg.caps,
5237
- eventPatterns
5252
+ eventPatterns,
5253
+ incarnation
5238
5254
  });
5239
5255
  this.logPatternSet(msg.childId, eventPatterns);
5240
5256
  this.registeredHandler({
5241
5257
  childId: msg.childId,
5242
- caps: msg.caps
5258
+ caps: msg.caps,
5259
+ incarnation
5243
5260
  });
5244
5261
  return { ok: true };
5245
5262
  }
@@ -5274,11 +5291,21 @@ var LocalChildRegistry = class {
5274
5291
  throw new Error(`unknown child request kind: ${msg.kind}`);
5275
5292
  });
5276
5293
  channel.onClose(() => {
5277
- if (childId !== null && this.children.delete(childId)) {
5278
- this.childEventStats.delete(childId);
5279
- this.loggedPatternSet.delete(childId);
5280
- this.goneHandler(childId);
5294
+ if (childId === null) return;
5295
+ const entry = this.children.get(childId);
5296
+ if (entry === void 0) return;
5297
+ if (entry.channel !== channel) {
5298
+ this.logger?.info("ignoring close of a superseded local child connection", {
5299
+ childId,
5300
+ incarnation,
5301
+ liveIncarnation: entry.incarnation
5302
+ });
5303
+ return;
5281
5304
  }
5305
+ this.children.delete(childId);
5306
+ this.childEventStats.delete(childId);
5307
+ this.loggedPatternSet.delete(childId);
5308
+ this.goneHandler(childId, incarnation);
5282
5309
  });
5283
5310
  }
5284
5311
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.110",
3
+ "version": "1.2.111",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",