@alfe.ai/integrations 0.2.5 → 0.2.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 +127 -13
- package/dist/index.js +128 -63
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -746,6 +746,79 @@ declare function runHook(integrationPath: string, hookScript: string, env?: Reco
|
|
|
746
746
|
*/
|
|
747
747
|
declare function runHookWithContext(integrationPath: string, hookScript: string, options: HookEnvOptions): Promise<HookResult>;
|
|
748
748
|
//#endregion
|
|
749
|
+
//#region src/openclaw-cli-lock.d.ts
|
|
750
|
+
/**
|
|
751
|
+
* OpenClawCliLock — one process-wide async mutex that every openclaw
|
|
752
|
+
* config/state-mutating CLI invocation acquires.
|
|
753
|
+
*
|
|
754
|
+
* ## Why this exists
|
|
755
|
+
*
|
|
756
|
+
* OpenClaw's config store uses OPTIMISTIC CONCURRENCY — a `config set` /
|
|
757
|
+
* `plugins install` reads the current config version, applies its change, and
|
|
758
|
+
* writes back only if the version is unchanged. Two openclaw CLI processes that
|
|
759
|
+
* mutate the store concurrently therefore lose the race: the second write fails
|
|
760
|
+
* with `ConfigMutationConflictError: config changed since last load` (exit 1).
|
|
761
|
+
* And when the long-running runtime (`openclaw gateway run`) is ALSO writing the
|
|
762
|
+
* same `~/.openclaw/state/openclaw.sqlite`, two independent SQLite writers with
|
|
763
|
+
* no shared locking corrupt the DB ("database disk image is malformed").
|
|
764
|
+
*
|
|
765
|
+
* During a DESIRED_STATE reconcile the daemon fires a SWARM of openclaw CLI
|
|
766
|
+
* processes — integration `config set` / `plugins install/uninstall`, the
|
|
767
|
+
* mcp-cleanup `config unset`, the `alfe.config_set` raw write — all mutating the
|
|
768
|
+
* one config store at the same time. This lock serializes ALL of them into a
|
|
769
|
+
* single strict queue so no two openclaw mutations ever overlap.
|
|
770
|
+
*
|
|
771
|
+
* This is the second of two layers. The first is the gateway's `RuntimeGate`,
|
|
772
|
+
* which suspends the runtime CHILD around mutating reconcile branches (so the
|
|
773
|
+
* runtime isn't writing while a CLI subprocess is). This lock covers the daemon's
|
|
774
|
+
* OWN subprocesses racing EACH OTHER — including cross-subsystem races the
|
|
775
|
+
* RuntimeGate doesn't see (mcp-cleanup vs a reconcile plugins-install, a raw
|
|
776
|
+
* `alfe.config_set` vs an in-flight integration apply). Both layers are needed.
|
|
777
|
+
*
|
|
778
|
+
* ## Read-modify-write atomicity
|
|
779
|
+
*
|
|
780
|
+
* A `config get` whose value is then written back (e.g. read `plugins.allow`,
|
|
781
|
+
* union in a plugin, write it back) MUST hold the lock across the WHOLE get→set,
|
|
782
|
+
* not lock the get and set independently — otherwise another openclaw process can
|
|
783
|
+
* mutate the same key between the read and the write and the write clobbers it.
|
|
784
|
+
* Callers pass the entire read-modify-write as one `lock.run(async () => { … })`.
|
|
785
|
+
*
|
|
786
|
+
* ## Re-entrancy
|
|
787
|
+
*
|
|
788
|
+
* This is a plain promise-chain mutex — it is NOT re-entrant. A `run()` callback
|
|
789
|
+
* MUST NOT call `run()` again (it would deadlock: the inner call chains behind
|
|
790
|
+
* the outer, which can't complete until the inner does). Consumers keep their
|
|
791
|
+
* locked-section internals as UNLOCKED helpers and acquire the lock exactly once
|
|
792
|
+
* at the public entry point.
|
|
793
|
+
*/
|
|
794
|
+
interface OpenClawCliLock {
|
|
795
|
+
/**
|
|
796
|
+
* Run `fn` with exclusive access to the openclaw config/state store. Calls are
|
|
797
|
+
* serialized in invocation order; each waits for the previous to settle. The
|
|
798
|
+
* caller's result (or rejection) is returned faithfully — a rejection does NOT
|
|
799
|
+
* poison the queue for the next caller.
|
|
800
|
+
*/
|
|
801
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Promise-chain mutex. The tail is kept as a swallowed continuation so a rejected
|
|
805
|
+
* critical section never fails the NEXT unrelated caller, while the current caller
|
|
806
|
+
* still sees the real result/rejection.
|
|
807
|
+
*/
|
|
808
|
+
declare class SerialOpenClawCliLock implements OpenClawCliLock {
|
|
809
|
+
private tail;
|
|
810
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* A lock that provides NO serialization — every `run` executes immediately. For
|
|
814
|
+
* runtimes/contexts with no concurrent-openclaw-writer hazard, and for tests that
|
|
815
|
+
* don't need serialization. Keeps consumers from having to special-case an absent
|
|
816
|
+
* lock.
|
|
817
|
+
*/
|
|
818
|
+
declare class NoopOpenClawCliLock implements OpenClawCliLock {
|
|
819
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
820
|
+
}
|
|
821
|
+
//#endregion
|
|
749
822
|
//#region src/appliers/openclaw-applier.d.ts
|
|
750
823
|
interface OpenClawApplierOptions {
|
|
751
824
|
/**
|
|
@@ -771,6 +844,14 @@ interface OpenClawApplierOptions {
|
|
|
771
844
|
configSetRetries?: number;
|
|
772
845
|
/** Backoff between `openclaw config set` retries, ms (default 750; set 0 in tests) */
|
|
773
846
|
configSetRetryDelayMs?: number;
|
|
847
|
+
/**
|
|
848
|
+
* Shared process-wide lock serializing ALL openclaw config/state-mutating CLI
|
|
849
|
+
* calls (this applier's + the gateway's mcp-cleanup / alfe.config_set). Inject
|
|
850
|
+
* the daemon's single instance so every openclaw mutation across BOTH packages
|
|
851
|
+
* queues behind one mutex. Defaults to a private `SerialOpenClawCliLock` so a
|
|
852
|
+
* standalone applier (and existing tests) still serialize their own writes.
|
|
853
|
+
*/
|
|
854
|
+
cliLock?: OpenClawCliLock;
|
|
774
855
|
}
|
|
775
856
|
declare class OpenClawApplier implements RuntimeApplier {
|
|
776
857
|
readonly runtime = "openclaw";
|
|
@@ -780,22 +861,38 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
780
861
|
private trackingPath;
|
|
781
862
|
private configSetRetries;
|
|
782
863
|
private configSetRetryDelayMs;
|
|
783
|
-
/**
|
|
784
|
-
|
|
864
|
+
/**
|
|
865
|
+
* Shared process-wide mutex serializing ALL openclaw config/state-mutating CLI
|
|
866
|
+
* calls. Replaces the old per-applier `configSetQueue` promise-chain — a single
|
|
867
|
+
* lock so config-set can't interleave with plugins-install (or the gateway's
|
|
868
|
+
* mcp-cleanup / alfe.config_set), which is what corrupts the config store under
|
|
869
|
+
* OpenClaw's optimistic concurrency. NOT re-entrant: every locked public method
|
|
870
|
+
* acquires it ONCE at the top and calls only `*Unlocked` helpers inside.
|
|
871
|
+
*/
|
|
872
|
+
private cliLock;
|
|
785
873
|
constructor(options: OpenClawApplierOptions);
|
|
786
|
-
/** Convenience: `openclaw config set <args>`, serialized + retried. */
|
|
787
|
-
private runConfigSet;
|
|
788
874
|
/**
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
875
|
+
* Convenience: `openclaw config set <args>`, UNLOCKED + retried.
|
|
876
|
+
*
|
|
877
|
+
* Assumes the shared CLI lock is already held by the calling public method.
|
|
878
|
+
* Never call this from outside a `cliLock.run(...)` section.
|
|
879
|
+
*/
|
|
880
|
+
private runConfigSetUnlocked;
|
|
881
|
+
/**
|
|
882
|
+
* Run `openclaw config <args>` (set/unset), UNLOCKED, retried with backoff. See
|
|
883
|
+
* CONFIG_SET_RETRIES for why the retry loop exists: each write triggers a
|
|
884
|
+
* runtime hot-reload that rewrites openclaw.json, and a follow-up command that
|
|
885
|
+
* races the reload fails with a bare "Command failed".
|
|
886
|
+
*
|
|
887
|
+
* Serialization across every openclaw mutation is provided by `cliLock`, held
|
|
888
|
+
* by the public entry point — this helper assumes it and MUST NOT acquire the
|
|
889
|
+
* lock itself (that would deadlock the non-re-entrant promise-chain mutex).
|
|
793
890
|
*
|
|
794
891
|
* Throws an Error whose (scrubbed) message includes stderr after retries are
|
|
795
892
|
* exhausted, so the real cause propagates to the integration errorMessage
|
|
796
893
|
* without leaking the value payload.
|
|
797
894
|
*/
|
|
798
|
-
private
|
|
895
|
+
private runConfigCommandUnlocked;
|
|
799
896
|
/**
|
|
800
897
|
* Read back a single config path and compare it to the value we tried to
|
|
801
898
|
* write. Used by `applyConfig`'s verify-after-write: a `config set` can exit
|
|
@@ -819,21 +916,30 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
819
916
|
applyPlugin(spec: string, _installPath?: string, opts?: {
|
|
820
917
|
force?: boolean;
|
|
821
918
|
}): Promise<void>;
|
|
919
|
+
private applyPluginLocked;
|
|
822
920
|
/**
|
|
823
|
-
* Ensure one or more plugins are in plugins.allow in openclaw.json.
|
|
921
|
+
* Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
|
|
824
922
|
* Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
|
|
825
923
|
*
|
|
924
|
+
* This is a read-modify-write (`config get plugins.allow` → union → `config
|
|
925
|
+
* set`). The caller MUST hold the shared CLI lock across the WHOLE call so no
|
|
926
|
+
* other openclaw process mutates `plugins.allow` between the read and the write
|
|
927
|
+
* (that would clobber the concurrent change). All callers wrap it in
|
|
928
|
+
* `cliLock.run(...)` — never invoke it outside a locked section.
|
|
929
|
+
*
|
|
826
930
|
* Prefer passing the FULL set of plugins for an integration in a single call
|
|
827
931
|
* (see `ensurePluginsAllowed`): each write triggers a runtime hot-reload, so
|
|
828
932
|
* one write for N plugins is one reload instead of N.
|
|
829
933
|
*/
|
|
830
|
-
private
|
|
934
|
+
private ensurePluginsAllowUnlocked;
|
|
831
935
|
/**
|
|
832
936
|
* Pre-trust every plugin an integration ships in a SINGLE plugins.allow
|
|
833
937
|
* write, before any are installed. Called once by the manager ahead of the
|
|
834
938
|
* per-plugin install loop so activation triggers one hot-reload for the
|
|
835
939
|
* allowlist instead of one per plugin. `applyPlugin`'s own per-plugin
|
|
836
|
-
* `
|
|
940
|
+
* `ensurePluginsAllowUnlocked` then finds nothing missing and is a no-op.
|
|
941
|
+
*
|
|
942
|
+
* Acquires the shared CLI lock for the whole read-modify-write.
|
|
837
943
|
*/
|
|
838
944
|
ensurePluginsAllowed(specs: string[]): Promise<void>;
|
|
839
945
|
/**
|
|
@@ -856,6 +962,12 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
856
962
|
*/
|
|
857
963
|
private cleanupUntrackedExtensionInstall;
|
|
858
964
|
removePlugin(spec: string): Promise<void>;
|
|
965
|
+
/**
|
|
966
|
+
* `openclaw plugins uninstall`, UNLOCKED. Assumes the shared CLI lock is held
|
|
967
|
+
* by the caller (the public `removePlugin`, or `applyPluginLocked`'s force
|
|
968
|
+
* path). Never call outside a locked section.
|
|
969
|
+
*/
|
|
970
|
+
private removePluginUnlocked;
|
|
859
971
|
applySkill(name: string, srcPath: string): Promise<void>;
|
|
860
972
|
applyClawHubSkill(slug: string): Promise<void>;
|
|
861
973
|
removeClawHubSkill(slug: string): Promise<void>;
|
|
@@ -867,6 +979,7 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
867
979
|
* (config.json) so it can be cleanly removed later.
|
|
868
980
|
*/
|
|
869
981
|
applyConfig(integrationId: string, config: Record<string, unknown>): Promise<void>;
|
|
982
|
+
private applyConfigLocked;
|
|
870
983
|
/**
|
|
871
984
|
* Remove config previously applied by an integration.
|
|
872
985
|
*
|
|
@@ -874,6 +987,7 @@ declare class OpenClawApplier implements RuntimeApplier {
|
|
|
874
987
|
* then removes them via `openclaw config unset`.
|
|
875
988
|
*/
|
|
876
989
|
removeConfig(integrationId: string): Promise<void>;
|
|
990
|
+
private removeConfigLocked;
|
|
877
991
|
/**
|
|
878
992
|
* Raw single-key config write — `openclaw config set <key> <value>`.
|
|
879
993
|
*
|
|
@@ -1261,4 +1375,4 @@ declare class IntegrationManagerAdapter implements IIntegrationManager {
|
|
|
1261
1375
|
resetReinstallAttempts(integrationId: string): void;
|
|
1262
1376
|
}
|
|
1263
1377
|
//#endregion
|
|
1264
|
-
export { type CredentialsResolver, DEFAULT_REGISTRY_TTL_MS, HermesApplier, type HermesApplierOptions, HermesMcpSync, type HermesMcpSyncOptions, 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, type McpStoreReader, OpenClawApplier, type OpenClawApplierOptions, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, type RegistryOptions, RegistryResolveError, type ResolveOptions, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeConfigEntry, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
|
|
1378
|
+
export { type CredentialsResolver, DEFAULT_REGISTRY_TTL_MS, HermesApplier, type HermesApplierOptions, HermesMcpSync, type HermesMcpSyncOptions, 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, type McpStoreReader, NoopOpenClawCliLock, OpenClawApplier, type OpenClawApplierOptions, type OpenClawCliLock, type PlatformContext, Registry, type RegistryEntry, type RegistryFetcher, type RegistryIndex, type RegistryOptions, RegistryResolveError, type ResolveOptions, type ResolvedIntegration, Resolver, type RuntimeApplier, type RuntimeConfigEntry, type RuntimeDesiredState, type RuntimeLockFile, type RuntimePluginEntry, type RuntimeSkillEntry, SerialOpenClawCliLock, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
|
package/dist/index.js
CHANGED
|
@@ -1617,6 +1617,32 @@ var IntegrationManager = class {
|
|
|
1617
1617
|
}
|
|
1618
1618
|
};
|
|
1619
1619
|
//#endregion
|
|
1620
|
+
//#region src/openclaw-cli-lock.ts
|
|
1621
|
+
/**
|
|
1622
|
+
* Promise-chain mutex. The tail is kept as a swallowed continuation so a rejected
|
|
1623
|
+
* critical section never fails the NEXT unrelated caller, while the current caller
|
|
1624
|
+
* still sees the real result/rejection.
|
|
1625
|
+
*/
|
|
1626
|
+
var SerialOpenClawCliLock = class {
|
|
1627
|
+
tail = Promise.resolve();
|
|
1628
|
+
run(fn) {
|
|
1629
|
+
const result = this.tail.then(fn, fn);
|
|
1630
|
+
this.tail = result.then(() => void 0, () => void 0);
|
|
1631
|
+
return result;
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
/**
|
|
1635
|
+
* A lock that provides NO serialization — every `run` executes immediately. For
|
|
1636
|
+
* runtimes/contexts with no concurrent-openclaw-writer hazard, and for tests that
|
|
1637
|
+
* don't need serialization. Keeps consumers from having to special-case an absent
|
|
1638
|
+
* lock.
|
|
1639
|
+
*/
|
|
1640
|
+
var NoopOpenClawCliLock = class {
|
|
1641
|
+
run(fn) {
|
|
1642
|
+
return fn();
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
//#endregion
|
|
1620
1646
|
//#region src/appliers/config-flatten.ts
|
|
1621
1647
|
function flattenConfig(obj, prefix = "") {
|
|
1622
1648
|
const entries = [];
|
|
@@ -1791,8 +1817,15 @@ var OpenClawApplier = class {
|
|
|
1791
1817
|
trackingPath;
|
|
1792
1818
|
configSetRetries;
|
|
1793
1819
|
configSetRetryDelayMs;
|
|
1794
|
-
/**
|
|
1795
|
-
|
|
1820
|
+
/**
|
|
1821
|
+
* Shared process-wide mutex serializing ALL openclaw config/state-mutating CLI
|
|
1822
|
+
* calls. Replaces the old per-applier `configSetQueue` promise-chain — a single
|
|
1823
|
+
* lock so config-set can't interleave with plugins-install (or the gateway's
|
|
1824
|
+
* mcp-cleanup / alfe.config_set), which is what corrupts the config store under
|
|
1825
|
+
* OpenClaw's optimistic concurrency. NOT re-entrant: every locked public method
|
|
1826
|
+
* acquires it ONCE at the top and calls only `*Unlocked` helpers inside.
|
|
1827
|
+
*/
|
|
1828
|
+
cliLock;
|
|
1796
1829
|
constructor(options) {
|
|
1797
1830
|
const home = options.home ?? options.workspace;
|
|
1798
1831
|
if (!home) throw new Error("OpenClawApplier requires `home` (or legacy `workspace`) option");
|
|
@@ -1802,37 +1835,42 @@ var OpenClawApplier = class {
|
|
|
1802
1835
|
this.trackingPath = options.configPath ?? join(this.home, "config.json");
|
|
1803
1836
|
this.configSetRetries = options.configSetRetries ?? CONFIG_SET_RETRIES$1;
|
|
1804
1837
|
this.configSetRetryDelayMs = options.configSetRetryDelayMs ?? CONFIG_SET_RETRY_DELAY_MS$1;
|
|
1838
|
+
this.cliLock = options.cliLock ?? new SerialOpenClawCliLock();
|
|
1805
1839
|
}
|
|
1806
|
-
/**
|
|
1807
|
-
|
|
1808
|
-
|
|
1840
|
+
/**
|
|
1841
|
+
* Convenience: `openclaw config set <args>`, UNLOCKED + retried.
|
|
1842
|
+
*
|
|
1843
|
+
* Assumes the shared CLI lock is already held by the calling public method.
|
|
1844
|
+
* Never call this from outside a `cliLock.run(...)` section.
|
|
1845
|
+
*/
|
|
1846
|
+
runConfigSetUnlocked(setArgs, opts = {}) {
|
|
1847
|
+
return this.runConfigCommandUnlocked(["set", ...setArgs], opts);
|
|
1809
1848
|
}
|
|
1810
1849
|
/**
|
|
1811
|
-
* Run `openclaw config <args>` (set/unset),
|
|
1812
|
-
*
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1850
|
+
* Run `openclaw config <args>` (set/unset), UNLOCKED, retried with backoff. See
|
|
1851
|
+
* CONFIG_SET_RETRIES for why the retry loop exists: each write triggers a
|
|
1852
|
+
* runtime hot-reload that rewrites openclaw.json, and a follow-up command that
|
|
1853
|
+
* races the reload fails with a bare "Command failed".
|
|
1854
|
+
*
|
|
1855
|
+
* Serialization across every openclaw mutation is provided by `cliLock`, held
|
|
1856
|
+
* by the public entry point — this helper assumes it and MUST NOT acquire the
|
|
1857
|
+
* lock itself (that would deadlock the non-re-entrant promise-chain mutex).
|
|
1815
1858
|
*
|
|
1816
1859
|
* Throws an Error whose (scrubbed) message includes stderr after retries are
|
|
1817
1860
|
* exhausted, so the real cause propagates to the integration errorMessage
|
|
1818
1861
|
* without leaking the value payload.
|
|
1819
1862
|
*/
|
|
1820
|
-
|
|
1863
|
+
async runConfigCommandUnlocked(args, opts = {}) {
|
|
1821
1864
|
const timeout = opts.timeout ?? 1e4;
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
throw new Error(configSetErrorMessage(lastErr, args));
|
|
1832
|
-
};
|
|
1833
|
-
const result = this.configSetQueue.then(run, run);
|
|
1834
|
-
this.configSetQueue = result.catch(() => void 0);
|
|
1835
|
-
return result;
|
|
1865
|
+
let lastErr;
|
|
1866
|
+
for (let attempt = 0; attempt <= this.configSetRetries; attempt++) try {
|
|
1867
|
+
await execFileAsync$1("openclaw", ["config", ...args], { timeout });
|
|
1868
|
+
return;
|
|
1869
|
+
} catch (err) {
|
|
1870
|
+
lastErr = err;
|
|
1871
|
+
if (attempt < this.configSetRetries && this.configSetRetryDelayMs > 0) await delay$1(this.configSetRetryDelayMs * 2 ** attempt);
|
|
1872
|
+
}
|
|
1873
|
+
throw new Error(configSetErrorMessage(lastErr, args));
|
|
1836
1874
|
}
|
|
1837
1875
|
/**
|
|
1838
1876
|
* Read back a single config path and compare it to the value we tried to
|
|
@@ -1873,9 +1911,12 @@ var OpenClawApplier = class {
|
|
|
1873
1911
|
}
|
|
1874
1912
|
return false;
|
|
1875
1913
|
}
|
|
1876
|
-
|
|
1914
|
+
applyPlugin(spec, _installPath, opts) {
|
|
1915
|
+
return this.cliLock.run(() => this.applyPluginLocked(spec, opts));
|
|
1916
|
+
}
|
|
1917
|
+
async applyPluginLocked(spec, opts) {
|
|
1877
1918
|
const pkg = stripPluginVersion(spec);
|
|
1878
|
-
await this.
|
|
1919
|
+
await this.ensurePluginsAllowUnlocked(pkg);
|
|
1879
1920
|
this.cleanupUntrackedExtensionInstall(pkg);
|
|
1880
1921
|
if (opts?.force && this.isPluginInstalled(pkg)) {
|
|
1881
1922
|
log$3.info({
|
|
@@ -1883,7 +1924,7 @@ var OpenClawApplier = class {
|
|
|
1883
1924
|
spec
|
|
1884
1925
|
}, "Force mode — uninstalling plugin before reinstall");
|
|
1885
1926
|
try {
|
|
1886
|
-
await this.
|
|
1927
|
+
await this.removePluginUnlocked(pkg);
|
|
1887
1928
|
} catch (err) {
|
|
1888
1929
|
log$3.warn({
|
|
1889
1930
|
pkg,
|
|
@@ -1928,14 +1969,20 @@ var OpenClawApplier = class {
|
|
|
1928
1969
|
}
|
|
1929
1970
|
}
|
|
1930
1971
|
/**
|
|
1931
|
-
* Ensure one or more plugins are in plugins.allow in openclaw.json.
|
|
1972
|
+
* Ensure one or more plugins are in plugins.allow in openclaw.json, UNLOCKED.
|
|
1932
1973
|
* Uses `openclaw config set` to avoid clobbering OpenClaw's own file format.
|
|
1933
1974
|
*
|
|
1975
|
+
* This is a read-modify-write (`config get plugins.allow` → union → `config
|
|
1976
|
+
* set`). The caller MUST hold the shared CLI lock across the WHOLE call so no
|
|
1977
|
+
* other openclaw process mutates `plugins.allow` between the read and the write
|
|
1978
|
+
* (that would clobber the concurrent change). All callers wrap it in
|
|
1979
|
+
* `cliLock.run(...)` — never invoke it outside a locked section.
|
|
1980
|
+
*
|
|
1934
1981
|
* Prefer passing the FULL set of plugins for an integration in a single call
|
|
1935
1982
|
* (see `ensurePluginsAllowed`): each write triggers a runtime hot-reload, so
|
|
1936
1983
|
* one write for N plugins is one reload instead of N.
|
|
1937
1984
|
*/
|
|
1938
|
-
async
|
|
1985
|
+
async ensurePluginsAllowUnlocked(pkgs) {
|
|
1939
1986
|
const wanted = Array.isArray(pkgs) ? pkgs : [pkgs];
|
|
1940
1987
|
let currentAllow = [];
|
|
1941
1988
|
try {
|
|
@@ -1951,7 +1998,7 @@ var OpenClawApplier = class {
|
|
|
1951
1998
|
if (missing.length === 0) return;
|
|
1952
1999
|
const updated = [...currentAllow, ...missing];
|
|
1953
2000
|
try {
|
|
1954
|
-
await this.
|
|
2001
|
+
await this.runConfigSetUnlocked([
|
|
1955
2002
|
"plugins.allow",
|
|
1956
2003
|
JSON.stringify(updated),
|
|
1957
2004
|
"--merge"
|
|
@@ -1968,11 +2015,13 @@ var OpenClawApplier = class {
|
|
|
1968
2015
|
* write, before any are installed. Called once by the manager ahead of the
|
|
1969
2016
|
* per-plugin install loop so activation triggers one hot-reload for the
|
|
1970
2017
|
* allowlist instead of one per plugin. `applyPlugin`'s own per-plugin
|
|
1971
|
-
* `
|
|
2018
|
+
* `ensurePluginsAllowUnlocked` then finds nothing missing and is a no-op.
|
|
2019
|
+
*
|
|
2020
|
+
* Acquires the shared CLI lock for the whole read-modify-write.
|
|
1972
2021
|
*/
|
|
1973
|
-
|
|
1974
|
-
if (specs.length === 0) return;
|
|
1975
|
-
|
|
2022
|
+
ensurePluginsAllowed(specs) {
|
|
2023
|
+
if (specs.length === 0) return Promise.resolve();
|
|
2024
|
+
return this.cliLock.run(() => this.ensurePluginsAllowUnlocked(specs.map(stripPluginVersion)));
|
|
1976
2025
|
}
|
|
1977
2026
|
/**
|
|
1978
2027
|
* Check if a plugin is already installed.
|
|
@@ -2034,7 +2083,15 @@ var OpenClawApplier = class {
|
|
|
2034
2083
|
}
|
|
2035
2084
|
}
|
|
2036
2085
|
}
|
|
2037
|
-
|
|
2086
|
+
removePlugin(spec) {
|
|
2087
|
+
return this.cliLock.run(() => this.removePluginUnlocked(spec));
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* `openclaw plugins uninstall`, UNLOCKED. Assumes the shared CLI lock is held
|
|
2091
|
+
* by the caller (the public `removePlugin`, or `applyPluginLocked`'s force
|
|
2092
|
+
* path). Never call outside a locked section.
|
|
2093
|
+
*/
|
|
2094
|
+
async removePluginUnlocked(spec) {
|
|
2038
2095
|
await execFileAsync$1("openclaw", [
|
|
2039
2096
|
"plugins",
|
|
2040
2097
|
"uninstall",
|
|
@@ -2048,26 +2105,28 @@ var OpenClawApplier = class {
|
|
|
2048
2105
|
cpSync(srcPath, join(this.skillsDir, name), { recursive: true });
|
|
2049
2106
|
return Promise.resolve();
|
|
2050
2107
|
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
"
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2108
|
+
applyClawHubSkill(slug) {
|
|
2109
|
+
return this.cliLock.run(async () => {
|
|
2110
|
+
log$3.info({ slug }, "Installing skill from ClawHub");
|
|
2111
|
+
try {
|
|
2112
|
+
await execFileAsync$1("openclaw", [
|
|
2113
|
+
"skills",
|
|
2114
|
+
"install",
|
|
2115
|
+
slug
|
|
2116
|
+
], { timeout: 6e4 });
|
|
2117
|
+
log$3.info({ slug }, "ClawHub skill installed");
|
|
2118
|
+
} catch (err) {
|
|
2119
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2120
|
+
if (msg.includes("already exists") || msg.includes("Skill already exists")) {
|
|
2121
|
+
log$3.info({ slug }, "ClawHub skill already installed (skipped)");
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
log$3.error({
|
|
2125
|
+
slug,
|
|
2126
|
+
err: msg
|
|
2127
|
+
}, "ClawHub skill install failed");
|
|
2065
2128
|
}
|
|
2066
|
-
|
|
2067
|
-
slug,
|
|
2068
|
-
err: msg
|
|
2069
|
-
}, "ClawHub skill install failed");
|
|
2070
|
-
}
|
|
2129
|
+
});
|
|
2071
2130
|
}
|
|
2072
2131
|
removeClawHubSkill(slug) {
|
|
2073
2132
|
const workspaceSkillsDir = join(this.agentWorkspace, "skills", slug);
|
|
@@ -2094,7 +2153,10 @@ var OpenClawApplier = class {
|
|
|
2094
2153
|
* Each integration's config contribution is tracked in the tracking file
|
|
2095
2154
|
* (config.json) so it can be cleanly removed later.
|
|
2096
2155
|
*/
|
|
2097
|
-
|
|
2156
|
+
applyConfig(integrationId, config) {
|
|
2157
|
+
return this.cliLock.run(() => this.applyConfigLocked(integrationId, config));
|
|
2158
|
+
}
|
|
2159
|
+
async applyConfigLocked(integrationId, config) {
|
|
2098
2160
|
const tracking = this.readTracking();
|
|
2099
2161
|
const integrations = tracking._integrations ?? {};
|
|
2100
2162
|
integrations[integrationId] = config;
|
|
@@ -2105,7 +2167,7 @@ var OpenClawApplier = class {
|
|
|
2105
2167
|
const merged = { ...await readParentObject(parentPath) };
|
|
2106
2168
|
for (const [k, v] of dottedKvs) merged[k] = v;
|
|
2107
2169
|
try {
|
|
2108
|
-
await this.
|
|
2170
|
+
await this.runConfigSetUnlocked([
|
|
2109
2171
|
parentPath,
|
|
2110
2172
|
JSON.stringify(merged),
|
|
2111
2173
|
"--merge"
|
|
@@ -2126,7 +2188,7 @@ var OpenClawApplier = class {
|
|
|
2126
2188
|
}
|
|
2127
2189
|
}
|
|
2128
2190
|
if (leaves.length > 0) try {
|
|
2129
|
-
await this.
|
|
2191
|
+
await this.runConfigSetUnlocked([
|
|
2130
2192
|
"--batch-json",
|
|
2131
2193
|
JSON.stringify(leaves),
|
|
2132
2194
|
"--replace"
|
|
@@ -2150,14 +2212,17 @@ var OpenClawApplier = class {
|
|
|
2150
2212
|
* Reads the tracking file to find which config keys this integration set,
|
|
2151
2213
|
* then removes them via `openclaw config unset`.
|
|
2152
2214
|
*/
|
|
2153
|
-
|
|
2215
|
+
removeConfig(integrationId) {
|
|
2216
|
+
return this.cliLock.run(() => this.removeConfigLocked(integrationId));
|
|
2217
|
+
}
|
|
2218
|
+
async removeConfigLocked(integrationId) {
|
|
2154
2219
|
const tracking = this.readTracking();
|
|
2155
2220
|
const integrations = tracking._integrations ?? {};
|
|
2156
2221
|
if (!(integrationId in integrations)) return;
|
|
2157
2222
|
const integrationConfig = integrations[integrationId];
|
|
2158
2223
|
const { leaves, subtreesByParent } = partitionEntries(flattenConfig(integrationConfig));
|
|
2159
2224
|
for (const { path } of leaves) try {
|
|
2160
|
-
await this.
|
|
2225
|
+
await this.runConfigCommandUnlocked(["unset", path]);
|
|
2161
2226
|
} catch (err) {
|
|
2162
2227
|
log$3.warn({
|
|
2163
2228
|
err: err instanceof Error ? err.message : String(err),
|
|
@@ -2169,8 +2234,8 @@ var OpenClawApplier = class {
|
|
|
2169
2234
|
const remaining = Object.fromEntries(Object.entries(existing).filter(([k]) => !dottedKvs.has(k)));
|
|
2170
2235
|
if (Object.keys(existing).length === 0) continue;
|
|
2171
2236
|
try {
|
|
2172
|
-
if (Object.keys(remaining).length === 0) await this.
|
|
2173
|
-
else await this.
|
|
2237
|
+
if (Object.keys(remaining).length === 0) await this.runConfigCommandUnlocked(["unset", parentPath]);
|
|
2238
|
+
else await this.runConfigSetUnlocked([
|
|
2174
2239
|
parentPath,
|
|
2175
2240
|
JSON.stringify(remaining),
|
|
2176
2241
|
"--replace"
|
|
@@ -2196,7 +2261,7 @@ var OpenClawApplier = class {
|
|
|
2196
2261
|
* config writes trigger.
|
|
2197
2262
|
*/
|
|
2198
2263
|
setConfigRaw(key, value) {
|
|
2199
|
-
return this.
|
|
2264
|
+
return this.cliLock.run(() => this.runConfigSetUnlocked([key, value]));
|
|
2200
2265
|
}
|
|
2201
2266
|
isAvailable() {
|
|
2202
2267
|
return Promise.resolve(existsSync(this.home));
|
|
@@ -2948,4 +3013,4 @@ var IntegrationManagerAdapter = class {
|
|
|
2948
3013
|
}
|
|
2949
3014
|
};
|
|
2950
3015
|
//#endregion
|
|
2951
|
-
export { DEFAULT_REGISTRY_TTL_MS, HermesApplier, HermesMcpSync, Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, OpenClawApplier, Registry, RegistryResolveError, Resolver, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
|
|
3016
|
+
export { DEFAULT_REGISTRY_TTL_MS, HermesApplier, HermesMcpSync, Installer, InstallerError, IntegrationManager, IntegrationManagerAdapter, LockManager, McpApplier, NoopOpenClawCliLock, OpenClawApplier, Registry, RegistryResolveError, Resolver, SerialOpenClawCliLock, StateManager, buildHookEnv, resolveInstallsForRuntime, runHook, runHookWithContext, upsertEnvVar };
|
package/package.json
CHANGED