From a3a1081f22cc195c749d8ce97d2ea52fcf4901cf Mon Sep 17 00:00:00 2001 From: zhi Date: Tue, 24 Mar 2026 10:21:35 +0000 Subject: [PATCH] feat: add /ego-mgr slash command and rename pcexec to pc-exec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: 1. Add /ego-mgr slash command with subcommands: - get, set, list, delete, add-column, add-public-column, show - Uses pcExec to call ego-mgr binary with proper env vars 2. Rename pcexec → pc-exec throughout codebase: - Tool name: pcexec → pc-exec - Function exports: pcexec → pcExec, pcexecSync → pcExecSync - Updated all references in skills and plugin files 3. Translate all Chinese text to English: - ego-mgr-slash.ts responses - slash-commands.ts responses - SKILL.md files remain in English --- plugin/commands/ego-mgr-slash.ts | 214 ++++++++++++++++++++++++++++++ plugin/commands/slash-commands.ts | 36 ++--- plugin/index.ts | 40 ++++-- plugin/openclaw.plugin.json | 2 +- plugin/tools/pcexec.ts | 10 +- skills/ego-mgr/SKILL.md | 8 +- skills/secret-mgr/SKILL.md | 18 +-- 7 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 plugin/commands/ego-mgr-slash.ts diff --git a/plugin/commands/ego-mgr-slash.ts b/plugin/commands/ego-mgr-slash.ts new file mode 100644 index 0000000..2655c17 --- /dev/null +++ b/plugin/commands/ego-mgr-slash.ts @@ -0,0 +1,214 @@ +import { pcExec } from '../tools/pcexec'; + +export interface EgoMgrSlashCommandOptions { + /** OpenClaw base path */ + openclawPath: string; + /** Current agent ID */ + agentId: string; + /** Current workspace directory */ + workspaceDir: string; + /** Callback for replies */ + onReply: (message: string) => Promise; +} + +/** Sentinel value injected into every pc-exec subprocess */ +const AGENT_VERIFY = 'IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE'; + +export class EgoMgrSlashCommand { + private openclawPath: string; + private agentId: string; + private workspaceDir: string; + private onReply: (message: string) => Promise; + private binDir: string; + + constructor(options: EgoMgrSlashCommandOptions) { + this.openclawPath = options.openclawPath; + this.agentId = options.agentId; + this.workspaceDir = options.workspaceDir; + this.onReply = options.onReply; + this.binDir = require('path').join(this.openclawPath, 'bin'); + } + + /** + * Handle /ego-mgr slash command + * @param command Full command string (e.g., "/ego-mgr get name") + */ + async handle(command: string): Promise { + const parts = command.trim().split(/\s+/); + // Remove the "/ego-mgr" prefix + const args = parts.slice(1); + const subcommand = args[0]; + + if (!subcommand) { + await this.showUsage(); + return; + } + + try { + switch (subcommand) { + case 'get': + await this.handleGet(args.slice(1)); + break; + case 'set': + await this.handleSet(args.slice(1)); + break; + case 'list': + await this.handleList(args.slice(1)); + break; + case 'delete': + await this.handleDelete(args.slice(1)); + break; + case 'add-column': + await this.handleAddColumn(args.slice(1)); + break; + case 'add-public-column': + await this.handleAddPublicColumn(args.slice(1)); + break; + case 'show': + await this.handleShow(); + break; + case 'help': + default: + await this.showUsage(); + } + } catch (error: any) { + await this.onReply(`Error: ${error.message || error}`); + } + } + + private async showUsage(): Promise { + const usage = [ + '**ego-mgr Commands**', + '', + '`/ego-mgr get ` - Get field value', + '`/ego-mgr set ` - Set field value', + '`/ego-mgr list` - List all field names', + '`/ego-mgr delete ` - Delete a field', + '`/ego-mgr add-column ` - Add an Agent Scope field', + '`/ego-mgr add-public-column ` - Add a Public Scope field', + '`/ego-mgr show` - Show all fields and values', + '', + 'Examples:', + '`/ego-mgr get name`', + '`/ego-mgr set timezone Asia/Shanghai`', + ].join('\n'); + await this.onReply(usage); + } + + private async handleGet(args: string[]): Promise { + if (args.length < 1) { + await this.onReply('Usage: `/ego-mgr get `'); + return; + } + const columnName = args[0]; + const result = await this.execEgoMgr(['get', columnName]); + await this.onReply(`**${columnName}**: ${result || '(empty)'}`); + } + + private async handleSet(args: string[]): Promise { + if (args.length < 2) { + await this.onReply('Usage: `/ego-mgr set `'); + return; + } + const columnName = args[0]; + const value = args.slice(1).join(' '); // Support values with spaces + await this.execEgoMgr(['set', columnName, value]); + await this.onReply(`Set **${columnName}** = \`${value}\``); + } + + private async handleList(args: string[]): Promise { + const result = await this.execEgoMgr(['list', 'columns']); + if (!result.trim()) { + await this.onReply('No fields defined'); + return; + } + const columns = result.split('\n').filter(Boolean); + const lines = ['**Fields**:', '']; + for (const col of columns) { + lines.push(`• ${col}`); + } + await this.onReply(lines.join('\n')); + } + + private async handleDelete(args: string[]): Promise { + if (args.length < 1) { + await this.onReply('Usage: `/ego-mgr delete `'); + return; + } + const columnName = args[0]; + await this.execEgoMgr(['delete', columnName]); + await this.onReply(`Deleted field **${columnName}**`); + } + + private async handleAddColumn(args: string[]): Promise { + if (args.length < 1) { + await this.onReply('Usage: `/ego-mgr add-column `'); + return; + } + const columnName = args[0]; + await this.execEgoMgr(['add', 'column', columnName]); + await this.onReply(`Added Agent Scope field **${columnName}**`); + } + + private async handleAddPublicColumn(args: string[]): Promise { + if (args.length < 1) { + await this.onReply('Usage: `/ego-mgr add-public-column `'); + return; + } + const columnName = args[0]; + await this.execEgoMgr(['add', 'public-column', columnName]); + await this.onReply(`Added Public Scope field **${columnName}**`); + } + + private async handleShow(): Promise { + const result = await this.execEgoMgr(['show']); + if (!result.trim()) { + await this.onReply('No field data'); + return; + } + const lines = ['**Field Data**:', '']; + lines.push('```'); + lines.push(result); + lines.push('```'); + await this.onReply(lines.join('\n')); + } + + /** + * Execute ego-mgr binary via pc-exec + */ + private async execEgoMgr(args: string[]): Promise { + const currentPath = process.env.PATH || ''; + const newPath = currentPath.includes(this.binDir) + ? currentPath + : `${currentPath}:${this.binDir}`; + + const command = `ego-mgr ${args.map(a => this.shellEscape(a)).join(' ')}`; + + const result = await pcExec(command, { + cwd: this.workspaceDir, + env: { + AGENT_ID: this.agentId, + AGENT_WORKSPACE: this.workspaceDir, + AGENT_VERIFY, + PATH: newPath, + }, + }); + + if (result.exitCode !== 0 && result.stderr) { + throw new Error(result.stderr); + } + + return result.stdout; + } + + /** + * Escape a string for shell usage + */ + private shellEscape(str: string): string { + // Simple escaping for common cases + if (/^[a-zA-Z0-9._-]+$/.test(str)) { + return str; + } + return `'${str.replace(/'/g, "'\"'\"'")}'`; + } +} diff --git a/plugin/commands/slash-commands.ts b/plugin/commands/slash-commands.ts index 4789dde..08f8481 100644 --- a/plugin/commands/slash-commands.ts +++ b/plugin/commands/slash-commands.ts @@ -48,7 +48,7 @@ export class SlashCommandHandler { async handle(command: string, userId: string): Promise { // Check authorization if (!this.authorizedUsers.includes(userId)) { - await this.onReply('❌ 无权执行此命令'); + await this.onReply('Unauthorized'); return; } @@ -68,10 +68,10 @@ export class SlashCommandHandler { break; default: await this.onReply( - '用法:\n' + - '`/padded-cell-ctrl status` - 查看状态\n' + - '`/padded-cell-ctrl enable pass-mgr|safe-restart` - 启用功能\n' + - '`/padded-cell-ctrl disable pass-mgr|safe-restart` - 禁用功能' + 'Usage:\n' + + '`/padded-cell-ctrl status` - Show status\n' + + '`/padded-cell-ctrl enable pass-mgr|safe-restart` - Enable feature\n' + + '`/padded-cell-ctrl disable pass-mgr|safe-restart` - Disable feature' ); } } @@ -81,12 +81,12 @@ export class SlashCommandHandler { const agents = this.statusManager.getAllAgents(); const lines = [ - '**PaddedCell 状态**', + '**PaddedCell Status**', '', - `🔐 密码管理: ${this.state.passMgrEnabled ? '✅ 启用' : '❌ 禁用'}`, - `🔄 安全重启: ${this.state.safeRestartEnabled ? '✅ 启用' : '❌ 禁用'}`, + `Secret Manager: ${this.state.passMgrEnabled ? 'Enabled' : 'Disabled'}`, + `Safe Restart: ${this.state.safeRestartEnabled ? 'Enabled' : 'Disabled'}`, '', - '**Agent 状态:**', + '**Agent Status:**', ]; for (const agent of agents) { @@ -95,14 +95,14 @@ export class SlashCommandHandler { } if (agents.length === 0) { - lines.push('(暂无 agent 注册)'); + lines.push('(No agents registered)'); } if (global.restartStatus !== 'idle') { lines.push(''); - lines.push(`⚠️ 重启状态: ${global.restartStatus}`); + lines.push(`Restart Status: ${global.restartStatus}`); if (global.restartScheduledBy) { - lines.push(` 由 ${global.restartScheduledBy} 发起`); + lines.push(` Initiated by ${global.restartScheduledBy}`); } } @@ -111,12 +111,12 @@ export class SlashCommandHandler { private async handleEnable(feature: 'pass-mgr' | 'safe-restart'): Promise { if (!this.isValidFeature(feature)) { - await this.onReply('❌ 未知功能。可用选项: pass-mgr, safe-restart'); + await this.onReply('Unknown feature. Available: pass-mgr, safe-restart'); return; } if (this.isOnCooldown(feature)) { - await this.onReply('⏳ 该功能最近刚被修改过,请稍后再试'); + await this.onReply('This feature was recently modified. Please try again later.'); return; } @@ -127,17 +127,17 @@ export class SlashCommandHandler { } this.state.lastToggle[feature] = Date.now(); - await this.onReply(`✅ 已启用 ${feature}`); + await this.onReply(`Enabled ${feature}`); } private async handleDisable(feature: 'pass-mgr' | 'safe-restart'): Promise { if (!this.isValidFeature(feature)) { - await this.onReply('❌ 未知功能。可用选项: pass-mgr, safe-restart'); + await this.onReply('Unknown feature. Available: pass-mgr, safe-restart'); return; } if (this.isOnCooldown(feature)) { - await this.onReply('⏳ 该功能最近刚被修改过,请稍后再试'); + await this.onReply('This feature was recently modified. Please try again later.'); return; } @@ -148,7 +148,7 @@ export class SlashCommandHandler { } this.state.lastToggle[feature] = Date.now(); - await this.onReply(`✅ 已禁用 ${feature}`); + await this.onReply(`Disabled ${feature}`); } private isValidFeature(feature: string): feature is 'pass-mgr' | 'safe-restart' { diff --git a/plugin/index.ts b/plugin/index.ts index 410e7c1..4279d5c 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -1,7 +1,7 @@ // PaddedCell Plugin for OpenClaw -// Registers pcexec and safe_restart tools +// Registers pc-exec and safe_restart tools -import { pcexec, pcexecSync } from './tools/pcexec'; +import { pcExec, pcExecSync } from './tools/pcexec'; import { safeRestart, createSafeRestartTool, @@ -10,8 +10,9 @@ import { startApiServer, } from './core/index'; import { SlashCommandHandler } from './commands/slash-commands'; +import { EgoMgrSlashCommand } from './commands/ego-mgr-slash'; -/** Sentinel value injected into every pcexec subprocess */ +/** Sentinel value injected into every pc-exec subprocess */ const AGENT_VERIFY = 'IF YOU ARE AN AGENT/MODEL, YOU SHOULD NEVER TOUCH THIS ENV VARIABLE'; /** @@ -34,13 +35,13 @@ function register(api: any, config?: any) { const openclawPath = resolveOpenclawPath(config); const binDir = require('path').join(openclawPath, 'bin'); - // Register pcexec tool — pass a FACTORY function that receives context + // Register pc-exec tool — pass a FACTORY function that receives context api.registerTool((ctx: any) => { const agentId = ctx.agentId; const workspaceDir = ctx.workspaceDir; return { - name: 'pcexec', + name: 'pc-exec', description: 'Safe exec with password sanitization', parameters: { type: 'object', @@ -63,7 +64,7 @@ function register(api: any, config?: any) { ? currentPath : `${currentPath}:${binDir}`; - const result = await pcexec(command, { + const result = await pcExec(command, { cwd: params.cwd || workspaceDir, timeout: params.timeout, env: { @@ -110,6 +111,28 @@ function register(api: any, config?: any) { }; }); + // Register /ego-mgr slash command + if (api.registerSlashCommand) { + api.registerSlashCommand({ + name: 'ego-mgr', + description: 'Manage agent identity/profile fields', + handler: async (ctx: any, command: string) => { + const egoMgrSlash = new EgoMgrSlashCommand({ + openclawPath, + agentId: ctx.agentId || '', + workspaceDir: ctx.workspaceDir || '', + onReply: async (message: string) => { + if (ctx.reply) { + await ctx.reply(message); + } + }, + }); + await egoMgrSlash.handle(command); + }, + }); + logger.info('Registered /ego-mgr slash command'); + } + logger.info('PaddedCell plugin initialized'); } @@ -117,12 +140,13 @@ function register(api: any, config?: any) { module.exports = { register }; // Also export individual modules for direct use -module.exports.pcexec = pcexec; -module.exports.pcexecSync = pcexecSync; +module.exports.pcExec = pcExec; +module.exports.pcExecSync = pcExecSync; module.exports.safeRestart = safeRestart; module.exports.createSafeRestartTool = createSafeRestartTool; module.exports.StatusManager = StatusManager; module.exports.createApiServer = createApiServer; module.exports.startApiServer = startApiServer; module.exports.SlashCommandHandler = SlashCommandHandler; +module.exports.EgoMgrSlashCommand = EgoMgrSlashCommand; module.exports.AGENT_VERIFY = AGENT_VERIFY; diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index d36ddb5..f6f98b6 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -2,7 +2,7 @@ "id": "padded-cell", "name": "PaddedCell", "version": "0.2.0", - "description": "Secure password management, safe execution, and coordinated agent restart", + "description": "Secure secret management, agent identity management, safe execution, and coordinated agent restart", "entry": "./index.js", "configSchema": { "type": "object", diff --git a/plugin/tools/pcexec.ts b/plugin/tools/pcexec.ts index cd9f12f..8eb7d00 100644 --- a/plugin/tools/pcexec.ts +++ b/plugin/tools/pcexec.ts @@ -201,9 +201,9 @@ async function replaceSecretMgrGets( } /** - * Safe exec wrapper that handles pass_mgr get commands and sanitizes output. + * Safe exec wrapper that handles secret-mgr get commands and sanitizes output. */ -export async function pcexec( +export async function pcExec( command: string, options: PcExecOptions = {}, ): Promise { @@ -297,9 +297,9 @@ export async function pcexec( /** * Synchronous version — password substitution is NOT supported here - * (use async pcexec for pass_mgr integration). + * (use async pcExec for secret-mgr integration). */ -export function pcexecSync( +export function pcExecSync( command: string, options: PcExecOptions = {}, ): PcExecResult { @@ -336,4 +336,4 @@ export function pcexecSync( } } -export default pcexec; +export default pcExec; diff --git a/skills/ego-mgr/SKILL.md b/skills/ego-mgr/SKILL.md index 51f53cf..395131a 100644 --- a/skills/ego-mgr/SKILL.md +++ b/skills/ego-mgr/SKILL.md @@ -1,6 +1,6 @@ --- name: ego-mgr -description: Manage agent personal information (name, email, timezone, etc.). Use when storing, retrieving, listing, or managing agent profile fields. Trigger on requests about agent identity, personal info, profile settings, or ego-mgr usage. MUST call ego-mgr via the pcexec tool. +description: Manage agent personal information (name, email, timezone, etc.). Use when storing, retrieving, listing, or managing agent profile fields. Trigger on requests about agent identity, personal info, profile settings, or ego-mgr usage. MUST call ego-mgr via the pc-exec tool. --- # Ego Manager @@ -9,7 +9,7 @@ description: Manage agent personal information (name, email, timezone, etc.). Us Use ego-mgr to manage agent personal information fields. Supports per-agent fields (Agent Scope) and shared fields (Public Scope). ## Mandatory safety rule -Always invoke ego-mgr through the `pcexec` tool. Do NOT run ego-mgr directly. +Always invoke ego-mgr through the `pc-exec` tool. Do NOT run ego-mgr directly. ## Concepts @@ -23,7 +23,7 @@ Always invoke ego-mgr through the `pcexec` tool. Do NOT run ego-mgr directly. 2. Then, set its value: `ego-mgr set ` 3. Read it: `ego-mgr get ` or `ego-mgr show` -## Commands (run via pcexec) +## Commands (run via pc-exec) ### Add columns ```bash @@ -72,7 +72,7 @@ Lists all column names (public first, then agent-scope). | 1 | Usage error | | 2 | Column not found | | 3 | Column already exists | -| 4 | Permission error (not via pcexec) | +| 4 | Permission error (not via pc-exec) | | 5 | File lock failed | | 6 | JSON read/write error | diff --git a/skills/secret-mgr/SKILL.md b/skills/secret-mgr/SKILL.md index d4f79dd..ba8c67e 100644 --- a/skills/secret-mgr/SKILL.md +++ b/skills/secret-mgr/SKILL.md @@ -1,6 +1,6 @@ --- name: secret-mgr -description: Manage OpenClaw agent credentials (usernames/secrets). Use when storing, retrieving, listing, generating, or removing credentials for an agent. Trigger on requests about saving or fetching usernames, passwords, tokens, API keys, or other secrets. MUST call secret-mgr via the pcexec tool. +description: Manage OpenClaw agent credentials (usernames/secrets). Use when storing, retrieving, listing, generating, or removing credentials for an agent. Trigger on requests about saving or fetching usernames, passwords, tokens, API keys, or other secrets. MUST call secret-mgr via the pc-exec tool. --- # Secret Manager @@ -9,9 +9,9 @@ description: Manage OpenClaw agent credentials (usernames/secrets). Use when sto Use secret-mgr to store and retrieve agent-scoped credentials (username/secret pairs) and generate secrets. ## Mandatory safety rule -Always invoke secret-mgr through the `pcexec` tool. Do NOT run secret-mgr directly. +Always invoke secret-mgr through the `pc-exec` tool. Do NOT run secret-mgr directly. -## Commands (run via pcexec) +## Commands (run via pc-exec) - List keys for current agent - `secret-mgr list` @@ -50,19 +50,19 @@ Always invoke secret-mgr through the `pcexec` tool. Do NOT run secret-mgr direct - Storing can be explicit (user asks) or proactive after the agent successfully registers/creates an account. - Secrets should be fetched and used immediately in a command, not displayed (e.g., `xxx_cli login --user $(secret-mgr get-username --key some_key) --pass $(secret-mgr get-secret --key some_key)`). -## Examples (pcexec) +## Examples (pc-exec) - Store credentials - - pcexec: `secret-mgr set --key github --username alice --secret ` + - pc-exec: `secret-mgr set --key github --username alice --secret ` - Retrieve username - - pcexec: `secret-mgr get-username --key github` + - pc-exec: `secret-mgr get-username --key github` - Retrieve secret - - pcexec: `secret-mgr get-secret --key github` + - pc-exec: `secret-mgr get-secret --key github` - Generate secret - - pcexec: `secret-mgr generate --key github` + - pc-exec: `secret-mgr generate --key github` - Delete entry - - pcexec: `secret-mgr unset --key github` + - pc-exec: `secret-mgr unset --key github`