Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions backend-api/__tests__/controlPlane.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ describe("gateway control-plane embed", () => {
host: "10.0.0.10",
gateway_token: "gateway-password",
gateway_host_port: null,
status: "running",
}],
});
global.fetch.mockResolvedValue({
Expand Down Expand Up @@ -143,6 +144,7 @@ describe("gateway control-plane embed", () => {
host: "10.0.0.10",
gateway_token: "gateway-password",
gateway_host_port: 19123,
status: "running",
}],
});
global.fetch.mockResolvedValue({
Expand All @@ -161,4 +163,22 @@ describe("gateway control-plane embed", () => {
expect.any(Object)
);
});

it("rejects embed for error agents so failed control-plane state stays closed", async () => {
mockDb.query.mockResolvedValueOnce({
rows: [{
host: "10.0.0.10",
gateway_token: "gateway-password",
gateway_host_port: 19123,
status: "error",
}],
});

const res = await request(app)
.get(`/agents/agent-1/gateway/embed?token=${encodeURIComponent(token)}`)
.set("Host", "nora.test");

expect(res.status).toBe(404);
expect(global.fetch).not.toHaveBeenCalled();
});
});
6 changes: 5 additions & 1 deletion backend-api/agentStatus.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
function isGatewayAvailableStatus(status) {
return ["running", "warning"].includes(status);
}

function reconcileAgentStatus(currentStatus, liveRunning) {
if (currentStatus === "queued" || currentStatus === "deploying") {
return currentStatus;
Expand All @@ -16,4 +20,4 @@ function reconcileAgentStatus(currentStatus, liveRunning) {
return currentStatus;
}

module.exports = { reconcileAgentStatus };
module.exports = { isGatewayAvailableStatus, reconcileAgentStatus };
4 changes: 2 additions & 2 deletions backend-api/routes/agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const billing = require("../billing");
const scheduler = require("../scheduler");
const containerManager = require("../containerManager");
const monitoring = require("../monitoring");
const { reconcileAgentStatus } = require("../agentStatus");
const { isGatewayAvailableStatus, reconcileAgentStatus } = require("../agentStatus");
const { OPENCLAW_GATEWAY_PORT } = require("../../agent-runtime/lib/contracts");
const { asyncHandler } = require("../middleware/errorHandler");

Expand Down Expand Up @@ -93,7 +93,7 @@ router.get("/:id/gateway-url", asyncHandler(async (req, res) => {
);
const agent = result.rows[0];
if (!agent) return res.status(404).json({ error: "Agent not found" });
if (!["running", "warning"].includes(agent.status)) {
if (!isGatewayAvailableStatus(agent.status)) {
return res.status(409).json({ error: "Agent gateway is only available while running" });
}
if (!agent.container_id) return res.status(409).json({ error: "No container" });
Expand Down
12 changes: 7 additions & 5 deletions backend-api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const { getBootstrapAdminSeedConfig } = require("./bootstrapAdmin");
const { authenticateToken } = require("./middleware/auth");
const { correlationId, errorHandler } = require("./middleware/errorHandler");
const { createGatewayRouter, attachGatewayWS } = require("./gatewayProxy");
const { reconcileAgentStatus } = require("./agentStatus");
const { isGatewayAvailableStatus, reconcileAgentStatus } = require("./agentStatus");
const { OPENCLAW_GATEWAY_PORT } = require("../agent-runtime/lib/contracts");

// ─── JWT Secret ───────────────────────────────────────────────────
Expand Down Expand Up @@ -138,10 +138,12 @@ gatewayUIAssetProxy.get("/agents/:agentId/gateway/embed", async (req, res) => {

const agentId = req.params.agentId;
const result = await db.query(
"SELECT host, gateway_token, gateway_host_port FROM agents WHERE id = $1 AND user_id = $2 AND status IN ('running','warning') AND host IS NOT NULL",
"SELECT host, gateway_token, gateway_host_port, status FROM agents WHERE id = $1 AND user_id = $2 AND host IS NOT NULL",
[agentId, payload.id]
);
if (!result.rows[0]) return res.status(404).send("agent not found or not running");
if (!result.rows[0] || !isGatewayAvailableStatus(result.rows[0].status)) {
return res.status(404).send("agent not found or not running");
}

const gwHost = result.rows[0].gateway_host_port ? (process.env.GATEWAY_HOST || "host.docker.internal") : result.rows[0].host;
const gwPort = result.rows[0].gateway_host_port || OPENCLAW_GATEWAY_PORT;
Expand Down Expand Up @@ -205,10 +207,10 @@ async function proxyGatewayAsset(req, res) {
const db = require("./db");
const agentId = req.params.agentId;
const result = await db.query(
"SELECT host, gateway_host_port FROM agents WHERE id = $1 AND status IN ('running','warning') AND host IS NOT NULL",
"SELECT host, gateway_host_port, status FROM agents WHERE id = $1 AND host IS NOT NULL",
[agentId]
);
if (!result.rows[0]) return res.status(404).end();
if (!result.rows[0] || !isGatewayAvailableStatus(result.rows[0].status)) return res.status(404).end();
const gwHost = result.rows[0].gateway_host_port ? (process.env.GATEWAY_HOST || "host.docker.internal") : result.rows[0].host;
const gwPort = result.rows[0].gateway_host_port || OPENCLAW_GATEWAY_PORT;
const gatewayPath = req.path.split("/gateway/")[1] || "";
Expand Down
Loading