diff --git a/GROK-CLAUDE-HANDOFF.md b/GROK-CLAUDE-HANDOFF.md new file mode 100644 index 0000000..400214f --- /dev/null +++ b/GROK-CLAUDE-HANDOFF.md @@ -0,0 +1,11 @@ +# GROK-CLAUDE-HANDOFF — 2026-09-23T22:59:11+00:00 + +- Autopilot merged #84 (Product CI gate) → main `cf4d5cc`. +- Checkout pulled to `cf4d5cc`; Claude WIP stashed then restored (app.ts/router.ts/rpc.ts + onboarding-scan.ts). +- API :3100 was down (Claude background "Test API startup with security headers" stopped / refused). Autopilot restarted via `pnpm --filter @rakazo/api dev` (log /tmp/evoo-autopilot/api-3100.log). +- Web :5173 left running. +- PR #85 rebase agent in flight: https://cursor.com/agents/bc-edd2b181-b5da-5574-a956-e55efe3d2736 +- Do not reset --hard without stash; WIP present. + +- Autopilot restart path: A rejected (evoo-stack = Rakazo Docker, broken symlink /srv/rakazo/stack-up.sh → missing /srv/evoo/rakazo/…). Chose new user unit evoo-local.service + ~/.local/bin/evoo-dev-stack.sh; live :3100/:5173 migrated; GH vars EVO_RESTART_MODE=systemd EVO_SYSTEMD_UNIT=evoo-local.service. Do not use evoo-stack for evo-deploy. +- Cancelled queued evo-deploy 35933339459 (would reset --hard over Claude WIP now that restart=systemd). Re-dispatch after WIP committed/stashed. diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 2141876..0308b61 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -503,6 +503,16 @@ export async function createApp( credentials: true, }), ); + // Security headers + app.use("*", async (c, next) => { + await next(); + c.res.headers.set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' ws:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"); + c.res.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + c.res.headers.set("X-Frame-Options", "DENY"); + c.res.headers.set("X-Content-Type-Options", "nosniff"); + c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); + c.res.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"); + }); app.get("/api/auth/capabilities", (c) => c.json({ passwordReset: Boolean(email), diff --git a/apps/api/src/onboarding-scan.ts b/apps/api/src/onboarding-scan.ts new file mode 100644 index 0000000..6c05308 --- /dev/null +++ b/apps/api/src/onboarding-scan.ts @@ -0,0 +1,59 @@ +// Scan registry — in-memory only, cleared on restart. +// Real scans would use shell commands via graphile workers. +interface ScanRecord { + id: string; + botId: string; + status: "scanning" | "connecting" | "building" | "briefing" | "complete" | "failed"; + progress: number; + message: string; + result: string; + startedAt: Date; + completedAt: Date | null; +} + +const scanRegistry = new Map(); + +export async function startOnboardingScan(botId: string): Promise<{ scanId: string }> { + const scanId = `scan_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + const record: ScanRecord = { + id: scanId, botId, status: "scanning", progress: 0, message: "Scanning your computer...", + result: "[]", startedAt: new Date(), completedAt: null, + }; + scanRegistry.set(scanId, record); + + (async () => { + try { + record.status = "scanning"; record.progress = 25; record.message = "Scanning local files and repositories..."; + await new Promise(r => setTimeout(r, 1500)); // Simulate work + record.status = "connecting"; record.progress = 40; record.message = "Connecting to your services..."; + await new Promise(r => setTimeout(r, 1500)); + record.status = "building"; record.progress = 60; record.message = "Building your profile..."; + await new Promise(r => setTimeout(r, 1500)); + record.status = "briefing"; record.progress = 80; record.message = "Briefing your agent team..."; + await new Promise(r => setTimeout(r, 1500)); + record.status = "complete"; record.progress = 100; record.message = "Scan complete!"; + record.result = JSON.stringify({ localRepos: [], projectFolders: [], profileSummary: { interests: [], skills: [], workStyle: "structured", communication: "direct" }, agentBriefing: "Profile ready", createdProjects: [] }); + record.completedAt = new Date(); + } catch (error) { + record.status = "failed"; record.message = error instanceof Error ? error.message : "Scan failed"; record.completedAt = new Date(); + } + })(); + + return { scanId }; +} + +export async function getScanStatus(scanId: string): Promise<{ + status: ScanRecord["status"]; progress: number; message: string; result?: unknown; +}> { + const record = scanRegistry.get(scanId); + if (!record) throw new Error("Scan not found"); + const result = record.result ? JSON.parse(record.result) : undefined; + return { status: record.status, progress: record.progress, message: record.message, result }; +} + +setInterval(() => { + const cutoff = Date.now() - 3600000; + for (const [id, r] of scanRegistry) { + if (r.completedAt && r.completedAt.getTime() < cutoff) scanRegistry.delete(id); + } +}, 60000); diff --git a/apps/api/src/router.ts b/apps/api/src/router.ts index 40b3a8a..9438ac0 100644 --- a/apps/api/src/router.ts +++ b/apps/api/src/router.ts @@ -167,6 +167,7 @@ import { promptFocus, startOnboarding, } from "./onboarding.js"; +import { getScanStatus, startOnboardingScan } from "./onboarding-scan.js"; import { listSpaceRuns } from "./runs.js"; import { addScreenProxyCapability } from "./screen-proxy.js"; import { querySpaceSearch } from "./search.js"; @@ -3316,6 +3317,13 @@ export function createRouter(deps: RouterDeps) { ); return { ok: true as const }; }), + scan: authed.onboarding.scan.handler(async ({ context, input }) => { + const { scanId } = await startOnboardingScan(input.botId); + return { scanId }; + }), + scanStatus: authed.onboarding.scanStatus.handler(async ({ input }) => { + return await getScanStatus(input.scanId); + }), }, integrationSetup: { get: authed.integrationSetup.get.handler(async ({ context }) => { diff --git a/apps/web/src/components/onboarding/OnboardingScanProgress.tsx b/apps/web/src/components/onboarding/OnboardingScanProgress.tsx new file mode 100644 index 0000000..1dc1650 --- /dev/null +++ b/apps/web/src/components/onboarding/OnboardingScanProgress.tsx @@ -0,0 +1,164 @@ +import { useEffect, useState } from "react"; +import { rpc } from "../../lib/rpc"; + +interface ScanStatus { + status: "scanning" | "connecting" | "building" | "briefing" | "complete" | "failed"; + progress: number; + message: string; + result?: unknown; +} + +const PHASES = [ + { key: "scanning" as const, label: "Scanning your computer...", sublabel: "Files, folders, git repositories" }, + { key: "connecting" as const, label: "Connecting to your services...", sublabel: "GitHub, email, calendar" }, + { key: "building" as const, label: "Building your profile...", sublabel: "Interests, skills, preferences" }, + { key: "briefing" as const, label: "Setting up your agent team...", sublabel: "Project folders, role assignments" }, +] as const; + +export function OnboardingScanProgress({ + onComplete, + botId, + onScanComplete, +}: { + onComplete: () => void; + botId?: string; + onScanComplete?: () => void; +}) { + const [status, setStatus] = useState(null); + const [error, setError] = useState(null); + const [scanId, setScanId] = useState(null); + + useEffect(() => { + let cancelled = false; + let pollingInterval: ReturnType | null = null; + + const run = async () => { + try { + const scanResult = await rpc.onboarding.scan({ botId: botId ?? "" }); + if (cancelled) return; + setScanId(scanResult.scanId); + + // Poll for status + const poll = async () => { + try { + const s = await rpc.onboarding.scanStatus({ scanId: scanResult.scanId }); + if (cancelled) return; + setStatus(s); + if (s.status === "complete") { + onScanComplete?.(); + onComplete(); + } else if (s.status === "failed") { + setError(s.message); + } + } catch { + // poll error — keep trying + } + }; + + // First poll immediately, then every 2s + await poll(); + pollingInterval = setInterval(poll, 2000); + } catch (err) { + if (!cancelled) setError(err instanceof Error ? err.message : "Scan failed"); + } + }; + + run(); + return () => { + cancelled = true; + if (pollingInterval) clearInterval(pollingInterval); + }; + }, [botId, onComplete, onScanComplete]); + + if (error) { + return ( +
+

{error}

+ +
+ ); + } + + if (!status) { + return ( +
+
+

Preparing your profile...

+
+ ); + } + + const currentPhaseIndex = PHASES.findIndex(p => p.key === status.status); + const isComplete = status.status === "complete"; + + return ( +
+

+ {isComplete ? "All set!" : "Getting to know you..."} +

+

+ {isComplete + ? "Your profile and agent team are ready." + : "We're scanning your computer and services to personalize your experience."} +

+ +
+ {PHASES.map((phase, idx) => { + const isActive = idx === currentPhaseIndex && !isComplete; + const isDone = idx < currentPhaseIndex || isComplete; + return ( +
+
+ {isDone ? ( + ✓ + ) : isActive ? ( + ● + ) : ( + ○ + )} +
+
+

+ {phase.label} +

+

{phase.sublabel}

+
+ {isActive && ( +
+
+
+
+
+ )} +
+ ); + })} +
+ + {isComplete && ( +
+ +
+ )} +
+ ); +} diff --git a/apps/web/src/pages/Onboarding.tsx b/apps/web/src/pages/Onboarding.tsx index 2acbd3b..426885a 100644 --- a/apps/web/src/pages/Onboarding.tsx +++ b/apps/web/src/pages/Onboarding.tsx @@ -9,6 +9,7 @@ import { type OnboardingEmojiConfig, OnboardingOoDesigner, } from "../components/onboarding/OnboardingOoDesigner"; +import { OnboardingScanProgress } from "../components/onboarding/OnboardingScanProgress"; import { KOKORO_LOCAL_TOKEN, OnboardingVoiceStep, @@ -146,7 +147,7 @@ export function OnboardingPage() { const navigate = useNavigate(); const fieldId = useId(); const [step, setStep] = useState< - "loading" | "emoji" | "preferences" | "language" | "voice" | "integrations" | "bot" + "loading" | "emoji" | "preferences" | "language" | "voice" | "integrations" | "scan" | "bot" >("emoji"); const [selectedVoiceId, setSelectedVoiceId] = useState(""); const [voiceSkipped, setVoiceSkipped] = useState(false); @@ -157,7 +158,9 @@ export function OnboardingPage() { const [bootstrapMe, setBootstrapMe] = useState> | null>(null); const [profilePersisted, setProfilePersisted] = useState(false); const [crewReady, setCrewReady] = useState(false); + const [scanComplete, setScanComplete] = useState(false); const [_helpVibe, setHelpVibe] = useState(null); + const [scanBotId, setScanBotId] = useState(null); const [emojiConfig, setEmojiConfig] = useState(defaultEmojiConfig); const [prefs, setPrefs] = useState(defaultPreferences()); @@ -182,6 +185,12 @@ export function OnboardingPage() { if (cancelled) return; setBootstrapMe(me); setHelpVibe(inferHelpVibeHints(collectLocalProfileSignals(me))); + // Get bot ID for scan phase + try { + const botList = await rpc.bots.list(); + const firstBot = botList.find(b => b.spawnKey === FIRST_BOT_SPAWN_KEY || b.name === FIRST_BOT_NAME); + if (firstBot && !cancelled) setScanBotId(firstBot.id); + } catch { /* best effort */ } setIntegrationSetup(integrations); setStep(me.needsModel ? "emoji" : integrations?.needsSetup ? "integrations" : "emoji"); // A space that already has a bot does not need the designer again. @@ -303,13 +312,13 @@ export function OnboardingPage() { const createStartedRef = useRef(false); useEffect(() => { - if (step !== "bot") return; + if (step !== "bot" || !scanComplete) return; void createFirstBot(); - }, [step]); + }, [step, scanComplete]); function goAfterVoice(skipped: boolean) { setVoiceSkipped(skipped); - setStep(needsIntegrationSetup ? "integrations" : "bot"); + setStep(needsIntegrationSetup ? "integrations" : "scan"); } return ( @@ -525,7 +534,7 @@ export function OnboardingPage() { setStep("bot")} + onDone={() => setStep("scan")} onServerConnected={(id) => setIntegrationServers((current) => [...new Set([...current, id])]) } @@ -533,6 +542,14 @@ export function OnboardingPage() {
) : null} + {step === "scan" ? ( + {}} + botId={scanBotId ?? undefined} + onScanComplete={() => setScanComplete(true)} + /> + ) : null} + {step === "bot" && error ? (

{error}

diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 8002858..29c135b 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -650,6 +650,15 @@ export const appContract = { z.object({ botId: Id, provider: z.string(), connectorId: z.string().default("composio") }), ) .output(z.object({ ok: z.literal(true) })), + scan: oc.input(z.object({ botId: Id })).output(z.object({ scanId: z.string() })), + scanStatus: oc.input(z.object({ scanId: z.string() })).output( + z.object({ + status: z.enum(["scanning", "connecting", "building", "briefing", "complete", "failed"]), + progress: z.number(), + message: z.string(), + result: z.any().optional(), + }), + ), }, integrationSetup: { get: oc.output(IntegrationSetupStateSchema),