@alfe.ai/integrations 0.3.0 → 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 +55 -23
- package/dist/index.js +116 -30
- package/package.json +1 -1
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
|
-
/**
|
|
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
|
|
@@ -689,6 +697,13 @@ declare class IntegrationManager {
|
|
|
689
697
|
clear(): void;
|
|
690
698
|
private checkHealth;
|
|
691
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;
|
|
692
707
|
/**
|
|
693
708
|
* Compute the plugin (bare package) names and skill names still claimed by
|
|
694
709
|
* SOME integration in the CURRENT lock state, keyed by runtime. Both
|
|
@@ -778,31 +793,16 @@ declare class StateManager {
|
|
|
778
793
|
}
|
|
779
794
|
//#endregion
|
|
780
795
|
//#region src/hooks.d.ts
|
|
781
|
-
/**
|
|
782
|
-
* Hook Runner — executes integration lifecycle hook scripts.
|
|
783
|
-
*
|
|
784
|
-
* Hooks are scripts defined in the integration manifest. The runner
|
|
785
|
-
* auto-detects the interpreter from the script's shebang line or file
|
|
786
|
-
* extension (.js/.mjs → node, .py → python3, default → bash).
|
|
787
|
-
*
|
|
788
|
-
* Scripts run as child processes with a 30-second timeout.
|
|
789
|
-
* stdout/stderr are captured and returned.
|
|
790
|
-
*
|
|
791
|
-
* The runner injects standard environment variables:
|
|
792
|
-
* - ALFE_INTEGRATION_DIR — path to the integration install directory
|
|
793
|
-
* - ALFE_STATE_DIR — path to the integration state directory (created if needed)
|
|
794
|
-
* - ALFE_<NAME>_<KEY> — config values (non-secret AND secret, both injected as env vars)
|
|
795
|
-
*
|
|
796
|
-
* Secrets are acceptable as env vars because:
|
|
797
|
-
* - Child process env is ephemeral (dies with the process)
|
|
798
|
-
* - Same security model as Docker secrets / systemd credentials
|
|
799
|
-
* - Never written to disk
|
|
800
|
-
*/
|
|
801
796
|
interface HookResult {
|
|
802
797
|
exitCode: number;
|
|
803
798
|
stdout: string;
|
|
804
799
|
stderr: string;
|
|
805
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;
|
|
806
806
|
}
|
|
807
807
|
interface HookEnvOptions {
|
|
808
808
|
/** Integration name (e.g. "voice") */
|
|
@@ -849,9 +849,12 @@ declare function buildHookEnv(options: HookEnvOptions, additionalEnv?: Record<st
|
|
|
849
849
|
* @param integrationPath - Base path of the integration (where alfe-integration.yaml lives)
|
|
850
850
|
* @param hookScript - Relative path to the hook script (e.g. "scripts/activate.sh")
|
|
851
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}.
|
|
852
855
|
* @returns Hook execution result
|
|
853
856
|
*/
|
|
854
|
-
declare function runHook(integrationPath: string, hookScript: string, env?: Record<string, string
|
|
857
|
+
declare function runHook(integrationPath: string, hookScript: string, env?: Record<string, string>, timeoutMs?: number): Promise<HookResult>;
|
|
855
858
|
/**
|
|
856
859
|
* Run a hook script with full integration context (config + secrets as env vars).
|
|
857
860
|
*
|
|
@@ -862,9 +865,12 @@ declare function runHook(integrationPath: string, hookScript: string, env?: Reco
|
|
|
862
865
|
* @param integrationPath - Base path of the integration
|
|
863
866
|
* @param hookScript - Relative path to the hook script
|
|
864
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}.
|
|
865
871
|
* @returns Hook execution result
|
|
866
872
|
*/
|
|
867
|
-
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>;
|
|
868
874
|
//#endregion
|
|
869
875
|
//#region src/openclaw-cli-lock.d.ts
|
|
870
876
|
/**
|
|
@@ -995,6 +1001,24 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
995
1001
|
* this instance performed (0 = never). See HEAL_MIN_INTERVAL_MS.
|
|
996
1002
|
*/
|
|
997
1003
|
private lastHealAt;
|
|
1004
|
+
/**
|
|
1005
|
+
* Cross-integration plugin-pin claims made during THIS process lifetime,
|
|
1006
|
+
* keyed by bare package name. `applyPlugin` is now version-aware for both
|
|
1007
|
+
* force and non-force callers — it actively reinstalls a plugin whose
|
|
1008
|
+
* installed version differs from the pinned spec (previously the non-force
|
|
1009
|
+
* path keyed only on bare-name presence and silently ignored a pin bump). If
|
|
1010
|
+
* two integrations pin the SAME plugin at DIFFERENT versions, that
|
|
1011
|
+
* version-aware reinstall would make them fight — each reconcile pass
|
|
1012
|
+
* uninstall+reinstalls the other's version forever. This map records the
|
|
1013
|
+
* first-applied pin per package so a later, divergent pin from a DIFFERENT
|
|
1014
|
+
* integration is refused (first-writer-wins) with a WARN instead of thrashing.
|
|
1015
|
+
* A later apply from the SAME integration (a legitimate pin bump across an
|
|
1016
|
+
* upgrade) is NOT a conflict and proceeds. Cleared per-package on
|
|
1017
|
+
* `removePlugin`, and wholesale on daemon restart. Convention is single-owner
|
|
1018
|
+
* per plugin (see the `alfe` manifest note) — this only turns a convention
|
|
1019
|
+
* violation into a stable warning.
|
|
1020
|
+
*/
|
|
1021
|
+
private readonly appliedPluginPins;
|
|
998
1022
|
constructor(options: OpenClawApplierOptions);
|
|
999
1023
|
/**
|
|
1000
1024
|
* Convenience: `openclaw config set <args>`, UNLOCKED + retried.
|
|
@@ -1080,8 +1104,16 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
1080
1104
|
private healMalformedStateDb;
|
|
1081
1105
|
applyPlugin(spec: string, _installPath?: string, opts?: {
|
|
1082
1106
|
force?: boolean;
|
|
1107
|
+
integrationId?: string;
|
|
1083
1108
|
}): Promise<void>;
|
|
1084
1109
|
private applyPluginLocked;
|
|
1110
|
+
/**
|
|
1111
|
+
* Record the pin a caller just applied for a package, keyed by bare name, so
|
|
1112
|
+
* a later divergent pin from a DIFFERENT integration in this process can be
|
|
1113
|
+
* refused (first-writer-wins). Bare (versionless) pins are untrackable and
|
|
1114
|
+
* ignored — they can't conflict on version. See `appliedPluginPins`.
|
|
1115
|
+
*/
|
|
1116
|
+
private recordPluginPin;
|
|
1085
1117
|
/**
|
|
1086
1118
|
* Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
|
|
1087
1119
|
* Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
|
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
|
-
},
|
|
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(
|
|
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(
|
|
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",
|
|
@@ -1271,7 +1290,10 @@ var IntegrationManager = class {
|
|
|
1271
1290
|
for (const plugin of plugins) {
|
|
1272
1291
|
this.log.info(`Applying plugin ${plugin.package} to ${runtimeName}`);
|
|
1273
1292
|
try {
|
|
1274
|
-
await applier.applyPlugin(plugin.package, installPath, {
|
|
1293
|
+
await applier.applyPlugin(plugin.package, installPath, {
|
|
1294
|
+
force: opts?.forcePlugins,
|
|
1295
|
+
integrationId
|
|
1296
|
+
});
|
|
1275
1297
|
} catch (err) {
|
|
1276
1298
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1277
1299
|
this.log.error(`Failed to apply plugin ${plugin.package}: ${msg}`);
|
|
@@ -1325,8 +1347,9 @@ var IntegrationManager = class {
|
|
|
1325
1347
|
runtimes: [...this.runtimeAppliers.keys()]
|
|
1326
1348
|
});
|
|
1327
1349
|
if (hookResult.exitCode !== 0) {
|
|
1328
|
-
this.
|
|
1329
|
-
|
|
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);
|
|
1330
1353
|
}
|
|
1331
1354
|
}
|
|
1332
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(", ")})`);
|
|
@@ -1339,8 +1362,9 @@ var IntegrationManager = class {
|
|
|
1339
1362
|
runtimes: [...this.runtimeAppliers.keys()]
|
|
1340
1363
|
});
|
|
1341
1364
|
if (hookResult.exitCode !== 0) {
|
|
1342
|
-
this.
|
|
1343
|
-
|
|
1365
|
+
const message = this.hookFailureMessage("Health check failed", hookResult);
|
|
1366
|
+
this.state.setStatus(integrationId, "error", message);
|
|
1367
|
+
return this.err("HEALTH_CHECK_FAILED", message);
|
|
1344
1368
|
}
|
|
1345
1369
|
}
|
|
1346
1370
|
this.log.info(`Integration "${integrationId}" activated`);
|
|
@@ -1482,8 +1506,8 @@ var IntegrationManager = class {
|
|
|
1482
1506
|
config: entry.config,
|
|
1483
1507
|
secrets: this.secrets.get(name),
|
|
1484
1508
|
runtimes: [...this.runtimeAppliers.keys()]
|
|
1485
|
-
});
|
|
1486
|
-
if (hookResult.exitCode !== 0) this.log.warn(
|
|
1509
|
+
}, INSTALL_HOOK_TIMEOUT_MS);
|
|
1510
|
+
if (hookResult.exitCode !== 0) this.log.warn(this.hookFailureMessage("pre_uninstall hook failed (continuing)", hookResult));
|
|
1487
1511
|
}
|
|
1488
1512
|
if (uninstallHooksSupported && manifest?.hooks.post_uninstall) {
|
|
1489
1513
|
this.log.info(`Running post_uninstall hook: ${manifest.hooks.post_uninstall}`);
|
|
@@ -1492,8 +1516,8 @@ var IntegrationManager = class {
|
|
|
1492
1516
|
config: entry.config,
|
|
1493
1517
|
secrets: this.secrets.get(name),
|
|
1494
1518
|
runtimes: [...this.runtimeAppliers.keys()]
|
|
1495
|
-
});
|
|
1496
|
-
if (hookResult.exitCode !== 0) this.log.warn(
|
|
1519
|
+
}, INSTALL_HOOK_TIMEOUT_MS);
|
|
1520
|
+
if (hookResult.exitCode !== 0) this.log.warn(this.hookFailureMessage("post_uninstall hook failed (non-fatal)", hookResult));
|
|
1497
1521
|
}
|
|
1498
1522
|
if (this.installer.isInstalled(name)) {
|
|
1499
1523
|
await this.installer.remove(name);
|
|
@@ -1644,8 +1668,8 @@ var IntegrationManager = class {
|
|
|
1644
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(", ")})`);
|
|
1645
1669
|
if (installHooksSupported && newManifest.hooks.pre_install) {
|
|
1646
1670
|
this.log.info(`Running pre_install hook: ${newManifest.hooks.pre_install}`);
|
|
1647
|
-
const hookResult = await runHook(this.installer.getInstallPath(name), newManifest.hooks.pre_install);
|
|
1648
|
-
if (hookResult.exitCode !== 0) throw new Error(
|
|
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));
|
|
1649
1673
|
}
|
|
1650
1674
|
if (installHooksSupported && newManifest.hooks.post_install) {
|
|
1651
1675
|
this.log.info(`Running post_install hook: ${newManifest.hooks.post_install}`);
|
|
@@ -1654,8 +1678,8 @@ var IntegrationManager = class {
|
|
|
1654
1678
|
config: config ?? existing.config,
|
|
1655
1679
|
secrets: this.secrets.get(name),
|
|
1656
1680
|
runtimes: [...this.runtimeAppliers.keys()]
|
|
1657
|
-
});
|
|
1658
|
-
if (hookResult.exitCode !== 0) throw new Error(
|
|
1681
|
+
}, INSTALL_HOOK_TIMEOUT_MS);
|
|
1682
|
+
if (hookResult.exitCode !== 0) throw new Error(this.hookFailureMessage("post_install hook failed", hookResult));
|
|
1659
1683
|
}
|
|
1660
1684
|
await this.applyUpgradeDiffRemovals(name, oldManifest, newManifest);
|
|
1661
1685
|
return await this.activate(name, { forcePlugins: true });
|
|
@@ -1834,7 +1858,7 @@ var IntegrationManager = class {
|
|
|
1834
1858
|
healthy: hookResult.exitCode === 0,
|
|
1835
1859
|
status: entry.status,
|
|
1836
1860
|
version: manifest.version,
|
|
1837
|
-
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)})`,
|
|
1838
1862
|
stdout: hookResult.stdout || void 0,
|
|
1839
1863
|
stderr: hookResult.stderr || void 0
|
|
1840
1864
|
};
|
|
@@ -1858,6 +1882,17 @@ var IntegrationManager = class {
|
|
|
1858
1882
|
};
|
|
1859
1883
|
}
|
|
1860
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
|
+
/**
|
|
1861
1896
|
* Compute the plugin (bare package) names and skill names still claimed by
|
|
1862
1897
|
* SOME integration in the CURRENT lock state, keyed by runtime. Both
|
|
1863
1898
|
* `deactivate` and `upgrade`'s diff-removal call this AFTER
|
|
@@ -2301,6 +2336,24 @@ var OpenClawApplier = class {
|
|
|
2301
2336
|
* this instance performed (0 = never). See HEAL_MIN_INTERVAL_MS.
|
|
2302
2337
|
*/
|
|
2303
2338
|
lastHealAt = 0;
|
|
2339
|
+
/**
|
|
2340
|
+
* Cross-integration plugin-pin claims made during THIS process lifetime,
|
|
2341
|
+
* keyed by bare package name. `applyPlugin` is now version-aware for both
|
|
2342
|
+
* force and non-force callers — it actively reinstalls a plugin whose
|
|
2343
|
+
* installed version differs from the pinned spec (previously the non-force
|
|
2344
|
+
* path keyed only on bare-name presence and silently ignored a pin bump). If
|
|
2345
|
+
* two integrations pin the SAME plugin at DIFFERENT versions, that
|
|
2346
|
+
* version-aware reinstall would make them fight — each reconcile pass
|
|
2347
|
+
* uninstall+reinstalls the other's version forever. This map records the
|
|
2348
|
+
* first-applied pin per package so a later, divergent pin from a DIFFERENT
|
|
2349
|
+
* integration is refused (first-writer-wins) with a WARN instead of thrashing.
|
|
2350
|
+
* A later apply from the SAME integration (a legitimate pin bump across an
|
|
2351
|
+
* upgrade) is NOT a conflict and proceeds. Cleared per-package on
|
|
2352
|
+
* `removePlugin`, and wholesale on daemon restart. Convention is single-owner
|
|
2353
|
+
* per plugin (see the `alfe` manifest note) — this only turns a convention
|
|
2354
|
+
* violation into a stable warning.
|
|
2355
|
+
*/
|
|
2356
|
+
appliedPluginPins = /* @__PURE__ */ new Map();
|
|
2304
2357
|
constructor(options) {
|
|
2305
2358
|
const home = options.home ?? options.workspace;
|
|
2306
2359
|
if (!home) throw new Error("OpenClawApplier requires `home` (or legacy `workspace`) option");
|
|
@@ -2484,25 +2537,43 @@ var OpenClawApplier = class {
|
|
|
2484
2537
|
}
|
|
2485
2538
|
async applyPluginLocked(spec, opts) {
|
|
2486
2539
|
const pkg = stripPluginVersion(spec);
|
|
2540
|
+
const pinnedVersion = pluginSpecVersion(spec);
|
|
2541
|
+
const claim = this.appliedPluginPins.get(pkg);
|
|
2542
|
+
if (pinnedVersion !== void 0 && claim !== void 0 && claim.version !== pinnedVersion && claim.integrationId !== opts?.integrationId) {
|
|
2543
|
+
log$3.warn({
|
|
2544
|
+
pkg,
|
|
2545
|
+
requestedVersion: pinnedVersion,
|
|
2546
|
+
keptVersion: claim.version,
|
|
2547
|
+
requestedBy: opts?.integrationId,
|
|
2548
|
+
ownedBy: claim.integrationId
|
|
2549
|
+
}, "Conflicting plugin pin across integrations — keeping the first-applied version (first-writer-wins)");
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2487
2552
|
await this.ensurePluginsAllowUnlocked(pkg);
|
|
2488
2553
|
this.cleanupUntrackedExtensionInstall(pkg);
|
|
2489
|
-
if (
|
|
2490
|
-
const pinnedVersion = pluginSpecVersion(spec);
|
|
2554
|
+
if (this.isPluginInstalled(pkg)) {
|
|
2491
2555
|
const installedVersion = this.installedPluginVersion(pkg);
|
|
2492
|
-
|
|
2493
|
-
|
|
2556
|
+
const versionsComparable = pinnedVersion !== void 0 && installedVersion !== void 0;
|
|
2557
|
+
if (versionsComparable && installedVersion === pinnedVersion) {
|
|
2558
|
+
if (opts?.force) log$3.info({
|
|
2494
2559
|
pkg,
|
|
2495
2560
|
spec,
|
|
2496
2561
|
version: installedVersion
|
|
2497
2562
|
}, "Force mode — installed version already matches pinned spec, skipping uninstall+reinstall");
|
|
2563
|
+
this.recordPluginPin(pkg, pinnedVersion, opts?.integrationId);
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
if (!(versionsComparable && installedVersion !== pinnedVersion) && !opts?.force) {
|
|
2567
|
+
this.recordPluginPin(pkg, pinnedVersion, opts?.integrationId);
|
|
2498
2568
|
return;
|
|
2499
2569
|
}
|
|
2500
2570
|
log$3.info({
|
|
2501
2571
|
pkg,
|
|
2502
2572
|
spec,
|
|
2503
2573
|
installedVersion,
|
|
2504
|
-
pinnedVersion
|
|
2505
|
-
|
|
2574
|
+
pinnedVersion,
|
|
2575
|
+
force: opts?.force ?? false
|
|
2576
|
+
}, "Reinstalling plugin to converge on the pinned version");
|
|
2506
2577
|
try {
|
|
2507
2578
|
await this.removePluginUnlocked(pkg);
|
|
2508
2579
|
} catch (err) {
|
|
@@ -2510,7 +2581,7 @@ var OpenClawApplier = class {
|
|
|
2510
2581
|
pkg,
|
|
2511
2582
|
spec,
|
|
2512
2583
|
err: err instanceof Error ? err.message : String(err)
|
|
2513
|
-
}, "Failed to uninstall plugin
|
|
2584
|
+
}, "Failed to uninstall plugin before reinstall — proceeding");
|
|
2514
2585
|
}
|
|
2515
2586
|
}
|
|
2516
2587
|
if (!this.isPluginInstalled(pkg)) {
|
|
@@ -2547,6 +2618,20 @@ var OpenClawApplier = class {
|
|
|
2547
2618
|
setTimeout(r, 500);
|
|
2548
2619
|
});
|
|
2549
2620
|
}
|
|
2621
|
+
this.recordPluginPin(pkg, pinnedVersion, opts?.integrationId);
|
|
2622
|
+
}
|
|
2623
|
+
/**
|
|
2624
|
+
* Record the pin a caller just applied for a package, keyed by bare name, so
|
|
2625
|
+
* a later divergent pin from a DIFFERENT integration in this process can be
|
|
2626
|
+
* refused (first-writer-wins). Bare (versionless) pins are untrackable and
|
|
2627
|
+
* ignored — they can't conflict on version. See `appliedPluginPins`.
|
|
2628
|
+
*/
|
|
2629
|
+
recordPluginPin(pkg, version, integrationId) {
|
|
2630
|
+
if (version === void 0) return;
|
|
2631
|
+
this.appliedPluginPins.set(pkg, {
|
|
2632
|
+
version,
|
|
2633
|
+
integrationId
|
|
2634
|
+
});
|
|
2550
2635
|
}
|
|
2551
2636
|
/**
|
|
2552
2637
|
* Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
|
|
@@ -2692,6 +2777,7 @@ var OpenClawApplier = class {
|
|
|
2692
2777
|
*/
|
|
2693
2778
|
async removePluginUnlocked(spec) {
|
|
2694
2779
|
const pkg = stripPluginVersion(spec);
|
|
2780
|
+
this.appliedPluginPins.delete(pkg);
|
|
2695
2781
|
await this.execOpenClawHealing([
|
|
2696
2782
|
"plugins",
|
|
2697
2783
|
"uninstall",
|
package/package.json
CHANGED