@alfe.ai/integrations 0.1.4 → 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 +97 -8
- package/dist/index.js +140 -51
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,14 @@ import { Manager } from "@alfe.ai/mcp-bundler";
|
|
|
8
8
|
*
|
|
9
9
|
* Calls GET /integrations/registry (public, no auth required) and caches the result.
|
|
10
10
|
* The API URL can be passed explicitly or set via ALFE_API_URL env var.
|
|
11
|
+
*
|
|
12
|
+
* The cache is time-bounded by a TTL (default 60s). Long-running consumers — chiefly
|
|
13
|
+
* the agent daemon, which constructs the Registry once at startup and never restarts —
|
|
14
|
+
* would otherwise be pinned to the registry snapshot taken at process boot, and could
|
|
15
|
+
* never resolve a version published after the daemon came up (the "stale cache" bug).
|
|
16
|
+
* With a TTL, every consumer self-heals within a bounded window. The resolve-for-install
|
|
17
|
+
* path additionally forces a fresh read (see `load({ fresh: true })`) because installing
|
|
18
|
+
* a specific version is a rare, correctness-critical action that must never race the TTL.
|
|
11
19
|
*/
|
|
12
20
|
interface RegistryEntry {
|
|
13
21
|
/** Human-readable display name (e.g. 'Alfe Voice') */
|
|
@@ -69,26 +77,51 @@ interface RegistryIndex {
|
|
|
69
77
|
type RegistryFetcher = () => Promise<(RegistryEntry & {
|
|
70
78
|
id: string;
|
|
71
79
|
})[]>;
|
|
80
|
+
/** Default cache TTL — refetch the registry index after this many ms. */
|
|
81
|
+
declare const DEFAULT_REGISTRY_TTL_MS = 60000;
|
|
82
|
+
interface RegistryOptions {
|
|
83
|
+
/**
|
|
84
|
+
* How long (ms) a loaded index is considered fresh before `load()` refetches.
|
|
85
|
+
* Defaults to {@link DEFAULT_REGISTRY_TTL_MS} (60s). A non-positive value
|
|
86
|
+
* disables time-based caching (every `load()` refetches).
|
|
87
|
+
*/
|
|
88
|
+
ttlMs?: number;
|
|
89
|
+
}
|
|
90
|
+
/** Options for a single `load()` call. */
|
|
91
|
+
interface LoadOptions {
|
|
92
|
+
/** Bypass the cache and refetch the index even if it's still within TTL. */
|
|
93
|
+
fresh?: boolean;
|
|
94
|
+
}
|
|
72
95
|
declare class Registry {
|
|
73
96
|
private index;
|
|
97
|
+
private loadedAt;
|
|
74
98
|
private fetcher;
|
|
99
|
+
private ttlMs;
|
|
75
100
|
/**
|
|
76
101
|
* @param fetcher - Function that fetches the integrations array from the registry API.
|
|
77
102
|
* Typically backed by api-client's IntegrationsService.getRegistry().
|
|
103
|
+
* @param options - Optional cache configuration (TTL).
|
|
78
104
|
*/
|
|
79
|
-
constructor(fetcher: RegistryFetcher);
|
|
105
|
+
constructor(fetcher: RegistryFetcher, options?: RegistryOptions);
|
|
106
|
+
/** True when the cached index is absent or older than the TTL. */
|
|
107
|
+
private isStale;
|
|
80
108
|
/**
|
|
81
|
-
* Load the registry index.
|
|
109
|
+
* Load the registry index. Returns the cached index while it's still within
|
|
110
|
+
* TTL; refetches once the cache is stale (or when `{ fresh: true }` is passed).
|
|
82
111
|
*/
|
|
83
|
-
load(): Promise<RegistryIndex>;
|
|
112
|
+
load(options?: LoadOptions): Promise<RegistryIndex>;
|
|
84
113
|
/**
|
|
85
|
-
* Force reload the index (bypass cache).
|
|
114
|
+
* Force reload the index (bypass cache). Equivalent to `load({ fresh: true })`.
|
|
86
115
|
*/
|
|
87
116
|
reload(): Promise<RegistryIndex>;
|
|
88
117
|
/**
|
|
89
118
|
* Get a specific integration entry by name.
|
|
119
|
+
*
|
|
120
|
+
* @param fresh - When true, force a fresh fetch before reading (used by the
|
|
121
|
+
* resolve-for-install path so a stale version list never blocks
|
|
122
|
+
* a just-published version).
|
|
90
123
|
*/
|
|
91
|
-
get(id: string): Promise<RegistryEntry | undefined>;
|
|
124
|
+
get(id: string, fresh?: boolean): Promise<RegistryEntry | undefined>;
|
|
92
125
|
/**
|
|
93
126
|
* List all integrations in the registry.
|
|
94
127
|
*/
|
|
@@ -115,6 +148,20 @@ interface ResolvedIntegration {
|
|
|
115
148
|
subdir?: string;
|
|
116
149
|
description: string;
|
|
117
150
|
}
|
|
151
|
+
/** Options for a single `resolve()` call. */
|
|
152
|
+
interface ResolveOptions {
|
|
153
|
+
/**
|
|
154
|
+
* Bypass the registry cache and read a fresh index before resolving.
|
|
155
|
+
*
|
|
156
|
+
* The install/reconcile path sets this: installing a specific version is a
|
|
157
|
+
* rare, user-triggered, correctness-critical action that must never resolve
|
|
158
|
+
* against a stale cached version list (the cause of the
|
|
159
|
+
* `Version "x" not found … Available versions: …` install failure on a
|
|
160
|
+
* long-running daemon). High-frequency read-only uses (marketplace
|
|
161
|
+
* listing/search) leave it unset and keep the TTL cache.
|
|
162
|
+
*/
|
|
163
|
+
fresh?: boolean;
|
|
164
|
+
}
|
|
118
165
|
declare class RegistryResolveError extends Error {
|
|
119
166
|
constructor(message: string);
|
|
120
167
|
}
|
|
@@ -126,9 +173,11 @@ declare class Resolver {
|
|
|
126
173
|
*
|
|
127
174
|
* @param name - Integration name (e.g. "discord")
|
|
128
175
|
* @param version - Specific version (e.g. "1.0.0") or undefined for latest
|
|
176
|
+
* @param options - Pass `{ fresh: true }` to bypass the registry cache
|
|
177
|
+
* (used by the install/reconcile path).
|
|
129
178
|
* @returns Resolved integration with repo URL and commit hash
|
|
130
179
|
*/
|
|
131
|
-
resolve(id: string, version?: string): Promise<ResolvedIntegration>;
|
|
180
|
+
resolve(id: string, version?: string, options?: ResolveOptions): Promise<ResolvedIntegration>;
|
|
132
181
|
/**
|
|
133
182
|
* Check if an integration exists in the registry.
|
|
134
183
|
*/
|
|
@@ -238,6 +287,13 @@ interface RuntimeApplier {
|
|
|
238
287
|
applyPlugin(pkg: string, integrationInstallPath: string, opts?: {
|
|
239
288
|
force?: boolean;
|
|
240
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>;
|
|
241
297
|
/** Remove a plugin package from this runtime */
|
|
242
298
|
removePlugin(pkg: string): Promise<void>;
|
|
243
299
|
/** Copy a skill directory into this runtime's skills location */
|
|
@@ -699,6 +755,10 @@ interface OpenClawApplierOptions {
|
|
|
699
755
|
skillsDir?: string;
|
|
700
756
|
/** Path to the integration tracking file (defaults to {home}/config.json) */
|
|
701
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;
|
|
702
762
|
}
|
|
703
763
|
declare class OpenClawApplier implements RuntimeApplier {
|
|
704
764
|
readonly runtime = "openclaw";
|
|
@@ -706,15 +766,44 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
706
766
|
private agentWorkspace;
|
|
707
767
|
private skillsDir;
|
|
708
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;
|
|
709
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;
|
|
710
787
|
applyPlugin(spec: string, _installPath?: string, opts?: {
|
|
711
788
|
force?: boolean;
|
|
712
789
|
}): Promise<void>;
|
|
713
790
|
/**
|
|
714
|
-
* Ensure
|
|
791
|
+
* Ensure one or more plugins are in plugins.allow in openclaw.json.
|
|
715
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.
|
|
716
797
|
*/
|
|
717
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>;
|
|
718
807
|
/**
|
|
719
808
|
* Check if a plugin is already installed.
|
|
720
809
|
*
|
|
@@ -875,4 +964,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
|
|
|
875
964
|
resetReinstallAttempts(integrationId: string): void;
|
|
876
965
|
}
|
|
877
966
|
//#endregion
|
|
878
|
-
export { type CredentialsResolver, type HookEnvOptions, type HookResult, type IIntegrationManager, type InstalledInfo, Installer, InstallerError, type IntegrationConfigureParams, type IntegrationHealthParams, type IntegrationInfo, type IntegrationInstallParams, IntegrationManager, IntegrationManagerAdapter, type IntegrationManagerOptions, type IntegrationRemoveParams, LockManager, McpApplier, type McpApplierOptions, OpenClawApplier, type OpenClawApplierOptions, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, RegistryResolveError, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
|
|
967
|
+
export { type CredentialsResolver, DEFAULT_REGISTRY_TTL_MS, type HookEnvOptions, type HookResult, type IIntegrationManager, type InstalledInfo, Installer, InstallerError, type IntegrationConfigureParams, type IntegrationHealthParams, type IntegrationInfo, type IntegrationInstallParams, IntegrationManager, IntegrationManagerAdapter, type IntegrationManagerOptions, type IntegrationRemoveParams, type LoadOptions, LockManager, McpApplier, type McpApplierOptions, OpenClawApplier, type OpenClawApplierOptions, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, type RegistryOptions, RegistryResolveError, type ResolveOptions, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
|
package/dist/index.js
CHANGED
|
@@ -6,21 +6,34 @@ import { closeSync, copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, op
|
|
|
6
6
|
import { buildConfigValidationSchema, parseManifestFile } from "@alfe.ai/integration-manifest";
|
|
7
7
|
import { createLogger } from "@auriclabs/logger";
|
|
8
8
|
//#region src/registry.ts
|
|
9
|
+
/** Default cache TTL — refetch the registry index after this many ms. */
|
|
10
|
+
const DEFAULT_REGISTRY_TTL_MS = 6e4;
|
|
9
11
|
var Registry = class {
|
|
10
12
|
index = null;
|
|
13
|
+
loadedAt = 0;
|
|
11
14
|
fetcher;
|
|
15
|
+
ttlMs;
|
|
12
16
|
/**
|
|
13
17
|
* @param fetcher - Function that fetches the integrations array from the registry API.
|
|
14
18
|
* Typically backed by api-client's IntegrationsService.getRegistry().
|
|
19
|
+
* @param options - Optional cache configuration (TTL).
|
|
15
20
|
*/
|
|
16
|
-
constructor(fetcher) {
|
|
21
|
+
constructor(fetcher, options = {}) {
|
|
17
22
|
this.fetcher = fetcher;
|
|
23
|
+
this.ttlMs = options.ttlMs ?? 6e4;
|
|
24
|
+
}
|
|
25
|
+
/** True when the cached index is absent or older than the TTL. */
|
|
26
|
+
isStale() {
|
|
27
|
+
if (!this.index) return true;
|
|
28
|
+
if (this.ttlMs <= 0) return true;
|
|
29
|
+
return Date.now() - this.loadedAt >= this.ttlMs;
|
|
18
30
|
}
|
|
19
31
|
/**
|
|
20
|
-
* Load the registry index.
|
|
32
|
+
* Load the registry index. Returns the cached index while it's still within
|
|
33
|
+
* TTL; refetches once the cache is stale (or when `{ fresh: true }` is passed).
|
|
21
34
|
*/
|
|
22
|
-
async load() {
|
|
23
|
-
if (this.index) return this.index;
|
|
35
|
+
async load(options = {}) {
|
|
36
|
+
if (!options.fresh && this.index && !this.isStale()) return this.index;
|
|
24
37
|
const raw = await this.fetcher();
|
|
25
38
|
const integrations = {};
|
|
26
39
|
for (const entry of raw) {
|
|
@@ -31,20 +44,25 @@ var Registry = class {
|
|
|
31
44
|
version: 1,
|
|
32
45
|
integrations
|
|
33
46
|
};
|
|
47
|
+
this.loadedAt = Date.now();
|
|
34
48
|
return this.index;
|
|
35
49
|
}
|
|
36
50
|
/**
|
|
37
|
-
* Force reload the index (bypass cache).
|
|
51
|
+
* Force reload the index (bypass cache). Equivalent to `load({ fresh: true })`.
|
|
38
52
|
*/
|
|
39
53
|
async reload() {
|
|
40
54
|
this.index = null;
|
|
41
|
-
return this.load();
|
|
55
|
+
return this.load({ fresh: true });
|
|
42
56
|
}
|
|
43
57
|
/**
|
|
44
58
|
* Get a specific integration entry by name.
|
|
59
|
+
*
|
|
60
|
+
* @param fresh - When true, force a fresh fetch before reading (used by the
|
|
61
|
+
* resolve-for-install path so a stale version list never blocks
|
|
62
|
+
* a just-published version).
|
|
45
63
|
*/
|
|
46
|
-
async get(id) {
|
|
47
|
-
return (await this.load()).integrations[id];
|
|
64
|
+
async get(id, fresh = false) {
|
|
65
|
+
return (await this.load({ fresh })).integrations[id];
|
|
48
66
|
}
|
|
49
67
|
/**
|
|
50
68
|
* List all integrations in the registry.
|
|
@@ -83,10 +101,12 @@ var Resolver = class {
|
|
|
83
101
|
*
|
|
84
102
|
* @param name - Integration name (e.g. "discord")
|
|
85
103
|
* @param version - Specific version (e.g. "1.0.0") or undefined for latest
|
|
104
|
+
* @param options - Pass `{ fresh: true }` to bypass the registry cache
|
|
105
|
+
* (used by the install/reconcile path).
|
|
86
106
|
* @returns Resolved integration with repo URL and commit hash
|
|
87
107
|
*/
|
|
88
|
-
async resolve(id, version) {
|
|
89
|
-
const entry = await this.registry.get(id);
|
|
108
|
+
async resolve(id, version, options = {}) {
|
|
109
|
+
const entry = await this.registry.get(id, options.fresh);
|
|
90
110
|
if (!entry) throw new RegistryResolveError(`Integration "${id}" not found in registry`);
|
|
91
111
|
const resolvedVersion = version && version.length > 0 ? version : entry.latest;
|
|
92
112
|
if (!entry.versions.includes(resolvedVersion)) throw new RegistryResolveError(`Version "${resolvedVersion}" not found for integration "${id}". Available versions: ${entry.versions.join(", ")}`);
|
|
@@ -966,7 +986,7 @@ var IntegrationManager = class {
|
|
|
966
986
|
resolved = buildCustomResolved(name, customSource);
|
|
967
987
|
this.log.info(`Custom Connection install: ${name} from ${resolved.repository}@${resolved.commit}`);
|
|
968
988
|
} else {
|
|
969
|
-
resolved = await this.resolver.resolve(name, version);
|
|
989
|
+
resolved = await this.resolver.resolve(name, version, { fresh: true });
|
|
970
990
|
this.log.info(`Resolved ${name}@${resolved.version} from ${resolved.repository}`);
|
|
971
991
|
}
|
|
972
992
|
const installPath = await this.installer.install(resolved);
|
|
@@ -1093,7 +1113,7 @@ var IntegrationManager = class {
|
|
|
1093
1113
|
message: "Already active"
|
|
1094
1114
|
}
|
|
1095
1115
|
};
|
|
1096
|
-
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`);
|
|
1097
1117
|
this.log.info(`Activating integration: ${integrationId}`);
|
|
1098
1118
|
try {
|
|
1099
1119
|
const installPath = this.installer.getInstallPath(integrationId);
|
|
@@ -1116,6 +1136,11 @@ var IntegrationManager = class {
|
|
|
1116
1136
|
continue;
|
|
1117
1137
|
}
|
|
1118
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
|
+
}
|
|
1119
1144
|
const pluginFailures = [];
|
|
1120
1145
|
for (const plugin of plugins) {
|
|
1121
1146
|
this.log.info(`Applying plugin ${plugin.package} to ${runtimeName}`);
|
|
@@ -1593,6 +1618,45 @@ const DEFAULT_SKILLS_DIR = join(homedir(), ".alfe", "skills");
|
|
|
1593
1618
|
* LLM/voice traffic through its own AI proxy, so leaving them gated off is correct.
|
|
1594
1619
|
*/
|
|
1595
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
|
+
}
|
|
1596
1660
|
function flattenConfig(obj, prefix = "") {
|
|
1597
1661
|
const entries = [];
|
|
1598
1662
|
for (const [key, val] of Object.entries(obj)) {
|
|
@@ -1657,6 +1721,10 @@ var OpenClawApplier = class {
|
|
|
1657
1721
|
agentWorkspace;
|
|
1658
1722
|
skillsDir;
|
|
1659
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();
|
|
1660
1728
|
constructor(options) {
|
|
1661
1729
|
const home = options.home ?? options.workspace;
|
|
1662
1730
|
if (!home) throw new Error("OpenClawApplier requires `home` (or legacy `workspace`) option");
|
|
@@ -1664,6 +1732,39 @@ var OpenClawApplier = class {
|
|
|
1664
1732
|
this.agentWorkspace = options.agentWorkspace ?? join(home, "workspace");
|
|
1665
1733
|
this.skillsDir = options.skillsDir ?? DEFAULT_SKILLS_DIR;
|
|
1666
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;
|
|
1667
1768
|
}
|
|
1668
1769
|
async applyPlugin(spec, _installPath, opts) {
|
|
1669
1770
|
const pkg = stripPluginVersion(spec);
|
|
@@ -1720,10 +1821,15 @@ var OpenClawApplier = class {
|
|
|
1720
1821
|
}
|
|
1721
1822
|
}
|
|
1722
1823
|
/**
|
|
1723
|
-
* Ensure
|
|
1824
|
+
* Ensure one or more plugins are in plugins.allow in openclaw.json.
|
|
1724
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.
|
|
1725
1830
|
*/
|
|
1726
|
-
async ensurePluginsAllow(
|
|
1831
|
+
async ensurePluginsAllow(pkgs) {
|
|
1832
|
+
const wanted = Array.isArray(pkgs) ? pkgs : [pkgs];
|
|
1727
1833
|
let currentAllow = [];
|
|
1728
1834
|
try {
|
|
1729
1835
|
const { stdout } = await execFileAsync("openclaw", [
|
|
@@ -1734,24 +1840,30 @@ var OpenClawApplier = class {
|
|
|
1734
1840
|
const parsed = JSON.parse(stdout.trim());
|
|
1735
1841
|
if (Array.isArray(parsed)) currentAllow = parsed;
|
|
1736
1842
|
} catch {}
|
|
1737
|
-
const missing = [...new Set([
|
|
1843
|
+
const missing = [...new Set([...wanted, ...BUNDLED_BASELINE_ALLOW])].filter((id) => !currentAllow.includes(id));
|
|
1738
1844
|
if (missing.length === 0) return;
|
|
1739
1845
|
const updated = [...currentAllow, ...missing];
|
|
1740
1846
|
try {
|
|
1741
|
-
await
|
|
1742
|
-
"config",
|
|
1743
|
-
"set",
|
|
1744
|
-
"plugins.allow",
|
|
1745
|
-
JSON.stringify(updated)
|
|
1746
|
-
], { timeout: 1e4 });
|
|
1847
|
+
await this.runConfigSet(["plugins.allow", JSON.stringify(updated)]);
|
|
1747
1848
|
} catch (err) {
|
|
1748
1849
|
log$1.warn({
|
|
1749
1850
|
err: err instanceof Error ? err.message : String(err),
|
|
1750
|
-
|
|
1851
|
+
pkgs: wanted
|
|
1751
1852
|
}, "Failed to set plugins.allow via openclaw config set");
|
|
1752
1853
|
}
|
|
1753
1854
|
}
|
|
1754
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
|
+
/**
|
|
1755
1867
|
* Check if a plugin is already installed.
|
|
1756
1868
|
*
|
|
1757
1869
|
* OpenClaw 2026.4 stored plugins under `~/.openclaw/extensions/{pkg-name-with-dashes}-{hash}`.
|
|
@@ -1882,12 +1994,7 @@ var OpenClawApplier = class {
|
|
|
1882
1994
|
const merged = { ...await readParentObject(parentPath) };
|
|
1883
1995
|
for (const [k, v] of dottedKvs) merged[k] = v;
|
|
1884
1996
|
try {
|
|
1885
|
-
await
|
|
1886
|
-
"config",
|
|
1887
|
-
"set",
|
|
1888
|
-
parentPath,
|
|
1889
|
-
JSON.stringify(merged)
|
|
1890
|
-
], { timeout: 1e4 });
|
|
1997
|
+
await this.runConfigSet([parentPath, JSON.stringify(merged)]);
|
|
1891
1998
|
} catch (err) {
|
|
1892
1999
|
log$1.error({
|
|
1893
2000
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -1897,12 +2004,7 @@ var OpenClawApplier = class {
|
|
|
1897
2004
|
}
|
|
1898
2005
|
}
|
|
1899
2006
|
if (leaves.length > 0) try {
|
|
1900
|
-
await
|
|
1901
|
-
"config",
|
|
1902
|
-
"set",
|
|
1903
|
-
"--batch-json",
|
|
1904
|
-
JSON.stringify(leaves)
|
|
1905
|
-
], { timeout: 1e4 });
|
|
2007
|
+
await this.runConfigSet(["--batch-json", JSON.stringify(leaves)]);
|
|
1906
2008
|
} catch (err) {
|
|
1907
2009
|
log$1.error({
|
|
1908
2010
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -1924,11 +2026,7 @@ var OpenClawApplier = class {
|
|
|
1924
2026
|
const integrationConfig = integrations[integrationId];
|
|
1925
2027
|
const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
|
|
1926
2028
|
for (const { path } of leaves) try {
|
|
1927
|
-
await
|
|
1928
|
-
"config",
|
|
1929
|
-
"unset",
|
|
1930
|
-
path
|
|
1931
|
-
], { timeout: 1e4 });
|
|
2029
|
+
await this.runConfigCommand(["unset", path]);
|
|
1932
2030
|
} catch (err) {
|
|
1933
2031
|
log$1.warn({
|
|
1934
2032
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -1940,17 +2038,8 @@ var OpenClawApplier = class {
|
|
|
1940
2038
|
const remaining = Object.fromEntries(Object.entries(existing).filter(([k]) => !dottedKvs.has(k)));
|
|
1941
2039
|
if (Object.keys(existing).length === 0) continue;
|
|
1942
2040
|
try {
|
|
1943
|
-
if (Object.keys(remaining).length === 0) await
|
|
1944
|
-
|
|
1945
|
-
"unset",
|
|
1946
|
-
parentPath
|
|
1947
|
-
], { timeout: 1e4 });
|
|
1948
|
-
else await execFileAsync("openclaw", [
|
|
1949
|
-
"config",
|
|
1950
|
-
"set",
|
|
1951
|
-
parentPath,
|
|
1952
|
-
JSON.stringify(remaining)
|
|
1953
|
-
], { timeout: 1e4 });
|
|
2041
|
+
if (Object.keys(remaining).length === 0) await this.runConfigCommand(["unset", parentPath]);
|
|
2042
|
+
else await this.runConfigSet([parentPath, JSON.stringify(remaining)]);
|
|
1954
2043
|
} catch (err) {
|
|
1955
2044
|
log$1.warn({
|
|
1956
2045
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -2157,4 +2246,4 @@ var IntegrationManagerAdapter = class {
|
|
|
2157
2246
|
}
|
|
2158
2247
|
};
|
|
2159
2248
|
//#endregion
|
|
2160
|
-
export { Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, OpenClawApplier, Registry, RegistryResolveError, Resolver, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
|
|
2249
|
+
export { DEFAULT_REGISTRY_TTL_MS, Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, OpenClawApplier, Registry, RegistryResolveError, Resolver, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext };
|
package/package.json
CHANGED