feat: rename pass_mgr → secret-mgr, add ego-mgr binary and skill

M1: Rename pass_mgr to secret-mgr
- Rename directory, binary, and Go module
- Update install.mjs to build/install secret-mgr
- Update pcexec.ts to support secret-mgr patterns (with legacy pass_mgr compat)
- Update plugin config schema (passMgrPath → secretMgrPath)
- Create new skills/secret-mgr/SKILL.md
- install.mjs now initializes ego.json on install

M2: Implement ego-mgr binary (Go)
- Agent Scope and Public Scope column management
- Commands: add column/public-column, delete, set, get, show, list columns
- pcexec environment validation (AGENT_VERIFY, AGENT_ID, AGENT_WORKSPACE)
- File locking for concurrent write safety
- Proper exit codes per spec (0-6)
- Agent auto-registration on read/write
- Global column name uniqueness enforcement

M3: ego-mgr Skill
- Create skills/ego-mgr/SKILL.md with usage guide and examples

Ref: REQUIREMENTS_EGO_MGR.md
This commit is contained in:
zhi
2026-03-24 09:36:03 +00:00
parent be0f194f47
commit 98fc3da39c
13 changed files with 821 additions and 132 deletions

View File

@@ -8,7 +8,7 @@
"type": "object",
"properties": {
"enabled": { "type": "boolean", "default": true },
"passMgrPath": { "type": "string", "default": "" },
"secretMgrPath": { "type": "string", "default": "" },
"openclawProfilePath": { "type": "string", "default": "" }
}
}

View File

@@ -46,33 +46,55 @@ export interface PcExecError extends Error {
}
/**
* Extract pass_mgr invocations from a command string.
* Extract secret-mgr (and legacy pass_mgr) invocations from a command string.
*
* Supports both legacy and new formats:
* Legacy: $(pass_mgr get <key>) / `pass_mgr get <key>`
* New: $(pass_mgr get-secret --key <key>) / `pass_mgr get-secret --key <key>`
* Supports:
* Current: $(secret-mgr get-secret --key <key>) / `secret-mgr get-secret --key <key>`
* Legacy: $(pass_mgr get-secret --key <key>) / `pass_mgr get-secret --key <key>`
* Legacy: $(pass_mgr get <key>) / `pass_mgr get <key>`
*
* Returns array of { fullMatch, subcommand, key } where subcommand is
* Returns array of { fullMatch, subcommand, key, binary } where subcommand is
* "get" | "get-secret".
*/
function extractPassMgrGets(
function extractSecretMgrGets(
command: string,
): Array<{ key: string; fullMatch: string; subcommand: string }> {
const results: Array<{ key: string; fullMatch: string; subcommand: string }> = [];
): Array<{ key: string; fullMatch: string; subcommand: string; binary: string }> {
const results: Array<{ key: string; fullMatch: string; subcommand: string; binary: string }> = [];
const seen = new Set<string>();
// New format: pass_mgr get-secret --key <key>
// secret-mgr get-secret --key <key>
const secretMgrPatterns = [
/\$\(\s*secret-mgr\s+get-secret\s+--key\s+(\S+)\s*\)/g,
/`\s*secret-mgr\s+get-secret\s+--key\s+(\S+)\s*`/g,
];
// Legacy pass_mgr get-secret --key <key>
const newPatterns = [
/\$\(\s*pass_mgr\s+get-secret\s+--key\s+(\S+)\s*\)/g,
/`\s*pass_mgr\s+get-secret\s+--key\s+(\S+)\s*`/g,
];
// Legacy format: pass_mgr get <key>
// Legacy pass_mgr get <key>
const legacyPatterns = [
/\$\(\s*pass_mgr\s+get\s+(\S+)\s*\)/g,
/`\s*pass_mgr\s+get\s+(\S+)\s*`/g,
];
for (const pattern of secretMgrPatterns) {
let match;
while ((match = pattern.exec(command)) !== null) {
if (!seen.has(match[0])) {
seen.add(match[0]);
results.push({
key: match[1],
fullMatch: match[0],
subcommand: 'get-secret',
binary: 'secret-mgr',
});
}
}
}
for (const pattern of newPatterns) {
let match;
while ((match = pattern.exec(command)) !== null) {
@@ -82,6 +104,7 @@ function extractPassMgrGets(
key: match[1],
fullMatch: match[0],
subcommand: 'get-secret',
binary: 'pass_mgr',
});
}
}
@@ -96,6 +119,7 @@ function extractPassMgrGets(
key: match[1],
fullMatch: match[0],
subcommand: 'get',
binary: 'pass_mgr',
});
}
}
@@ -105,22 +129,24 @@ function extractPassMgrGets(
}
/**
* Execute pass_mgr to retrieve a secret.
* Execute secret-mgr (or legacy pass_mgr) to retrieve a secret.
* Uses the same env vars that the caller passes so pcguard checks pass.
*/
async function fetchPassword(
subcommand: string,
key: string,
env: Record<string, string>,
binary: string = 'secret-mgr',
): Promise<string> {
return new Promise((resolve, reject) => {
const passMgrPath = env.PASS_MGR_PATH || process.env.PASS_MGR_PATH || 'pass_mgr';
// Prefer SECRET_MGR_PATH, fall back to PASS_MGR_PATH for legacy compat
const binaryPath = env.SECRET_MGR_PATH || env.PASS_MGR_PATH || process.env.SECRET_MGR_PATH || process.env.PASS_MGR_PATH || 'secret-mgr';
const args =
subcommand === 'get-secret'
? ['get-secret', '--key', key]
: ['get', key];
const child = spawn(passMgrPath, args, {
const child = spawn(binaryPath, args, {
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, ...env },
});
@@ -131,7 +157,7 @@ async function fetchPassword(
child.stderr.on('data', (d) => (stderr += d.toString()));
child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`pass_mgr ${subcommand} failed: ${stderr || stdout}`));
reject(new Error(`secret-mgr ${subcommand} failed: ${stderr || stdout}`));
} else {
resolve(stdout.trim());
}
@@ -155,18 +181,18 @@ function sanitizeOutput(output: string, passwords: string[]): string {
}
/**
* Pre-resolve pass_mgr invocations, replace them inline, and collect passwords.
* Pre-resolve secret-mgr (and legacy pass_mgr) invocations, replace them inline, and collect passwords.
*/
async function replacePassMgrGets(
async function replaceSecretMgrGets(
command: string,
env: Record<string, string>,
): Promise<{ command: string; passwords: string[] }> {
const matches = extractPassMgrGets(command);
const matches = extractSecretMgrGets(command);
const passwords: string[] = [];
let replaced = command;
for (const { key, fullMatch, subcommand } of matches) {
const pw = await fetchPassword(subcommand, key, env);
for (const { key, fullMatch, subcommand, binary } of matches) {
const pw = await fetchPassword(subcommand, key, env, binary);
passwords.push(pw);
replaced = replaced.split(fullMatch).join(pw);
}
@@ -193,7 +219,7 @@ export async function pcexec(
let finalCommand = command;
let passwords: string[] = [];
const resolved = await replacePassMgrGets(command, env);
const resolved = await replaceSecretMgrGets(command, env);
finalCommand = resolved.command;
passwords = resolved.passwords;