@alfe.ai/integrations 0.3.0 → 0.3.1

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.
package/dist/index.d.ts CHANGED
@@ -314,9 +314,17 @@ declare class Installer {
314
314
  interface RuntimeApplier {
315
315
  /** The runtime identifier (e.g. 'openclaw', 'nanoclaw') */
316
316
  readonly runtime: string;
317
- /** Install a plugin package into this runtime */
317
+ /**
318
+ * Install a plugin package into this runtime.
319
+ *
320
+ * `opts.integrationId` identifies the owning integration so the applier can
321
+ * distinguish a legitimate same-integration pin bump from a cross-integration
322
+ * pin conflict (two integrations pinning the same plugin at different
323
+ * versions). See `OpenClawApplier`'s first-writer-wins guard.
324
+ */
318
325
  applyPlugin(pkg: string, integrationInstallPath: string, opts?: {
319
326
  force?: boolean;
327
+ integrationId?: string;
320
328
  }): Promise<void>;
321
329
  /**
322
330
  * Optional: pre-trust an integration's full plugin set in one write before
@@ -995,6 +1003,24 @@ declare class OpenClawApplier implements RuntimeApplier {
995
1003
  * this instance performed (0 = never). See HEAL_MIN_INTERVAL_MS.
996
1004
  */
997
1005
  private lastHealAt;
1006
+ /**
1007
+ * Cross-integration plugin-pin claims made during THIS process lifetime,
1008
+ * keyed by bare package name. `applyPlugin` is now version-aware for both
1009
+ * force and non-force callers — it actively reinstalls a plugin whose
1010
+ * installed version differs from the pinned spec (previously the non-force
1011
+ * path keyed only on bare-name presence and silently ignored a pin bump). If
1012
+ * two integrations pin the SAME plugin at DIFFERENT versions, that
1013
+ * version-aware reinstall would make them fight — each reconcile pass
1014
+ * uninstall+reinstalls the other's version forever. This map records the
1015
+ * first-applied pin per package so a later, divergent pin from a DIFFERENT
1016
+ * integration is refused (first-writer-wins) with a WARN instead of thrashing.
1017
+ * A later apply from the SAME integration (a legitimate pin bump across an
1018
+ * upgrade) is NOT a conflict and proceeds. Cleared per-package on
1019
+ * `removePlugin`, and wholesale on daemon restart. Convention is single-owner
1020
+ * per plugin (see the `alfe` manifest note) — this only turns a convention
1021
+ * violation into a stable warning.
1022
+ */
1023
+ private readonly appliedPluginPins;
998
1024
  constructor(options: OpenClawApplierOptions);
999
1025
  /**
1000
1026
  * Convenience: `openclaw config set <args>`, UNLOCKED + retried.
@@ -1080,8 +1106,16 @@ declare class OpenClawApplier implements RuntimeApplier {
1080
1106
  private healMalformedStateDb;
1081
1107
  applyPlugin(spec: string, _installPath?: string, opts?: {
1082
1108
  force?: boolean;
1109
+ integrationId?: string;
1083
1110
  }): Promise<void>;
1084
1111
  private applyPluginLocked;
1112
+ /**
1113
+ * Record the pin a caller just applied for a package, keyed by bare name, so
1114
+ * a later divergent pin from a DIFFERENT integration in this process can be
1115
+ * refused (first-writer-wins). Bare (versionless) pins are untrackable and
1116
+ * ignored — they can't conflict on version. See `appliedPluginPins`.
1117
+ */
1118
+ private recordPluginPin;
1085
1119
  /**
1086
1120
  * Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
1087
1121
  * Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
package/dist/index.js CHANGED
@@ -1271,7 +1271,10 @@ var IntegrationManager = class {
1271
1271
  for (const plugin of plugins) {
1272
1272
  this.log.info(`Applying plugin ${plugin.package} to ${runtimeName}`);
1273
1273
  try {
1274
- await applier.applyPlugin(plugin.package, installPath, { force: opts?.forcePlugins });
1274
+ await applier.applyPlugin(plugin.package, installPath, {
1275
+ force: opts?.forcePlugins,
1276
+ integrationId
1277
+ });
1275
1278
  } catch (err) {
1276
1279
  const msg = err instanceof Error ? err.message : String(err);
1277
1280
  this.log.error(`Failed to apply plugin ${plugin.package}: ${msg}`);
@@ -2301,6 +2304,24 @@ var OpenClawApplier = class {
2301
2304
  * this instance performed (0 = never). See HEAL_MIN_INTERVAL_MS.
2302
2305
  */
2303
2306
  lastHealAt = 0;
2307
+ /**
2308
+ * Cross-integration plugin-pin claims made during THIS process lifetime,
2309
+ * keyed by bare package name. `applyPlugin` is now version-aware for both
2310
+ * force and non-force callers — it actively reinstalls a plugin whose
2311
+ * installed version differs from the pinned spec (previously the non-force
2312
+ * path keyed only on bare-name presence and silently ignored a pin bump). If
2313
+ * two integrations pin the SAME plugin at DIFFERENT versions, that
2314
+ * version-aware reinstall would make them fight — each reconcile pass
2315
+ * uninstall+reinstalls the other's version forever. This map records the
2316
+ * first-applied pin per package so a later, divergent pin from a DIFFERENT
2317
+ * integration is refused (first-writer-wins) with a WARN instead of thrashing.
2318
+ * A later apply from the SAME integration (a legitimate pin bump across an
2319
+ * upgrade) is NOT a conflict and proceeds. Cleared per-package on
2320
+ * `removePlugin`, and wholesale on daemon restart. Convention is single-owner
2321
+ * per plugin (see the `alfe` manifest note) — this only turns a convention
2322
+ * violation into a stable warning.
2323
+ */
2324
+ appliedPluginPins = /* @__PURE__ */ new Map();
2304
2325
  constructor(options) {
2305
2326
  const home = options.home ?? options.workspace;
2306
2327
  if (!home) throw new Error("OpenClawApplier requires `home` (or legacy `workspace`) option");
@@ -2484,25 +2505,43 @@ var OpenClawApplier = class {
2484
2505
  }
2485
2506
  async applyPluginLocked(spec, opts) {
2486
2507
  const pkg = stripPluginVersion(spec);
2508
+ const pinnedVersion = pluginSpecVersion(spec);
2509
+ const claim = this.appliedPluginPins.get(pkg);
2510
+ if (pinnedVersion !== void 0 && claim !== void 0 && claim.version !== pinnedVersion && claim.integrationId !== opts?.integrationId) {
2511
+ log$3.warn({
2512
+ pkg,
2513
+ requestedVersion: pinnedVersion,
2514
+ keptVersion: claim.version,
2515
+ requestedBy: opts?.integrationId,
2516
+ ownedBy: claim.integrationId
2517
+ }, "Conflicting plugin pin across integrations — keeping the first-applied version (first-writer-wins)");
2518
+ return;
2519
+ }
2487
2520
  await this.ensurePluginsAllowUnlocked(pkg);
2488
2521
  this.cleanupUntrackedExtensionInstall(pkg);
2489
- if (opts?.force && this.isPluginInstalled(pkg)) {
2490
- const pinnedVersion = pluginSpecVersion(spec);
2522
+ if (this.isPluginInstalled(pkg)) {
2491
2523
  const installedVersion = this.installedPluginVersion(pkg);
2492
- if (pinnedVersion && installedVersion && installedVersion === pinnedVersion) {
2493
- log$3.info({
2524
+ const versionsComparable = pinnedVersion !== void 0 && installedVersion !== void 0;
2525
+ if (versionsComparable && installedVersion === pinnedVersion) {
2526
+ if (opts?.force) log$3.info({
2494
2527
  pkg,
2495
2528
  spec,
2496
2529
  version: installedVersion
2497
2530
  }, "Force mode — installed version already matches pinned spec, skipping uninstall+reinstall");
2531
+ this.recordPluginPin(pkg, pinnedVersion, opts?.integrationId);
2532
+ return;
2533
+ }
2534
+ if (!(versionsComparable && installedVersion !== pinnedVersion) && !opts?.force) {
2535
+ this.recordPluginPin(pkg, pinnedVersion, opts?.integrationId);
2498
2536
  return;
2499
2537
  }
2500
2538
  log$3.info({
2501
2539
  pkg,
2502
2540
  spec,
2503
2541
  installedVersion,
2504
- pinnedVersion
2505
- }, "Force mode — uninstalling plugin before reinstall");
2542
+ pinnedVersion,
2543
+ force: opts?.force ?? false
2544
+ }, "Reinstalling plugin to converge on the pinned version");
2506
2545
  try {
2507
2546
  await this.removePluginUnlocked(pkg);
2508
2547
  } catch (err) {
@@ -2510,7 +2549,7 @@ var OpenClawApplier = class {
2510
2549
  pkg,
2511
2550
  spec,
2512
2551
  err: err instanceof Error ? err.message : String(err)
2513
- }, "Failed to uninstall plugin during force reinstall — proceeding");
2552
+ }, "Failed to uninstall plugin before reinstall — proceeding");
2514
2553
  }
2515
2554
  }
2516
2555
  if (!this.isPluginInstalled(pkg)) {
@@ -2547,6 +2586,20 @@ var OpenClawApplier = class {
2547
2586
  setTimeout(r, 500);
2548
2587
  });
2549
2588
  }
2589
+ this.recordPluginPin(pkg, pinnedVersion, opts?.integrationId);
2590
+ }
2591
+ /**
2592
+ * Record the pin a caller just applied for a package, keyed by bare name, so
2593
+ * a later divergent pin from a DIFFERENT integration in this process can be
2594
+ * refused (first-writer-wins). Bare (versionless) pins are untrackable and
2595
+ * ignored — they can't conflict on version. See `appliedPluginPins`.
2596
+ */
2597
+ recordPluginPin(pkg, version, integrationId) {
2598
+ if (version === void 0) return;
2599
+ this.appliedPluginPins.set(pkg, {
2600
+ version,
2601
+ integrationId
2602
+ });
2550
2603
  }
2551
2604
  /**
2552
2605
  * Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
@@ -2692,6 +2745,7 @@ var OpenClawApplier = class {
2692
2745
  */
2693
2746
  async removePluginUnlocked(spec) {
2694
2747
  const pkg = stripPluginVersion(spec);
2748
+ this.appliedPluginPins.delete(pkg);
2695
2749
  await this.execOpenClawHealing([
2696
2750
  "plugins",
2697
2751
  "uninstall",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",