Skip to content
Open
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
7 changes: 5 additions & 2 deletions server/src/computer/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,13 @@ export function createComputerRoutes(
* The computers, for the admin surface.
*
* Not per-Bot in the path the way the acting routes are: this asks the computer what it holds, and
* it holds a list. `:botId` is still there because every route under this router has it and the
* gateway wants somebody to attribute the call to.
* it holds a list. `:botId` is still there because every route under this router has it. The
* list itself is every computer, so a signed-in user is not enough; an administrator has to ask.
*/
routes.get("/:botId/computers", requireUser, async (context) => {
const denied = requireAdmin(context);
if (denied) return denied;

try {
return context.json(await gateway.computers());
} catch (error) {
Expand Down
95 changes: 95 additions & 0 deletions server/tests/computer-routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect, test } from "bun:test";
import type { MiddlewareHandler } from "hono";
import type { AppVariables, AuthenticatedActor } from "../src/auth/guards";
import type { ComputerClient } from "../src/computer/client";
import type { ComputerGateway } from "../src/computer/gateway";
import type { PolicyStore } from "../src/computer/policy-store";
import { createComputerRoutes } from "../src/computer/routes";

const member: AuthenticatedActor = {
id: "user-1",
email: "member@openbot.test",
role: "user",
};

const administrator: AuthenticatedActor = {
id: "admin-1",
email: "admin@openbot.test",
role: "admin",
};

function asActor(
actor: AuthenticatedActor,
): MiddlewareHandler<{ Variables: AppVariables }> {
return async (context, next) => {
context.set("actor", actor);
await next();
};
}

function appFor(actor: AuthenticatedActor, computers: () => Promise<unknown>) {
const gateway = {
async computers() {
return computers();
},
} as ComputerGateway;
let listed = 0;
const countingGateway = {
async computers() {
listed += 1;
return gateway.computers();
},
} as ComputerGateway;

const app = createComputerRoutes(
{} as ComputerClient,
countingGateway,
{} as PolicyStore,
asActor(actor),
);

return {
app,
listed: () => listed,
};
}

describe("computer fleet listing", () => {
test("refuses a signed-in user the fleet, and does not ask the gateway", async () => {
const { app, listed } = appFor(member, async () => ({
isolation: "per-bot",
computers: [
{ botId: "private-coworker", running: true, startedAt: null },
],
}));

const response = await app.request("http://openbot.test/any-bot/computers");

expect(response.status).toBe(403);
await expect(response.json()).resolves.toEqual({
error: "Administrator access required.",
});
expect(listed()).toBe(0);
});

test("lets an administrator see the fleet", async () => {
const fleet = {
isolation: "per-bot" as const,
computers: [
{
botId: "private-coworker",
running: true,
startedAt: "2026-08-20T00:00:00.000Z",
egress: null,
},
],
};
const { app, listed } = appFor(administrator, async () => fleet);

const response = await app.request("http://openbot.test/any-bot/computers");

expect(response.status).toBe(200);
await expect(response.json()).resolves.toEqual(fleet);
expect(listed()).toBe(1);
});
});