@alfe.ai/integrations 0.4.0 → 0.4.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 +23 -0
- package/dist/index.js +128 -29
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1223,6 +1223,29 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
1223
1223
|
* the lock is NOT re-entrant, so this stays an `*Unlocked` internal.
|
|
1224
1224
|
*/
|
|
1225
1225
|
private dropSubtreeKeysUnlocked;
|
|
1226
|
+
/**
|
|
1227
|
+
* `openclaw config unset <leafPath>`, UNLOCKED, with a fallback for custom
|
|
1228
|
+
* model providers. Unsetting a single leaf under a custom provider (e.g.
|
|
1229
|
+
* `models.providers.zhipu.baseUrl`) is REJECTED by schema validation — a
|
|
1230
|
+
* custom provider without baseUrl is an invalid partial — so the leaf is
|
|
1231
|
+
* never removed and stale/broken config leaks on every removal cycle. On that
|
|
1232
|
+
* specific validation failure, fall back to unsetting the IMMEDIATE parent
|
|
1233
|
+
* subtree (`models.providers.zhipu`), which validates cleanly and removes the
|
|
1234
|
+
* provider whole. The rejection is itself the signal that our key is
|
|
1235
|
+
* STRUCTURAL to the provider (the object can't validly survive without it), so
|
|
1236
|
+
* removing the provider node is the correct terminal state — and we only ever
|
|
1237
|
+
* escalate to the immediate parent, never a wider path.
|
|
1238
|
+
*
|
|
1239
|
+
* `unsetParents` dedupes across the sibling leaves of one removal pass: once
|
|
1240
|
+
* the provider subtree is unset for `models.providers.zhipu.baseUrl`, the
|
|
1241
|
+
* follow-up `.apiKey` / `.models` leaves skip re-issuing the parent unset.
|
|
1242
|
+
*
|
|
1243
|
+
* Warn-tolerant (a failed unset of an already-gone key must never fail the
|
|
1244
|
+
* caller) and assumes the shared CLI lock is held — stays an `*Unlocked`
|
|
1245
|
+
* internal since the lock is NOT re-entrant. Shared by `removeConfig`
|
|
1246
|
+
* (whole-integration teardown) and `applyConfig`'s stale-leaf diff.
|
|
1247
|
+
*/
|
|
1248
|
+
private unsetLeafPathUnlocked;
|
|
1226
1249
|
/**
|
|
1227
1250
|
* Raw single-key config write — `openclaw config set <key> <value>`.
|
|
1228
1251
|
*
|
package/dist/index.js
CHANGED
|
@@ -1302,15 +1302,27 @@ var IntegrationManager = class {
|
|
|
1302
1302
|
}
|
|
1303
1303
|
if (pluginFailures.length > 0 && pluginFailures.length === plugins.length) throw new Error(`All plugins failed to install: ${pluginFailures.join(", ")}`);
|
|
1304
1304
|
if (pluginFailures.length > 0) this.log.warn(`Continuing activation with ${String(pluginFailures.length)} failed plugin(s): ${pluginFailures.join(", ")}`);
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1305
|
+
const skillFailures = [];
|
|
1306
|
+
for (const skill of skills) {
|
|
1307
|
+
const skillLabel = skill.clawhub ?? skill.path ?? "unknown";
|
|
1308
|
+
try {
|
|
1309
|
+
if (skill.clawhub) {
|
|
1310
|
+
this.log.info(`Installing ClawHub skill ${skill.clawhub} to ${runtimeName}`);
|
|
1311
|
+
await applier.applyClawHubSkill(skill.clawhub);
|
|
1312
|
+
} else if (skill.path) {
|
|
1313
|
+
const skillName = skill.path.split("/").pop() ?? skill.path;
|
|
1314
|
+
const srcPath = join(installPath, skill.path);
|
|
1315
|
+
this.log.info(`Applying skill ${skillName} to ${runtimeName}`);
|
|
1316
|
+
await applier.applySkill(skillName, srcPath);
|
|
1317
|
+
}
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1320
|
+
this.log.error(`Failed to apply skill ${skillLabel}: ${msg}`);
|
|
1321
|
+
skillFailures.push(skillLabel);
|
|
1322
|
+
}
|
|
1313
1323
|
}
|
|
1324
|
+
if (skillFailures.length > 0 && skillFailures.length === skills.length) throw new Error(`All skills failed to install: ${skillFailures.join(", ")}`);
|
|
1325
|
+
if (skillFailures.length > 0) this.log.warn(`Continuing activation with ${String(skillFailures.length)} failed skill(s): ${skillFailures.join(", ")}`);
|
|
1314
1326
|
let runtimeConfigApplied = false;
|
|
1315
1327
|
if (runtimeConfig && Object.keys(runtimeConfig).length > 0) {
|
|
1316
1328
|
const agentConfig = entry.config;
|
|
@@ -2113,6 +2125,22 @@ const BUNDLED_BASELINE_ALLOW = ["browser"];
|
|
|
2113
2125
|
const CONFIG_SET_RETRIES$1 = 3;
|
|
2114
2126
|
const CONFIG_SET_RETRY_DELAY_MS$1 = 750;
|
|
2115
2127
|
/**
|
|
2128
|
+
* Boot-safe exec timeout for the OUT-OF-BAND `setConfigRaw` write (the daemon's
|
|
2129
|
+
* config-reconcile pass / `alfe.config_set`). Unlike integration `applyConfig`,
|
|
2130
|
+
* which runs under the gateway RuntimeGate with the runtime SUSPENDED (no CPU
|
|
2131
|
+
* contention, so the 10s execFile default is plenty), `setConfigRaw` fires while
|
|
2132
|
+
* `openclaw gateway run` is live and — on a fresh managed box — still BOOTING.
|
|
2133
|
+
* On a 2-vCPU Hetzner cx-class instance the `openclaw config set` CLI cold-start
|
|
2134
|
+
* takes >10s under that boot contention, so the 10s default SIGKILLs the child
|
|
2135
|
+
* mid-write and every retry lands inside the same ~30s boot window and times out
|
|
2136
|
+
* identically (observed in prod: two `config set agents.defaults.model` attempts
|
|
2137
|
+
* logged exactly 10s apart with no exit line, then a 3rd succeeded once load
|
|
2138
|
+
* eased). 60s clears the cold-start-under-contention worst case while still
|
|
2139
|
+
* bounding a genuinely hung CLI. This is ONLY for `setConfigRaw` — the
|
|
2140
|
+
* integration `applyConfig` paths keep the 10s default (they're not contended).
|
|
2141
|
+
*/
|
|
2142
|
+
const CONFIG_SET_RAW_TIMEOUT_MS = 6e4;
|
|
2143
|
+
/**
|
|
2116
2144
|
* Max `openclaw config get` spawns in flight at once during the batch
|
|
2117
2145
|
* verify-after-write. A naive `Promise.all(leaves.map(...))` fires one CLI
|
|
2118
2146
|
* process per leaf simultaneously — a 36-leaf model-provider batch on a 2-vCPU
|
|
@@ -2166,6 +2194,22 @@ function isMalformedStateDbError(err) {
|
|
|
2166
2194
|
const e = err;
|
|
2167
2195
|
return `${e.message}\n${e.stderr ?? ""}\n${e.stdout ?? ""}`.toLowerCase().includes(MALFORMED_STATE_DB_PHRASE);
|
|
2168
2196
|
}
|
|
2197
|
+
/**
|
|
2198
|
+
* OpenClaw rejects `config unset` of a single leaf under a CUSTOM model provider
|
|
2199
|
+
* when the removal would leave a schema-invalid partial — e.g. unsetting
|
|
2200
|
+
* `models.providers.zhipu.baseUrl` fails with "custom model providers must
|
|
2201
|
+
* declare baseUrl; provider overlays without baseUrl are only supported for
|
|
2202
|
+
* bundled providers." Unsetting the WHOLE provider subtree
|
|
2203
|
+
* (`models.providers.zhipu`) validates cleanly. Detect this failure so the
|
|
2204
|
+
* removal path can fall back to unsetting the parent subtree instead of leaving
|
|
2205
|
+
* stale/broken config behind. The phrase is stable and specific enough to match
|
|
2206
|
+
* directly (case-insensitive); it covers baseUrl and any other required
|
|
2207
|
+
* structural field ("...must declare <field>").
|
|
2208
|
+
*/
|
|
2209
|
+
const INVALID_PARTIAL_PROVIDER_PHRASE = "custom model providers must declare";
|
|
2210
|
+
function isInvalidPartialProviderUnset(err) {
|
|
2211
|
+
return (err instanceof Error ? err.message : String(err)).toLowerCase().includes(INVALID_PARTIAL_PROVIDER_PHRASE);
|
|
2212
|
+
}
|
|
2169
2213
|
const delay$1 = (ms) => new Promise((resolve) => {
|
|
2170
2214
|
setTimeout(resolve, ms);
|
|
2171
2215
|
});
|
|
@@ -2650,23 +2694,39 @@ var OpenClawApplier = class {
|
|
|
2650
2694
|
async ensurePluginsAllowUnlocked(pkgs) {
|
|
2651
2695
|
const wanted = Array.isArray(pkgs) ? pkgs : [pkgs];
|
|
2652
2696
|
let currentAllow = [];
|
|
2697
|
+
let readTrustworthy = true;
|
|
2653
2698
|
try {
|
|
2654
2699
|
const { stdout } = await execFileAsync$1("openclaw", [
|
|
2655
2700
|
"config",
|
|
2656
2701
|
"get",
|
|
2657
2702
|
"plugins.allow"
|
|
2658
2703
|
], { timeout: 1e4 });
|
|
2659
|
-
const
|
|
2660
|
-
if (
|
|
2661
|
-
|
|
2704
|
+
const trimmed = stdout.trim();
|
|
2705
|
+
if (trimmed === "") {} else try {
|
|
2706
|
+
const parsed = JSON.parse(trimmed);
|
|
2707
|
+
if (Array.isArray(parsed)) currentAllow = parsed;
|
|
2708
|
+
else if (parsed === null) {} else readTrustworthy = false;
|
|
2709
|
+
} catch {
|
|
2710
|
+
readTrustworthy = false;
|
|
2711
|
+
}
|
|
2712
|
+
} catch {
|
|
2713
|
+
readTrustworthy = false;
|
|
2714
|
+
}
|
|
2662
2715
|
const missing = [...new Set([...wanted, ...BUNDLED_BASELINE_ALLOW])].filter((id) => !currentAllow.includes(id));
|
|
2663
2716
|
if (missing.length === 0) return;
|
|
2717
|
+
if (!readTrustworthy) {
|
|
2718
|
+
log$3.warn({
|
|
2719
|
+
pkgs: wanted,
|
|
2720
|
+
missing
|
|
2721
|
+
}, "plugins.allow read was not trustworthy (non-empty parse failure or command error) — skipping the allow-list write to avoid replacing a value not proven to be a superset");
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2664
2724
|
const updated = [...currentAllow, ...missing];
|
|
2665
2725
|
try {
|
|
2666
2726
|
await this.runConfigSetUnlocked([
|
|
2667
2727
|
"plugins.allow",
|
|
2668
2728
|
JSON.stringify(updated),
|
|
2669
|
-
"--
|
|
2729
|
+
"--replace"
|
|
2670
2730
|
]);
|
|
2671
2731
|
} catch (err) {
|
|
2672
2732
|
log$3.warn({
|
|
@@ -2811,6 +2871,7 @@ var OpenClawApplier = class {
|
|
|
2811
2871
|
slug,
|
|
2812
2872
|
err: msg
|
|
2813
2873
|
}, "ClawHub skill install failed");
|
|
2874
|
+
throw new Error(`ClawHub skill install failed for "${slug}": ${msg}`);
|
|
2814
2875
|
}
|
|
2815
2876
|
});
|
|
2816
2877
|
}
|
|
@@ -2851,14 +2912,8 @@ var OpenClawApplier = class {
|
|
|
2851
2912
|
const prev = partitionEntries(flattenConfig(previous));
|
|
2852
2913
|
const nextLeafPaths = new Set(leaves.map((l) => l.path));
|
|
2853
2914
|
const staleLeaves = prev.leaves.filter((l) => !nextLeafPaths.has(l.path));
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
} catch (err) {
|
|
2857
|
-
log$3.warn({
|
|
2858
|
-
err: err instanceof Error ? err.message : String(err),
|
|
2859
|
-
path
|
|
2860
|
-
}, "Failed to unset stale config leaf during applyConfig diff");
|
|
2861
|
-
}
|
|
2915
|
+
const unsetParents = /* @__PURE__ */ new Set();
|
|
2916
|
+
for (const { path } of staleLeaves) await this.unsetLeafPathUnlocked(path, unsetParents);
|
|
2862
2917
|
for (const [parentPath, prevKvs] of prev.subtreesByParent) {
|
|
2863
2918
|
const nextKvs = subtreesByParent.get(parentPath);
|
|
2864
2919
|
const goneKeys = [...prevKvs.keys()].filter((k) => !nextKvs?.has(k));
|
|
@@ -2926,14 +2981,8 @@ var OpenClawApplier = class {
|
|
|
2926
2981
|
if (!(integrationId in integrations)) return;
|
|
2927
2982
|
const integrationConfig = integrations[integrationId];
|
|
2928
2983
|
const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
} catch (err) {
|
|
2932
|
-
log$3.warn({
|
|
2933
|
-
err: err instanceof Error ? err.message : String(err),
|
|
2934
|
-
path
|
|
2935
|
-
}, "Failed to unset config via openclaw config unset");
|
|
2936
|
-
}
|
|
2984
|
+
const unsetParents = /* @__PURE__ */ new Set();
|
|
2985
|
+
for (const { path } of leaves) await this.unsetLeafPathUnlocked(path, unsetParents);
|
|
2937
2986
|
for (const [parentPath, dottedKvs] of subtreesByParent) await this.dropSubtreeKeysUnlocked(parentPath, new Set(dottedKvs.keys()));
|
|
2938
2987
|
tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
|
|
2939
2988
|
this.writeTracking(tracking);
|
|
@@ -2968,6 +3017,56 @@ var OpenClawApplier = class {
|
|
|
2968
3017
|
}
|
|
2969
3018
|
}
|
|
2970
3019
|
/**
|
|
3020
|
+
* `openclaw config unset <leafPath>`, UNLOCKED, with a fallback for custom
|
|
3021
|
+
* model providers. Unsetting a single leaf under a custom provider (e.g.
|
|
3022
|
+
* `models.providers.zhipu.baseUrl`) is REJECTED by schema validation — a
|
|
3023
|
+
* custom provider without baseUrl is an invalid partial — so the leaf is
|
|
3024
|
+
* never removed and stale/broken config leaks on every removal cycle. On that
|
|
3025
|
+
* specific validation failure, fall back to unsetting the IMMEDIATE parent
|
|
3026
|
+
* subtree (`models.providers.zhipu`), which validates cleanly and removes the
|
|
3027
|
+
* provider whole. The rejection is itself the signal that our key is
|
|
3028
|
+
* STRUCTURAL to the provider (the object can't validly survive without it), so
|
|
3029
|
+
* removing the provider node is the correct terminal state — and we only ever
|
|
3030
|
+
* escalate to the immediate parent, never a wider path.
|
|
3031
|
+
*
|
|
3032
|
+
* `unsetParents` dedupes across the sibling leaves of one removal pass: once
|
|
3033
|
+
* the provider subtree is unset for `models.providers.zhipu.baseUrl`, the
|
|
3034
|
+
* follow-up `.apiKey` / `.models` leaves skip re-issuing the parent unset.
|
|
3035
|
+
*
|
|
3036
|
+
* Warn-tolerant (a failed unset of an already-gone key must never fail the
|
|
3037
|
+
* caller) and assumes the shared CLI lock is held — stays an `*Unlocked`
|
|
3038
|
+
* internal since the lock is NOT re-entrant. Shared by `removeConfig`
|
|
3039
|
+
* (whole-integration teardown) and `applyConfig`'s stale-leaf diff.
|
|
3040
|
+
*/
|
|
3041
|
+
async unsetLeafPathUnlocked(path, unsetParents) {
|
|
3042
|
+
try {
|
|
3043
|
+
await this.runConfigCommandUnlocked(["unset", path]);
|
|
3044
|
+
} catch (err) {
|
|
3045
|
+
const parentPath = path.includes(".") ? path.slice(0, path.lastIndexOf(".")) : void 0;
|
|
3046
|
+
if (parentPath && isInvalidPartialProviderUnset(err)) {
|
|
3047
|
+
if (unsetParents.has(parentPath)) return;
|
|
3048
|
+
unsetParents.add(parentPath);
|
|
3049
|
+
try {
|
|
3050
|
+
await this.runConfigCommandUnlocked(["unset", parentPath]);
|
|
3051
|
+
log$3.warn({
|
|
3052
|
+
path,
|
|
3053
|
+
parentPath
|
|
3054
|
+
}, "Leaf unset rejected as an invalid partial custom provider — unset the parent subtree instead");
|
|
3055
|
+
} catch (parentErr) {
|
|
3056
|
+
log$3.warn({
|
|
3057
|
+
err: parentErr instanceof Error ? parentErr.message : String(parentErr),
|
|
3058
|
+
parentPath
|
|
3059
|
+
}, "Failed to unset parent subtree after an invalid-partial leaf unset");
|
|
3060
|
+
}
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
log$3.warn({
|
|
3064
|
+
err: err instanceof Error ? err.message : String(err),
|
|
3065
|
+
path
|
|
3066
|
+
}, "Failed to unset config via openclaw config unset");
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
/**
|
|
2971
3070
|
* Raw single-key config write — `openclaw config set <key> <value>`.
|
|
2972
3071
|
*
|
|
2973
3072
|
* Deliberately bypasses the `_integrations` tracking that `applyConfig`
|
|
@@ -2978,7 +3077,7 @@ var OpenClawApplier = class {
|
|
|
2978
3077
|
* config writes trigger.
|
|
2979
3078
|
*/
|
|
2980
3079
|
setConfigRaw(key, value) {
|
|
2981
|
-
return this.cliLock.run(() => this.runConfigSetUnlocked([key, value]));
|
|
3080
|
+
return this.cliLock.run(() => this.runConfigSetUnlocked([key, value], { timeout: CONFIG_SET_RAW_TIMEOUT_MS }));
|
|
2982
3081
|
}
|
|
2983
3082
|
/**
|
|
2984
3083
|
* Read a single config key back — `openclaw config get <key>` — and normalize
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/integrations",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"@auriclabs/logger": "^0.1.1",
|
|
16
16
|
"yaml": ">=2.8.3",
|
|
17
17
|
"@alfe.ai/integration-manifest": "^0.3.1",
|
|
18
|
-
"@alfe.ai/mcp-bundler": "^0.3.
|
|
18
|
+
"@alfe.ai/mcp-bundler": "^0.3.1"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|