From 936e01b2fefce65fdeb43959120d9ff5b5a4e1bf Mon Sep 17 00:00:00 2001 From: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:36:17 +0000 Subject: [PATCH 1/3] feat: Add openclaw resource (auto-generated from issue #53) --- docs/resources/(resources)/openclaw.mdx | 67 ++++++++++ src/index.ts | 2 + src/resources/openclaw/openclaw.ts | 127 +++++++++++++++++++ src/resources/openclaw/settings-parameter.ts | 75 +++++++++++ test/openclaw/openclaw.test.ts | 74 +++++++++++ 5 files changed, 345 insertions(+) create mode 100644 docs/resources/(resources)/openclaw.mdx create mode 100644 src/resources/openclaw/openclaw.ts create mode 100644 src/resources/openclaw/settings-parameter.ts create mode 100644 test/openclaw/openclaw.test.ts diff --git a/docs/resources/(resources)/openclaw.mdx b/docs/resources/(resources)/openclaw.mdx new file mode 100644 index 00000000..a2d1e1d5 --- /dev/null +++ b/docs/resources/(resources)/openclaw.mdx @@ -0,0 +1,67 @@ +--- +title: openclaw +description: A reference page for the openclaw resource +--- + +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. + +## Parameters + +- **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: + - `gateway` — `mode` (`"local"` | `"remote"`), `port` (default `18789`), `bind` (`"loopback"` | `"lan"` | `"tailnet"` | `"auto"` | `"custom"`), `auth`, `tls`, `controlUi` + - `agents` — `defaults.{workspace,model,thinking,heartbeat,memory,media,skills}`, `list[]` for per-agent overrides + - `models` — `pricing.enabled`, `mode` (`"merge"` | `"replace"`), `providers` (custom/local model providers such as Ollama or LM Studio) + - `channels` — per-provider sections under `channels.` (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.) + - `tools` — `policy.{allow,deny}` lists controlling which tools (`exec`, `read`, `write`, `browser`, `web_search`, `cron`, etc.) agents can call + - `skills` — `allowBundled`, `load.extraDirs`, `install.nodeManager` + - `plugins` — `enabled`, `allow`/`deny`, `entries.*` + - `mcp` — `servers`, `sessionIdleTtlMs` + - `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 + +## Example usage + +### Install OpenClaw with gateway and agent defaults + +```json title="codify.jsonc" +[ + { + "type": "openclaw", + "settings": { + "gateway": { "port": 18789, "bind": "loopback" }, + "agents": { "defaults": { "model": "anthropic/claude-sonnet-4-6" } } + } + } +] +``` + +### OpenClaw with a Telegram channel and restricted tools + +```json title="codify.jsonc" +[ + { + "type": "openclaw", + "settings": { + "channels": { + "telegram": { + "botToken": "", + "dmPolicy": "allowlist", + "allowFrom": ["123456789"] + } + }, + "tools": { + "policy": { "allow": ["exec", "read", "write", "web_search"] } + } + } + } +] +``` + +## Notes + +- OpenClaw is installed via the official installer (`curl -fsSL https://openclaw.ai/install.sh | bash`) on both macOS and Linux. +- 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). +- The `settings` parameter merges only the declared top-level keys. Existing sections not in your Codify config are left untouched. +- After applying settings changes, Codify runs `openclaw gateway restart` so the running gateway picks up the new configuration. +- On destroy, the declared `settings` keys are removed and the OpenClaw binary, config, and state directory (`~/.openclaw`) are removed. +- 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. +- See the [OpenClaw configuration reference](https://docs.openclaw.ai/gateway/configuration-reference) for the complete list of configuration sections and fields. diff --git a/src/index.ts b/src/index.ts index b0145bc8..e3ee3418 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import { Pnpm } from './resources/javascript/pnpm/pnpm.js'; import { MacosSettingsResource } from './resources/macos/macos-settings/macos-settings-resource.js'; import { MacportsResource } from './resources/macports/macports.js'; import { ClaudeCodeResource } from './resources/claude-code/claude-code.js'; +import { OpenClawResource } from './resources/openclaw/openclaw.js'; import { ClaudeCodeProjectResource } from './resources/claude-code/claude-code-project.js'; import { OllamaResource } from './resources/ollama/ollama.js'; import { PgcliResource } from './resources/pgcli/pgcli.js'; @@ -127,6 +128,7 @@ runPlugin(Plugin.create( new SyncthingDeviceResource(), new SyncthingFolderResource(), new RbenvResource(), + new OpenClawResource(), ], { minSupportedCliVersion: MIN_SUPPORTED_CLI_VERSION } )) diff --git a/src/resources/openclaw/openclaw.ts b/src/resources/openclaw/openclaw.ts new file mode 100644 index 00000000..2b1e8607 --- /dev/null +++ b/src/resources/openclaw/openclaw.ts @@ -0,0 +1,127 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + Resource, + ResourceSettings, + SpawnStatus, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { OpenClawSettingsParameter } from './settings-parameter.js'; + +const schema = z + .object({ + settings: z + .record(z.string(), z.unknown()) + .optional() + .describe( + 'Top-level keys to merge into ~/.openclaw/openclaw.json. Supports gateway, agents, ' + + 'models, channels, tools, skills, plugins, mcp, browser, cron, and all other ' + + 'OpenClaw configuration sections.' + ), + }) + .meta({ $comment: 'https://codifycli.com/docs/resources/openclaw/openclaw' }) + .describe('OpenClaw installation and gateway configuration management'); + +export type OpenClawConfig = z.infer; + +const defaultConfig: Partial = { + settings: {}, +}; + +const exampleBasic: ExampleConfig = { + title: 'Install OpenClaw with gateway and agent defaults', + description: + 'Install OpenClaw and configure the local gateway port/bind address along with the ' + + 'default agent model.', + configs: [ + { + type: 'openclaw', + settings: { + gateway: { port: 18789, bind: 'loopback' }, + agents: { defaults: { model: 'anthropic/claude-sonnet-4-6' } }, + }, + }, + ], +}; + +const exampleWithChannels: ExampleConfig = { + title: 'OpenClaw with a Telegram channel and restricted tools', + description: + 'Install OpenClaw, connect a Telegram bot channel restricted to an allowlist, and limit ' + + 'the agent tool policy to a safe subset.', + configs: [ + { + type: 'openclaw', + settings: { + channels: { + telegram: { + botToken: '', + dmPolicy: 'allowlist', + allowFrom: ['123456789'], + }, + }, + tools: { + policy: { allow: ['exec', 'read', 'write', 'web_search'] }, + }, + }, + }, + ], +}; + +export class OpenClawResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'openclaw', + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleWithChannels, + }, + operatingSystems: [OS.Darwin, OS.Linux], + schema, + parameterSettings: { + settings: { type: 'stateful', definition: new OpenClawSettingsParameter(), order: 1 }, + }, + }; + } + + async refresh(_parameters: Partial): Promise | null> { + const $ = getPty(); + + const { status } = await $.spawnSafe('which openclaw'); + if (status !== SpawnStatus.SUCCESS) { + return null; + } + + return {}; + } + + async create(_plan: CreatePlan): Promise { + const $ = getPty(); + + await $.spawn( + 'bash -c "curl -fsSL https://openclaw.ai/install.sh | bash"', + { interactive: true }, + ); + + // Ensure PATH is updated so subsequent lifecycle methods can call `openclaw` + const localBin = path.join(os.homedir(), '.local', 'bin'); + process.env['PATH'] = `${localBin}:${process.env['PATH'] ?? ''}`; + } + + async destroy(_plan: DestroyPlan): Promise { + const $ = getPty(); + + await $.spawnSafe('openclaw gateway stop', { interactive: true }); + await $.spawnSafe('rm -f ~/.local/bin/openclaw', { interactive: true }); + + await fs.rm(path.join(os.homedir(), '.openclaw'), { recursive: true, force: true }); + } +} diff --git a/src/resources/openclaw/settings-parameter.ts b/src/resources/openclaw/settings-parameter.ts new file mode 100644 index 00000000..39a59013 --- /dev/null +++ b/src/resources/openclaw/settings-parameter.ts @@ -0,0 +1,75 @@ +import { ParameterSetting, Plan, StatefulParameter, getPty } from '@codifycli/plugin-core'; +import { StringIndexedObject } from '@codifycli/schemas'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +type Settings = Record; + +export const OPENCLAW_CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json'); + +export class OpenClawSettingsParameter extends StatefulParameter { + getSettings(): ParameterSetting { + return { type: 'object' }; + } + + override async refresh(_desired: Settings | null): Promise { + try { + const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8'); + return JSON.parse(content) as Settings; + } catch { + return null; + } + } + + async add(valueToAdd: Settings, plan: Plan): Promise { + await this.mergeIntoFile(valueToAdd); + await this.restartGateway(); + } + + async modify(newValue: Settings, previousValue: Settings, plan: Plan): Promise { + let existing: Settings = {}; + try { + existing = JSON.parse(await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8')); + } catch { /* file may not exist */ } + + for (const key of Object.keys(previousValue)) { + if (!(key in newValue)) { + delete existing[key]; + } + } + + Object.assign(existing, newValue); + + await fs.mkdir(path.dirname(OPENCLAW_CONFIG_PATH), { recursive: true }); + await fs.writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(existing, null, 2)); + await this.restartGateway(); + } + + async remove(valueToRemove: Settings, plan: Plan): Promise { + try { + const existing = JSON.parse(await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8')) as Settings; + for (const key of Object.keys(valueToRemove)) { + delete existing[key]; + } + await fs.writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(existing, null, 2)); + } catch { /* nothing to do if file doesn't exist */ } + + await this.restartGateway(); + } + + private async mergeIntoFile(settings: Settings): Promise { + let existing: Settings = {}; + try { + existing = JSON.parse(await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8')); + } catch { /* file may not exist yet */ } + + await fs.mkdir(path.dirname(OPENCLAW_CONFIG_PATH), { recursive: true }); + await fs.writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify({ ...existing, ...settings }, null, 2)); + } + + private async restartGateway(): Promise { + const $ = getPty(); + await $.spawnSafe('openclaw gateway restart', { interactive: true }); + } +} diff --git a/test/openclaw/openclaw.test.ts b/test/openclaw/openclaw.test.ts new file mode 100644 index 00000000..be3a8290 --- /dev/null +++ b/test/openclaw/openclaw.test.ts @@ -0,0 +1,74 @@ +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { afterAll, describe, expect, it } from 'vitest'; + +const OPENCLAW_CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json'); + +describe('openclaw resource integration tests', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + it('Can install openclaw', { timeout: 300_000 }, async () => { + await PluginTester.fullTest( + pluginPath, + [{ type: 'openclaw' }], + { + skipUninstall: true, + validateApply: async () => { + const { data } = await testSpawn('which openclaw'); + expect(data.trim().length).toBeGreaterThan(0); + }, + }, + ); + }); + + it('Can manage settings', { timeout: 300_000 }, async () => { + const initialSettings = { + gateway: { port: 18789, bind: 'loopback' }, + logging: { level: 'debug' }, + }; + + const modifiedSettings = { + gateway: { port: 18790, bind: 'loopback' }, + logging: { level: 'debug' }, + }; + + await PluginTester.fullTest( + pluginPath, + [{ type: 'openclaw', settings: initialSettings }], + { + validateApply: async () => { + const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8'); + const parsed = JSON.parse(content); + expect(parsed.gateway.port).toBe(18789); + expect(parsed.logging.level).toBe('debug'); + }, + testModify: { + modifiedConfigs: [{ type: 'openclaw', settings: modifiedSettings }], + validateModify: async () => { + const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8'); + const parsed = JSON.parse(content); + expect(parsed.gateway.port).toBe(18790); + }, + }, + validateDestroy: async () => { + try { + const content = await fs.readFile(OPENCLAW_CONFIG_PATH, 'utf8'); + const parsed = JSON.parse(content); + expect(parsed.gateway).toBeUndefined(); + } catch { + // file removed entirely is also acceptable + } + }, + }, + ); + }); + + afterAll(async () => { + // Best-effort cleanup in case tests left openclaw installed + await testSpawn('openclaw gateway stop'); + await testSpawn('rm -f ~/.local/bin/openclaw'); + await testSpawn('rm -rf ~/.openclaw'); + }, 60_000); +}); From a177f6734064bda0637013bfd16e54a1fab0890a Mon Sep 17 00:00:00 2001 From: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:45:39 +0000 Subject: [PATCH 2/3] fix: Linux test fixes for openclaw resource --- src/resources/openclaw/openclaw.ts | 1 + test/openclaw/openclaw.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/resources/openclaw/openclaw.ts b/src/resources/openclaw/openclaw.ts index 2b1e8607..ec7ce1ae 100644 --- a/src/resources/openclaw/openclaw.ts +++ b/src/resources/openclaw/openclaw.ts @@ -120,6 +120,7 @@ export class OpenClawResource extends Resource { const $ = getPty(); await $.spawnSafe('openclaw gateway stop', { interactive: true }); + await $.spawnSafe('npm uninstall -g openclaw', { interactive: true }); await $.spawnSafe('rm -f ~/.local/bin/openclaw', { interactive: true }); await fs.rm(path.join(os.homedir(), '.openclaw'), { recursive: true, force: true }); diff --git a/test/openclaw/openclaw.test.ts b/test/openclaw/openclaw.test.ts index be3a8290..1387dc48 100644 --- a/test/openclaw/openclaw.test.ts +++ b/test/openclaw/openclaw.test.ts @@ -68,6 +68,7 @@ describe('openclaw resource integration tests', async () => { afterAll(async () => { // Best-effort cleanup in case tests left openclaw installed await testSpawn('openclaw gateway stop'); + await testSpawn('npm uninstall -g openclaw'); await testSpawn('rm -f ~/.local/bin/openclaw'); await testSpawn('rm -rf ~/.openclaw'); }, 60_000); From c82da4b8cc46dec42b9c0338e0dd076b41d48556 Mon Sep 17 00:00:00 2001 From: kevinwang5658 <20214115+kevinwang5658@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:58:39 +0000 Subject: [PATCH 3/3] fix: macOS test fixes for openclaw resource --- src/resources/openclaw/openclaw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/openclaw/openclaw.ts b/src/resources/openclaw/openclaw.ts index ec7ce1ae..248451dd 100644 --- a/src/resources/openclaw/openclaw.ts +++ b/src/resources/openclaw/openclaw.ts @@ -107,7 +107,7 @@ export class OpenClawResource extends Resource { const $ = getPty(); await $.spawn( - 'bash -c "curl -fsSL https://openclaw.ai/install.sh | bash"', + 'bash -c "curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard --no-prompt"', { interactive: true }, );