@alfe.ai/integrations 0.1.5 → 0.1.6
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 +41 -1
- package/dist/index.js +108 -39
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -287,6 +287,13 @@ interface RuntimeApplier {
|
|
|
287
287
|
applyPlugin(pkg: string, integrationInstallPath: string, opts?: {
|
|
288
288
|
force?: boolean;
|
|
289
289
|
}): Promise<void>;
|
|
290
|
+
/**
|
|
291
|
+
* Optional: pre-trust an integration's full plugin set in one write before
|
|
292
|
+
* the per-plugin install loop. Runtimes whose config writes trigger a reload
|
|
293
|
+
* (OpenClaw) implement this to collapse N allowlist writes into one. Absent
|
|
294
|
+
* → the manager relies on `applyPlugin` to allowlist each plugin itself.
|
|
295
|
+
*/
|
|
296
|
+
ensurePluginsAllowed?(pkgs: string[]): Promise<void>;
|
|
290
297
|
/** Remove a plugin package from this runtime */
|
|
291
298
|
removePlugin(pkg: string): Promise<void>;
|
|
292
299
|
/** Copy a skill directory into this runtime's skills location */
|
|
@@ -748,6 +755,10 @@ interface OpenClawApplierOptions {
|
|
|
748
755
|
skillsDir?: string;
|
|
749
756
|
/** Path to the integration tracking file (defaults to {home}/config.json) */
|
|
750
757
|
configPath?: string;
|
|
758
|
+
/** Max retries for a transient `openclaw config set` failure (default 3) */
|
|
759
|
+
configSetRetries?: number;
|
|
760
|
+
/** Backoff between `openclaw config set` retries, ms (default 750; set 0 in tests) */
|
|
761
|
+
configSetRetryDelayMs?: number;
|
|
751
762
|
}
|
|
752
763
|
declare class OpenClawApplier implements RuntimeApplier {
|
|
753
764
|
readonly runtime = "openclaw";
|
|
@@ -755,15 +766,44 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
755
766
|
private agentWorkspace;
|
|
756
767
|
private skillsDir;
|
|
757
768
|
private trackingPath;
|
|
769
|
+
private configSetRetries;
|
|
770
|
+
private configSetRetryDelayMs;
|
|
771
|
+
/** Serializes all `openclaw config set` writes so they never interleave with each other or the hot-reload they trigger. */
|
|
772
|
+
private configSetQueue;
|
|
758
773
|
constructor(options: OpenClawApplierOptions);
|
|
774
|
+
/** Convenience: `openclaw config set <args>`, serialized + retried. */
|
|
775
|
+
private runConfigSet;
|
|
776
|
+
/**
|
|
777
|
+
* Run `openclaw config <args>` (set/unset), serialized against every other
|
|
778
|
+
* config write and retried with backoff. See CONFIG_SET_RETRIES for why:
|
|
779
|
+
* each write triggers a runtime hot-reload that rewrites openclaw.json, and a
|
|
780
|
+
* follow-up command that races the reload fails with a bare "Command failed".
|
|
781
|
+
*
|
|
782
|
+
* Throws an Error whose (scrubbed) message includes stderr after retries are
|
|
783
|
+
* exhausted, so the real cause propagates to the integration errorMessage
|
|
784
|
+
* without leaking the value payload.
|
|
785
|
+
*/
|
|
786
|
+
private runConfigCommand;
|
|
759
787
|
applyPlugin(spec: string, _installPath?: string, opts?: {
|
|
760
788
|
force?: boolean;
|
|
761
789
|
}): Promise<void>;
|
|
762
790
|
/**
|
|
763
|
-
* Ensure
|
|
791
|
+
* Ensure one or more plugins are in plugins.allow in openclaw.json.
|
|
764
792
|
* Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
|
|
793
|
+
*
|
|
794
|
+
* Prefer passing the FULL set of plugins for an integration in a single call
|
|
795
|
+
* (see `ensurePluginsAllowed`): each write triggers a runtime hot-reload, so
|
|
796
|
+
* one write for N plugins is one reload instead of N.
|
|
765
797
|
*/
|
|
766
798
|
private ensurePluginsAllow;
|
|
799
|
+
/**
|
|
800
|
+
* Pre-trust every plugin an integration ships in a SINGLE plugins.allow
|
|
801
|
+
* write, before any are installed. Called once by the manager ahead of the
|
|
802
|
+
* per-plugin install loop so activation triggers one hot-reload for the
|
|
803
|
+
* allowlist instead of one per plugin. `applyPlugin`'s own per-plugin
|
|
804
|
+
* `ensurePluginsAllow` then finds nothing missing and is a no-op.
|
|
805
|
+
*/
|
|
806
|
+
ensurePluginsAllowed(specs: string[]): Promise<void>;
|
|
767
807
|
/**
|
|
768
808
|
* Check if a plugin is already installed.
|
|
769
809
|
*
|
package/dist/index.js
CHANGED
|
@@ -1113,7 +1113,7 @@ var IntegrationManager = class {
|
|
|
1113
1113
|
message: "Already active"
|
|
1114
1114
|
}
|
|
1115
1115
|
};
|
|
1116
|
-
if (entry.status !== "configured" && entry.status !== "installed") return this.err("INVALID_STATE", `Cannot activate integration in "${entry.status}" state`);
|
|
1116
|
+
if (entry.status !== "configured" && entry.status !== "installed" && entry.status !== "error") return this.err("INVALID_STATE", `Cannot activate integration in "${entry.status}" state`);
|
|
1117
1117
|
this.log.info(`Activating integration: ${integrationId}`);
|
|
1118
1118
|
try {
|
|
1119
1119
|
const installPath = this.installer.getInstallPath(integrationId);
|
|
@@ -1136,6 +1136,11 @@ var IntegrationManager = class {
|
|
|
1136
1136
|
continue;
|
|
1137
1137
|
}
|
|
1138
1138
|
const { plugins, skills, config: runtimeConfig } = resolveInstallsForRuntime(manifest, runtimeName);
|
|
1139
|
+
if (applier.ensurePluginsAllowed && plugins.length > 0) try {
|
|
1140
|
+
await applier.ensurePluginsAllowed(plugins.map((p) => p.package));
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
this.log.warn(`Failed to pre-trust plugins for ${integrationId} on ${runtimeName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1143
|
+
}
|
|
1139
1144
|
const pluginFailures = [];
|
|
1140
1145
|
for (const plugin of plugins) {
|
|
1141
1146
|
this.log.info(`Applying plugin ${plugin.package} to ${runtimeName}`);
|
|
@@ -1613,6 +1618,45 @@ const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
|
|
|
1613
1618
|
* LLM/voice traffic through its own AI proxy, so leaving them gated off is correct.
|
|
1614
1619
|
*/
|
|
1615
1620
|
const BUNDLED_BASELINE_ALLOW = ["browser"];
|
|
1621
|
+
/**
|
|
1622
|
+
* Every `openclaw config set` triggers an async hot-reload of the running
|
|
1623
|
+
* OpenClaw runtime, which itself rewrites openclaw.json. If the next
|
|
1624
|
+
* `config set` fires before that reload settles it collides with the
|
|
1625
|
+
* in-flight write (OpenClaw's clobber protection) and exits non-zero with a
|
|
1626
|
+
* bare "Command failed". We serialize all config writes through one promise
|
|
1627
|
+
* chain and retry transient failures with a short backoff so the reload has
|
|
1628
|
+
* time to settle between writes.
|
|
1629
|
+
*/
|
|
1630
|
+
const CONFIG_SET_RETRIES = 3;
|
|
1631
|
+
const CONFIG_SET_RETRY_DELAY_MS = 750;
|
|
1632
|
+
const delay = (ms) => new Promise((resolve) => {
|
|
1633
|
+
setTimeout(resolve, ms);
|
|
1634
|
+
});
|
|
1635
|
+
/**
|
|
1636
|
+
* Describe a `config set` target WITHOUT leaking values. The value argument is
|
|
1637
|
+
* the JSON payload (model config, the gateway loopback token via the alfe hook,
|
|
1638
|
+
* etc.) and must never reach the integration's `errorMessage`, which projects
|
|
1639
|
+
* to the user-facing dashboard. Keep only the program + the path/flag.
|
|
1640
|
+
*/
|
|
1641
|
+
function redactConfigSetTarget(args) {
|
|
1642
|
+
return `openclaw config ${args.slice(0, 2).join(" ")}`.trim();
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Build a diagnosable BUT scrubbed message from an execFile rejection. Node's
|
|
1646
|
+
* execFile error `message` is "Command failed: <full argv>" — which for
|
|
1647
|
+
* `config set` embeds the value payload — so we never surface it. We keep the
|
|
1648
|
+
* redacted target, the exit code, and `stderr` (openclaw's own error text).
|
|
1649
|
+
*/
|
|
1650
|
+
function configSetErrorMessage(err, args) {
|
|
1651
|
+
const target = redactConfigSetTarget(args);
|
|
1652
|
+
if (err instanceof Error) {
|
|
1653
|
+
const e = err;
|
|
1654
|
+
const stderr = typeof e.stderr === "string" ? e.stderr.trim() : "";
|
|
1655
|
+
const code = e.code !== void 0 ? ` (exit ${e.code})` : "";
|
|
1656
|
+
return stderr ? `${target} failed${code}: ${stderr}` : `${target} failed${code}`;
|
|
1657
|
+
}
|
|
1658
|
+
return `${target} failed: ${String(err)}`;
|
|
1659
|
+
}
|
|
1616
1660
|
function flattenConfig(obj, prefix = "") {
|
|
1617
1661
|
const entries = [];
|
|
1618
1662
|
for (const [key, val] of Object.entries(obj)) {
|
|
@@ -1677,6 +1721,10 @@ var OpenClawApplier = class {
|
|
|
1677
1721
|
agentWorkspace;
|
|
1678
1722
|
skillsDir;
|
|
1679
1723
|
trackingPath;
|
|
1724
|
+
configSetRetries;
|
|
1725
|
+
configSetRetryDelayMs;
|
|
1726
|
+
/** Serializes all `openclaw config set` writes so they never interleave with each other or the hot-reload they trigger. */
|
|
1727
|
+
configSetQueue = Promise.resolve();
|
|
1680
1728
|
constructor(options) {
|
|
1681
1729
|
const home = options.home ?? options.workspace;
|
|
1682
1730
|
if (!home) throw new Error("OpenClawApplier requires `home` (or legacy `workspace`) option");
|
|
@@ -1684,6 +1732,39 @@ var OpenClawApplier = class {
|
|
|
1684
1732
|
this.agentWorkspace = options.agentWorkspace ?? join(home, "workspace");
|
|
1685
1733
|
this.skillsDir = options.skillsDir ?? DEFAULT_SKILLS_DIR;
|
|
1686
1734
|
this.trackingPath = options.configPath ?? join(this.home, "config.json");
|
|
1735
|
+
this.configSetRetries = options.configSetRetries ?? CONFIG_SET_RETRIES;
|
|
1736
|
+
this.configSetRetryDelayMs = options.configSetRetryDelayMs ?? CONFIG_SET_RETRY_DELAY_MS;
|
|
1737
|
+
}
|
|
1738
|
+
/** Convenience: `openclaw config set <args>`, serialized + retried. */
|
|
1739
|
+
runConfigSet(setArgs, opts = {}) {
|
|
1740
|
+
return this.runConfigCommand(["set", ...setArgs], opts);
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Run `openclaw config <args>` (set/unset), serialized against every other
|
|
1744
|
+
* config write and retried with backoff. See CONFIG_SET_RETRIES for why:
|
|
1745
|
+
* each write triggers a runtime hot-reload that rewrites openclaw.json, and a
|
|
1746
|
+
* follow-up command that races the reload fails with a bare "Command failed".
|
|
1747
|
+
*
|
|
1748
|
+
* Throws an Error whose (scrubbed) message includes stderr after retries are
|
|
1749
|
+
* exhausted, so the real cause propagates to the integration errorMessage
|
|
1750
|
+
* without leaking the value payload.
|
|
1751
|
+
*/
|
|
1752
|
+
runConfigCommand(args, opts = {}) {
|
|
1753
|
+
const timeout = opts.timeout ?? 1e4;
|
|
1754
|
+
const run = async () => {
|
|
1755
|
+
let lastErr;
|
|
1756
|
+
for (let attempt = 0; attempt <= this.configSetRetries; attempt++) try {
|
|
1757
|
+
await execFileAsync("openclaw", ["config", ...args], { timeout });
|
|
1758
|
+
return;
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
lastErr = err;
|
|
1761
|
+
if (attempt < this.configSetRetries && this.configSetRetryDelayMs > 0) await delay(this.configSetRetryDelayMs * 2 ** attempt);
|
|
1762
|
+
}
|
|
1763
|
+
throw new Error(configSetErrorMessage(lastErr, args));
|
|
1764
|
+
};
|
|
1765
|
+
const result = this.configSetQueue.then(run, run);
|
|
1766
|
+
this.configSetQueue = result.catch(() => void 0);
|
|
1767
|
+
return result;
|
|
1687
1768
|
}
|
|
1688
1769
|
async applyPlugin(spec, _installPath, opts) {
|
|
1689
1770
|
const pkg = stripPluginVersion(spec);
|
|
@@ -1740,10 +1821,15 @@ var OpenClawApplier = class {
|
|
|
1740
1821
|
}
|
|
1741
1822
|
}
|
|
1742
1823
|
/**
|
|
1743
|
-
* Ensure
|
|
1824
|
+
* Ensure one or more plugins are in plugins.allow in openclaw.json.
|
|
1744
1825
|
* Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
|
|
1826
|
+
*
|
|
1827
|
+
* Prefer passing the FULL set of plugins for an integration in a single call
|
|
1828
|
+
* (see `ensurePluginsAllowed`): each write triggers a runtime hot-reload, so
|
|
1829
|
+
* one write for N plugins is one reload instead of N.
|
|
1745
1830
|
*/
|
|
1746
|
-
async ensurePluginsAllow(
|
|
1831
|
+
async ensurePluginsAllow(pkgs) {
|
|
1832
|
+
const wanted = Array.isArray(pkgs) ? pkgs : [pkgs];
|
|
1747
1833
|
let currentAllow = [];
|
|
1748
1834
|
try {
|
|
1749
1835
|
const { stdout } = await execFileAsync("openclaw", [
|
|
@@ -1754,24 +1840,30 @@ var OpenClawApplier = class {
|
|
|
1754
1840
|
const parsed = JSON.parse(stdout.trim());
|
|
1755
1841
|
if (Array.isArray(parsed)) currentAllow = parsed;
|
|
1756
1842
|
} catch {}
|
|
1757
|
-
const missing = [...new Set([
|
|
1843
|
+
const missing = [...new Set([...wanted, ...BUNDLED_BASELINE_ALLOW])].filter((id) => !currentAllow.includes(id));
|
|
1758
1844
|
if (missing.length === 0) return;
|
|
1759
1845
|
const updated = [...currentAllow, ...missing];
|
|
1760
1846
|
try {
|
|
1761
|
-
await
|
|
1762
|
-
"config",
|
|
1763
|
-
"set",
|
|
1764
|
-
"plugins.allow",
|
|
1765
|
-
JSON.stringify(updated)
|
|
1766
|
-
], { timeout: 1e4 });
|
|
1847
|
+
await this.runConfigSet(["plugins.allow", JSON.stringify(updated)]);
|
|
1767
1848
|
} catch (err) {
|
|
1768
1849
|
log$1.warn({
|
|
1769
1850
|
err: err instanceof Error ? err.message : String(err),
|
|
1770
|
-
|
|
1851
|
+
pkgs: wanted
|
|
1771
1852
|
}, "Failed to set plugins.allow via openclaw config set");
|
|
1772
1853
|
}
|
|
1773
1854
|
}
|
|
1774
1855
|
/**
|
|
1856
|
+
* Pre-trust every plugin an integration ships in a SINGLE plugins.allow
|
|
1857
|
+
* write, before any are installed. Called once by the manager ahead of the
|
|
1858
|
+
* per-plugin install loop so activation triggers one hot-reload for the
|
|
1859
|
+
* allowlist instead of one per plugin. `applyPlugin`'s own per-plugin
|
|
1860
|
+
* `ensurePluginsAllow` then finds nothing missing and is a no-op.
|
|
1861
|
+
*/
|
|
1862
|
+
async ensurePluginsAllowed(specs) {
|
|
1863
|
+
if (specs.length === 0) return;
|
|
1864
|
+
await this.ensurePluginsAllow(specs.map(stripPluginVersion));
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1775
1867
|
* Check if a plugin is already installed.
|
|
1776
1868
|
*
|
|
1777
1869
|
* OpenClaw 2026.4 stored plugins under `~/.openclaw/extensions/{pkg-name-with-dashes}-{hash}`.
|
|
@@ -1902,12 +1994,7 @@ var OpenClawApplier = class {
|
|
|
1902
1994
|
const merged = { ...await readParentObject(parentPath) };
|
|
1903
1995
|
for (const [k, v] of dottedKvs) merged[k] = v;
|
|
1904
1996
|
try {
|
|
1905
|
-
await
|
|
1906
|
-
"config",
|
|
1907
|
-
"set",
|
|
1908
|
-
parentPath,
|
|
1909
|
-
JSON.stringify(merged)
|
|
1910
|
-
], { timeout: 1e4 });
|
|
1997
|
+
await this.runConfigSet([parentPath, JSON.stringify(merged)]);
|
|
1911
1998
|
} catch (err) {
|
|
1912
1999
|
log$1.error({
|
|
1913
2000
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -1917,12 +2004,7 @@ var OpenClawApplier = class {
|
|
|
1917
2004
|
}
|
|
1918
2005
|
}
|
|
1919
2006
|
if (leaves.length > 0) try {
|
|
1920
|
-
await
|
|
1921
|
-
"config",
|
|
1922
|
-
"set",
|
|
1923
|
-
"--batch-json",
|
|
1924
|
-
JSON.stringify(leaves)
|
|
1925
|
-
], { timeout: 1e4 });
|
|
2007
|
+
await this.runConfigSet(["--batch-json", JSON.stringify(leaves)]);
|
|
1926
2008
|
} catch (err) {
|
|
1927
2009
|
log$1.error({
|
|
1928
2010
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -1944,11 +2026,7 @@ var OpenClawApplier = class {
|
|
|
1944
2026
|
const integrationConfig = integrations[integrationId];
|
|
1945
2027
|
const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
|
|
1946
2028
|
for (const { path } of leaves) try {
|
|
1947
|
-
await
|
|
1948
|
-
"config",
|
|
1949
|
-
"unset",
|
|
1950
|
-
path
|
|
1951
|
-
], { timeout: 1e4 });
|
|
2029
|
+
await this.runConfigCommand(["unset", path]);
|
|
1952
2030
|
} catch (err) {
|
|
1953
2031
|
log$1.warn({
|
|
1954
2032
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -1960,17 +2038,8 @@ var OpenClawApplier = class {
|
|
|
1960
2038
|
const remaining = Object.fromEntries(Object.entries(existing).filter(([k]) => !dottedKvs.has(k)));
|
|
1961
2039
|
if (Object.keys(existing).length === 0) continue;
|
|
1962
2040
|
try {
|
|
1963
|
-
if (Object.keys(remaining).length === 0) await
|
|
1964
|
-
|
|
1965
|
-
"unset",
|
|
1966
|
-
parentPath
|
|
1967
|
-
], { timeout: 1e4 });
|
|
1968
|
-
else await execFileAsync("openclaw", [
|
|
1969
|
-
"config",
|
|
1970
|
-
"set",
|
|
1971
|
-
parentPath,
|
|
1972
|
-
JSON.stringify(remaining)
|
|
1973
|
-
], { timeout: 1e4 });
|
|
2041
|
+
if (Object.keys(remaining).length === 0) await this.runConfigCommand(["unset", parentPath]);
|
|
2042
|
+
else await this.runConfigSet([parentPath, JSON.stringify(remaining)]);
|
|
1974
2043
|
} catch (err) {
|
|
1975
2044
|
log$1.warn({
|
|
1976
2045
|
err: err instanceof Error ? err.message : String(err),
|
package/package.json
CHANGED