diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e8ecd4a --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +VITE_SUPABASE_URL=https://tmlrveloeirxsankeqrs.supabase.co +VITE_SUPABASE_PUBLISHABLE_KEY=replace_with_supabase_publishable_key +VITE_CHAT_FUNCTION_URL=https://tmlrveloeirxsankeqrs.supabase.co/functions/v1/jarvis-chat + +# Server-side only — set in Supabase Edge Function Secrets, never in VITE_ variables: +# GEMINI_API_KEY= +# GITHUB_TOKEN= +# CLOUDFLARE_API_TOKEN= +# CLOUDFLARE_ACCOUNT_ID= +# TAVILY_API_KEY= +# APP_ORIGIN= +# JARVIS_GATEWAY_URL=https://.workers.dev diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed0d31e..610c072 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,13 +1,11 @@ -name: Production build and browser smoke tests - +name: JARVIS X build and tests on: push: - branches: [main] + branches: [main, jarvis-x-foundation] pull_request: branches: [main] - jobs: - build-and-test: + verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -15,6 +13,6 @@ jobs: with: node-version: 22 - run: npm install - - run: npx playwright install --with-deps chromium + - run: npm test + - run: node --check cloudflare/worker.js - run: npm run build - - run: npm run test:e2e diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml new file mode 100644 index 0000000..44ea259 --- /dev/null +++ b/.github/workflows/deploy-cloudflare.yml @@ -0,0 +1,77 @@ +name: Deploy generated project to Cloudflare Pages +on: + workflow_dispatch: + inputs: + project_name: + description: Cloudflare Pages project name + required: true + type: string + branch: + description: Source repository branch + required: true + default: main + type: string + repository: + description: owner/repository to build and deploy + required: true + type: string +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.branch }} + token: ${{ secrets.GITHUB_TOKEN_JARVIS }} + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install dependencies without lifecycle scripts + run: npm install --ignore-scripts --no-audit --no-fund + - name: Run project tests when available + run: npm test --if-present + - name: Build project + env: + VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} + VITE_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.VITE_SUPABASE_PUBLISHABLE_KEY }} + VITE_CHAT_FUNCTION_URL: ${{ secrets.VITE_CHAT_FUNCTION_URL }} + run: npm run build + - uses: actions/upload-artifact@v4 + with: + name: jarvis-pages-dist + path: dist/ + if-no-files-found: error + retention-days: 1 + deploy: + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/download-artifact@v4 + with: + name: jarvis-pages-dist + path: dist + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install Wrangler + run: npm install --global wrangler + - name: Create Pages project if needed + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + PROJECT_NAME: ${{ inputs.project_name }} + BRANCH: ${{ inputs.branch }} + run: wrangler pages project create "$PROJECT_NAME" --production-branch "$BRANCH" || true + - name: Deploy static build + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + PROJECT_NAME: ${{ inputs.project_name }} + BRANCH: ${{ inputs.branch }} + run: wrangler pages deploy dist --project-name "$PROJECT_NAME" --branch "$BRANCH" diff --git a/.github/workflows/deploy-worker.yml b/.github/workflows/deploy-worker.yml new file mode 100644 index 0000000..2a171d1 --- /dev/null +++ b/.github/workflows/deploy-worker.yml @@ -0,0 +1,39 @@ +name: Deploy JARVIS X gateway Worker +on: + workflow_dispatch: +jobs: + deploy-worker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: jarvis-x-foundation + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install Wrangler + run: npm install --global wrangler + - name: Configure Worker secrets + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_PUBLISHABLE_KEY: ${{ secrets.SUPABASE_PUBLISHABLE_KEY }} + JARVIS_OWNER_EMAILS: ${{ secrets.JARVIS_OWNER_EMAILS }} + APP_ORIGIN: ${{ secrets.APP_ORIGIN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN_JARVIS }} + GITHUB_OWNER: ${{ secrets.GITHUB_OWNER }} + GITHUB_REPO: ${{ secrets.GITHUB_REPO }} + CLOUDFLARE_PAGES_READY: 'true' + run: | + set -euo pipefail + for key in SUPABASE_URL SUPABASE_PUBLISHABLE_KEY JARVIS_OWNER_EMAILS APP_ORIGIN GITHUB_TOKEN GITHUB_OWNER GITHUB_REPO CLOUDFLARE_PAGES_READY; do + value="${!key}" + if [ -z "$value" ]; then echo "Missing required secret: $key"; exit 1; fi + printf '%s' "$value" | wrangler secret put "$key" --config cloudflare/wrangler.toml + done + - name: Deploy Worker + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: wrangler deploy --config cloudflare/wrangler.toml diff --git a/README-JARVIS-X.md b/README-JARVIS-X.md new file mode 100644 index 0000000..63a020d --- /dev/null +++ b/README-JARVIS-X.md @@ -0,0 +1,31 @@ +# JARVIS X + +A light, premium personal AI workspace built with Vite and Supabase. The visual system uses pearl, ivory, warm stone and champagne accents; no dark theme or neon gradients. + +## Current foundation +- Responsive web app/PWA shell and Google OAuth entry point. +- Supabase Auth, persistent chat history, projects, memory, tasks, approvals, agent runs and audit log. +- Supabase Edge Functions: `jarvis-chat` and `jarvis-agent`. +- Provider status screen and secret-name checklist. +- Cloudflare Worker gateway starter and Wrangler deployment config. +- Risky actions are designed around explicit approvals; credentials remain server-side. + +## Local setup +1. `npm install` +2. Copy `.env.example` to `.env.local` and set the Supabase URL and publishable key. +3. In Supabase Auth, enable Google and configure the OAuth redirect URLs for the app domain. +4. Add `GEMINI_API_KEY` to Supabase Edge Function secrets. +5. Run `npm run dev`; validate with `npm run build`. + +## Supabase Edge Function secrets +Required for AI chat: `GEMINI_API_KEY`. + +Optional integrations: `GITHUB_TOKEN`, `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `SEARCH_API_KEY`, `IMAGE_PROVIDER_API_KEY`, `TTS_API_KEY`, `EMAIL_PROVIDER_API_KEY`. + +Supabase URL and publishable key are public client configuration values. Never place service-role keys, OAuth client secrets, or provider tokens in frontend variables or source control. + +## Cloudflare +Set `APP_ORIGIN` and provider credentials as Worker secrets/vars. Deploy using Wrangler only after configuring the Cloudflare account and reviewing the deployment target. + +## Current limitations +External GitHub actions, real web search, image generation, file parsing, background scheduling and production Cloudflare deployments still require their provider adapters, credentials, tests and explicit authorization. The presence of a settings entry does not mean an integration is active. diff --git a/README.md b/README.md index e42efa3..4c22415 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,47 @@ -# NEON HORIZON +# JARVIS X — Personal Intelligence -A browser-based 3D night-drive prototype built with Three.js, Vite and cannon-es. +A light-first personal AI workspace with a pearl, ivory and champagne visual system. The canonical app is the Vite project at the repository root. -## Requirements -- Node.js 22 or newer -- npm +## Implemented in this branch +- Responsive chat UI and installable PWA shell. +- Google OAuth flow with a database-backed email allowlist. +- Supabase conversation history, model selection, search and custom instructions. +- Optional automatic memory extraction (off until enabled in Settings). +- Private file library; image, PDF and plain-text/CSV/JSON analysis. +- Gemini image generation and natural text-to-speech. +- Browser speech input with language preference. +- Tavily web research with source links. +- Projects, memories, tasks, agent runs and approval queue. +- Agent planning for generated starter projects, approval-gated GitHub repository creation and Cloudflare Pages deployment. +- Supabase RLS, private Storage policies and JWT-verified Edge Functions. + +See [the feature matrix](docs/FEATURE-MATRIX.md) for limitations and features not yet at full ChatGPT parity. ## Local development -```bash -npm install -npm run dev -``` - -## Production build -```bash -npm run build -npm run preview -``` - -The production site is generated in dist/. - -## Deployment -- Vercel: Framework preset Vite, build command `npm run build`, output directory `dist`. -- Netlify: build command `npm run build`, publish directory `dist`. - -## Controls -- A / D or Left / Right: steer -- W / Up: accelerate -- S / Down: brake -- Space / Shift: nitro -- Escape: pause -- Touch: drag left/right to steer; on-screen pedals on touch devices - -## Notes -The vehicle simulation uses a rigid chassis and raycast suspension. It is not a soft-body deformation simulator and does not claim BeamNG.drive parity. Third-party model and texture assets should only be added after their licenses are checked. +1. Copy `.env.example` to `.env`. +2. Set `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`. +3. Run `npm install`, then `npm run dev`. +4. Run `npm test` and `npm run build`. + +## Supabase setup +- Project ref: `tmlrveloeirxsankeqrs` (Driver saathi; Plivex is not used). +- Migrations: `supabase/migrations`. +- Edge Functions: `jarvis-chat`, `jarvis-agent`, `jarvis-research`, `jarvis-image`, `jarvis-tts`. +- Configure Google OAuth under Supabase Auth → Providers and set allowed redirect URLs. +- Add approved Gmail addresses to `public.jarvis_allowed_users` using the SQL example in [Provider setup](docs/PROVIDER-SETUP.md). The allowlist is empty by default. +- Keep all server-side keys in the correct provider's secrets store. + +## Credentials by location +- Supabase Edge Function Secrets: `GEMINI_API_KEY`, `TAVILY_API_KEY`, `JARVIS_GATEWAY_URL`, and optional `APP_ORIGIN`. +- GitHub Actions Secrets: `GITHUB_TOKEN_JARVIS`, `GITHUB_OWNER`, `GITHUB_REPO`, `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, `JARVIS_OWNER_EMAILS`, `APP_ORIGIN`, and the `VITE_*` frontend build variables. +- Google OAuth: configure the Client ID and Client Secret in Supabase Auth provider settings. +- Never use a Supabase service-role or secret key in a `VITE_*` variable or browser code. + +## Safety model +- User data is isolated with RLS and an email allowlist. +- Repository creation and deployment require explicit approval. +- The gateway verifies the signed-in user and the approved action payload before performing side effects. +- Provider keys are never stored in Git history or chat. + +## Current status +This is a working foundation in the `jarvis-x-foundation` branch with CI passing. Provider credentials, OAuth configuration, end-to-end tests and Cloudflare deployment are still required before the live app can be used. This project does not claim full ChatGPT feature parity yet. diff --git a/cloudflare/worker.js b/cloudflare/worker.js new file mode 100644 index 0000000..4120dfb --- /dev/null +++ b/cloudflare/worker.js @@ -0,0 +1,99 @@ +const json=(data,status=200,headers={})=>new Response(JSON.stringify(data),{status,headers:{"content-type":"application/json; charset=utf-8","cache-control":"no-store",...headers}}); +const cors=(env,origin)=>origin&&origin===env.APP_ORIGIN?{"access-control-allow-origin":origin,"access-control-allow-methods":"GET,POST,OPTIONS","access-control-allow-headers":"authorization,content-type","vary":"Origin"}:{}; +async function authenticate(request,env){ + const token=(request.headers.get("authorization")||"").replace(/^Bearer\s+/i,""); + if(!token)throw new Error("Sign in required."); + if(!env.SUPABASE_URL||!env.SUPABASE_PUBLISHABLE_KEY)throw new Error("Supabase gateway settings are missing."); + const response=await fetch(env.SUPABASE_URL+"/auth/v1/user",{headers:{apikey:env.SUPABASE_PUBLISHABLE_KEY,authorization:"Bearer "+token}}); + if(!response.ok)throw new Error("Your JARVIS X session is invalid."); + const user=await response.json(); + const allowed=(env.JARVIS_OWNER_EMAILS||"").split(",").map(x=>x.trim().toLowerCase()).filter(Boolean); + if(!user.email||!allowed.includes(user.email.toLowerCase()))throw new Error("This workspace is not authorised for this account."); + return {user,token}; +} +async function approval(env,auth,id,expectedAction){ + if(!id)throw new Error("An approved action ID is required."); + const url=new URL(env.SUPABASE_URL+"/rest/v1/jarvis_approvals"); + url.searchParams.set("id","eq."+id);url.searchParams.set("user_id","eq."+auth.user.id);url.searchParams.set("status","eq.approved");url.searchParams.set("select","id,action,payload"); + const response=await fetch(url,{headers:{apikey:env.SUPABASE_PUBLISHABLE_KEY,authorization:"Bearer "+auth.token}}); + if(!response.ok)throw new Error("Could not verify the approval record."); + const rows=await response.json();const row=rows[0]; + if(!row||row.action!==expectedAction)throw new Error("No matching approved action was found."); + return row; +} +async function audit(env,auth,action,target,details){ + await fetch(env.SUPABASE_URL+"/rest/v1/jarvis_audit_logs",{method:"POST",headers:{apikey:env.SUPABASE_PUBLISHABLE_KEY,authorization:"Bearer "+auth.token,"content-type":"application/json",prefer:"return=minimal"},body:JSON.stringify({user_id:auth.user.id,action,target,details})}); +} +export default { + async fetch(request,env){ + const origin=request.headers.get("origin")||""; + const headers=cors(env,origin); + if(request.method==="OPTIONS")return new Response(null,{status:204,headers}); + const url=new URL(request.url); + if(url.pathname==="/health"&&request.method==="GET")return json({service:"jarvis-x-gateway",status:"ready"},200,headers); + try{ + const auth=await authenticate(request,env); + if(url.pathname==="/capabilities"&&request.method==="GET"){ + return json({providers:{gemini:Boolean(env.GEMINI_API_KEY),github:Boolean(env.GITHUB_TOKEN),cloudflare:Boolean(env.GITHUB_TOKEN&&env.GITHUB_OWNER&&env.GITHUB_REPO&&env.CLOUDFLARE_PAGES_READY==="true"),supabase:Boolean(env.SUPABASE_URL&&env.SUPABASE_PUBLISHABLE_KEY)}},200,headers); + } + if(url.pathname==="/api/github/repositories"&&request.method==="POST"){ + const body=await request.json(); + const approved=await approval(env,auth,body.approvalId,"github.create_repository"); + const approvedPayload=approved.payload||{}; + const approvedName=String(approvedPayload.name||"").trim().toLowerCase().replace(/[^a-z0-9._-]/g,"-").slice(0,80); + const files=Array.isArray(approvedPayload.files)?approvedPayload.files:[]; + if(!approvedName)return json({error:"The approved request does not include a repository name."},400,headers); + if(!files.length||files.length>30)return json({error:"The approved request must include 1–30 generated project files."},400,headers); + let totalBytes=0; + for(const file of files){ + if(typeof file.path!=="string"||!file.path||file.path.startsWith("/")||file.path.includes("\\")||file.path.split("/").includes("..")||file.path.startsWith(".github/workflows/")||file.path===".npmrc"||typeof file.content!=="string")return json({error:"A generated file path or content is invalid."},400,headers); + totalBytes+=new TextEncoder().encode(file.content).length; + } + if(totalBytes>300000)return json({error:"Generated project files exceed the 300 KB initial commit limit."},413,headers); + if(!env.GITHUB_TOKEN)return json({error:"GitHub token is not configured."},503,headers); + const response=await fetch("https://api.github.com/user/repos",{method:"POST",headers:{authorization:"Bearer "+env.GITHUB_TOKEN,accept:"application/vnd.github+json","content-type":"application/json","x-github-api-version":"2022-11-28","user-agent":"JARVIS-X"},body:JSON.stringify({name:approvedName,description:String(approvedPayload.description||"Created by JARVIS X").slice(0,300),private:approvedPayload.private!==false,auto_init:false})}); + const result=await response.json();if(!response.ok)return json({error:result.message||"GitHub repository creation failed."},response.status,headers); + const apiHeaders={authorization:"Bearer "+env.GITHUB_TOKEN,accept:"application/vnd.github+json","content-type":"application/json","x-github-api-version":"2022-11-28","user-agent":"JARVIS-X"}; + const treeResponse=await fetch("https://api.github.com/repos/"+result.full_name+"/git/trees",{method:"POST",headers:apiHeaders,body:JSON.stringify({tree:files.map(file=>({path:file.path,mode:"100644",type:"blob",content:file.content}))})}); + const tree=await treeResponse.json();if(!treeResponse.ok)return json({error:"Repository created, but initial file tree failed: "+(tree.message||"GitHub error"),repository:result.html_url},502,headers); + const commitResponse=await fetch("https://api.github.com/repos/"+result.full_name+"/git/commits",{method:"POST",headers:apiHeaders,body:JSON.stringify({message:"Initial project generated by JARVIS X",tree:tree.sha})}); + const commit=await commitResponse.json();if(!commitResponse.ok)return json({error:"Repository created, but initial commit failed: "+(commit.message||"GitHub error"),repository:result.html_url},502,headers); + const refResponse=await fetch("https://api.github.com/repos/"+result.full_name+"/git/refs",{method:"POST",headers:apiHeaders,body:JSON.stringify({ref:"refs/heads/main",sha:commit.sha})}); + const refResult=await refResponse.json();if(!refResponse.ok)return json({error:"Repository created, but main branch creation failed: "+(refResult.message||"GitHub error"),repository:result.html_url},502,headers); + let projectId=null; + const projectResponse=await fetch(env.SUPABASE_URL+"/rest/v1/jarvis_projects",{method:"POST",headers:{apikey:env.SUPABASE_PUBLISHABLE_KEY,authorization:"Bearer "+auth.token,"content-type":"application/json",prefer:"return=representation"},body:JSON.stringify({user_id:auth.user.id,name:approvedName,description:String(approvedPayload.description||"Created by JARVIS X"),project_type:"app",status:"active",repository_url:result.html_url,metadata:{generated_by:"jarvis-x",commit:commit.sha,branch:"main",files_count:files.length}})}); + if(projectResponse.ok){const rows=await projectResponse.json();projectId=rows[0]?.id||null;} + await audit(env,auth,"github.repository_created",result.full_name,{url:result.html_url,private:result.private,fileCount:files.length,commit:commit.sha,projectId}); + return json({success:true,projectId,repository:{name:result.full_name,url:result.html_url,private:result.private,defaultBranch:"main",fileCount:files.length,commit:commit.sha}},201,headers); + } + if(url.pathname==="/api/deploy/cloudflare"&&request.method==="POST"){ + const body=await request.json(); + const approved=await approval(env,auth,body.approvalId,"cloudflare.deploy_pages"); + const projectName=String(approved.payload?.projectName||"").toLowerCase().replace(/[^a-z0-9-]/g,"-").slice(0,58); + const repository=String(approved.payload?.repository||""); + if(!projectName||!repository.startsWith((env.GITHUB_OWNER||"")+"/"))return json({error:"The approved request must include a valid Pages project name and repository owned by this GitHub account."},400,headers); + if(!env.GITHUB_TOKEN||!env.GITHUB_OWNER||!env.GITHUB_REPO)return json({error:"GitHub workflow dispatch integration is not configured."},503,headers); + const ref=String(approved.payload?.branch||"main"); + const endpoint="https://api.github.com/repos/"+env.GITHUB_OWNER+"/"+env.GITHUB_REPO+"/actions/workflows/deploy-cloudflare.yml/dispatches"; + const response=await fetch(endpoint,{method:"POST",headers:{authorization:"Bearer "+env.GITHUB_TOKEN,accept:"application/vnd.github+json","content-type":"application/json","x-github-api-version":"2022-11-28","user-agent":"JARVIS-X"},body:JSON.stringify({ref:"jarvis-x-foundation",inputs:{project_name:projectName,branch:ref,repository}})}); + if(!response.ok){const result=await response.json();return json({error:result.message||"Could not start Cloudflare deployment."},response.status,headers);} + const deploymentUrl="https://"+projectName+".pages.dev"; + const projectQuery=new URL(env.SUPABASE_URL+"/rest/v1/jarvis_projects"); + projectQuery.searchParams.set("user_id","eq."+auth.user.id); + projectQuery.searchParams.set("repository_url","eq.https://github.com/"+repository); + projectQuery.searchParams.set("select","id,metadata"); + const projectLookup=await fetch(projectQuery,{headers:{apikey:env.SUPABASE_PUBLISHABLE_KEY,authorization:"Bearer "+auth.token}}); + if(projectLookup.ok){ + const rows=await projectLookup.json(); + if(rows[0]){ + const metadata={...(rows[0].metadata||{}),deployment_status:"queued",cloudflare_project:projectName}; + await fetch(env.SUPABASE_URL+"/rest/v1/jarvis_projects?id=eq."+rows[0].id,{method:"PATCH",headers:{apikey:env.SUPABASE_PUBLISHABLE_KEY,authorization:"Bearer "+auth.token,"content-type":"application/json",prefer:"return=minimal"},body:JSON.stringify({deployment_url:deploymentUrl,metadata,updated_at:new Date().toISOString()})}); + } + } + await audit(env,auth,"cloudflare.deployment_requested",projectName,{ref,repository,deploymentUrl}); + return json({success:true,status:"queued",message:"Cloudflare deployment workflow started.",projectName,deploymentUrl,repository,branch:ref,workflow:"deploy-cloudflare.yml"},202,headers); + } + return json({error:"Route not found."},404,headers); + }catch(error){return json({error:error instanceof Error?error.message:"Gateway error."},401,headers);} + } +}; diff --git a/cloudflare/wrangler.toml b/cloudflare/wrangler.toml new file mode 100644 index 0000000..dc5e622 --- /dev/null +++ b/cloudflare/wrangler.toml @@ -0,0 +1,4 @@ +name = "jarvis-x-gateway" +main = "worker.js" +compatibility_date = "2026-11-07" +workers_dev = true diff --git a/docs/FEATURE-MATRIX.md b/docs/FEATURE-MATRIX.md new file mode 100644 index 0000000..c3cf78a --- /dev/null +++ b/docs/FEATURE-MATRIX.md @@ -0,0 +1,46 @@ +# JARVIS X feature status + +JARVIS X is being built as a light-first personal AI workspace. This file separates working code paths from provider-dependent and future work. + +## Implemented in the current branch +- Responsive pearl/ivory/champagne chat interface and PWA shell. +- Google OAuth sign-in flow with a database-backed email allowlist. +- Supabase chat history, model selection, conversation search and custom instructions. +- Optional automatic memory extraction for non-sensitive preferences and project details; the user must enable it in Settings. +- Private file storage and library. +- Chat analysis for images, PDFs and plain-text/CSV/JSON files (15 MB total per request); Word/Excel file content extraction is not yet implemented. +- Gemini image generation and natural text-to-speech endpoints. +- Browser speech input, with language selection. +- Tavily web search with Gemini summaries and source links. +- Projects, memories, tasks, agent run history and approval queue. +- Agent planning that can prepare a new private GitHub repository with generated starter source files. +- Approval-gated Cloudflare Pages deployment workflow and a Cloudflare Worker gateway. +- Supabase Row Level Security on user data and private storage; all deployed Edge Functions verify JWTs. + +## Requires provider configuration before live use +- Gemini API key for chat, image generation, agent planning and natural speech. +- Tavily API key for web research. +- Google OAuth client configured in Supabase Auth. +- GitHub token and Cloudflare API token/account ID for repository creation and deployment. +- Approved Gmail addresses in the database allowlist and Worker allowlist. +- Frontend Supabase URL and publishable key in the Cloudflare Pages build secrets. + +## Not yet at full ChatGPT parity +- Live, interruptible real-time voice conversations. +- Gmail inbox access, drafting and sending. +- General-purpose GitHub issue/PR management and editing arbitrary existing repositories. +- Full DOCX/XLSX extraction, video understanding/generation and long-form audio transcription. +- A secure interactive code execution/data-analysis sandbox. +- Background scheduling and unattended task execution. +- Broad third-party integrations and automatic tool selection across every provider. + +## Execution policy +- Read-only operations may run when the connected account grants access. +- Repository creation and deployment require explicit approval. +- The gateway verifies the signed-in user and checks the approved action payload before performing side effects. +- Provider keys remain in server-side secrets; never place them in browser variables, database rows, logs or Git history. +- A provider marked needs setup must never be represented as connected. +- Actions are recorded in the audit log. + +## Configuration guide +See [Provider setup](PROVIDER-SETUP.md) and [.env.example](../.env.example). A key being present does not imply that OAuth consent, API permissions, billing or provider-side configuration is complete. diff --git a/docs/PROVIDER-SETUP.md b/docs/PROVIDER-SETUP.md new file mode 100644 index 0000000..e160456 --- /dev/null +++ b/docs/PROVIDER-SETUP.md @@ -0,0 +1,53 @@ +# JARVIS X provider setup + +Do not paste secrets into the chat, source code, issue comments or frontend environment variables. + +## Frontend host variables +- VITE_SUPABASE_URL — project URL. +- VITE_SUPABASE_PUBLISHABLE_KEY — publishable key only. Never use the secret/service-role key in VITE_ variables. +- VITE_CHAT_FUNCTION_URL — jarvis-chat function URL. + +## Supabase Edge Function Secrets +Add these under Supabase Dashboard → Edge Functions → Secrets: +- GEMINI_API_KEY — required for chat and agent planning. +- TAVILY_API_KEY — Tavily web search key used by the Research workspace. +- Image generation uses GEMINI_API_KEY with Gemini 3.1 Flash Image; the image model may require billing or model access in the Google AI project. +- APP_ORIGIN — exact deployed frontend origin for CORS, e.g. https://app.example.com. +- JARVIS_GATEWAY_URL — deployed Cloudflare Worker gateway URL used by the agent after approval. + +## Frontend build variables +Add these as GitHub Actions secrets for the Cloudflare Pages build: VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY, and VITE_CHAT_FUNCTION_URL. These are injected at build time; the publishable key is designed for browser use, but never use a Supabase service-role or secret key in a VITE_ variable. + +## Cloudflare Worker and GitHub Actions secrets +The Worker deployment workflow reads these GitHub repository Actions secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, JARVIS_OWNER_EMAILS, APP_ORIGIN, GITHUB_TOKEN_JARVIS, GITHUB_OWNER, and GITHUB_REPO. Set GITHUB_OWNER=pycgamingstudio-sys and GITHUB_REPO=Game for the gateway workflow host. The Worker only accepts users whose verified email is listed in JARVIS_OWNER_EMAILS (comma-separated). The Pages deployment workflow also requires CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, GITHUB_TOKEN_JARVIS for private source checkout, and the three VITE_ frontend build variables above. Give the GitHub token only the permissions required to create repositories, write repository contents, read metadata, and dispatch Actions. A GitHub App is preferred where possible; verify repository-creation support before enabling it. The Cloudflare token should be scoped to Pages: Edit and Workers Scripts: Edit on the intended account. + +## Dashboard configuration (not Edge Function secrets) +- Supabase Auth → Providers → Google: configure OAuth Client ID and Client Secret. +- Google Cloud Console: add the Supabase OAuth callback URL to authorized redirect URIs. +- Supabase project URL and publishable key are frontend build-time environment variables. +- Enable the Data API for the app and verify the public schema is exposed if the frontend uses supabase-js table queries. The SQL migrations grant only authenticated access and enable owner-scoped RLS on each application table. +- Enable leaked-password protection in Supabase Auth security settings (the current Supabase security advisor flags this as disabled). +- Configure allowed redirect URLs for local development and the deployed app. + +## Private account allowlist +The database starts with no allowed emails, so no Google account can access JARVIS data until you add your approved accounts. In Supabase SQL Editor, run this after replacing the examples with your own Gmail addresses: + +```sql +insert into public.jarvis_allowed_users(email) values + ('primary@gmail.com'), + ('second-account@gmail.com') +on conflict (email) do update set enabled = true; +``` + +Add the same comma-separated addresses to the GitHub Actions secret JARVIS_OWNER_EMAILS. The app checks the database allowlist at sign-in, and the Cloudflare gateway separately checks JARVIS_OWNER_EMAILS before GitHub or deployment actions. + +## OAuth permissions +Start with Google sign-in only. Gmail access must be separately consented and should request read-only scopes first. Draft/send permissions must be added only when those features are implemented and must always use an explicit send approval. + +## Before going live +1. Confirm every provider key is stored server-side. +2. Test a signed-in user's access and confirm another account cannot read or modify their rows or files. +3. Test chat and agent planning with the real provider key. +4. Test provider permissions with a restricted GitHub repository and a non-production Cloudflare Pages project. +5. Review Supabase Security and Performance Advisors. +6. Configure rate limits, usage alerts and provider spend limits. diff --git a/index.html b/index.html index f303eeb..207e951 100644 --- a/index.html +++ b/index.html @@ -3,56 +3,56 @@ - - - NEON HORIZON — Night Drive - + + + JARVIS X — Personal Intelligence - + + -
-
-
-
- ◈NEON HORIZONNIGHT DRIVE SIMULATOR -
-
-
-