@alfe.ai/integrations 0.1.1 → 0.1.3

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
@@ -264,7 +264,15 @@ interface RuntimeApplier {
264
264
  * for the eventual `getCredentials(provider)` generalisation.
265
265
  */
266
266
  interface CredentialsResolver {
267
- getCredentials: (provider: string) => Promise<Record<string, unknown> | undefined>;
267
+ /**
268
+ * Resolve credentials for env interpolation. `opts.connectionId`
269
+ * scopes resolution to a specific connection (Custom Connections),
270
+ * returning a flat field map; without it, resolution is
271
+ * provider-scoped (the agent's primary connection for the provider).
272
+ */
273
+ getCredentials: (provider: string, opts?: {
274
+ connectionId?: string;
275
+ }) => Promise<Record<string, unknown> | undefined>;
268
276
  }
269
277
  /**
270
278
  * Daemon-level platform context exposed to `{{alfe.<key>}}` env
@@ -298,7 +306,9 @@ declare class McpApplier {
298
306
  * the daemon-hosted bundler will pick the change up via the store
299
307
  * watcher and reconcile its children.
300
308
  */
301
- applyForIntegration(integrationId: string, servers: McpServerDeclaration[], mergedConfig: Record<string, unknown>): Promise<string[]>;
309
+ applyForIntegration(integrationId: string, servers: McpServerDeclaration[], mergedConfig: Record<string, unknown>, opts?: {
310
+ connectionId?: string;
311
+ }): Promise<string[]>;
302
312
  /**
303
313
  * Drop every entry owned by this integration. Store writes are
304
314
  * durable before `removeServersByOwner` resolves; the daemon's
@@ -314,10 +324,37 @@ declare class McpApplier {
314
324
  *
315
325
  * These define the payloads for integration lifecycle operations.
316
326
  */
327
+ /**
328
+ * Embedded git pointer for a Custom Connection-driven install. When
329
+ * present on an install, the manager clones `manifestRepo` at
330
+ * `manifestCommit` and installs from the directory containing
331
+ * `manifestPath`, bypassing the central registry resolver (Custom
332
+ * Connection manifests are per-tenant and never registry-published).
333
+ * Defined structurally so the gateway's `DesiredCustomSource` is
334
+ * assignable without a cross-package import.
335
+ */
336
+ interface CustomInstallSource {
337
+ /** Backing custom Connection — used to scope credential resolution. */
338
+ connectionId: string;
339
+ /** `https://github.com/{owner}/{repo}` clone URL. */
340
+ manifestRepo: string;
341
+ /** Path to the manifest inside the repo (its directory is the install root). */
342
+ manifestPath: string;
343
+ /** Symbolic ref the operator pinned (display/provenance only). */
344
+ manifestRef: string;
345
+ /** Blob SHA of the manifest file content (not checkout-able). */
346
+ manifestSha: string;
347
+ /** Checkout-able commit SHA to clone. */
348
+ manifestCommit: string;
349
+ /** Already-parsed-and-validated manifest blob (optional convenience). */
350
+ manifest?: unknown;
351
+ }
317
352
  interface IntegrationInstallParams {
318
353
  name: string;
319
354
  version?: string;
320
355
  config?: Record<string, unknown>;
356
+ /** Set for Custom Connection-driven installs (clone-from-git). */
357
+ customSource?: CustomInstallSource;
321
358
  }
322
359
  interface IntegrationRemoveParams {
323
360
  name: string;
@@ -670,7 +707,7 @@ declare class OpenClawApplier implements RuntimeApplier {
670
707
  private skillsDir;
671
708
  private trackingPath;
672
709
  constructor(options: OpenClawApplierOptions);
673
- applyPlugin(pkg: string, _installPath?: string, opts?: {
710
+ applyPlugin(spec: string, _installPath?: string, opts?: {
674
711
  force?: boolean;
675
712
  }): Promise<void>;
676
713
  /**
@@ -697,7 +734,7 @@ declare class OpenClawApplier implements RuntimeApplier {
697
734
  * write through the npm path with a proper tracking record.
698
735
  */
699
736
  private cleanupUntrackedExtensionInstall;
700
- removePlugin(pkg: string): Promise<void>;
737
+ removePlugin(spec: string): Promise<void>;
701
738
  applySkill(name: string, srcPath: string): Promise<void>;
702
739
  applyClawHubSkill(slug: string): Promise<void>;
703
740
  removeClawHubSkill(slug: string): Promise<void>;
@@ -723,6 +760,13 @@ declare class OpenClawApplier implements RuntimeApplier {
723
760
  //#endregion
724
761
  //#region src/lock.d.ts
725
762
  interface RuntimePluginEntry {
763
+ /**
764
+ * The full npm install spec from the manifest (e.g. `@scope/name@1.2.3`).
765
+ * Consumers that key off the installed plugin's identity (file-system
766
+ * lookups, openclaw allowlist, cross-integration claim matching) must
767
+ * pass this through `stripPluginVersion` from `./plugin-spec.js` first.
768
+ * Only `openclaw plugins install` should receive the unstripped spec.
769
+ */
726
770
  package: string;
727
771
  sourceIntegration: string;
728
772
  integrationVersion: string;
@@ -785,7 +829,7 @@ interface IIntegrationManager {
785
829
  installedAt?: string;
786
830
  }[]>;
787
831
  /** Install an integration at a specific version with config */
788
- install(integrationId: string, version: string, config: unknown): Promise<void>;
832
+ install(integrationId: string, version: string, config: unknown, customSource?: CustomInstallSource): Promise<void>;
789
833
  /** Activate an installed integration */
790
834
  activate(integrationId: string): Promise<{
791
835
  configApplied: boolean;
@@ -795,7 +839,7 @@ interface IIntegrationManager {
795
839
  /** Uninstall an integration */
796
840
  uninstall(integrationId: string): Promise<void>;
797
841
  /** Reinstall an integration — full teardown then fresh install + activate */
798
- reinstall(integrationId: string, version: string, config: unknown): Promise<{
842
+ reinstall(integrationId: string, version: string, config: unknown, customSource?: CustomInstallSource): Promise<{
799
843
  configApplied: boolean;
800
844
  }>;
801
845
  /** Check if an integration's install directory and manifest are intact on disk */
@@ -816,13 +860,13 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
816
860
  status: string;
817
861
  installedAt?: string;
818
862
  }[]>;
819
- install(integrationId: string, version: string, config: unknown): Promise<void>;
863
+ install(integrationId: string, version: string, config: unknown, customSource?: CustomInstallSource): Promise<void>;
820
864
  activate(integrationId: string): Promise<{
821
865
  configApplied: boolean;
822
866
  }>;
823
867
  deactivate(integrationId: string): Promise<void>;
824
868
  uninstall(integrationId: string): Promise<void>;
825
- reinstall(integrationId: string, version: string, config: unknown): Promise<{
869
+ reinstall(integrationId: string, version: string, config: unknown, customSource?: CustomInstallSource): Promise<{
826
870
  configApplied: boolean;
827
871
  }>;
828
872
  isInstallIntact(integrationId: string): Promise<boolean>;
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { dirname, join } from "node:path";
3
+ import { basename, dirname, join } from "node:path";
4
4
  import { homedir, platform, tmpdir } from "node:os";
5
- import { closeSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { buildConfigValidationSchema, parseManifestFile } from "@alfe.ai/integration-manifest";
7
7
  import { createLogger } from "@auriclabs/logger";
8
8
  //#region src/registry.ts
@@ -796,6 +796,26 @@ async function runHookWithContext(integrationPath, hookScript, options) {
796
796
  return runHook(integrationPath, hookScript, buildHookEnv(options));
797
797
  }
798
798
  //#endregion
799
+ //#region src/plugin-spec.ts
800
+ /**
801
+ * Plugin spec parsing.
802
+ *
803
+ * Manifests express plugin installs as `package: "@scope/name@version"` strings
804
+ * (npm specifier form). The npm install command accepts the full spec, but file-
805
+ * system checks, plugin-allowlist entries, and cross-integration claim matching
806
+ * need the bare package name (npm stores `node_modules/<name>`, not
807
+ * `node_modules/<name>@<ver>`).
808
+ *
809
+ * `stripPluginVersion` returns the bare package name from any spec, handling
810
+ * scoped (`@scope/name@1.2.3`) and unscoped (`name@1.2.3`) forms. Specs without
811
+ * a version are returned unchanged.
812
+ */
813
+ function stripPluginVersion(spec) {
814
+ const at = spec.lastIndexOf("@");
815
+ if (at <= 0) return spec;
816
+ return spec.slice(0, at);
817
+ }
818
+ //#endregion
799
819
  //#region src/integration-manager.ts
800
820
  /**
801
821
  * Integration Manager — full lifecycle management for Alfe integrations.
@@ -848,6 +868,37 @@ function resolveInstallsForRuntime(manifest, runtime) {
848
868
  config: runtimeSpecific?.config
849
869
  };
850
870
  }
871
+ /**
872
+ * Build a clone target for a Custom Connection-driven install,
873
+ * bypassing the central registry resolver. The manifest's directory
874
+ * inside the repo becomes the install root (subdir extraction); the
875
+ * checkout-able `manifestCommit` pins the clone.
876
+ */
877
+ function buildCustomResolved(id, cs) {
878
+ if (id.includes("/") || id.includes("\\") || id.includes("..")) throw new Error(`Refusing custom integration id with path separators: ${id}`);
879
+ const dir = dirname(cs.manifestPath);
880
+ const subdir = dir === "." || dir === "" ? void 0 : dir;
881
+ const manifestVersion = cs.manifest?.version;
882
+ return {
883
+ id,
884
+ repository: cs.manifestRepo,
885
+ version: manifestVersion ?? "unknown",
886
+ commit: cs.manifestCommit,
887
+ subdir,
888
+ description: ""
889
+ };
890
+ }
891
+ /**
892
+ * The lifecycle assumes the manifest is named `alfe-integration.yaml`.
893
+ * Custom Connections may point at a differently-named file, so after
894
+ * cloning we alias the actual file to the canonical name if needed.
895
+ */
896
+ function ensureCanonicalManifestName(installPath, manifestPath) {
897
+ const canonical = join(installPath, "alfe-integration.yaml");
898
+ if (existsSync(canonical)) return;
899
+ const actual = join(installPath, basename(manifestPath));
900
+ if (existsSync(actual)) copyFileSync(actual, canonical);
901
+ }
851
902
  var IntegrationManager = class {
852
903
  log = createLogger("IntegrationManager");
853
904
  state;
@@ -883,7 +934,7 @@ var IntegrationManager = class {
883
934
  * They are applied during activate() via runtime appliers.
884
935
  */
885
936
  async install(params) {
886
- const { name, version, config } = params;
937
+ const { name, version, config, customSource } = params;
887
938
  if (!name) return this.err("INVALID_PARAMS", "Integration name is required");
888
939
  const existing = this.state.get(name);
889
940
  if (existing) {
@@ -906,13 +957,21 @@ var IntegrationManager = class {
906
957
  status: "installing",
907
958
  version: version ?? "unknown",
908
959
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
909
- config: {}
960
+ config: {},
961
+ customConnectionId: customSource?.connectionId
910
962
  });
911
963
  try {
912
- const resolved = await this.resolver.resolve(name, version);
913
- this.log.info(`Resolved ${name}@${resolved.version} from ${resolved.repository}`);
964
+ let resolved;
965
+ if (customSource) {
966
+ resolved = buildCustomResolved(name, customSource);
967
+ this.log.info(`Custom Connection install: ${name} from ${resolved.repository}@${resolved.commit}`);
968
+ } else {
969
+ resolved = await this.resolver.resolve(name, version);
970
+ this.log.info(`Resolved ${name}@${resolved.version} from ${resolved.repository}`);
971
+ }
914
972
  const installPath = await this.installer.install(resolved);
915
973
  this.log.info(`Cloned to ${installPath}`);
974
+ if (customSource) ensureCanonicalManifestName(installPath, customSource.manifestPath);
916
975
  const manifestPath = join(installPath, "alfe-integration.yaml");
917
976
  if (!existsSync(manifestPath)) throw new Error(`No alfe-integration.yaml found in ${installPath}`);
918
977
  const manifest = parseManifestFile(manifestPath);
@@ -939,7 +998,8 @@ var IntegrationManager = class {
939
998
  status: "installed",
940
999
  version: manifest.version,
941
1000
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
942
- config: config ?? {}
1001
+ config: config ?? {},
1002
+ customConnectionId: customSource?.connectionId
943
1003
  });
944
1004
  this.log.info(`Integration "${name}" installed successfully`);
945
1005
  return {
@@ -1097,7 +1157,7 @@ var IntegrationManager = class {
1097
1157
  ...secretEntries ? Object.fromEntries(secretEntries) : {}
1098
1158
  };
1099
1159
  this.log.info(`Applying ${String(mcpServers.length)} MCP server(s) for ${integrationId} via bundler manager`);
1100
- await this.mcpApplier.applyForIntegration(integrationId, mcpServers, mergedConfig);
1160
+ await this.mcpApplier.applyForIntegration(integrationId, mcpServers, mergedConfig, { connectionId: entry.customConnectionId });
1101
1161
  }
1102
1162
  this.state.setStatus(integrationId, "active");
1103
1163
  if (manifest.hooks.post_activate) {
@@ -1162,6 +1222,13 @@ var IntegrationManager = class {
1162
1222
  this.log.info(`Deactivating integration: ${integrationId}`);
1163
1223
  try {
1164
1224
  const removed = this.lockManager.removeEntries(integrationId);
1225
+ const remaining = this.lockManager.read();
1226
+ const claimedPluginsByRuntime = /* @__PURE__ */ new Map();
1227
+ const claimedSkillsByRuntime = /* @__PURE__ */ new Map();
1228
+ for (const [rtName, state] of Object.entries(remaining.runtimes)) {
1229
+ claimedPluginsByRuntime.set(rtName, new Set(state.plugins.map((p) => stripPluginVersion(p.package))));
1230
+ claimedSkillsByRuntime.set(rtName, new Set(state.skills.map((s) => s.name)));
1231
+ }
1165
1232
  for (const [runtimeName, entries] of Object.entries(removed)) {
1166
1233
  const applier = this.runtimeAppliers.get(runtimeName);
1167
1234
  if (!applier) {
@@ -1172,7 +1239,13 @@ var IntegrationManager = class {
1172
1239
  this.log.warn(`Runtime "${runtimeName}" is not available — skipping removal`);
1173
1240
  continue;
1174
1241
  }
1242
+ const keepPlugins = claimedPluginsByRuntime.get(runtimeName) ?? /* @__PURE__ */ new Set();
1243
+ const keepSkills = claimedSkillsByRuntime.get(runtimeName) ?? /* @__PURE__ */ new Set();
1175
1244
  for (const plugin of entries.plugins) {
1245
+ if (keepPlugins.has(stripPluginVersion(plugin.package))) {
1246
+ this.log.info(`Keeping plugin ${plugin.package} on ${runtimeName} — still claimed by another active integration`);
1247
+ continue;
1248
+ }
1176
1249
  this.log.info(`Removing plugin ${plugin.package} from ${runtimeName}`);
1177
1250
  try {
1178
1251
  await applier.removePlugin(plugin.package);
@@ -1181,6 +1254,10 @@ var IntegrationManager = class {
1181
1254
  }
1182
1255
  }
1183
1256
  for (const skill of entries.skills) {
1257
+ if (keepSkills.has(skill.name)) {
1258
+ this.log.info(`Keeping skill ${skill.name} on ${runtimeName} — still claimed by another active integration`);
1259
+ continue;
1260
+ }
1184
1261
  this.log.info(`Removing skill ${skill.name} from ${runtimeName}`);
1185
1262
  try {
1186
1263
  if (skill.sourcePath.startsWith("clawhub:")) await applier.removeClawHubSkill(skill.name);
@@ -1575,16 +1652,21 @@ var OpenClawApplier = class {
1575
1652
  this.skillsDir = options.skillsDir ?? DEFAULT_SKILLS_DIR;
1576
1653
  this.trackingPath = options.configPath ?? join(this.home, "config.json");
1577
1654
  }
1578
- async applyPlugin(pkg, _installPath, opts) {
1655
+ async applyPlugin(spec, _installPath, opts) {
1656
+ const pkg = stripPluginVersion(spec);
1579
1657
  await this.ensurePluginsAllow(pkg);
1580
1658
  this.cleanupUntrackedExtensionInstall(pkg);
1581
1659
  if (opts?.force && this.isPluginInstalled(pkg)) {
1582
- log$1.info({ pkg }, "Force mode — uninstalling plugin before reinstall");
1660
+ log$1.info({
1661
+ pkg,
1662
+ spec
1663
+ }, "Force mode — uninstalling plugin before reinstall");
1583
1664
  try {
1584
1665
  await this.removePlugin(pkg);
1585
1666
  } catch (err) {
1586
1667
  log$1.warn({
1587
1668
  pkg,
1669
+ spec,
1588
1670
  err: err instanceof Error ? err.message : String(err)
1589
1671
  }, "Failed to uninstall plugin during force reinstall — proceeding");
1590
1672
  }
@@ -1593,7 +1675,7 @@ var OpenClawApplier = class {
1593
1675
  const baseArgs = [
1594
1676
  "plugins",
1595
1677
  "install",
1596
- pkg,
1678
+ spec,
1597
1679
  "--force"
1598
1680
  ];
1599
1681
  const useUnsafeFlag = pkg.startsWith("@alfe.ai/");
@@ -1615,6 +1697,7 @@ var OpenClawApplier = class {
1615
1697
  if (!this.isPluginInstalled(pkg)) throw err;
1616
1698
  log$1.warn({
1617
1699
  pkg,
1700
+ spec,
1618
1701
  err: err instanceof Error ? err.message : String(err)
1619
1702
  }, "openclaw plugins install exited with warnings but plugin is installed");
1620
1703
  }
@@ -1714,12 +1797,12 @@ var OpenClawApplier = class {
1714
1797
  }
1715
1798
  }
1716
1799
  }
1717
- async removePlugin(pkg) {
1800
+ async removePlugin(spec) {
1718
1801
  await execFileAsync("openclaw", [
1719
1802
  "plugins",
1720
1803
  "uninstall",
1721
1804
  "--force",
1722
- pkg
1805
+ stripPluginVersion(spec)
1723
1806
  ], { timeout: 3e4 });
1724
1807
  }
1725
1808
  applySkill(name, srcPath) {
@@ -1906,12 +1989,12 @@ var McpApplier = class {
1906
1989
  * the daemon-hosted bundler will pick the change up via the store
1907
1990
  * watcher and reconcile its children.
1908
1991
  */
1909
- async applyForIntegration(integrationId, servers, mergedConfig) {
1992
+ async applyForIntegration(integrationId, servers, mergedConfig, opts) {
1910
1993
  const owner = `integration:${integrationId}`;
1911
1994
  const applied = [];
1912
1995
  for (const server of servers) {
1913
1996
  const id = `${integrationId}-${server.id}`;
1914
- const envResolved = await this.resolveEnv(server, mergedConfig);
1997
+ const envResolved = await this.resolveEnv(server, mergedConfig, opts?.connectionId);
1915
1998
  if (envResolved == null) {
1916
1999
  log.info({
1917
2000
  integrationId,
@@ -1937,17 +2020,19 @@ var McpApplier = class {
1937
2020
  const owner = `integration:${integrationId}`;
1938
2021
  return this.manager.removeServersByOwner(owner);
1939
2022
  }
1940
- async resolveEnv(server, mergedConfig) {
2023
+ async resolveEnv(server, mergedConfig, connectionId) {
1941
2024
  if (!server.env || Object.keys(server.env).length === 0) return {};
1942
- const needsCredentials = server.requires_credentials;
2025
+ const provider = server.requires_credentials;
2026
+ const needsCredentials = Boolean(provider) || Boolean(connectionId);
1943
2027
  let credentialsCache;
1944
2028
  if (needsCredentials) {
1945
2029
  try {
1946
- credentialsCache = await this.credentials.getCredentials(needsCredentials);
2030
+ credentialsCache = await this.credentials.getCredentials(provider ?? "custom", connectionId ? { connectionId } : void 0);
1947
2031
  } catch (err) {
1948
2032
  log.warn({
1949
2033
  err: errMsg(err),
1950
- provider: needsCredentials
2034
+ provider,
2035
+ connectionId
1951
2036
  }, "Credentials fetch threw — skipping MCP registration");
1952
2037
  return null;
1953
2038
  }
@@ -1958,7 +2043,8 @@ var McpApplier = class {
1958
2043
  const interpolated = interpolateString(value, mergedConfig, credentialsCache, this.platform);
1959
2044
  if (needsCredentials && hasCredentialsPlaceholder(interpolated)) {
1960
2045
  log.warn({
1961
- provider: needsCredentials,
2046
+ provider,
2047
+ connectionId,
1962
2048
  key
1963
2049
  }, "Credentials interpolation left a placeholder — skipping MCP registration");
1964
2050
  return null;
@@ -2011,11 +2097,12 @@ var IntegrationManagerAdapter = class {
2011
2097
  installedAt: i.installedAt
2012
2098
  })));
2013
2099
  }
2014
- async install(integrationId, version, config) {
2100
+ async install(integrationId, version, config, customSource) {
2015
2101
  const result = await this.manager.install({
2016
2102
  name: integrationId,
2017
2103
  version,
2018
- config
2104
+ config,
2105
+ customSource
2019
2106
  });
2020
2107
  if (!result.ok) throw new Error(result.error?.message ?? `Failed to install ${integrationId}`);
2021
2108
  }
@@ -2032,11 +2119,12 @@ var IntegrationManagerAdapter = class {
2032
2119
  const result = await this.manager.uninstall({ name: integrationId });
2033
2120
  if (!result.ok) throw new Error(result.error?.message ?? `Failed to uninstall ${integrationId}`);
2034
2121
  }
2035
- async reinstall(integrationId, version, config) {
2122
+ async reinstall(integrationId, version, config, customSource) {
2036
2123
  const result = await this.manager.reinstall({
2037
2124
  name: integrationId,
2038
2125
  version,
2039
- config
2126
+ config,
2127
+ customSource
2040
2128
  });
2041
2129
  if (!result.ok) throw new Error(result.error?.message ?? `Failed to reinstall ${integrationId}`);
2042
2130
  return { configApplied: result.payload?.configApplied ?? false };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,8 +13,8 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@auriclabs/logger": "^0.1.1",
16
- "@alfe.ai/integration-manifest": "^0.1.0",
17
- "@alfe.ai/mcp-bundler": "^0.1.1"
16
+ "@alfe.ai/integration-manifest": "^0.2.1",
17
+ "@alfe.ai/mcp-bundler": "^0.2.0"
18
18
  },
19
19
  "files": [
20
20
  "dist"