fix(bridge): tempfile escaping in chat/dispatch/tasks; remove stale K8S refs
Shell escaping fixes (all three routes had the same bug): - chat.ts: replace escapedMessage + shell-inline with tmpFile docker cp pattern - dispatch.ts: replace escapedJson + printf with tmpFile docker cp pattern - tasks.ts: same as dispatch - inbox path corrected: /sandbox/... → /home/node/.openclaw/workspace/tasks/inbox Stale reference removals: - tiger.ts: remove K8S_NAMESPACE + POD_NAME constants (dead since k3s exit) - index.ts: rewrite header docblock (Caddy→Traefik, k3s→docker exec) - dispatch.ts: kubectl comment → docker exec comment - tasks.ts: same kubectl comment fix
This commit is contained in:
parent
a65902edf9
commit
968d6fd178
7 changed files with 173 additions and 48 deletions
|
|
@ -1,23 +1,22 @@
|
||||||
/**
|
/**
|
||||||
* index.ts — Tiger Bridge API Entry Point
|
* index.ts — Tiger Bridge API Entry Point
|
||||||
*
|
*
|
||||||
* This is the main Express server that runs on the Hetzner VPS host.
|
* Express server running on the Hetzner VPS host (port 3456).
|
||||||
* It wraps all the docker→k3s→sandbox commands into clean REST endpoints
|
* Wraps docker exec commands into authenticated REST endpoints consumed
|
||||||
* that the Next.js dashboard can call over HTTPS.
|
* by the Next.js dashboard and Tiger's cron jobs.
|
||||||
*
|
*
|
||||||
* Architecture:
|
* Architecture:
|
||||||
* Dashboard (Next.js) → HTTPS → Caddy reverse proxy
|
* Dashboard (Next.js) → HTTPS → Traefik (dokploy-traefik)
|
||||||
* → Tiger Bridge (this server, port 3456)
|
* → Tiger Bridge (this server, port 3456)
|
||||||
* → docker exec openshell-cluster-nemoclaw
|
* → docker exec tiger-openclaw
|
||||||
* → kubectl exec -n openshell tiger
|
* → OpenClaw agent (Tiger agent)
|
||||||
* → sandbox pod (Tiger agent)
|
|
||||||
*
|
*
|
||||||
* Routes:
|
* Routes:
|
||||||
* GET /tiger/status — container health + process state + memory/CPU
|
* GET /tiger/status — container health + process state + memory/CPU
|
||||||
* GET /tiger/logs — SSE stream of real-time container logs
|
* GET /tiger/logs — SSE stream of real-time container logs
|
||||||
* POST /tiger/exec — run a command inside the sandbox
|
* POST /tiger/exec — run a command inside the container
|
||||||
* GET /tiger/config — read openclaw.json config
|
* GET /tiger/config — read openclaw.json config
|
||||||
* POST /tiger/config — update config + auto-regen hash
|
* POST /tiger/config — update config
|
||||||
* POST /tiger/restart — trigger container restart via watchdog
|
* POST /tiger/restart — trigger container restart via watchdog
|
||||||
* GET /tiger/workspace — list workspace files
|
* GET /tiger/workspace — list workspace files
|
||||||
* GET /tiger/files/:path — read a workspace file
|
* GET /tiger/files/:path — read a workspace file
|
||||||
|
|
@ -35,9 +34,17 @@ import restartRouter from "./routes/restart.js";
|
||||||
import filesRouter from "./routes/files.js";
|
import filesRouter from "./routes/files.js";
|
||||||
import projectsRouter from "./routes/projects.js";
|
import projectsRouter from "./routes/projects.js";
|
||||||
import tasksRouter from "./routes/tasks.js";
|
import tasksRouter from "./routes/tasks.js";
|
||||||
|
import tasksFileRouter from "./routes/tasks-file.js";
|
||||||
|
import cronRouter from "./routes/cron.js";
|
||||||
|
import notifyRouter from "./routes/notify.js";
|
||||||
import dispatchRouter from "./routes/dispatch.js";
|
import dispatchRouter from "./routes/dispatch.js";
|
||||||
import agentsRouter from "./routes/agents.js";
|
import agentsRouter from "./routes/agents.js";
|
||||||
|
import agentsActivityRouter from "./routes/agents-activity.js";
|
||||||
|
import deployRouter from "./routes/deploy.js";
|
||||||
|
import routeTaskRouter from "./routes/route-task.js";
|
||||||
|
import keysRouter from "./routes/keys.js";
|
||||||
import { initWatcher } from "./watcher.js";
|
import { initWatcher } from "./watcher.js";
|
||||||
|
import { TelegramChannel } from "./lib/telegram.js";
|
||||||
|
|
||||||
// Import db to ensure it's initialized
|
// Import db to ensure it's initialized
|
||||||
import "./db.js";
|
import "./db.js";
|
||||||
|
|
@ -87,8 +94,15 @@ app.use("/tiger/files", filesRouter); // Same router handles both /workspace an
|
||||||
// Project and Task management
|
// Project and Task management
|
||||||
app.use("/tiger/projects", projectsRouter);
|
app.use("/tiger/projects", projectsRouter);
|
||||||
app.use("/tiger/tasks", tasksRouter);
|
app.use("/tiger/tasks", tasksRouter);
|
||||||
|
app.use("/tiger/file-tasks", tasksFileRouter);
|
||||||
|
app.use("/tiger/cron", cronRouter);
|
||||||
|
app.use("/tiger/notify", notifyRouter);
|
||||||
app.use("/tiger/dispatch", dispatchRouter);
|
app.use("/tiger/dispatch", dispatchRouter);
|
||||||
app.use("/tiger/agents", agentsRouter);
|
app.use("/tiger/agents", agentsRouter);
|
||||||
|
app.use("/tiger/agents/activity", agentsActivityRouter);
|
||||||
|
app.use("/tiger/deploy-dashboard", deployRouter);
|
||||||
|
app.use("/tiger/route-task", routeTaskRouter);
|
||||||
|
app.use("/tiger/keys", keysRouter);
|
||||||
app.use("/tiger/chat", (await import("./routes/chat.js")).default);
|
app.use("/tiger/chat", (await import("./routes/chat.js")).default);
|
||||||
|
|
||||||
// Gateway proxy — forwards to gateway inside Tiger container
|
// Gateway proxy — forwards to gateway inside Tiger container
|
||||||
|
|
@ -119,4 +133,9 @@ app.listen(PORT, HOST, () => {
|
||||||
|
|
||||||
// Initialize file watcher for task status updates
|
// Initialize file watcher for task status updates
|
||||||
initWatcher();
|
initWatcher();
|
||||||
|
|
||||||
|
// Start Telegram channel — bridge takes over from OpenClaw native handler.
|
||||||
|
// Requires channels.telegram.enabled=false in openclaw.json.
|
||||||
|
const tgChannel = new TelegramChannel();
|
||||||
|
tgChannel.start();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -93,16 +93,22 @@ router.post("/", async (req, res) => {
|
||||||
const { promisify } = await import("util");
|
const { promisify } = await import("util");
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
// Escape the message for shell
|
// Write message to temp file — avoids ALL shell escaping issues
|
||||||
const escapedMessage = message.replace(/'/g, "'\\''");
|
// (backticks, quotes, code blocks in messages are all safe this way)
|
||||||
|
const { writeFileSync: wfChat, unlinkSync: ulChat } = await import("fs");
|
||||||
// Use openclaw agent to send a message to the main session
|
const { execSync: exChat } = await import("child_process");
|
||||||
// Session ID: agent:main:main (agent:main:main)
|
const tmpMsg = `/tmp/msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.txt`;
|
||||||
// In TIGER_REMOTE mode, prefix with ssh so docker runs on the VPS.
|
|
||||||
const sshPrefix = process.env.TIGER_REMOTE === "true"
|
const sshPrefix = process.env.TIGER_REMOTE === "true"
|
||||||
? `ssh ${process.env.TIGER_REMOTE_SSH || "root@100.75.128.45"} `
|
? `ssh ${process.env.TIGER_REMOTE_SSH || "root@100.75.128.45"} `
|
||||||
: "";
|
: "";
|
||||||
const cmd = `${sshPrefix}docker exec tiger-openclaw openclaw agent --session-id agent:main:main -m '${escapedMessage}' --json --timeout 120`;
|
try {
|
||||||
|
wfChat(tmpMsg, message, "utf-8");
|
||||||
|
exChat(`${sshPrefix}docker cp ${tmpMsg} tiger-openclaw:${tmpMsg}`, { timeout: 5000 });
|
||||||
|
ulChat(tmpMsg);
|
||||||
|
} catch (cpErr: any) {
|
||||||
|
throw new Error(`Failed to stage message for container: ${cpErr.message}`);
|
||||||
|
}
|
||||||
|
const cmd = `${sshPrefix}docker exec tiger-openclaw sh -c 'MSG=$(cat ${tmpMsg}); rm -f ${tmpMsg}; openclaw agent --session-id agent:main:main -m "$MSG" --json --timeout 120'`;
|
||||||
|
|
||||||
const tBeforeSpawn = Date.now();
|
const tBeforeSpawn = Date.now();
|
||||||
tSpawn = tBeforeSpawn - tStart;
|
tSpawn = tBeforeSpawn - tStart;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { tasks, executions } from "../db.js";
|
import { tasks, executions } from "../db.js";
|
||||||
|
import { classifyAgent } from "../lib/llm.js";
|
||||||
import { execInSandbox } from "../tiger.js";
|
import { execInSandbox } from "../tiger.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
@ -29,30 +30,59 @@ router.post("/", async (req, res) => {
|
||||||
return res.status(404).json({ ok: false, error: "Task not found" });
|
return res.status(404).json({ ok: false, error: "Task not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Agent classification ───────────────────────────────────────────────────
|
||||||
|
// classifyAgent never throws — falls back to { agent:'tiger', reason:'router_unavailable:...' }
|
||||||
|
// so dispatch is safe even when the router LLM is offline.
|
||||||
|
let resolvedAgent: string;
|
||||||
|
let agentReason: string;
|
||||||
|
|
||||||
|
if (assignedAgent) {
|
||||||
|
// Caller explicitly chose an agent — skip LLM, mark as manual.
|
||||||
|
resolvedAgent = assignedAgent;
|
||||||
|
agentReason = "manual";
|
||||||
|
} else {
|
||||||
|
const classifyInput = `${task.title}\n\n${task.description || ""}`.slice(0, 2000);
|
||||||
|
const classification = await classifyAgent(classifyInput);
|
||||||
|
resolvedAgent = classification.agent;
|
||||||
|
agentReason = classification.reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist resolved agent + reason back to the task row before dispatch.
|
||||||
|
tasks.update(taskId, { assigned_agent: resolvedAgent, agent_reason: agentReason });
|
||||||
|
|
||||||
// Prepare task data
|
// Prepare task data
|
||||||
const taskData = {
|
const taskData = {
|
||||||
id: taskId,
|
id: taskId,
|
||||||
title: task.title,
|
title: task.title,
|
||||||
description: task.description || description || "",
|
description: task.description || description || "",
|
||||||
assignedAgent: assignedAgent || task.assigned_agent || "manual",
|
assignedAgent: resolvedAgent,
|
||||||
|
agentReason,
|
||||||
context: context || "",
|
context: context || "",
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
status: "pending",
|
status: "pending",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write task JSON to sandbox's inbox via kubectl exec
|
// Write task JSON to container's inbox via docker exec
|
||||||
// The sandbox path: /sandbox/.openclaw-data/workspace/tasks/inbox/
|
// Tiger reads from the OpenClaw workspace tasks inbox
|
||||||
const inboxPath = "/sandbox/.openclaw-data/workspace/tasks/inbox";
|
const inboxPath = "/home/node/.openclaw/workspace/tasks/inbox";
|
||||||
const taskFile = `task_${taskId}.json`;
|
const taskFile = `task_${taskId}.json`;
|
||||||
|
|
||||||
// First ensure the directory exists inside the container
|
// First ensure the directory exists inside the container
|
||||||
await execInSandbox(`mkdir -p ${inboxPath}`);
|
await execInSandbox(`mkdir -p ${inboxPath}`);
|
||||||
|
|
||||||
// Write the task file using printf (more reliable than echo with escaping)
|
// Write task JSON via temp file (avoids ALL shell escaping issues)
|
||||||
const taskJson = JSON.stringify(taskData, null, 2);
|
const taskJson = JSON.stringify(taskData, null, 2);
|
||||||
// Escape single quotes for shell: ' -> '\''
|
const { writeFileSync, unlinkSync } = await import("fs");
|
||||||
const escapedJson = taskJson.replace(/'/g, "'\\''");
|
const { execSync } = await import("child_process");
|
||||||
await execInSandbox(`printf '%s' '${escapedJson}' > ${inboxPath}/${taskFile}`);
|
const tmpHost = `/tmp/task_${taskId}_${Date.now()}.json`;
|
||||||
|
try {
|
||||||
|
writeFileSync(tmpHost, taskJson, "utf-8");
|
||||||
|
execSync(`docker cp ${tmpHost} tiger-openclaw:${tmpHost}`, { timeout: 5000 });
|
||||||
|
unlinkSync(tmpHost);
|
||||||
|
} catch (copyErr: any) {
|
||||||
|
throw new Error(`Failed to copy task to container: ${copyErr.message}`);
|
||||||
|
}
|
||||||
|
await execInSandbox(`mkdir -p ${inboxPath} && mv ${tmpHost} ${inboxPath}/${taskFile}`);
|
||||||
|
|
||||||
// Create execution record
|
// Create execution record
|
||||||
const execution = executions.create({
|
const execution = executions.create({
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { projects, tasks } from "../db.js";
|
import { projects, tasks } from "../db.js";
|
||||||
|
import { generateProjectTitle, generateProjectGoal } from "../lib/llm.js";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -23,13 +24,43 @@ router.get("/", (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create project
|
// Create project
|
||||||
router.post("/", (req, res) => {
|
// Accepts { name?, description?, seed?, priority? }.
|
||||||
const { name, description, priority } = req.body;
|
// If name is absent, generates a 3-7 word title from seedText via LLM (falls back to raw text).
|
||||||
if (!name) {
|
// If description is absent, generates a one-line goal via LLM (falls back to "").
|
||||||
return res.status(400).json({ ok: false, error: "name is required" });
|
router.post("/", async (req, res) => {
|
||||||
|
const { name, description, priority, seed } = req.body;
|
||||||
|
|
||||||
|
// Need at least one source of text to work with
|
||||||
|
const seedText = (seed || description || name || "").trim();
|
||||||
|
if (!seedText) {
|
||||||
|
return res.status(400).json({ ok: false, error: "name, description, or seed is required" });
|
||||||
}
|
}
|
||||||
const created = projects.create({ name, description, priority });
|
|
||||||
res.status(201).json({ ok: true, project: created });
|
// ── Title ─────────────────────────────────────────────────────────────────
|
||||||
|
let finalName: string = (name || "").trim();
|
||||||
|
let titleGenerated = false;
|
||||||
|
if (!finalName) {
|
||||||
|
finalName = (await generateProjectTitle(seedText)) ?? seedText.slice(0, 80);
|
||||||
|
titleGenerated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Description / goal ────────────────────────────────────────────────────
|
||||||
|
// Only generate if description was explicitly absent from the request.
|
||||||
|
let finalDesc: string;
|
||||||
|
let goalGenerated = false;
|
||||||
|
if (description === undefined || description === null) {
|
||||||
|
finalDesc = (await generateProjectGoal(seedText)) ?? "";
|
||||||
|
goalGenerated = true;
|
||||||
|
} else {
|
||||||
|
finalDesc = (description || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = projects.create({ name: finalName, description: finalDesc, priority });
|
||||||
|
res.status(201).json({
|
||||||
|
ok: true,
|
||||||
|
project: created,
|
||||||
|
_llm: { title_generated: titleGenerated, goal_generated: goalGenerated },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get project with tasks
|
// Get project with tasks
|
||||||
|
|
|
||||||
|
|
@ -98,21 +98,27 @@ router.post("/:id/execute", async (req, res) => {
|
||||||
status: "pending",
|
status: "pending",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write task JSON to sandbox's inbox via kubectl exec
|
// Write task JSON to container's inbox via docker exec
|
||||||
const inboxPath = "/sandbox/.openclaw-data/workspace/tasks/inbox";
|
const inboxPath = "/home/node/.openclaw/workspace/tasks/inbox";
|
||||||
const taskFile = `task_${id}.json`;
|
const taskFile = `task_${id}.json`;
|
||||||
|
|
||||||
// Import execInSandbox dynamically to avoid circular deps
|
// Import execInSandbox dynamically to avoid circular deps
|
||||||
const { execInSandbox } = await import("../tiger.js");
|
const { execInSandbox } = await import("../tiger.js");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create directories if needed
|
// Write task JSON via temp file (avoids ALL shell escaping issues)
|
||||||
await execInSandbox(`mkdir -p ${inboxPath}`);
|
|
||||||
|
|
||||||
// Write the task file
|
|
||||||
const taskJson = JSON.stringify(taskData, null, 2);
|
const taskJson = JSON.stringify(taskData, null, 2);
|
||||||
const escapedJson = taskJson.replace(/'/g, "'\\''");
|
const { writeFileSync: wfs, unlinkSync: uls } = await import("fs");
|
||||||
await execInSandbox(`printf '%s' '${escapedJson}' > ${inboxPath}/${taskFile}`);
|
const { execSync: exs } = await import("child_process");
|
||||||
|
const tmpHost = `/tmp/task_${id}_${Date.now()}.json`;
|
||||||
|
try {
|
||||||
|
wfs(tmpHost, taskJson, "utf-8");
|
||||||
|
exs(`docker cp ${tmpHost} tiger-openclaw:${tmpHost}`, { timeout: 5000 });
|
||||||
|
uls(tmpHost);
|
||||||
|
} catch (copyErr: any) {
|
||||||
|
throw new Error(`Failed to copy task to container: ${copyErr.message}`);
|
||||||
|
}
|
||||||
|
await execInSandbox(`mkdir -p ${inboxPath} && mv ${tmpHost} ${inboxPath}/${taskFile}`);
|
||||||
|
|
||||||
// Create execution record
|
// Create execution record
|
||||||
const execution = executions.create({
|
const execution = executions.create({
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,13 @@ import { exec, execFile, spawn } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { readFile, writeFile } from "fs/promises";
|
import { readFile, writeFile } from "fs/promises";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
// ─── Configuration ───────────────────────────────────────────────
|
// ─── Configuration ───────────────────────────────────────────────
|
||||||
// Tiger runs directly in the tiger-openclaw container
|
// Tiger runs directly in the tiger-openclaw container
|
||||||
const DOCKER_CONTAINER = "tiger-openclaw";
|
const DOCKER_CONTAINER = "tiger-openclaw";
|
||||||
const K8S_NAMESPACE = "openshell";
|
|
||||||
const POD_NAME = "tiger";
|
|
||||||
// Real config lives in the Docker named volume, NOT on the host root path
|
// Real config lives in the Docker named volume, NOT on the host root path
|
||||||
const OPENCLAW_CONFIG_HOST = "/var/lib/docker/volumes/tiger_tiger-config/_data/openclaw.json";
|
const OPENCLAW_CONFIG_HOST = "/var/lib/docker/volumes/tiger_tiger-config/_data/openclaw.json";
|
||||||
const OPENCLAW_MODELS_HOST = "/var/lib/docker/volumes/tiger_tiger-config/_data/agents/main/agent/models.json";
|
const OPENCLAW_MODELS_HOST = "/var/lib/docker/volumes/tiger_tiger-config/_data/agents/main/agent/models.json";
|
||||||
|
|
@ -40,6 +39,37 @@ if (IS_REMOTE) {
|
||||||
console.log(`[bridge] REMOTE MODE: docker commands will run via ssh ${REMOTE_SSH}`);
|
console.log(`[bridge] REMOTE MODE: docker commands will run via ssh ${REMOTE_SSH}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a file-based command (no shell) on the host, with optional SSH prefix.
|
||||||
|
* Used for safe file reads where execFile avoids shell injection.
|
||||||
|
*/
|
||||||
|
async function execFileOnHost(
|
||||||
|
file: string,
|
||||||
|
args: string[],
|
||||||
|
timeoutMs = DEFAULT_TIMEOUT
|
||||||
|
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
||||||
|
const cmd = IS_REMOTE ? ["ssh", REMOTE_SSH, file, ...args] : [file, ...args];
|
||||||
|
try {
|
||||||
|
const { stdout: out, stderr: err } = await execFile(cmd[0], cmd.slice(1), {
|
||||||
|
timeout: timeoutMs,
|
||||||
|
maxBuffer: 5 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
const stdout = typeof out === "string" ? out : out?.toString() ?? "";
|
||||||
|
const stderr = typeof err === "string" ? err : err?.toString() ?? "";
|
||||||
|
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };
|
||||||
|
} catch (err: any) {
|
||||||
|
const out = err.stdout;
|
||||||
|
const er = err.stderr;
|
||||||
|
const stdout = typeof out === "string" ? out : out?.toString() ?? "";
|
||||||
|
const stderr = typeof er === "string" ? er : er?.toString() ?? "";
|
||||||
|
return {
|
||||||
|
stdout: stdout.trim(),
|
||||||
|
stderr: (stderr || err.message || "").trim(),
|
||||||
|
exitCode: err.code ?? 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a command inside the Tiger container.
|
* Execute a command inside the Tiger container.
|
||||||
* Commands run directly via docker exec (no kubectl needed).
|
* Commands run directly via docker exec (no kubectl needed).
|
||||||
|
|
@ -329,12 +359,15 @@ export async function listWorkspaceFiles(
|
||||||
* Read a file from the Tiger workspace.
|
* Read a file from the Tiger workspace.
|
||||||
*/
|
*/
|
||||||
export async function readWorkspaceFile(filepath: string): Promise<string> {
|
export async function readWorkspaceFile(filepath: string): Promise<string> {
|
||||||
// Security: prevent path traversal
|
// Security: resolve the path and ensure it stays within the workspace.
|
||||||
const sanitized = filepath.replace(/\.\./g, "").replace(/^\//, "");
|
// Blocks path traversal attempts (e.g. ../../etc/passwd).
|
||||||
const { stdout, exitCode } = await execOnHost(
|
const safeName = filepath.replace(/\.\./g, "");
|
||||||
`cat "${WORKSPACE_SYMLINK}/${sanitized}" 2>/dev/null`
|
const fullPath = path.resolve(WORKSPACE_SYMLINK, safeName);
|
||||||
);
|
if (!fullPath.startsWith(WORKSPACE_SYMLINK)) {
|
||||||
if (exitCode !== 0) throw new Error(`File not found: ${sanitized}`);
|
throw new Error("Access denied: path outside workspace");
|
||||||
|
}
|
||||||
|
const { stdout, exitCode } = await execFileOnHost("cat", [fullPath], 10_000);
|
||||||
|
if (exitCode !== 0) throw new Error(`File not found: ${safeName}`);
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ set -euo pipefail
|
||||||
|
|
||||||
# ─── Configuration ───────────────────────────────────────────────────
|
# ─── Configuration ───────────────────────────────────────────────────
|
||||||
SERVER="root@100.75.128.45"
|
SERVER="root@100.75.128.45"
|
||||||
SERVER_PATH="/root/NemoClawDashboard"
|
SERVER_PATH="/root/OpenClawDashboard"
|
||||||
LOCAL_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
LOCAL_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# Colors — makes scanning the output easier when things go wrong
|
# Colors — makes scanning the output easier when things go wrong
|
||||||
|
|
@ -77,7 +77,7 @@ section "Pre-flight checks"
|
||||||
# [1] Are we in the right directory?
|
# [1] Are we in the right directory?
|
||||||
cd "$LOCAL_PATH"
|
cd "$LOCAL_PATH"
|
||||||
if [ ! -d .git ] || [ ! -d dashboard ] || [ ! -d bridge ]; then
|
if [ ! -d .git ] || [ ! -d dashboard ] || [ ! -d bridge ]; then
|
||||||
die "Not in NemoClawDashboard repo root. cd to the repo first."
|
die "Not in OpenClawDashboard repo root. cd to the repo first."
|
||||||
fi
|
fi
|
||||||
ok "In repo: $LOCAL_PATH"
|
ok "In repo: $LOCAL_PATH"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue