Compare commits

..
Author SHA1 Message Date
taodengandClaude Sonnet 4.6 7a731fc36e fix(ocp-cli): replace eval-curl with bash array to preserve JSON body quoting
`eval curl "$_AUTH_HEADER" "$@"` re-tokenizes its argument list according
to bash word-splitting rules. When OCP_ADMIN_KEY is set, the JSON body
`'{"name": "laptop"}'` (which contains a space) gets split into
`'{name:'` and `'laptop}'` — two separate args — so curl receives a
malformed body and the server rejects the request.

Fix: replace the `_AUTH_HEADER` string + `eval` pattern with a bash array
`_AUTH_ARGS`. Array expansion via `"${_AUTH_ARGS[@]}"` preserves word
boundaries across substitution with no eval required. Both code paths
(OCP_ADMIN_KEY env var and ~/.ocp/admin-key file fallback) and the empty
case (no admin key) are preserved unchanged.

Verified via `bash -x` trace:
  Before: `curl … -d '{name:' 'laptop}'` (body split, malformed)
  After:  `curl … -d '{"name": "testkey-pr-c"}'` (body intact, single arg)

Identified during fresh-state Round 2 testing on MacBook.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:43:45 +10:00
2 changed files with 10 additions and 57 deletions
+6 -9
View File
@@ -8,21 +8,18 @@ set -euo pipefail
PROXY="http://127.0.0.1:3456" PROXY="http://127.0.0.1:3456"
# Auth header for multi-key mode: reads from OCP_ADMIN_KEY env or ~/.ocp/admin-key file # Auth args for multi-key mode: reads from OCP_ADMIN_KEY env or ~/.ocp/admin-key file
_AUTH_HEADER="" # Using a bash array preserves word boundaries — no eval needed.
_AUTH_ARGS=()
if [[ -n "${OCP_ADMIN_KEY:-}" ]]; then if [[ -n "${OCP_ADMIN_KEY:-}" ]]; then
_AUTH_HEADER="-H \"Authorization: Bearer $OCP_ADMIN_KEY\"" _AUTH_ARGS=(-H "Authorization: Bearer $OCP_ADMIN_KEY")
elif [[ -f "$HOME/.ocp/admin-key" ]]; then elif [[ -f "$HOME/.ocp/admin-key" ]]; then
_AUTH_HEADER="-H \"Authorization: Bearer $(cat "$HOME/.ocp/admin-key")\"" _AUTH_ARGS=(-H "Authorization: Bearer $(cat "$HOME/.ocp/admin-key")")
fi fi
# Wrapper: curl with optional auth # Wrapper: curl with optional auth
_curl() { _curl() {
if [[ -n "$_AUTH_HEADER" ]]; then curl "${_AUTH_ARGS[@]}" "$@"
eval curl "$_AUTH_HEADER" "$@"
else
curl "$@"
fi
} }
_json() { python3 -m json.tool 2>/dev/null || cat; } _json() { python3 -m json.tool 2>/dev/null || cat; }
+4 -48
View File
@@ -30,7 +30,7 @@
import { createServer } from "node:http"; import { createServer } from "node:http";
import { spawn, execFileSync } from "node:child_process"; import { spawn, execFileSync } from "node:child_process";
import { randomUUID, timingSafeEqual } from "node:crypto"; import { randomUUID, timingSafeEqual } from "node:crypto";
import { readFileSync, readdirSync, accessSync, existsSync, constants } from "node:fs"; import { readFileSync, accessSync, existsSync, constants } from "node:fs";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { homedir } from "node:os"; import { homedir } from "node:os";
@@ -41,48 +41,8 @@ const _pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
const modelsConfig = JSON.parse(readFileSync(join(__dirname, "models.json"), "utf8")); const modelsConfig = JSON.parse(readFileSync(join(__dirname, "models.json"), "utf8"));
// ── Resolve claude binary ─────────────────────────────────────────────── // ── Resolve claude binary ───────────────────────────────────────────────
// Priority: CLAUDE_BIN env > well-known paths > nvm/fnm/asdf user-local // Priority: CLAUDE_BIN env > well-known paths > which lookup
// installs > which lookup. Fail-fast if not found — never start with an // Fail-fast if not found — never start with an unresolvable binary.
// unresolvable binary.
function _listVersionDirs(parent) {
try { return readdirSync(parent); } catch { return []; }
}
function _collectNodeManagerCandidates(home) {
if (!home) return [];
const out = [];
// nvm: $HOME/.nvm/versions/node/<version>/bin/claude
const nvmRoot = join(home, ".nvm/versions/node");
for (const v of _listVersionDirs(nvmRoot)) {
out.push(join(nvmRoot, v, "bin/claude"));
}
// nvm default alias: resolve $HOME/.nvm/aliases/default if it points to a version
try {
const aliasFile = join(home, ".nvm/aliases/default");
const aliasVer = readFileSync(aliasFile, "utf8").trim();
if (aliasVer) {
const direct = join(nvmRoot, aliasVer, "bin/claude");
if (!out.includes(direct)) out.unshift(direct);
}
} catch {}
// fnm: $HOME/.fnm/node-versions/<version>/installation/bin/claude
const fnmRoot = join(home, ".fnm/node-versions");
for (const v of _listVersionDirs(fnmRoot)) {
out.push(join(fnmRoot, v, "installation/bin/claude"));
}
// asdf: $HOME/.asdf/installs/nodejs/<version>/bin/claude
const asdfRoot = join(home, ".asdf/installs/nodejs");
for (const v of _listVersionDirs(asdfRoot)) {
out.push(join(asdfRoot, v, "bin/claude"));
}
// npm prefix-relocated: $HOME/.npm-global/bin/claude
out.push(join(home, ".npm-global/bin/claude"));
return out;
}
function resolveClaude() { function resolveClaude() {
if (process.env.CLAUDE_BIN) { if (process.env.CLAUDE_BIN) {
try { try {
@@ -94,13 +54,11 @@ function resolveClaude() {
} }
} }
const home = process.env.HOME || "";
const candidates = [ const candidates = [
"/opt/homebrew/bin/claude", "/opt/homebrew/bin/claude",
"/usr/local/bin/claude", "/usr/local/bin/claude",
"/usr/bin/claude", "/usr/bin/claude",
join(home, ".local/bin/claude"), join(process.env.HOME || "", ".local/bin/claude"),
..._collectNodeManagerCandidates(home),
]; ];
for (const p of candidates) { for (const p of candidates) {
try { accessSync(p, constants.X_OK); console.warn(`[init] CLAUDE_BIN not set, resolved to ${p}`); return p; } catch {} try { accessSync(p, constants.X_OK); console.warn(`[init] CLAUDE_BIN not set, resolved to ${p}`); return p; } catch {}
@@ -114,8 +72,6 @@ function resolveClaude() {
console.error( console.error(
"FATAL: claude binary not found.\n" + "FATAL: claude binary not found.\n" +
" Set CLAUDE_BIN=/path/to/claude or ensure claude is in PATH.\n" + " Set CLAUDE_BIN=/path/to/claude or ensure claude is in PATH.\n" +
" Hint: if you use nvm/fnm/asdf, set CLAUDE_BIN to the absolute path\n" +
" shown by `which claude` in your interactive shell.\n" +
" Checked: " + candidates.join(", ") " Checked: " + candidates.join(", ")
); );
process.exit(1); process.exit(1);