@coffeexdev/openclaw-sentinel 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -47,6 +47,8 @@ npm i @coffeexdev/openclaw-sentinel
47
47
 
48
48
  ## Quick usage
49
49
 
50
+ No hosts are allowed by default — you must explicitly configure `allowedHosts` for watchers to connect to any endpoint.
51
+
50
52
  ```ts
51
53
  import { createSentinelPlugin } from "@coffeexdev/openclaw-sentinel";
52
54
 
@@ -0,0 +1,2 @@
1
+ import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk";
2
+ export declare const sentinelConfigSchema: OpenClawPluginConfigSchema;
@@ -0,0 +1,115 @@
1
+ import { z } from "zod";
2
+ const limitsSchema = z.object({
3
+ maxWatchersTotal: z.number().int().positive().default(200),
4
+ maxWatchersPerSkill: z.number().int().positive().default(20),
5
+ maxConditionsPerWatcher: z.number().int().positive().default(25),
6
+ maxIntervalMsFloor: z.number().int().positive().default(1000),
7
+ });
8
+ const configZodSchema = z.object({
9
+ allowedHosts: z.array(z.string()).default([]),
10
+ localDispatchBase: z.string().url().default("http://127.0.0.1:18789"),
11
+ dispatchAuthToken: z.string().optional(),
12
+ stateFilePath: z.string().optional(),
13
+ limits: limitsSchema.default({}),
14
+ });
15
+ export const sentinelConfigSchema = {
16
+ safeParse: (value) => {
17
+ if (value === undefined)
18
+ return { success: true, data: undefined };
19
+ return configZodSchema.safeParse(value);
20
+ },
21
+ jsonSchema: {
22
+ type: "object",
23
+ additionalProperties: false,
24
+ properties: {
25
+ allowedHosts: {
26
+ type: "array",
27
+ items: { type: "string" },
28
+ description: "Hostnames the watchers are permitted to connect to. Must be explicitly configured — no hosts are allowed by default.",
29
+ default: [],
30
+ },
31
+ localDispatchBase: {
32
+ type: "string",
33
+ format: "uri",
34
+ description: "Base URL for internal webhook dispatch",
35
+ default: "http://127.0.0.1:18789",
36
+ },
37
+ dispatchAuthToken: {
38
+ type: "string",
39
+ description: "Bearer token for authenticating webhook dispatch requests",
40
+ },
41
+ stateFilePath: {
42
+ type: "string",
43
+ description: "Custom path for the sentinel state persistence file",
44
+ },
45
+ limits: {
46
+ type: "object",
47
+ additionalProperties: false,
48
+ description: "Resource limits for watcher creation",
49
+ properties: {
50
+ maxWatchersTotal: {
51
+ type: "number",
52
+ description: "Maximum total watchers across all skills",
53
+ default: 200,
54
+ },
55
+ maxWatchersPerSkill: {
56
+ type: "number",
57
+ description: "Maximum watchers per skill",
58
+ default: 20,
59
+ },
60
+ maxConditionsPerWatcher: {
61
+ type: "number",
62
+ description: "Maximum conditions per watcher definition",
63
+ default: 25,
64
+ },
65
+ maxIntervalMsFloor: {
66
+ type: "number",
67
+ description: "Minimum allowed polling interval in milliseconds",
68
+ default: 1000,
69
+ },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ uiHints: {
75
+ allowedHosts: {
76
+ label: "Allowed Hosts",
77
+ help: "Hostnames the watchers are permitted to connect to",
78
+ },
79
+ localDispatchBase: {
80
+ label: "Dispatch Base URL",
81
+ help: "Base URL for internal webhook dispatch (default: http://127.0.0.1:18789)",
82
+ },
83
+ dispatchAuthToken: {
84
+ label: "Dispatch Auth Token",
85
+ help: "Bearer token for webhook dispatch authentication (or use SENTINEL_DISPATCH_TOKEN env var)",
86
+ sensitive: true,
87
+ placeholder: "sk-...",
88
+ },
89
+ stateFilePath: {
90
+ label: "State File Path",
91
+ help: "Custom path for sentinel state persistence file",
92
+ advanced: true,
93
+ },
94
+ "limits.maxWatchersTotal": {
95
+ label: "Max Watchers",
96
+ help: "Maximum total watchers across all skills",
97
+ advanced: true,
98
+ },
99
+ "limits.maxWatchersPerSkill": {
100
+ label: "Max Per Skill",
101
+ help: "Maximum watchers a single skill can create",
102
+ advanced: true,
103
+ },
104
+ "limits.maxConditionsPerWatcher": {
105
+ label: "Max Conditions",
106
+ help: "Maximum conditions per watcher definition",
107
+ advanced: true,
108
+ },
109
+ "limits.maxIntervalMsFloor": {
110
+ label: "Min Poll Interval (ms)",
111
+ help: "Minimum allowed polling interval in milliseconds",
112
+ advanced: true,
113
+ },
114
+ },
115
+ };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,19 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
2
  import { WatcherManager } from "./watcherManager.js";
2
3
  import { SentinelConfig } from "./types.js";
3
4
  export declare function createSentinelPlugin(overrides?: Partial<SentinelConfig>): {
4
5
  manager: WatcherManager;
5
6
  init(): Promise<void>;
6
- register(api: {
7
- registerTool: (name: string, handler: (input: unknown) => Promise<unknown>) => void;
8
- }): void;
7
+ register(api: OpenClawPluginApi): void;
9
8
  };
9
+ declare const sentinelPlugin: {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ configSchema: import("openclaw/plugin-sdk").OpenClawPluginConfigSchema;
14
+ register(api: OpenClawPluginApi): void;
15
+ };
16
+ export declare const register: (api: OpenClawPluginApi) => void;
17
+ export declare const activate: (api: OpenClawPluginApi) => void;
18
+ export default sentinelPlugin;
10
19
  export * from "./types.js";
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { registerSentinelControl } from "./tool.js";
2
2
  import { WatcherManager } from "./watcherManager.js";
3
+ import { sentinelConfigSchema } from "./configSchema.js";
3
4
  export function createSentinelPlugin(overrides) {
4
5
  const config = {
5
- allowedHosts: ["api.github.com", "api.coingecko.com", "example.com"],
6
+ allowedHosts: [],
6
7
  localDispatchBase: "http://127.0.0.1:18789",
7
8
  dispatchAuthToken: process.env.SENTINEL_DISPATCH_TOKEN,
8
9
  limits: {
@@ -31,8 +32,23 @@ export function createSentinelPlugin(overrides) {
31
32
  await manager.init();
32
33
  },
33
34
  register(api) {
34
- registerSentinelControl(api.registerTool, manager);
35
+ registerSentinelControl(api.registerTool.bind(api), manager);
35
36
  },
36
37
  };
37
38
  }
39
+ // OpenClaw plugin entrypoint (default plugin object with register)
40
+ const sentinelPlugin = {
41
+ id: "openclaw-sentinel",
42
+ name: "OpenClaw Sentinel",
43
+ description: "Secure declarative gateway-native watcher plugin for OpenClaw",
44
+ configSchema: sentinelConfigSchema,
45
+ register(api) {
46
+ const plugin = createSentinelPlugin(api.pluginConfig);
47
+ void plugin.init();
48
+ plugin.register(api);
49
+ },
50
+ };
51
+ export const register = sentinelPlugin.register.bind(sentinelPlugin);
52
+ export const activate = sentinelPlugin.register.bind(sentinelPlugin);
53
+ export default sentinelPlugin;
38
54
  export * from "./types.js";
package/dist/tool.d.ts CHANGED
@@ -1,2 +1,5 @@
1
+ import type { AnyAgentTool } from "openclaw/plugin-sdk";
1
2
  import { WatcherManager } from "./watcherManager.js";
2
- export declare function registerSentinelControl(registerTool: (name: string, handler: (input: unknown) => Promise<unknown>) => void, manager: WatcherManager): void;
3
+ type RegisterToolFn = (tool: AnyAgentTool) => void;
4
+ export declare function registerSentinelControl(registerTool: RegisterToolFn, manager: WatcherManager): void;
5
+ export {};
package/dist/tool.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { jsonResult } from "openclaw/plugin-sdk";
1
2
  import { z } from "zod";
2
- const inputSchema = z
3
+ import { SentinelToolSchema } from "./toolSchema.js";
4
+ const ParamsSchema = z
3
5
  .object({
4
6
  action: z.enum(["create", "enable", "disable", "remove", "status", "list"]),
5
7
  id: z.string().optional(),
@@ -7,32 +9,27 @@ const inputSchema = z
7
9
  })
8
10
  .strict();
9
11
  export function registerSentinelControl(registerTool, manager) {
10
- registerTool("sentinel_control", async (input) => {
11
- const parsed = inputSchema.parse(input);
12
- switch (parsed.action) {
13
- case "create":
14
- return manager.create(parsed.watcher);
15
- case "enable":
16
- if (!parsed.id)
17
- throw new Error("id required");
18
- await manager.enable(parsed.id);
19
- return { ok: true };
20
- case "disable":
21
- if (!parsed.id)
22
- throw new Error("id required");
23
- await manager.disable(parsed.id);
24
- return { ok: true };
25
- case "remove":
26
- if (!parsed.id)
27
- throw new Error("id required");
28
- await manager.remove(parsed.id);
29
- return { ok: true };
30
- case "status":
31
- if (!parsed.id)
32
- throw new Error("id required");
33
- return manager.status(parsed.id);
34
- case "list":
35
- return manager.list();
36
- }
12
+ registerTool({
13
+ name: "sentinel_control",
14
+ label: "sentinel_control",
15
+ description: "Create/manage sentinel watchers",
16
+ parameters: SentinelToolSchema,
17
+ async execute(_toolCallId, params) {
18
+ const payload = ParamsSchema.parse((params ?? {}));
19
+ switch (payload.action) {
20
+ case "create":
21
+ return jsonResult(await manager.create(payload.watcher));
22
+ case "enable":
23
+ return jsonResult(await manager.enable(payload.id ?? ""));
24
+ case "disable":
25
+ return jsonResult(await manager.disable(payload.id ?? ""));
26
+ case "remove":
27
+ return jsonResult(await manager.remove(payload.id ?? ""));
28
+ case "status":
29
+ return jsonResult(manager.status(payload.id ?? ""));
30
+ case "list":
31
+ return jsonResult(manager.list());
32
+ }
33
+ },
37
34
  });
38
35
  }
@@ -0,0 +1,34 @@
1
+ export declare const SentinelToolSchema: import("@sinclair/typebox").TObject<{
2
+ action: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"create">, import("@sinclair/typebox").TLiteral<"enable">, import("@sinclair/typebox").TLiteral<"disable">, import("@sinclair/typebox").TLiteral<"remove">, import("@sinclair/typebox").TLiteral<"status">, import("@sinclair/typebox").TLiteral<"list">]>;
3
+ id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
4
+ watcher: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
5
+ id: import("@sinclair/typebox").TString;
6
+ skillId: import("@sinclair/typebox").TString;
7
+ enabled: import("@sinclair/typebox").TBoolean;
8
+ strategy: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"http-poll">, import("@sinclair/typebox").TLiteral<"websocket">, import("@sinclair/typebox").TLiteral<"sse">, import("@sinclair/typebox").TLiteral<"http-long-poll">]>;
9
+ endpoint: import("@sinclair/typebox").TString;
10
+ method: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"GET">, import("@sinclair/typebox").TLiteral<"POST">]>>;
11
+ headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
12
+ body: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
13
+ intervalMs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
14
+ timeoutMs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
15
+ match: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"all">, import("@sinclair/typebox").TLiteral<"any">]>;
16
+ conditions: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
17
+ path: import("@sinclair/typebox").TString;
18
+ op: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"eq">, import("@sinclair/typebox").TLiteral<"neq">, import("@sinclair/typebox").TLiteral<"gt">, import("@sinclair/typebox").TLiteral<"gte">, import("@sinclair/typebox").TLiteral<"lt">, import("@sinclair/typebox").TLiteral<"lte">, import("@sinclair/typebox").TLiteral<"exists">, import("@sinclair/typebox").TLiteral<"absent">, import("@sinclair/typebox").TLiteral<"contains">, import("@sinclair/typebox").TLiteral<"matches">, import("@sinclair/typebox").TLiteral<"changed">]>;
19
+ value: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnknown>;
20
+ }>>;
21
+ fire: import("@sinclair/typebox").TObject<{
22
+ webhookPath: import("@sinclair/typebox").TString;
23
+ eventName: import("@sinclair/typebox").TString;
24
+ payloadTemplate: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TBoolean, import("@sinclair/typebox").TNull]>>;
25
+ }>;
26
+ retry: import("@sinclair/typebox").TObject<{
27
+ maxRetries: import("@sinclair/typebox").TNumber;
28
+ baseMs: import("@sinclair/typebox").TNumber;
29
+ maxMs: import("@sinclair/typebox").TNumber;
30
+ }>;
31
+ fireOnce: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
32
+ metadata: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
33
+ }>>;
34
+ }>;
@@ -0,0 +1,77 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ const ConditionSchema = Type.Object({
3
+ path: Type.String({ description: "JSONPath expression to evaluate against the response" }),
4
+ op: Type.Union([
5
+ Type.Literal("eq"),
6
+ Type.Literal("neq"),
7
+ Type.Literal("gt"),
8
+ Type.Literal("gte"),
9
+ Type.Literal("lt"),
10
+ Type.Literal("lte"),
11
+ Type.Literal("exists"),
12
+ Type.Literal("absent"),
13
+ Type.Literal("contains"),
14
+ Type.Literal("matches"),
15
+ Type.Literal("changed"),
16
+ ], { description: "Comparison operator" }),
17
+ value: Type.Optional(Type.Unknown({
18
+ description: "Value to compare against (not needed for exists/absent/changed)",
19
+ })),
20
+ });
21
+ const FireConfigSchema = Type.Object({
22
+ webhookPath: Type.String({
23
+ description: "Path appended to localDispatchBase for webhook delivery",
24
+ }),
25
+ eventName: Type.String({ description: "Event name included in the dispatched payload" }),
26
+ payloadTemplate: Type.Record(Type.String(), Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]), {
27
+ description: "Key-value template for the webhook payload. Supports {{mustache}} interpolation from matched response data.",
28
+ }),
29
+ });
30
+ const RetryPolicySchema = Type.Object({
31
+ maxRetries: Type.Number({ description: "Maximum number of retry attempts" }),
32
+ baseMs: Type.Number({ description: "Base delay in milliseconds for exponential backoff" }),
33
+ maxMs: Type.Number({ description: "Maximum delay cap in milliseconds" }),
34
+ });
35
+ const WatcherSchema = Type.Object({
36
+ id: Type.String({ description: "Unique watcher identifier" }),
37
+ skillId: Type.String({ description: "ID of the skill that owns this watcher" }),
38
+ enabled: Type.Boolean({ description: "Whether the watcher is actively polling" }),
39
+ strategy: Type.Union([
40
+ Type.Literal("http-poll"),
41
+ Type.Literal("websocket"),
42
+ Type.Literal("sse"),
43
+ Type.Literal("http-long-poll"),
44
+ ], { description: "Connection strategy" }),
45
+ endpoint: Type.String({ description: "URL to monitor" }),
46
+ method: Type.Optional(Type.Union([Type.Literal("GET"), Type.Literal("POST")], {
47
+ description: "HTTP method (default GET)",
48
+ })),
49
+ headers: Type.Optional(Type.Record(Type.String(), Type.String(), {
50
+ description: "HTTP headers to include in requests",
51
+ })),
52
+ body: Type.Optional(Type.String({ description: "Request body for POST requests" })),
53
+ intervalMs: Type.Optional(Type.Number({ description: "Polling interval in milliseconds" })),
54
+ timeoutMs: Type.Optional(Type.Number({ description: "Request timeout in milliseconds" })),
55
+ match: Type.Union([Type.Literal("all"), Type.Literal("any")], {
56
+ description: "Whether all or any conditions must match to trigger",
57
+ }),
58
+ conditions: Type.Array(ConditionSchema, {
59
+ description: "Conditions evaluated against each response",
60
+ }),
61
+ fire: FireConfigSchema,
62
+ retry: RetryPolicySchema,
63
+ fireOnce: Type.Optional(Type.Boolean({ description: "If true, the watcher disables itself after firing once" })),
64
+ metadata: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Arbitrary key-value metadata" })),
65
+ }, { description: "Full watcher definition" });
66
+ export const SentinelToolSchema = Type.Object({
67
+ action: Type.Union([
68
+ Type.Literal("create"),
69
+ Type.Literal("enable"),
70
+ Type.Literal("disable"),
71
+ Type.Literal("remove"),
72
+ Type.Literal("status"),
73
+ Type.Literal("list"),
74
+ ], { description: "The action to perform" }),
75
+ id: Type.Optional(Type.String({ description: "Watcher ID (required for enable/disable/remove/status)" })),
76
+ watcher: Type.Optional(WatcherSchema),
77
+ });
@@ -2,8 +2,97 @@
2
2
  "id": "openclaw-sentinel",
3
3
  "configSchema": {
4
4
  "type": "object",
5
- "additionalProperties": true,
6
- "properties": {}
5
+ "additionalProperties": false,
6
+ "properties": {
7
+ "allowedHosts": {
8
+ "type": "array",
9
+ "items": { "type": "string" },
10
+ "description": "Hostnames the watchers are permitted to connect to. Must be explicitly configured — no hosts are allowed by default.",
11
+ "default": []
12
+ },
13
+ "localDispatchBase": {
14
+ "type": "string",
15
+ "format": "uri",
16
+ "description": "Base URL for internal webhook dispatch",
17
+ "default": "http://127.0.0.1:18789"
18
+ },
19
+ "dispatchAuthToken": {
20
+ "type": "string",
21
+ "description": "Bearer token for authenticating webhook dispatch requests"
22
+ },
23
+ "stateFilePath": {
24
+ "type": "string",
25
+ "description": "Custom path for the sentinel state persistence file"
26
+ },
27
+ "limits": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "description": "Resource limits for watcher creation",
31
+ "properties": {
32
+ "maxWatchersTotal": {
33
+ "type": "number",
34
+ "description": "Maximum total watchers across all skills",
35
+ "default": 200
36
+ },
37
+ "maxWatchersPerSkill": {
38
+ "type": "number",
39
+ "description": "Maximum watchers per skill",
40
+ "default": 20
41
+ },
42
+ "maxConditionsPerWatcher": {
43
+ "type": "number",
44
+ "description": "Maximum conditions per watcher definition",
45
+ "default": 25
46
+ },
47
+ "maxIntervalMsFloor": {
48
+ "type": "number",
49
+ "description": "Minimum allowed polling interval in milliseconds",
50
+ "default": 1000
51
+ }
52
+ }
53
+ }
54
+ }
55
+ },
56
+ "uiHints": {
57
+ "allowedHosts": {
58
+ "label": "Allowed Hosts",
59
+ "help": "Hostnames the watchers are permitted to connect to"
60
+ },
61
+ "localDispatchBase": {
62
+ "label": "Dispatch Base URL",
63
+ "help": "Base URL for internal webhook dispatch"
64
+ },
65
+ "dispatchAuthToken": {
66
+ "label": "Dispatch Auth Token",
67
+ "help": "Bearer token for webhook dispatch authentication",
68
+ "sensitive": true,
69
+ "placeholder": "sk-..."
70
+ },
71
+ "stateFilePath": {
72
+ "label": "State File Path",
73
+ "help": "Custom path for sentinel state persistence file",
74
+ "advanced": true
75
+ },
76
+ "limits.maxWatchersTotal": {
77
+ "label": "Max Watchers",
78
+ "help": "Maximum total watchers across all skills",
79
+ "advanced": true
80
+ },
81
+ "limits.maxWatchersPerSkill": {
82
+ "label": "Max Per Skill",
83
+ "help": "Maximum watchers a single skill can create",
84
+ "advanced": true
85
+ },
86
+ "limits.maxConditionsPerWatcher": {
87
+ "label": "Max Conditions",
88
+ "help": "Maximum conditions per watcher definition",
89
+ "advanced": true
90
+ },
91
+ "limits.maxIntervalMsFloor": {
92
+ "label": "Min Poll Interval (ms)",
93
+ "help": "Minimum allowed polling interval in milliseconds",
94
+ "advanced": true
95
+ }
7
96
  },
8
97
  "install": {
9
98
  "npmSpec": "@coffeexdev/openclaw-sentinel"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffeexdev/openclaw-sentinel",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Secure declarative gateway-native watcher plugin for OpenClaw",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -38,9 +38,11 @@
38
38
  "changeset": "changeset",
39
39
  "version-packages": "changeset version",
40
40
  "release": "changeset publish",
41
- "prepack": "npm run build"
41
+ "prepack": "npm run build",
42
+ "prepare": "husky"
42
43
  },
43
44
  "dependencies": {
45
+ "@sinclair/typebox": "^0.34.48",
44
46
  "re2-wasm": "^1.0.2",
45
47
  "ws": "^8.18.3",
46
48
  "zod": "^3.24.1"
@@ -49,15 +51,24 @@
49
51
  "@changesets/cli": "^2.29.7",
50
52
  "@types/node": "^24.0.0",
51
53
  "@types/ws": "^8.5.13",
54
+ "husky": "^9.1.7",
55
+ "lint-staged": "^16.3.2",
56
+ "openclaw": "latest",
52
57
  "oxfmt": "^0.36.0",
53
58
  "typescript": "^5.8.2",
54
59
  "vitest": "^3.0.8"
55
60
  },
61
+ "peerDependencies": {
62
+ "openclaw": ">=2026.3.2"
63
+ },
56
64
  "optionalDependencies": {
57
65
  "re2": "^1.23.3"
58
66
  },
67
+ "lint-staged": {
68
+ "*.{ts,js,jsx,tsx,mjs,cjs,json,yml,yaml,md,css,html}": "oxfmt --write"
69
+ },
59
70
  "engines": {
60
- "node": ">=20"
71
+ "node": ">=22"
61
72
  },
62
73
  "openclaw": {
63
74
  "extensions": [