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
14 changes: 7 additions & 7 deletions app/components/AgentsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { useGlobalState } from "@/app/contexts/GlobalState";
import type { QueueBehavior } from "@/types/chat";
import { SandboxSelector } from "@/app/components/SandboxSelector";

// Production Convex URL (must match @hackerai/local package)
// Production Convex URL (must match @hackerai/local@latest package)
const PRODUCTION_CONVEX_URL = "https://convex.haiusercontent.com";

// Add --convex-url flag if running against non-production backend
Expand Down Expand Up @@ -348,21 +348,21 @@ const AgentsTab = () => {
{/* Docker command */}
<CommandBlock
label="Basic (Docker)"
command={`npx @hackerai/local --token ${showToken && token ? token : "<token>"} --name "My Machine"${convexUrlFlag}`}
command={`npx @hackerai/local@latest --token ${showToken && token ? token : "<token>"} --name "My Machine"${convexUrlFlag}`}
onCopy={() =>
handleCopyCommand(
`npx @hackerai/local --token ${token || "YOUR_TOKEN"} --name "My Machine"${convexUrlFlag}`,
`npx @hackerai/local@latest --token ${token || "YOUR_TOKEN"} --name "My Machine"${convexUrlFlag}`,
)
}
/>

{/* Kali command */}
<CommandBlock
label="Custom Image (Kali Linux)"
command={`npx @hackerai/local --token ${showToken && token ? token : "<token>"} --name "Kali" --image kalilinux/kali-rolling${convexUrlFlag}`}
command={`npx @hackerai/local@latest --token ${showToken && token ? token : "<token>"} --name "Kali" --image kalilinux/kali-rolling${convexUrlFlag}`}
onCopy={() =>
handleCopyCommand(
`npx @hackerai/local --token ${token || "YOUR_TOKEN"} --name "Kali" --image kalilinux/kali-rolling${convexUrlFlag}`,
`npx @hackerai/local@latest --token ${token || "YOUR_TOKEN"} --name "Kali" --image kalilinux/kali-rolling${convexUrlFlag}`,
)
}
/>
Expand All @@ -371,10 +371,10 @@ const AgentsTab = () => {
<CommandBlock
label="Dangerous Mode (No Docker)"
warning
command={`npx @hackerai/local --token ${showToken && token ? token : "<token>"} --name "Host" --dangerous${convexUrlFlag}`}
command={`npx @hackerai/local@latest --token ${showToken && token ? token : "<token>"} --name "Host" --dangerous${convexUrlFlag}`}
onCopy={() =>
handleCopyCommand(
`npx @hackerai/local --token ${token || "YOUR_TOKEN"} --name "Host" --dangerous${convexUrlFlag}`,
`npx @hackerai/local@latest --token ${token || "YOUR_TOKEN"} --name "Host" --dangerous${convexUrlFlag}`,
)
}
/>
Expand Down
6 changes: 3 additions & 3 deletions app/components/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,9 @@ export const Messages = ({
// Check if we should show branch indicator after this message
const shouldShowBranchIndicator = Boolean(
branchedFromChatId &&
branchedFromChatTitle &&
branchBoundaryIndex >= 0 &&
index === branchBoundaryIndex,
branchedFromChatTitle &&
branchBoundaryIndex >= 0 &&
index === branchBoundaryIndex,
);

return (
Expand Down
40 changes: 31 additions & 9 deletions app/components/SandboxSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { Check, Cloud, Laptop, AlertTriangle, ChevronDown } from "lucide-react";
import {
Check,
Cloud,
Laptop,
AlertTriangle,
ChevronDown,
Copy,
} from "lucide-react";
import {
Popover,
PopoverContent,
Expand All @@ -15,6 +22,7 @@ import {
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { toast } from "sonner";

interface SandboxSelectorProps {
value: string;
Expand Down Expand Up @@ -190,15 +198,29 @@ export function SandboxSelector({
);
})}
{connections && connections.length === 0 && (
<div className="px-2 py-2 text-xs text-muted-foreground border-t mt-1 pt-2">
No local connections.{" "}
<span className="text-foreground">
Run{" "}
<code className="bg-muted px-1 rounded">
npx @hackerai/local
<div className="px-2 py-2 border-t mt-1 pt-2 space-y-2">
<div className="text-xs text-muted-foreground">
No local connections.
</div>
<div className="flex gap-1.5">
<code className="flex-1 p-2 rounded-md font-mono text-xs bg-zinc-900 dark:bg-zinc-950 text-zinc-300 dark:text-zinc-400 overflow-x-auto">
npx @hackerai/local@latest
</code>
</span>{" "}
to enable local execution.
<Button
variant="outline"
size="icon"
className="shrink-0 h-8 w-8"
onClick={() => {
navigator.clipboard.writeText("npx @hackerai/local@latest");
toast.success("Command copied to clipboard");
}}
>
<Copy className="h-3.5 w-3.5" />
</Button>
</div>
<div className="text-xs text-muted-foreground">
Run this command to enable local execution.
</div>
</div>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/SidebarUserNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const SidebarUserNav = ({ isCollapsed = false }: { isCollapsed?: boolean }) => {

const handleXCom = () => {
const newWindow = window.open(
"https://x.com/hackerai_tech",
"https://x.com/pentestgpt",
"_blank",
"noopener,noreferrer",
);
Expand Down
3 changes: 2 additions & 1 deletion components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const badgeVariants = cva(
);

export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
extends
React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
Expand Down
19 changes: 9 additions & 10 deletions lib/ai/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ import type { Geo } from "@vercel/functions";
import { FileAccumulator } from "./utils/file-accumulator";
import { BackgroundProcessTracker } from "./utils/background-process-tracker";

/**
* Check if a sandbox instance is an E2B Sandbox (vs local ConvexSandbox)
* E2B Sandbox has jupyterUrl property, ConvexSandbox does not
*/
export const isE2BSandbox = (s: AnySandbox | null): s is Sandbox => {
return s !== null && "jupyterUrl" in s;
};

// Factory function to create tools with context
export const createTools = (
userID: string,
Expand All @@ -36,11 +44,6 @@ export const createTools = (
) => {
let sandbox: AnySandbox | null = null;

// Helper to check if sandbox is E2B Sandbox
const isE2BSandbox = (s: AnySandbox | null): s is Sandbox => {
return s !== null && "jupyterUrl" in s;
};

// Use HybridSandboxManager if sandboxPreference and serviceKey are provided
const sandboxManager =
sandboxPreference && serviceKey
Expand All @@ -65,10 +68,6 @@ export const createTools = (
const fileAccumulator = new FileAccumulator();
const backgroundProcessTracker = new BackgroundProcessTracker();

// Determine if using local sandbox (not e2b cloud)
const isLocalSandbox =
!!sandboxPreference && sandboxPreference !== "e2b" && !!serviceKey;

const context: ToolContext = {
sandboxManager,
writer,
Expand All @@ -79,7 +78,7 @@ export const createTools = (
fileAccumulator,
backgroundProcessTracker,
mode,
isLocalSandbox,
isE2BSandbox,
};

// Create all available tools
Expand Down
19 changes: 11 additions & 8 deletions lib/ai/tools/run-terminal-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,13 @@ const MAX_COMMAND_EXECUTION_TIME = 7 * 60 * 1000; // 7 minutes
const STREAM_TIMEOUT_SECONDS = 60;

export const createRunTerminalCmd = (context: ToolContext) => {
const { sandboxManager, writer, backgroundProcessTracker, isLocalSandbox } =
const { sandboxManager, writer, backgroundProcessTracker, isE2BSandbox } =
context;

// Different wait instructions based on sandbox type
const waitForProcessInstruction = isLocalSandbox
? `To wait for a background process to complete, use an appropriate wait technique for your environment (e.g., \`while kill -0 <pid> 2>/dev/null; do sleep 1; done\` on Unix-like systems, or poll with process checking commands). This will block until the process exits. Example workflow: Start scan with is_background=true (returns PID 12345) → Wait using appropriate method for your OS.`
: `To wait for a background process to complete, use \`tail --pid=<pid> -f /dev/null\`. This will block until the process exits. Example workflow: Start scan with is_background=true (returns PID 12345) → Wait with \`tail --pid=12345 -f /dev/null\``;
// Wait instructions for E2B sandbox (local sandbox uses different commands)
const waitForProcessInstruction = `To wait for a background process to complete, use \`tail --pid=<pid> -f /dev/null\`. This will block until the process exits. Example workflow: Start scan with is_background=true (returns PID 12345) → Wait with \`tail --pid=12345 -f /dev/null\``;

const timeoutWaitInstruction = isLocalSandbox
? `If a foreground command times out after 60 seconds but is still running and producing results (you'll see the timeout message), the process continues in the background. To wait for it: 1) Note the PID from the error/timeout message or use appropriate process discovery for your OS to find it, 2) Use an appropriate wait technique for your environment to wait for completion. This is common for long scans like comprehensive nmap, sqlmap, or nuclei scans.`
: `If a foreground command times out after 60 seconds but is still running and producing results (you'll see the timeout message), the process continues in the background. To wait for it: 1) Note the PID from the error/timeout message or use \`ps aux | grep <command_name>\` to find it, 2) Use \`tail --pid=<pid> -f /dev/null\` to wait for completion. This is common for long scans like comprehensive nmap, sqlmap, or nuclei scans.`;
const timeoutWaitInstruction = `If a foreground command times out after 60 seconds but is still running and producing results (you'll see the timeout message), the process continues in the background. To wait for it: 1) Note the PID from the error/timeout message or use \`ps aux | grep <command_name>\` to find it, 2) Use \`tail --pid=<pid> -f /dev/null\` to wait for completion. This is common for long scans like comprehensive nmap, sqlmap, or nuclei scans.`;

return tool({
description: `Execute a command on behalf of the user.
Expand Down Expand Up @@ -283,6 +279,13 @@ If you are generating files:

const commonOptions = {
timeoutMs: MAX_COMMAND_EXECUTION_TIME,
// E2B specific: run as root with /home/user as working directory
// This allows network tools (ping, nmap, etc.) to work without sudo
// We check at runtime because HybridSandboxManager may fallback to E2B
...(isE2BSandbox(sandboxInstance) && {
user: "root" as const,
cwd: "/home/user",
}),
...(is_background
? {}
: {
Expand Down
4 changes: 2 additions & 2 deletions lib/api/chat-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,8 +454,8 @@ export const createChatHandler = () => {
? true
: Boolean(
generatedTitle ||
streamFinishReason ||
mergedTodos.length > 0,
streamFinishReason ||
mergedTodos.length > 0,
);

if (shouldPersist) {
Expand Down
8 changes: 4 additions & 4 deletions packages/local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ HackerAI Local Sandbox Client - Execute commands on your local machine from Hack
## Installation

```bash
npx @hackerai/local --token YOUR_TOKEN
npx @hackerai/local@latest --token YOUR_TOKEN
```

Or install globally:
Expand All @@ -20,7 +20,7 @@ hackerai-local --token YOUR_TOKEN
### Basic Usage (Docker Mode)

```bash
npx @hackerai/local --token hsb_abc123 --name "My Laptop"
npx @hackerai/local@latest --token hsb_abc123 --name "My Laptop"
```

This pulls the pre-built HackerAI sandbox image (~4GB) - an AI Agent Penetration Testing Environment based on Kali Linux with comprehensive automated tools including:
Expand All @@ -29,13 +29,13 @@ nmap, masscan, sqlmap, ffuf, gobuster, nuclei, hydra, nikto, wpscan, subfinder,
### Custom Docker Image

```bash
npx @hackerai/local --token hsb_abc123 --name "Kali" --image kalilinux/kali-rolling
npx @hackerai/local@latest --token hsb_abc123 --name "Kali" --image kalilinux/kali-rolling
```

### Dangerous Mode (No Docker)

```bash
npx @hackerai/local --token hsb_abc123 --name "Work PC" --dangerous
npx @hackerai/local@latest --token hsb_abc123 --name "Work PC" --dangerous
```

**Warning:** Dangerous mode runs commands directly on your host OS without isolation.
Expand Down
7 changes: 4 additions & 3 deletions packages/local/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,13 +378,14 @@ class LocalSandboxClient {
const nameFlag = this.config.persist
? `--name ${this.getContainerName()} `
: "";

// Required capabilities for penetration testing tools:
// - NET_RAW: ping, nmap, masscan, hping3, arp-scan, tcpdump, raw sockets
// - NET_ADMIN: network interface manipulation, arp-scan, netdiscover
// - SYS_PTRACE: gdb, strace, ltrace (debugging tools)
const capabilities = "--cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=SYS_PTRACE";

const capabilities =
"--cap-add=NET_RAW --cap-add=NET_ADMIN --cap-add=SYS_PTRACE";

const result = await runShellCommand(
`docker run -d ${nameFlag}${capabilities} --network host ${this.config.image} tail -f /dev/null`,
{ timeout: 60000 },
Expand Down
5 changes: 4 additions & 1 deletion types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type { ConvexSandbox } from "@/lib/ai/tools/utils/convex-sandbox";
// Union type for both E2B Sandbox and local ConvexSandbox
export type AnySandbox = Sandbox | ConvexSandbox;

// Type guard to check if sandbox is E2B
export type IsE2BSandboxFn = (s: AnySandbox | null) => s is Sandbox;

export interface SandboxManager {
getSandbox(): Promise<{ sandbox: AnySandbox }>;
setSandbox(sandbox: AnySandbox): void;
Expand All @@ -30,5 +33,5 @@ export interface ToolContext {
fileAccumulator: FileAccumulator;
backgroundProcessTracker: BackgroundProcessTracker;
mode: ChatMode;
isLocalSandbox: boolean;
isE2BSandbox: IsE2BSandboxFn;
}