@alfe.ai/integrations 0.4.0 → 0.4.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
@@ -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
@@ -2166,6 +2166,22 @@ function isMalformedStateDbError(err) {
2166
2166
  const e = err;
2167
2167
  return `${e.message}\n${e.stderr ?? ""}\n${e.stdout ?? ""}`.toLowerCase().includes(MALFORMED_STATE_DB_PHRASE);
2168
2168
  }
2169
+ /**
2170
+ * OpenClaw rejects `config unset` of a single leaf under a CUSTOM model provider
2171
+ * when the removal would leave a schema-invalid partial — e.g. unsetting
2172
+ * `models.providers.zhipu.baseUrl` fails with "custom model providers must
2173
+ * declare baseUrl; provider overlays without baseUrl are only supported for
2174
+ * bundled providers." Unsetting the WHOLE provider subtree
2175
+ * (`models.providers.zhipu`) validates cleanly. Detect this failure so the
2176
+ * removal path can fall back to unsetting the parent subtree instead of leaving
2177
+ * stale/broken config behind. The phrase is stable and specific enough to match
2178
+ * directly (case-insensitive); it covers baseUrl and any other required
2179
+ * structural field ("...must declare <field>").
2180
+ */
2181
+ const INVALID_PARTIAL_PROVIDER_PHRASE = "custom model providers must declare";
2182
+ function isInvalidPartialProviderUnset(err) {
2183
+ return (err instanceof Error ? err.message : String(err)).toLowerCase().includes(INVALID_PARTIAL_PROVIDER_PHRASE);
2184
+ }
2169
2185
  const delay$1 = (ms) => new Promise((resolve) => {
2170
2186
  setTimeout(resolve, ms);
2171
2187
  });
@@ -2650,23 +2666,39 @@ var OpenClawApplier = class {
2650
2666
  async ensurePluginsAllowUnlocked(pkgs) {
2651
2667
  const wanted = Array.isArray(pkgs) ? pkgs : [pkgs];
2652
2668
  let currentAllow = [];
2669
+ let readTrustworthy = true;
2653
2670
  try {
2654
2671
  const { stdout } = await execFileAsync$1("openclaw", [
2655
2672
  "config",
2656
2673
  "get",
2657
2674
  "plugins.allow"
2658
2675
  ], { timeout: 1e4 });
2659
- const parsed = JSON.parse(stdout.trim());
2660
- if (Array.isArray(parsed)) currentAllow = parsed;
2661
- } catch {}
2676
+ const trimmed = stdout.trim();
2677
+ if (trimmed === "") {} else try {
2678
+ const parsed = JSON.parse(trimmed);
2679
+ if (Array.isArray(parsed)) currentAllow = parsed;
2680
+ else if (parsed === null) {} else readTrustworthy = false;
2681
+ } catch {
2682
+ readTrustworthy = false;
2683
+ }
2684
+ } catch {
2685
+ readTrustworthy = false;
2686
+ }
2662
2687
  const missing = [...new Set([...wanted, ...BUNDLED_BASELINE_ALLOW])].filter((id) => !currentAllow.includes(id));
2663
2688
  if (missing.length === 0) return;
2689
+ if (!readTrustworthy) {
2690
+ log$3.warn({
2691
+ pkgs: wanted,
2692
+ missing
2693
+ }, "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");
2694
+ return;
2695
+ }
2664
2696
  const updated = [...currentAllow, ...missing];
2665
2697
  try {
2666
2698
  await this.runConfigSetUnlocked([
2667
2699
  "plugins.allow",
2668
2700
  JSON.stringify(updated),
2669
- "--merge"
2701
+ "--replace"
2670
2702
  ]);
2671
2703
  } catch (err) {
2672
2704
  log$3.warn({
@@ -2851,14 +2883,8 @@ var OpenClawApplier = class {
2851
2883
  const prev = partitionEntries(flattenConfig(previous));
2852
2884
  const nextLeafPaths = new Set(leaves.map((l) => l.path));
2853
2885
  const staleLeaves = prev.leaves.filter((l) => !nextLeafPaths.has(l.path));
2854
- for (const { path } of staleLeaves) try {
2855
- await this.runConfigCommandUnlocked(["unset", path]);
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
- }
2886
+ const unsetParents = /* @__PURE__ */ new Set();
2887
+ for (const { path } of staleLeaves) await this.unsetLeafPathUnlocked(path, unsetParents);
2862
2888
  for (const [parentPath, prevKvs] of prev.subtreesByParent) {
2863
2889
  const nextKvs = subtreesByParent.get(parentPath);
2864
2890
  const goneKeys = [...prevKvs.keys()].filter((k) => !nextKvs?.has(k));
@@ -2926,14 +2952,8 @@ var OpenClawApplier = class {
2926
2952
  if (!(integrationId in integrations)) return;
2927
2953
  const integrationConfig = integrations[integrationId];
2928
2954
  const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
2929
- for (const { path } of leaves) try {
2930
- await this.runConfigCommandUnlocked(["unset", path]);
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
- }
2955
+ const unsetParents = /* @__PURE__ */ new Set();
2956
+ for (const { path } of leaves) await this.unsetLeafPathUnlocked(path, unsetParents);
2937
2957
  for (const [parentPath, dottedKvs] of subtreesByParent) await this.dropSubtreeKeysUnlocked(parentPath, new Set(dottedKvs.keys()));
2938
2958
  tracking._integrations = Object.fromEntries(Object.entries(integrations).filter(([key]) => key !== integrationId));
2939
2959
  this.writeTracking(tracking);
@@ -2968,6 +2988,56 @@ var OpenClawApplier = class {
2968
2988
  }
2969
2989
  }
2970
2990
  /**
2991
+ * `openclaw config unset <leafPath>`, UNLOCKED, with a fallback for custom
2992
+ * model providers. Unsetting a single leaf under a custom provider (e.g.
2993
+ * `models.providers.zhipu.baseUrl`) is REJECTED by schema validation — a
2994
+ * custom provider without baseUrl is an invalid partial — so the leaf is
2995
+ * never removed and stale/broken config leaks on every removal cycle. On that
2996
+ * specific validation failure, fall back to unsetting the IMMEDIATE parent
2997
+ * subtree (`models.providers.zhipu`), which validates cleanly and removes the
2998
+ * provider whole. The rejection is itself the signal that our key is
2999
+ * STRUCTURAL to the provider (the object can't validly survive without it), so
3000
+ * removing the provider node is the correct terminal state — and we only ever
3001
+ * escalate to the immediate parent, never a wider path.
3002
+ *
3003
+ * `unsetParents` dedupes across the sibling leaves of one removal pass: once
3004
+ * the provider subtree is unset for `models.providers.zhipu.baseUrl`, the
3005
+ * follow-up `.apiKey` / `.models` leaves skip re-issuing the parent unset.
3006
+ *
3007
+ * Warn-tolerant (a failed unset of an already-gone key must never fail the
3008
+ * caller) and assumes the shared CLI lock is held — stays an `*Unlocked`
3009
+ * internal since the lock is NOT re-entrant. Shared by `removeConfig`
3010
+ * (whole-integration teardown) and `applyConfig`'s stale-leaf diff.
3011
+ */
3012
+ async unsetLeafPathUnlocked(path, unsetParents) {
3013
+ try {
3014
+ await this.runConfigCommandUnlocked(["unset", path]);
3015
+ } catch (err) {
3016
+ const parentPath = path.includes(".") ? path.slice(0, path.lastIndexOf(".")) : void 0;
3017
+ if (parentPath && isInvalidPartialProviderUnset(err)) {
3018
+ if (unsetParents.has(parentPath)) return;
3019
+ unsetParents.add(parentPath);
3020
+ try {
3021
+ await this.runConfigCommandUnlocked(["unset", parentPath]);
3022
+ log$3.warn({
3023
+ path,
3024
+ parentPath
3025
+ }, "Leaf unset rejected as an invalid partial custom provider — unset the parent subtree instead");
3026
+ } catch (parentErr) {
3027
+ log$3.warn({
3028
+ err: parentErr instanceof Error ? parentErr.message : String(parentErr),
3029
+ parentPath
3030
+ }, "Failed to unset parent subtree after an invalid-partial leaf unset");
3031
+ }
3032
+ return;
3033
+ }
3034
+ log$3.warn({
3035
+ err: err instanceof Error ? err.message : String(err),
3036
+ path
3037
+ }, "Failed to unset config via openclaw config unset");
3038
+ }
3039
+ }
3040
+ /**
2971
3041
  * Raw single-key config write — `openclaw config set <key> <value>`.
2972
3042
  *
2973
3043
  * Deliberately bypasses the `_integrations` tracking that `applyConfig`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/integrations",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Integration lifecycle management for Alfe — registry, resolution, installation, and state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",