mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-19 09:44:07 +00:00
Three CLI/installer findings from the 2026-05-31 audit (no server.mjs): 1. ocp-plugin `cmdRestart` hardcoded uid 501 + the legacy `ai.openclaw.proxy` label, and fell through to a dangerous `pkill -f 'node.*server.mjs' && cd ~/.openclaw/projects/*/; node server.mjs &` that could kill an unrelated node process and relaunch an env-less ghost proxy from a glob-ambiguous dir. Now uses process.getuid() + the live labels (dev.ocp.proxy on macOS, ocp-proxy on Linux systemd; the OpenClaw gateway label is unchanged) and drops the pkill fallback entirely (returns a manual `ocp restart` message on failure). 2. ocp-connect wrote the quota key unquoted into rc files and a world-readable environment.d/ocp.conf. Now single-quotes the value and chmod 600s the rc files and ocp.conf (matching the existing auth-profiles.json 0o600 convention). 3. setup.mjs interpolated the injected service-unit secrets (CLAUDE_BIN, OCP_ADMIN_KEY, PROXY_ANONYMOUS_KEY) raw into plist <string> and systemd Environment= lines. Added xmlEscape() for all plist <string> values and assertSafeInjectValue() which rejects control characters (\x00-\x1f — newline, CR, tab) before any unit is written, blocking a newline-injected rogue Environment= directive. Spaces are intentionally allowed (CLAUDE_BIN paths may contain them). XML-escaping on write also resolves plist-merge.mjs's [^<]* regex concern transitively (no raw < reaches it) — comment added, logic unchanged. ALIGNMENT.md: CLI/installer scripts only, no Anthropic operation forwarded → cli.js citation N/A. No blacklisted tokens or port literals introduced; alignment.yml passes. Independent fresh-context reviewer (opus): APPROVE (Iron Rule 10) — verified the control-char regex byte-exact via od (no space-rejection regression), the pkill fallback fully removed, restart labels match setup.mjs ground truth, OCP keys (base64url) cannot break the single-quoting, and the validator runs before any write. Closes #113. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
3.1 KiB
JavaScript
89 lines
3.1 KiB
JavaScript
// scripts/lib/plist-merge.mjs
|
|
//
|
|
// Preserves user-customised env vars when setup.mjs rewrites the unit file.
|
|
//
|
|
// Rule:
|
|
// - keys present in NEW template → template value wins (template is source of truth)
|
|
// - keys ONLY in EXISTING (not in template) → preserved verbatim
|
|
//
|
|
// No new dependencies — regex-based, plist <key>X</key><string>Y</string> shape
|
|
// is stable enough for our hand-written templates in setup.mjs.
|
|
|
|
// Note: setup.mjs XML-escapes all injected values before writing (via xmlEscape()),
|
|
// so raw `<` / `>` / `&` never appear in plist <string> bodies — the [^<]* regex below is safe.
|
|
const PLIST_KV_RE = /<key>([^<]+)<\/key>\s*<string>([^<]*)<\/string>/g;
|
|
|
|
export function parsePlistEnv(plistContent) {
|
|
if (!plistContent) return {};
|
|
if (Buffer.isBuffer(plistContent)) plistContent = plistContent.toString("utf8");
|
|
// Restrict to the EnvironmentVariables dict to avoid catching Label, etc.
|
|
const envBlock = plistContent.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/);
|
|
if (!envBlock) return {};
|
|
const out = {};
|
|
let m;
|
|
PLIST_KV_RE.lastIndex = 0;
|
|
while ((m = PLIST_KV_RE.exec(envBlock[1])) !== null) {
|
|
out[m[1]] = m[2];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function mergePlistEnv(existing, template) {
|
|
if (!existing) return template;
|
|
const existingEnv = parsePlistEnv(existing);
|
|
const templateEnv = parsePlistEnv(template);
|
|
const KNOWN = new Set(Object.keys(templateEnv));
|
|
|
|
const preserved = {};
|
|
for (const [k, v] of Object.entries(existingEnv)) {
|
|
if (!KNOWN.has(k)) preserved[k] = v;
|
|
}
|
|
if (Object.keys(preserved).length === 0) return template;
|
|
|
|
const lines = Object.entries(preserved)
|
|
.map(([k, v]) => ` <key>${k}</key>\n <string>${v}</string>`)
|
|
.join("\n");
|
|
|
|
// Inject before the closing </dict> of EnvironmentVariables
|
|
return template.replace(
|
|
/(<key>EnvironmentVariables<\/key>\s*<dict>[\s\S]*?)(\n\s*<\/dict>)/,
|
|
`$1\n${lines}$2`
|
|
);
|
|
}
|
|
|
|
const SYSTEMD_KV_RE = /^Environment=([^=]+)=(.*)$/gm;
|
|
|
|
export function parseSystemdEnv(serviceContent) {
|
|
if (!serviceContent) return {};
|
|
if (Buffer.isBuffer(serviceContent)) serviceContent = serviceContent.toString("utf8");
|
|
const out = {};
|
|
let m;
|
|
SYSTEMD_KV_RE.lastIndex = 0;
|
|
while ((m = SYSTEMD_KV_RE.exec(serviceContent)) !== null) {
|
|
out[m[1]] = m[2];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function mergeSystemdEnv(existing, template) {
|
|
if (!existing) return template;
|
|
const existingEnv = parseSystemdEnv(existing);
|
|
const templateEnv = parseSystemdEnv(template);
|
|
const KNOWN = new Set(Object.keys(templateEnv));
|
|
|
|
const preservedLines = Object.entries(existingEnv)
|
|
.filter(([k]) => !KNOWN.has(k))
|
|
.map(([k, v]) => `Environment=${k}=${v}`);
|
|
if (preservedLines.length === 0) return template;
|
|
|
|
// Guard: if template has no Environment= anchor, cannot inject — return template as-is.
|
|
// (In practice the OCP systemd template always has Environment= lines.)
|
|
if (!/^Environment=/m.test(template)) return template;
|
|
|
|
// Inject after the last existing Environment= line in the template
|
|
return template.replace(
|
|
/(^Environment=[^\n]+\n)((?!Environment=).*$)/ms,
|
|
`$1${preservedLines.join("\n")}\n$2`
|
|
);
|
|
}
|