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
6 changes: 6 additions & 0 deletions apps/factory/agent/lib/sandbox-ssh.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export function sandboxSshCommand(name: string): string {
return `sandbox ssh ${name}`;
}

const SSHABLE_STATUSES = new Set(["running", "stopped"]);

export function isSandboxSSHable(status: string): boolean {
return SSHABLE_STATUSES.has(status);
}
9 changes: 9 additions & 0 deletions apps/factory/app/api/sandbox/terminal/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,14 @@ import { handleTerminalRequest } from "@/agent/lib/sandbox-terminal";
export const dynamic = "force-dynamic";

export async function POST(request: Request): Promise<Response> {
Comment thread
vercel[bot] marked this conversation as resolved.
if (
request.headers.get("origin") !== new URL(request.url).origin ||
request.headers.get("content-type")?.split(";", 1)[0] !== "application/json"
)
return Response.json(
{ error: "Invalid terminal request." },
{ status: 403 }
);

return handleTerminalRequest(request);
}
52 changes: 43 additions & 9 deletions apps/factory/app/run-observatory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useState
} from "react";

import { sandboxSshCommand } from "../agent/lib/sandbox-ssh";
import { isSandboxSSHable, sandboxSshCommand } from "../agent/lib/sandbox-ssh";
import type {
AgentRunRecord,
ControlPlaneSnapshot,
Expand Down Expand Up @@ -68,8 +68,16 @@ function duration(run: AgentRunRecord): string {
return `${Math.round(milliseconds / 60_000)}m`;
}

function RunTicket({ run }: { readonly run: AgentRunRecord }) {
function RunTicket({
run,
onTerminal
}: {
readonly run: AgentRunRecord;
readonly onTerminal: (name: string) => void;
}) {
const detailsUrl = run.source === "eve" ? AGENT_RUNS_URL : WORKFLOW_RUNS_URL;
const sandbox = run.sandbox;
const sshable = sandbox ? isSandboxSSHable(sandbox.status) : false;
return (
<li className={`runTicket runTicket-${run.status}`}>
<article aria-label={`${run.title}, ${run.status}`}>
Expand Down Expand Up @@ -112,11 +120,28 @@ function RunTicket({ run }: { readonly run: AgentRunRecord }) {
<span aria-hidden="true">→</span>
<span>{run.sandbox?.status ?? run.status}</span>
</div>
{run.sandbox && run.sandbox.provider !== "eve" ? (
<CopyCommand
command={sandboxSshCommand(run.sandbox.id)}
label="SSH command for this sandbox"
/>
{sandbox && sandbox.provider !== "eve" ? (
<>
<CopyCommand
command={sandboxSshCommand(sandbox.id)}
label="SSH command for this sandbox"
/>
<Button
className="sandboxTerminalButton"
disabled={!sshable}
onClick={() => onTerminal(sandbox.id)}
size="sm"
title={
sshable
? "Open a terminal session for this sandbox"
: "This sandbox is not currently reachable for a terminal session"
}
type="button"
variant="outline"
>
Terminal
</Button>
</>
) : null}
<a
className="runDetails"
Expand Down Expand Up @@ -168,9 +193,14 @@ function SandboxCard({
/>
<Button
className="sandboxTerminalButton"
disabled={sandbox.status === "failed"}
disabled={!isSandboxSSHable(sandbox.status)}
onClick={() => onTerminal(sandbox.name)}
size="sm"
title={
isSandboxSSHable(sandbox.status)
? "Open a terminal session for this sandbox"
: "This sandbox is not currently reachable for a terminal session"
}
type="button"
variant="outline"
>
Expand Down Expand Up @@ -304,7 +334,11 @@ export function RunObservatory({
{runs.length > 0 ? (
<ol className="runList">
{runs.map((run) => (
<RunTicket key={run.id} run={run} />
<RunTicket
key={run.id}
run={run}
onTerminal={setActiveTerminal}
/>
))}
</ol>
) : (
Expand Down
2 changes: 1 addition & 1 deletion apps/factory/app/sandbox-terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export function SandboxTerminal({ sandboxName, onExit }: SandboxTerminalProps) {
terminalRef.current = null;
fitAddonRef.current = null;
socketRef.current = null;
void cleanupPromise;
void cleanupPromise.then((cleanup) => cleanup?.());
};
}, [sandboxName]);

Expand Down
15 changes: 14 additions & 1 deletion apps/factory/tests/sandbox-ssh.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import assert from "node:assert/strict";
import test from "node:test";

import { sandboxSshCommand } from "../agent/lib/sandbox-ssh.ts";
import {
isSandboxSSHable,
sandboxSshCommand
} from "../agent/lib/sandbox-ssh.ts";

test("produces a sandbox ssh command for a given sandbox name", () => {
assert.equal(sandboxSshCommand("my-sandbox"), "sandbox ssh my-sandbox");
Expand All @@ -14,3 +17,13 @@ test("produces a sandbox ssh command for a given sandbox name", () => {
test("preserves special characters in sandbox names", () => {
assert.equal(sandboxSshCommand("a-b_c.d"), "sandbox ssh a-b_c.d");
});

test("isSandboxSSHable recognizes running and stopped sandboxes", () => {
assert.equal(isSandboxSSHable("running"), true);
assert.equal(isSandboxSSHable("stopped"), true);
assert.equal(isSandboxSSHable("provisioning"), false);
assert.equal(isSandboxSSHable("pending"), false);
assert.equal(isSandboxSSHable("failed"), false);
assert.equal(isSandboxSSHable("aborted"), false);
assert.equal(isSandboxSSHable("snapshotting"), false);
});
Loading