@alfe.ai/integrations 0.3.1 → 0.3.2

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
@@ -697,6 +697,13 @@ declare class IntegrationManager {
697
697
  clear(): void;
698
698
  private checkHealth;
699
699
  private err;
700
+ /**
701
+ * Build a hook-failure message. A hook we SIGKILLed at its timeout is rendered
702
+ * as `(timed out after <ms>ms)` so it is visually distinguishable in Sentry
703
+ * from a genuine non-zero exit `(exit <code>)` — the two have very different
704
+ * root causes (Sentry AGENT-DAEMON-6).
705
+ */
706
+ private hookFailureMessage;
700
707
  /**
701
708
  * Compute the plugin (bare package) names and skill names still claimed by
702
709
  * SOME integration in the CURRENT lock state, keyed by runtime. Both
@@ -786,31 +793,16 @@ declare class StateManager {
786
793
  }
787
794
  //#endregion
788
795
  //#region src/hooks.d.ts
789
- /**
790
- * Hook Runner — executes integration lifecycle hook scripts.
791
- *
792
- * Hooks are scripts defined in the integration manifest. The runner
793
- * auto-detects the interpreter from the script's shebang line or file
794
- * extension (.js/.mjs → node, .py → python3, default → bash).
795
- *
796
- * Scripts run as child processes with a 30-second timeout.
797
- * stdout/stderr are captured and returned.
798
- *
799
- * The runner injects standard environment variables:
800
- * - ALFE_INTEGRATION_DIR — path to the integration install directory
801
- * - ALFE_STATE_DIR — path to the integration state directory (created if needed)
802
- * - ALFE_<NAME>_<KEY> — config values (non-secret AND secret, both injected as env vars)
803
- *
804
- * Secrets are acceptable as env vars because:
805
- * - Child process env is ephemeral (dies with the process)
806
- * - Same security model as Docker secrets / systemd credentials
807
- * - Never written to disk
808
- */
809
796
  interface HookResult {
810
797
  exitCode: number;
811
798
  stdout: string;
812
799
  stderr: string;
813
800
  timedOut: boolean;
801
+ /**
802
+ * When `timedOut` is true, the timeout ceiling (ms) that fired the SIGKILL.
803
+ * Lets callers render a timeout distinctly from a genuine non-zero exit.
804
+ */
805
+ timedOutAfterMs?: number;
814
806
  }
815
807
  interface HookEnvOptions {
816
808
  /** Integration name (e.g. "voice") */
@@ -857,9 +849,12 @@ declare function buildHookEnv(options: HookEnvOptions, additionalEnv?: Record<st
857
849
  * @param integrationPath - Base path of the integration (where alfe-integration.yaml lives)
858
850
  * @param hookScript - Relative path to the hook script (e.g. "scripts/activate.sh")
859
851
  * @param env - Additional environment variables to pass to the script
852
+ * @param timeoutMs - Timeout before the hook is SIGKILLed. Defaults to the fast
853
+ * lifecycle ceiling ({@link HOOK_TIMEOUT_MS}); install-phase callers pass
854
+ * {@link INSTALL_HOOK_TIMEOUT_MS}.
860
855
  * @returns Hook execution result
861
856
  */
862
- declare function runHook(integrationPath: string, hookScript: string, env?: Record<string, string>): Promise<HookResult>;
857
+ declare function runHook(integrationPath: string, hookScript: string, env?: Record<string, string>, timeoutMs?: number): Promise<HookResult>;
863
858
  /**
864
859
  * Run a hook script with full integration context (config + secrets as env vars).
865
860
  *
@@ -870,9 +865,12 @@ declare function runHook(integrationPath: string, hookScript: string, env?: Reco
870
865
  * @param integrationPath - Base path of the integration
871
866
  * @param hookScript - Relative path to the hook script
872
867
  * @param options - Integration name, config, and secrets
868
+ * @param timeoutMs - Timeout before the hook is SIGKILLed. Defaults to the fast
869
+ * lifecycle ceiling ({@link HOOK_TIMEOUT_MS}); install-phase callers pass
870
+ * {@link INSTALL_HOOK_TIMEOUT_MS}.
873
871
  * @returns Hook execution result
874
872
  */
875
- declare function runHookWithContext(integrationPath: string, hookScript: string, options: HookEnvOptions): Promise<HookResult>;
873
+ declare function runHookWithContext(integrationPath: string, hookScript: string, options: HookEnvOptions, timeoutMs?: number): Promise<HookResult>;
876
874
  //#endregion
877
875
  //#region src/openclaw-cli-lock.d.ts
878
876
  /**
package/dist/index.js CHANGED
@@ -746,7 +746,19 @@ var LockManager = class {
746
746
  * - Same security model as Docker secrets / systemd credentials
747
747
  * - Never written to disk
748
748
  */
749
+ /**
750
+ * Default timeout for lifecycle hooks (activate/health_check/uninstall). These
751
+ * are expected to be fast; a long-running one is a bug and should be killed.
752
+ */
749
753
  const HOOK_TIMEOUT_MS = 3e4;
754
+ /**
755
+ * Timeout for install-phase hooks (pre_install/post_install). These legitimately
756
+ * take minutes — they clone repos, run `npm install`, download binaries — so the
757
+ * fast 30s ceiling would SIGKILL a healthy install mid-flight (Sentry
758
+ * AGENT-DAEMON-6). Callers opt into this longer ceiling explicitly per hook type;
759
+ * it is NOT the global default so lifecycle hooks stay fast.
760
+ */
761
+ const INSTALL_HOOK_TIMEOUT_MS = 6e5;
750
762
  const INTEGRATIONS_BASE_DIR = join(homedir(), ".alfe", "integrations");
751
763
  const STATE_BASE_DIR = join(homedir(), ".alfe", "state");
752
764
  /**
@@ -848,9 +860,12 @@ function resolveInterpreter(scriptPath) {
848
860
  * @param integrationPath - Base path of the integration (where alfe-integration.yaml lives)
849
861
  * @param hookScript - Relative path to the hook script (e.g. "scripts/activate.sh")
850
862
  * @param env - Additional environment variables to pass to the script
863
+ * @param timeoutMs - Timeout before the hook is SIGKILLed. Defaults to the fast
864
+ * lifecycle ceiling ({@link HOOK_TIMEOUT_MS}); install-phase callers pass
865
+ * {@link INSTALL_HOOK_TIMEOUT_MS}.
851
866
  * @returns Hook execution result
852
867
  */
853
- async function runHook(integrationPath, hookScript, env) {
868
+ async function runHook(integrationPath, hookScript, env, timeoutMs = HOOK_TIMEOUT_MS) {
854
869
  const scriptPath = join(integrationPath, hookScript);
855
870
  if (!existsSync(scriptPath)) return {
856
871
  exitCode: 0,
@@ -878,7 +893,7 @@ async function runHook(integrationPath, hookScript, env) {
878
893
  const timer = setTimeout(() => {
879
894
  timedOut = true;
880
895
  proc.kill("SIGKILL");
881
- }, HOOK_TIMEOUT_MS);
896
+ }, timeoutMs);
882
897
  proc.stdout.on("data", (data) => {
883
898
  stdout += data.toString();
884
899
  if (stdout.length > 1e5) stdout = stdout.slice(0, 1e5) + "\n[truncated]";
@@ -893,7 +908,8 @@ async function runHook(integrationPath, hookScript, env) {
893
908
  exitCode: code ?? 1,
894
909
  stdout: stdout.trim(),
895
910
  stderr: stderr.trim(),
896
- timedOut
911
+ timedOut,
912
+ timedOutAfterMs: timedOut ? timeoutMs : void 0
897
913
  });
898
914
  });
899
915
  proc.on("error", (err) => {
@@ -917,10 +933,13 @@ async function runHook(integrationPath, hookScript, env) {
917
933
  * @param integrationPath - Base path of the integration
918
934
  * @param hookScript - Relative path to the hook script
919
935
  * @param options - Integration name, config, and secrets
936
+ * @param timeoutMs - Timeout before the hook is SIGKILLed. Defaults to the fast
937
+ * lifecycle ceiling ({@link HOOK_TIMEOUT_MS}); install-phase callers pass
938
+ * {@link INSTALL_HOOK_TIMEOUT_MS}.
920
939
  * @returns Hook execution result
921
940
  */
922
- async function runHookWithContext(integrationPath, hookScript, options) {
923
- return runHook(integrationPath, hookScript, buildHookEnv(options));
941
+ async function runHookWithContext(integrationPath, hookScript, options, timeoutMs = HOOK_TIMEOUT_MS) {
942
+ return runHook(integrationPath, hookScript, buildHookEnv(options), timeoutMs);
924
943
  }
925
944
  //#endregion
926
945
  //#region src/plugin-spec.ts
@@ -1127,8 +1146,8 @@ var IntegrationManager = class {
1127
1146
  if (!installHooksSupported && (manifest.hooks.pre_install || manifest.hooks.post_install)) this.log.warn(`Integration "${name}" install hooks skipped — no registered runtime (${[...this.runtimeAppliers.keys()].join(", ")}) is in supported_agents (${(manifest.supported_agents ?? []).join(", ")})`);
1128
1147
  if (installHooksSupported && manifest.hooks.pre_install) {
1129
1148
  this.log.info(`Running pre_install hook: ${manifest.hooks.pre_install}`);
1130
- const hookResult = await runHook(installPath, manifest.hooks.pre_install);
1131
- if (hookResult.exitCode !== 0) throw new Error(`pre_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1149
+ const hookResult = await runHook(installPath, manifest.hooks.pre_install, void 0, INSTALL_HOOK_TIMEOUT_MS);
1150
+ if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("pre_install hook failed", hookResult));
1132
1151
  }
1133
1152
  if (installHooksSupported && manifest.hooks.post_install) {
1134
1153
  this.log.info(`Running post_install hook: ${manifest.hooks.post_install}`);
@@ -1137,8 +1156,8 @@ var IntegrationManager = class {
1137
1156
  config: config ?? {},
1138
1157
  secrets: this.secrets.get(name),
1139
1158
  runtimes: [...this.runtimeAppliers.keys()]
1140
- });
1141
- if (hookResult.exitCode !== 0) throw new Error(`post_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1159
+ }, INSTALL_HOOK_TIMEOUT_MS);
1160
+ if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("post_install hook failed", hookResult));
1142
1161
  }
1143
1162
  this.state.set(name, {
1144
1163
  status: "installed",
@@ -1328,8 +1347,9 @@ var IntegrationManager = class {
1328
1347
  runtimes: [...this.runtimeAppliers.keys()]
1329
1348
  });
1330
1349
  if (hookResult.exitCode !== 0) {
1331
- this.state.setStatus(integrationId, "error", `post_activate hook failed: ${hookResult.stderr || hookResult.stdout}`);
1332
- return this.err("POST_ACTIVATE_FAILED", `post_activate hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1350
+ const message = this.hookFailureMessage("post_activate hook failed", hookResult);
1351
+ this.state.setStatus(integrationId, "error", message);
1352
+ return this.err("POST_ACTIVATE_FAILED", message);
1333
1353
  }
1334
1354
  }
1335
1355
  if (manifest.hooks.health_check && !runtimeSupported) this.log.warn(`Integration "${integrationId}" health_check hook skipped — no registered runtime (${[...this.runtimeAppliers.keys()].join(", ")}) is in supported_agents (${(supportedAgents ?? []).join(", ")})`);
@@ -1342,8 +1362,9 @@ var IntegrationManager = class {
1342
1362
  runtimes: [...this.runtimeAppliers.keys()]
1343
1363
  });
1344
1364
  if (hookResult.exitCode !== 0) {
1345
- this.state.setStatus(integrationId, "error", `Health check failed: ${hookResult.stderr || hookResult.stdout}`);
1346
- return this.err("HEALTH_CHECK_FAILED", `Health check failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1365
+ const message = this.hookFailureMessage("Health check failed", hookResult);
1366
+ this.state.setStatus(integrationId, "error", message);
1367
+ return this.err("HEALTH_CHECK_FAILED", message);
1347
1368
  }
1348
1369
  }
1349
1370
  this.log.info(`Integration "${integrationId}" activated`);
@@ -1485,8 +1506,8 @@ var IntegrationManager = class {
1485
1506
  config: entry.config,
1486
1507
  secrets: this.secrets.get(name),
1487
1508
  runtimes: [...this.runtimeAppliers.keys()]
1488
- });
1489
- if (hookResult.exitCode !== 0) this.log.warn(`pre_uninstall hook failed (continuing): ${hookResult.stderr}`);
1509
+ }, INSTALL_HOOK_TIMEOUT_MS);
1510
+ if (hookResult.exitCode !== 0) this.log.warn(this.hookFailureMessage("pre_uninstall hook failed (continuing)", hookResult));
1490
1511
  }
1491
1512
  if (uninstallHooksSupported && manifest?.hooks.post_uninstall) {
1492
1513
  this.log.info(`Running post_uninstall hook: ${manifest.hooks.post_uninstall}`);
@@ -1495,8 +1516,8 @@ var IntegrationManager = class {
1495
1516
  config: entry.config,
1496
1517
  secrets: this.secrets.get(name),
1497
1518
  runtimes: [...this.runtimeAppliers.keys()]
1498
- });
1499
- if (hookResult.exitCode !== 0) this.log.warn(`post_uninstall hook failed (non-fatal): ${hookResult.stderr}`);
1519
+ }, INSTALL_HOOK_TIMEOUT_MS);
1520
+ if (hookResult.exitCode !== 0) this.log.warn(this.hookFailureMessage("post_uninstall hook failed (non-fatal)", hookResult));
1500
1521
  }
1501
1522
  if (this.installer.isInstalled(name)) {
1502
1523
  await this.installer.remove(name);
@@ -1647,8 +1668,8 @@ var IntegrationManager = class {
1647
1668
  if (!installHooksSupported && (newManifest.hooks.pre_install || newManifest.hooks.post_install)) this.log.warn(`Integration "${name}" upgrade install hooks skipped — no registered runtime (${[...this.runtimeAppliers.keys()].join(", ")}) is in supported_agents (${(newManifest.supported_agents ?? []).join(", ")})`);
1648
1669
  if (installHooksSupported && newManifest.hooks.pre_install) {
1649
1670
  this.log.info(`Running pre_install hook: ${newManifest.hooks.pre_install}`);
1650
- const hookResult = await runHook(this.installer.getInstallPath(name), newManifest.hooks.pre_install);
1651
- if (hookResult.exitCode !== 0) throw new Error(`pre_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1671
+ const hookResult = await runHook(this.installer.getInstallPath(name), newManifest.hooks.pre_install, void 0, INSTALL_HOOK_TIMEOUT_MS);
1672
+ if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("pre_install hook failed", hookResult));
1652
1673
  }
1653
1674
  if (installHooksSupported && newManifest.hooks.post_install) {
1654
1675
  this.log.info(`Running post_install hook: ${newManifest.hooks.post_install}`);
@@ -1657,8 +1678,8 @@ var IntegrationManager = class {
1657
1678
  config: config ?? existing.config,
1658
1679
  secrets: this.secrets.get(name),
1659
1680
  runtimes: [...this.runtimeAppliers.keys()]
1660
- });
1661
- if (hookResult.exitCode !== 0) throw new Error(`post_install hook failed (exit ${String(hookResult.exitCode)}): ${hookResult.stderr || hookResult.stdout}`);
1681
+ }, INSTALL_HOOK_TIMEOUT_MS);
1682
+ if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("post_install hook failed", hookResult));
1662
1683
  }
1663
1684
  await this.applyUpgradeDiffRemovals(name, oldManifest, newManifest);
1664
1685
  return await this.activate(name, { forcePlugins: true });
@@ -1837,7 +1858,7 @@ var IntegrationManager = class {
1837
1858
  healthy: hookResult.exitCode === 0,
1838
1859
  status: entry.status,
1839
1860
  version: manifest.version,
1840
- message: hookResult.exitCode === 0 ? "Healthy" : `Health check failed (exit ${String(hookResult.exitCode)})`,
1861
+ message: hookResult.exitCode === 0 ? "Healthy" : hookResult.timedOut ? `Health check failed (timed out after ${String(hookResult.timedOutAfterMs ?? "?")}ms)` : `Health check failed (exit ${String(hookResult.exitCode)})`,
1841
1862
  stdout: hookResult.stdout || void 0,
1842
1863
  stderr: hookResult.stderr || void 0
1843
1864
  };
@@ -1861,6 +1882,17 @@ var IntegrationManager = class {
1861
1882
  };
1862
1883
  }
1863
1884
  /**
1885
+ * Build a hook-failure message. A hook we SIGKILLed at its timeout is rendered
1886
+ * as `(timed out after <ms>ms)` so it is visually distinguishable in Sentry
1887
+ * from a genuine non-zero exit `(exit <code>)` — the two have very different
1888
+ * root causes (Sentry AGENT-DAEMON-6).
1889
+ */
1890
+ hookFailureMessage(label, hookResult) {
1891
+ const reason = hookResult.timedOut ? `timed out after ${String(hookResult.timedOutAfterMs ?? "?")}ms` : `exit ${String(hookResult.exitCode)}`;
1892
+ const output = hookResult.stderr || hookResult.stdout;
1893
+ return output ? `${label} (${reason}): ${output}` : `${label} (${reason})`;
1894
+ }
1895
+ /**
1864
1896
  * Compute the plugin (bare package) names and skill names still claimed by
1865
1897
  * SOME integration in the CURRENT lock state, keyed by runtime. Both
1866
1898
  * `deactivate` and `upgrade`'s diff-removal call this AFTER
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",