fix: cli tool page to update the config correctly

This commit is contained in:
2026-07-12 18:16:52 +07:00
parent be7c210161
commit 268e2ac21f
13 changed files with 141 additions and 34 deletions
@@ -6,6 +6,7 @@ import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { redactSecrets } from "@/lib/security/redactSecrets";
const execAsync = promisify(exec);
@@ -68,7 +69,7 @@ export async function GET() {
return NextResponse.json({
installed: true,
settings: settings,
settings: redactSecrets(settings),
has9Router: has9Router,
settingsPath: getClaudeSettingsPath(),
});
@@ -7,6 +7,7 @@ import fs from "fs/promises";
import path from "path";
import os from "os";
import { parseTOML, stringifyTOML } from "confbox";
import { redactSecretsInText } from "@/lib/security/redactSecrets";
const execAsync = promisify(exec);
@@ -96,7 +97,7 @@ export async function GET() {
return NextResponse.json({
installed: true,
config,
config: redactSecretsInText(config),
has9Router: has9RouterConfig(config),
configPath: getCodexConfigPath(),
});
@@ -4,6 +4,7 @@ import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { redactSecrets } from "@/lib/security/redactSecrets";
// Resolve chatLanguageModels.json path per OS
const getConfigPath = () => {
@@ -48,7 +49,7 @@ export async function GET() {
return NextResponse.json({
installed: true,
config,
config: redactSecrets(config),
has9Router: has9RouterConfig(config),
configPath: getConfigPath(),
currentModel: entry?.models?.[0]?.id || null,
@@ -8,6 +8,7 @@ import crypto from "crypto";
import { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins";
import { UPDATER_CONFIG } from "@/shared/constants/config";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { redactSecrets } from "@/lib/security/redactSecrets";
const APP_PORT = UPDATER_CONFIG.appPort;
const CLI_TOKEN_HEADER = "x-9r-cli-token";
@@ -273,7 +274,7 @@ export async function GET() {
return NextResponse.json({
installed: true,
config,
config: redactSecrets(config),
has9Router,
configPath,
cowork: {
@@ -6,6 +6,7 @@ import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { redactSecretsInText } from "@/lib/security/redactSecrets";
const execAsync = promisify(exec);
@@ -112,7 +113,7 @@ export async function GET() {
const config = parseToml(toml);
return NextResponse.json({
installed: true,
settings: config,
settings: redactSecretsInText(config),
has9Router: has9RouterConfig(config),
configPath: getDeepSeekConfigPath(),
});
@@ -6,6 +6,7 @@ import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { redactSecrets } from "@/lib/security/redactSecrets";
const execAsync = promisify(exec);
@@ -69,7 +70,7 @@ export async function GET() {
return NextResponse.json({
installed: true,
settings,
settings: redactSecrets(settings),
has9Router: has9RouterConfig(settings),
settingsPath: getDroidSettingsPath(),
});
@@ -7,6 +7,7 @@ import os from "os";
import { exec } from "child_process";
import { promisify } from "util";
import { parseTOML, stringifyTOML } from "confbox";
import { redactSecrets } from "@/lib/security/redactSecrets";
const execAsync = promisify(exec);
@@ -122,7 +123,7 @@ export async function GET() {
return NextResponse.json({
installed: true,
config,
config: redactSecrets(config),
has9Router,
configPath: getConfigPath(),
});
@@ -6,6 +6,7 @@ import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { redactSecrets } from "@/lib/security/redactSecrets";
const execAsync = promisify(exec);
@@ -103,8 +104,8 @@ export async function GET() {
return NextResponse.json({
installed: true,
settings,
agents: enrichedAgents,
settings: redactSecrets(settings),
agents: redactSecrets(enrichedAgents),
has9Router: has9RouterConfig(settings),
settingsPath: getOpenClawSettingsPath(),
});
@@ -6,6 +6,7 @@ import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { redactSecrets } from "@/lib/security/redactSecrets";
const execAsync = promisify(exec);
@@ -72,7 +73,7 @@ export async function GET() {
return NextResponse.json({
installed: true,
config,
config: redactSecrets(config),
has9Router: has9RouterConfig(config),
configPath: getConfigPath(),
opencode: {
+18 -5
View File
@@ -45,12 +45,14 @@ const ALWAYS_PROTECTED = [
];
// User administration is never exposed to normal users, even if dashboard login
// is disabled for local single-user deployments.
const ADMIN_ONLY_PATHS = ["/api/users", "/api/tunnel", "/api/headroom", "/api/pxpipe"];
// is disabled for local single-user deployments. CLI Tools directly read and
// mutate the account running 9Router's local CLI configuration, so they are
// host administration rather than per-user dashboard preferences.
const ADMIN_ONLY_PATHS = ["/api/users", "/api/tunnel", "/api/headroom", "/api/pxpipe", "/api/cli-tools"];
// Dashboard paths requiring an administrator. Combo access is handled by its
// owner-scoped API routes and is available to authenticated users.
const ADMIN_ONLY_DASHBOARD_PATHS = ["/dashboard/token-saver", "/dashboard/pxpipe"];
const ADMIN_ONLY_DASHBOARD_PATHS = ["/dashboard/token-saver", "/dashboard/pxpipe", "/dashboard/cli-tools"];
// Require auth, but allow through if requireLogin is disabled
const PROTECTED_API_PATHS = [
@@ -75,8 +77,6 @@ const PROTECTED_API_PATHS = [
// Routes that spawn child processes or read host secrets — restrict to localhost.
const LOCAL_ONLY_PATHS = [
"/api/cli-tools/cowork-settings",
"/api/cli-tools/antigravity-mitm",
"/api/mcp/",
"/api/tunnel/tailscale-install",
"/api/tunnel/tailscale-enable",
@@ -210,6 +210,19 @@ export const __test__ = {
export async function proxy(request) {
const { pathname } = request.nextUrl;
// CLI Tools access the server process's home directory and, in the MITM
// case, can change privileged system networking. Do not allow a shared CLI
// bearer token to become a remote host-administration credential: only a
// local, authenticated administrator may use these endpoints.
if (pathname === "/api/cli-tools" || pathname.startsWith("/api/cli-tools/")) {
if (!isLocalRequest(request)) {
return NextResponse.json({ error: "CLI Tools are available only from the local machine" }, { status: 403 });
}
if (!(await isAdmin(request))) {
return NextResponse.json({ error: "Administrator access required" }, { status: 403 });
}
}
// Local-only gate for spawn-capable / host-secret routes.
if (LOCAL_ONLY_PATHS.some((p) => pathname.startsWith(p))) {
if (!(await canAccessLocalOnlyRoute(request))) {
+30
View File
@@ -0,0 +1,30 @@
const SECRET_KEY_PATTERN = /(?:api[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|password|secret|credential|authorization|bearer|cookie|private[_-]?key)/i;
const REDACTED_VALUE = "[REDACTED]";
/**
* Return a deep copy suitable for an administrative status response.
* Configuration values are retained, but values stored under known credential
* keys are redacted before crossing the server-to-browser boundary.
*/
export function redactSecrets(value) {
if (Array.isArray(value)) return value.map(redactSecrets);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value).map(([key, child]) => [
key,
SECRET_KEY_PATTERN.test(key) ? REDACTED_VALUE : redactSecrets(child),
]),
);
}
/** Redact credential assignments embedded in text-based configuration formats. */
export function redactSecretsInText(value) {
if (typeof value !== "string") return value;
return value
.replace(/^(\s*(?:[A-Za-z0-9_.-]*?(?:api[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|password|secret|credential|authorization|private[_-]?key)[A-Za-z0-9_.-]*)\s*[=:]\s*)([^\r\n#]+)/gim, `$1${REDACTED_VALUE}`)
.replace(/("(?:api[_-]?key|auth[_-]?token|access[_-]?token|refresh[_-]?token|password|secret|credential|authorization|private[_-]?key)"\s*:\s*")[^"]*(")/gim, `$1${REDACTED_VALUE}$2`);
}
export { REDACTED_VALUE };