Skip to content

Commit 936e01b

Browse files
kevinwang5658github-actions[bot]
authored andcommitted
feat: Add openclaw resource (auto-generated from issue #53)
1 parent e80ed9c commit 936e01b

5 files changed

Lines changed: 345 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
title: openclaw
3+
description: A reference page for the openclaw resource
4+
---
5+
6+
The openclaw resource installs [OpenClaw](https://docs.openclaw.ai/) — a self-hosted gateway that connects chat channels (Discord, Slack, Telegram, WhatsApp, Signal, iMessage, Matrix, Teams, and more) to AI agents — and manages its configuration. It handles installation via the official installer script and gives you declarative control over the gateway, agents, models, channels, tools, and every other section of the OpenClaw config.
7+
8+
## Parameters
9+
10+
- **settings**: *(object, optional)* Top-level keys to merge into `~/.openclaw/openclaw.json`. On apply, the declared keys are written; on destroy, only the declared keys are removed. Common sections include:
11+
- `gateway` — `mode` (`"local"` | `"remote"`), `port` (default `18789`), `bind` (`"loopback"` | `"lan"` | `"tailnet"` | `"auto"` | `"custom"`), `auth`, `tls`, `controlUi`
12+
- `agents` — `defaults.{workspace,model,thinking,heartbeat,memory,media,skills}`, `list[]` for per-agent overrides
13+
- `models` — `pricing.enabled`, `mode` (`"merge"` | `"replace"`), `providers` (custom/local model providers such as Ollama or LM Studio)
14+
- `channels` — per-provider sections under `channels.<provider>` (e.g. `discord`, `slack`, `telegram`, `whatsapp`, `signal`, `imessage`, `matrix`, `teams`). A channel starts automatically once its config section exists (unless `enabled: false`). Common fields: `dmPolicy`, `groupPolicy`, `allowFrom`, `mediaMaxMb`, `historyLimit`, plus provider-specific credentials (`token`, `botToken`, `appToken`, etc.)
15+
- `tools` — `policy.{allow,deny}` lists controlling which tools (`exec`, `read`, `write`, `browser`, `web_search`, `cron`, etc.) agents can call
16+
- `skills` — `allowBundled`, `load.extraDirs`, `install.nodeManager`
17+
- `plugins` — `enabled`, `allow`/`deny`, `entries.*`
18+
- `mcp` — `servers`, `sessionIdleTtlMs`
19+
- `browser`, `logging`, `cron`, `hooks`, `ui`, `env`, `secrets`, `auth`, `discovery`, `acp` — see the [configuration reference](https://docs.openclaw.ai/gateway/configuration-reference) for the full list of fields
20+
21+
## Example usage
22+
23+
### Install OpenClaw with gateway and agent defaults
24+
25+
```json title="codify.jsonc"
26+
[
27+
{
28+
"type": "openclaw",
29+
"settings": {
30+
"gateway": { "port": 18789, "bind": "loopback" },
31+
"agents": { "defaults": { "model": "anthropic/claude-sonnet-4-6" } }
32+
}
33+
}
34+
]
35+
```
36+
37+
### OpenClaw with a Telegram channel and restricted tools
38+
39+
```json title="codify.jsonc"
40+
[
41+
{
42+
"type": "openclaw",
43+
"settings": {
44+
"channels": {
45+
"telegram": {
46+
"botToken": "<Replace me here!>",
47+
"dmPolicy": "allowlist",
48+
"allowFrom": ["123456789"]
49+
}
50+
},
51+
"tools": {
52+
"policy": { "allow": ["exec", "read", "write", "web_search"] }
53+
}
54+
}
55+
}
56+
]
57+
```
58+
59+
## Notes
60+
61+
- OpenClaw is installed via the official installer (`curl -fsSL https://openclaw.ai/install.sh | bash`) on both macOS and Linux.
62+
- The configuration file lives at `~/.openclaw/openclaw.json` and uses JSON5 (Codify reads/writes it as plain JSON, so any comments in a hand-edited file will not be preserved).
63+
- The `settings` parameter merges only the declared top-level keys. Existing sections not in your Codify config are left untouched.
64+
- After applying settings changes, Codify runs `openclaw gateway restart` so the running gateway picks up the new configuration.
65+
- On destroy, the declared `settings` keys are removed and the OpenClaw binary, config, and state directory (`~/.openclaw`) are removed.
66+
- Model provider authentication (API keys) and full guided onboarding are not managed by this resource — configure credentials under `settings.models` / `settings.auth`, or run `openclaw onboard` manually for an interactive setup.
67+
- See the [OpenClaw configuration reference](https://docs.openclaw.ai/gateway/configuration-reference) for the complete list of configuration sections and fields.

‎src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Pnpm } from './resources/javascript/pnpm/pnpm.js';
2828
import { MacosSettingsResource } from './resources/macos/macos-settings/macos-settings-resource.js';
2929
import { MacportsResource } from './resources/macports/macports.js';
3030
import { ClaudeCodeResource } from './resources/claude-code/claude-code.js';
31+
import { OpenClawResource } from './resources/openclaw/openclaw.js';
3132
import { ClaudeCodeProjectResource } from './resources/claude-code/claude-code-project.js';
3233
import { OllamaResource } from './resources/ollama/ollama.js';
3334
import { PgcliResource } from './resources/pgcli/pgcli.js';
@@ -127,6 +128,7 @@ runPlugin(Plugin.create(
127128
new SyncthingDeviceResource(),
128129
new SyncthingFolderResource(),
129130
new RbenvResource(),
131+
new OpenClawResource(),
130132
],
131133
{ minSupportedCliVersion: MIN_SUPPORTED_CLI_VERSION }
132134
))

‎src/resources/openclaw/openclaw.ts‎

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import {
2+
CreatePlan,
3+
DestroyPlan,
4+
ExampleConfig,
5+
Resource,
6+
ResourceSettings,
7+
SpawnStatus,
8+
getPty,
9+
z,
10+
} from '@codifycli/plugin-core';
11+
import { OS } from '@codifycli/schemas';
12+
import fs from 'node:fs/promises';
13+
import os from 'node:os';
14+
import path from 'node:path';
15+
16+
import { OpenClawSettingsParameter } from './settings-parameter.js';
17+
18+
const schema = z
19+
.object({
20+
settings: z
21+
.record(z.string(), z.unknown())
22+
.optional()
23+
.describe(
24+
'Top-level keys to merge into ~/.openclaw/openclaw.json. Supports gateway, agents, ' +
25+
'models, channels, tools, skills, plugins, mcp, browser, cron, and all other ' +
26+
'OpenClaw configuration sections.'
27+
),
28+
})
29+
.meta({ $comment: 'https://codifycli.com/docs/resources/openclaw/openclaw' })
30+
.describe('OpenClaw installation and gateway configuration management');
31+
32+
export type OpenClawConfig = z.infer<typeof schema>;
33+
34+
const defaultConfig: Partial<OpenClawConfig> = {
35+
settings: {},
36+
};
37+
38+
const exampleBasic: ExampleConfig = {
39+
title: 'Install OpenClaw with gateway and agent defaults',
40+
description:
41+
'Install OpenClaw and configure the local gateway port/bind address along with the ' +
42+
'default agent model.',
43+
configs: [
44+
{
45+
type: 'openclaw',
46+
settings: {
47+
gateway: { port: 18789, bind: 'loopback' },
48+
agents: { defaults: { model: 'anthropic/claude-sonnet-4-6' } },
49+
},
50+
},
51+
],
52+
};
53+
54+
const exampleWithChannels: ExampleConfig = {
55+
title: 'OpenClaw with a Telegram channel and restricted tools',
56+
description:
57+
'Install OpenClaw, connect a Telegram bot channel restricted to an allowlist, and limit ' +
58+
'the agent tool policy to a safe subset.',
59+
configs: [
60+
{
61+
type: 'openclaw',
62+
settings: {
63+
channels: {
64+
telegram: {
65+
botToken: '<Replace me here!>',
66+
dmPolicy: 'allowlist',
67+
allowFrom: ['123456789'],
68+
},
69+
},
70+
tools: {
71+
policy: { allow: ['exec', 'read', 'write', 'web_search'] },
72+
},
73+
},
74+
},
75+
],
76+
};
77+
78+
export class OpenClawResource extends Resource<OpenClawConfig> {
79+
getSettings(): ResourceSettings<OpenClawConfig> {
80+
return {
81+
id: 'openclaw',
82+
defaultConfig,
83+
exampleConfigs: {
84+
example1: exampleBasic,
85+
example2: exampleWithChannels,
86+
},
87+
operatingSystems: [OS.Darwin, OS.Linux],
88+
schema,
89+
parameterSettings: {
90+
settings: { type: 'stateful', definition: new OpenClawSettingsParameter(), order: 1 },
91+
},
92+
};
93+
}
94+
95+
async refresh(_parameters: Partial<OpenClawConfig>): Promise<Partial<OpenClawConfig> | null> {
96+
const $ = getPty();
97+
98+
const { status } = await $.spawnSafe('which openclaw');
99+
if (status !== SpawnStatus.SUCCESS) {
100+
return null;
101+
}
102+
103+
return {};
104+
}
105+
106+
async create(_plan: CreatePlan<OpenClawConfig>): Promise<void> {
107+
const $ = getPty();
108+
109+
await $.spawn(
110+
'bash -c "curl -fsSL https://openclaw.ai/install.sh | bash"',
111+
{ interactive: true },
112+
);
113+
114+
// Ensure PATH is updated so subsequent lifecycle methods can call `openclaw`
115+
const localBin = path.join(os.homedir(), '.local', 'bin');
116+
process.env['PATH'] = `${localBin}:${process.env['PATH'] ?? ''}`;
117+
}
118+
119+
async destroy(_plan: DestroyPlan<OpenClawConfig>): Promise<void> {
120+
const $ = getPty();
121+
122+
await $.spawnSafe('openclaw gateway stop', { interactive: true });
123+
await $.spawnSafe('rm -f ~/.local/bin/openclaw', { interactive: true });
124+
125+
await fs.rm(path.join(os.homedir(), '.openclaw'), { recursive: true, force: true });
126+
}
127+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ParameterSetting, Plan, StatefulParameter, getPty } from '@codifycli/plugin-core';
2+
import { StringIndexedObject } from '@codifycli/schemas';
3+
import fs from 'node:fs/promises';
4+
import os from 'node:os';
5+
import path from 'node:path';
6+
7+
type Settings = Record<string, unknown>;
8+
9+
export const OPENCLAW_CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json');
10+
11+
export class OpenClawSettingsParameter extends StatefulParameter<StringIndexedObject, Settings> {
12+
getSettings(): ParameterSetting {
13+
return { type: 'object' };
14+
}
15+
16+
override async refresh(_desired: Settings | null): Promise<Settings | null> {
17+
try {
18+
const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8');
19+
return JSON.parse(content) as Settings;
20+
} catch {
21+
return null;
22+
}
23+
}
24+
25+
async add(valueToAdd: Settings, plan: Plan<StringIndexedObject>): Promise<void> {
26+
await this.mergeIntoFile(valueToAdd);
27+
await this.restartGateway();
28+
}
29+
30+
async modify(newValue: Settings, previousValue: Settings, plan: Plan<StringIndexedObject>): Promise<void> {
31+
let existing: Settings = {};
32+
try {
33+
existing = JSON.parse(await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8'));
34+
} catch { /* file may not exist */ }
35+
36+
for (const key of Object.keys(previousValue)) {
37+
if (!(key in newValue)) {
38+
delete existing[key];
39+
}
40+
}
41+
42+
Object.assign(existing, newValue);
43+
44+
await fs.mkdir(path.dirname(OPENCLAW_CONFIG_PATH), { recursive: true });
45+
await fs.writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(existing, null, 2));
46+
await this.restartGateway();
47+
}
48+
49+
async remove(valueToRemove: Settings, plan: Plan<StringIndexedObject>): Promise<void> {
50+
try {
51+
const existing = JSON.parse(await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8')) as Settings;
52+
for (const key of Object.keys(valueToRemove)) {
53+
delete existing[key];
54+
}
55+
await fs.writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(existing, null, 2));
56+
} catch { /* nothing to do if file doesn't exist */ }
57+
58+
await this.restartGateway();
59+
}
60+
61+
private async mergeIntoFile(settings: Settings): Promise<void> {
62+
let existing: Settings = {};
63+
try {
64+
existing = JSON.parse(await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8'));
65+
} catch { /* file may not exist yet */ }
66+
67+
await fs.mkdir(path.dirname(OPENCLAW_CONFIG_PATH), { recursive: true });
68+
await fs.writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify({ ...existing, ...settings }, null, 2));
69+
}
70+
71+
private async restartGateway(): Promise<void> {
72+
const $ = getPty();
73+
await $.spawnSafe('openclaw gateway restart', { interactive: true });
74+
}
75+
}

‎test/openclaw/openclaw.test.ts‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { PluginTester, testSpawn } from '@codifycli/plugin-test';
2+
import fs from 'node:fs/promises';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
import { afterAll, describe, expect, it } from 'vitest';
6+
7+
const OPENCLAW_CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json');
8+
9+
describe('openclaw resource integration tests', async () => {
10+
const pluginPath = path.resolve('./src/index.ts');
11+
12+
it('Can install openclaw', { timeout: 300_000 }, async () => {
13+
await PluginTester.fullTest(
14+
pluginPath,
15+
[{ type: 'openclaw' }],
16+
{
17+
skipUninstall: true,
18+
validateApply: async () => {
19+
const { data } = await testSpawn('which openclaw');
20+
expect(data.trim().length).toBeGreaterThan(0);
21+
},
22+
},
23+
);
24+
});
25+
26+
it('Can manage settings', { timeout: 300_000 }, async () => {
27+
const initialSettings = {
28+
gateway: { port: 18789, bind: 'loopback' },
29+
logging: { level: 'debug' },
30+
};
31+
32+
const modifiedSettings = {
33+
gateway: { port: 18790, bind: 'loopback' },
34+
logging: { level: 'debug' },
35+
};
36+
37+
await PluginTester.fullTest(
38+
pluginPath,
39+
[{ type: 'openclaw', settings: initialSettings }],
40+
{
41+
validateApply: async () => {
42+
const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8');
43+
const parsed = JSON.parse(content);
44+
expect(parsed.gateway.port).toBe(18789);
45+
expect(parsed.logging.level).toBe('debug');
46+
},
47+
testModify: {
48+
modifiedConfigs: [{ type: 'openclaw', settings: modifiedSettings }],
49+
validateModify: async () => {
50+
const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8');
51+
const parsed = JSON.parse(content);
52+
expect(parsed.gateway.port).toBe(18790);
53+
},
54+
},
55+
validateDestroy: async () => {
56+
try {
57+
const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8');
58+
const parsed = JSON.parse(content);
59+
expect(parsed.gateway).toBeUndefined();
60+
} catch {
61+
// file removed entirely is also acceptable
62+
}
63+
},
64+
},
65+
);
66+
});
67+
68+
afterAll(async () => {
69+
// Best-effort cleanup in case tests left openclaw installed
70+
await testSpawn('openclaw gateway stop');
71+
await testSpawn('rm -f ~/.local/bin/openclaw');
72+
await testSpawn('rm -rf ~/.openclaw');
73+
}, 60_000);
74+
});

0 commit comments

Comments
 (0)