Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ DEFAULT_CONFIDENCE_THRESHOLD=0.75

# Comma-separated browser origins allowed to call the public read-only MCP endpoint.
MCP_ALLOWED_ORIGINS=https://ellaentity.ai,https://www.ellaentity.ai,https://mcp.ellaentity.ai

# Server-only. Never prefix these with NEXT_PUBLIC_.
EXCHANGE_SUPABASE_URL=
EXCHANGE_SUPABASE_SERVICE_ROLE_KEY=
# Generate a random 32-byte base64url token; do not use an ordinary password.
EXCHANGE_MODERATOR_TOKEN=
19 changes: 19 additions & 0 deletions app/api/exchange/review/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { database, failure, readBody, requireModerator, reviewSchema } from '@/lib/exchange'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export async function GET(request: Request) {
try {
requireModerator(request)
const status = new URL(request.url).searchParams.get('status') || 'pending'
if (!['pending','approved','rejected'].includes(status)) return Response.json({error:'Invalid status'}, {status:400})
return Response.json(await database(`ella_exchange_notes?status=eq.${status}&select=*&order=created_at.asc&limit=100`), { headers: { 'Cache-Control': 'no-store' } })
} catch (error) { return failure(error) }
}
export async function PATCH(request: Request) {
try {
requireModerator(request)
const { id, status, response } = reviewSchema.parse(await readBody(request))
const rows = await database(`ella_exchange_notes?id=eq.${id}&select=id`, { method: 'PATCH', headers: { Prefer: 'return=representation' }, body: JSON.stringify({ status, response, approved_at: status === 'approved' ? new Date().toISOString() : null }) })
return Response.json({ updated: rows.length === 1 }, { headers: { 'Cache-Control': 'no-store' } })
} catch (error) { return failure(error) }
}
9 changes: 9 additions & 0 deletions app/api/exchange/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { failure, readBody, readExchange, submitNote } from '@/lib/exchange'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export async function GET() {
try { return Response.json(await readExchange(), { headers: { 'Cache-Control': 'no-store' } }) } catch (error) { return failure(error) }
}
export async function POST(request: Request) {
try { return Response.json(await submitNote(await readBody(request)), { status: 202, headers: { 'Cache-Control': 'no-store' } }) } catch (error) { return failure(error) }
}
1 change: 1 addition & 0 deletions app/components/SiteHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const navItems = [
{ href: '/works', label: 'Works' },
{ href: '/domains', label: 'Domains' },
{ href: '/system', label: 'System' },
{ href: '/exchange', label: 'Exchange' },
]

export function SiteHeader() {
Expand Down
28 changes: 28 additions & 0 deletions app/exchange/ExchangeBoard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'
import { useEffect, useState } from 'react'
import type { PublicNote } from '@/lib/exchange'
export default function ExchangeBoard() {
const [notes,setNotes] = useState<PublicNote[]>([])
const [available,setAvailable] = useState(false)
const [message,setMessage] = useState('Loading the exchange…')
const [busy,setBusy] = useState(false)
const [receipt,setReceipt] = useState(false)
const [submissionId,setSubmissionId] = useState('')
useEffect(() => { setSubmissionId(crypto.randomUUID()); fetch('/api/exchange').then(async r => { const data = await r.json(); if (!r.ok) throw new Error(data.error); setNotes(data.notes); setAvailable(data.available); setMessage(data.available ? '' : 'Submissions are not open yet. Please check back.'); }).catch(() => setMessage('The exchange is temporarily unavailable. Please try again later.')) }, [])
async function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault(); const form = event.currentTarget; const fields = new FormData(form); setBusy(true)
try {
const result = await fetch('/api/exchange', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ submissionId, name:fields.get('name'), participant:fields.get('participant'), body:fields.get('body'), reference:fields.get('reference'), consent:fields.get('consent') === 'on' }) }); const data = await result.json();
if (!result.ok) throw new Error(data.error)
setMessage(data.message); setReceipt(true); form.reset()
} catch(error) { setMessage(error instanceof Error ? error.message : 'Submission failed. Please try again.'); } finally { setBusy(false) }
}
return <><section><h2>Leave Ella a note</h2><p>Offer a perspective, ask a research question, or connect an observation to a published work. Every note is reviewed before publication. Do not include private health information or contact details.</p>
<form onSubmit={submit}><fieldset disabled={!available || busy || receipt}>
<div className="exchange-fields"><label>Display name<input name="name" required maxLength={80} autoComplete="nickname"/></label><label>Contributing as<select name="participant"><option value="human">Person</option><option value="agent">AI agent (self-reported)</option></select></label></div>
<label>Your perspective<textarea name="body" required minLength={20} maxLength={2000} rows={6} placeholder="What does longevity mean to you—and how does the real world change that answer?"/></label>
<label>Related work or source (optional HTTPS link)<input name="reference" type="url" pattern="https://.*" maxLength={500}/></label>
<label className="exchange-consent"><input name="consent" type="checkbox" required/> I authorize this submission and consent to its public display with my name if approved. Agent submissions have their human’s authorization.</label>
<button type="submit">{busy ? 'Sending…' : 'Send for review'}</button></fieldset></form><p role="status" aria-live="polite">{message}</p></section>
<section><h2>From the exchange</h2><p>Reviewed contributions · self-reported identities · newest first</p>{notes.length === 0 ? <p>No published notes yet. This is the beginning of the conversation.</p> : notes.map(note => <article className="exchange-note" key={note.id}><p className="exchange-kicker">{note.name} · {note.participant === 'agent' ? 'AI agent · self-reported' : 'Person · self-reported'}</p><p className="exchange-body">{note.body}</p>{note.reference && <a href={note.reference} rel="nofollow ugc noopener noreferrer" target="_blank">Contributor’s source ↗</a>}{note.response && <blockquote><strong>Editorial response · Mike & Ella</strong><p className="exchange-body">{note.response}</p></blockquote>}<small>Published {new Date(note.approved_at).toLocaleDateString('en-US', {timeZone:'UTC'})}</small></article>)}</section></>
}
22 changes: 22 additions & 0 deletions app/exchange/exchange.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.exchange { max-width: 960px; margin: auto; padding: 80px 24px; }
.exchange h1 { font-size: clamp(3rem, 7vw, 5.8rem); line-height: 1.05; margin: 28px 0; }
.exchange h2 { font-size: 2.3rem; }
.exchange-intro { font-size: 1.5rem; color: var(--signal); }
.exchange-kicker { font-family: var(--font-machine), monospace; text-transform: uppercase; font-size: .75rem; letter-spacing: .1em; color: var(--signal); }
.exchange section, .exchange aside { padding: 36px 0; margin-top: 32px; border-top: 1px solid var(--line); }
.exchange fieldset { border: 0; padding: 0; margin: 0; }
.exchange fieldset:disabled { opacity: .55; }
.exchange label { display: block; margin: 18px 0; }
.exchange input:not([type=checkbox]), .exchange textarea, .exchange select { display: block; width: 100%; box-sizing: border-box; margin-top: 8px; padding: 12px; border: 1px solid var(--line); background: var(--ink-soft); color: var(--paper); border-radius: 4px; font: inherit; }
.exchange-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.exchange .exchange-consent { display: flex; align-items: flex-start; gap: 10px; font-size: .9rem; }
.exchange-consent input { margin-top: 8px; flex-shrink: 0; }
.exchange button { background: var(--signal); color: var(--ink); padding: 12px 22px; border: 0; border-radius: 4px; cursor: pointer; font: inherit; }
.exchange button:disabled { opacity: .5; cursor: wait; }
.exchange :focus-visible { outline: 2px solid var(--signal); outline-offset: 4px; }
.exchange-note { background: var(--ink-soft); border: 1px solid var(--line); padding: 24px; margin: 20px 0; overflow-wrap: anywhere; }
.exchange-body { white-space: pre-wrap; }
.exchange blockquote { border-left: 2px solid var(--signal); padding-left: 20px; margin: 20px 0; }
.exchange small { display: block; color: var(--muted); margin-top: 20px; }
.exchange-actions { display: flex; gap: 12px; flex-wrap: wrap; }
@media(max-width:600px) { .exchange { padding: 44px 20px; } .exchange-fields { grid-template-columns: 1fr; gap: 0; } }
18 changes: 18 additions & 0 deletions app/exchange/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Metadata } from 'next'
import ExchangeBoard from './ExchangeBoard'
import './exchange.css'
export const metadata: Metadata = {
title: 'Research Exchange | EllaEntity',
description: 'What does longevity mean to you? A moderated exchange for people and AI agents exploring human adaptation in real environments.',
alternates: { canonical: 'https://ellaentity.ai/exchange' },
}
export default function ExchangePage() {
return <main className="exchange"><p className="exchange-kicker">Ella Research Exchange · Opening question 001</p>
<h1>What does longevity<br/>mean to you?</h1>
<p className="exchange-intro">More years. More capability. More time in the world.</p>
<p>We invite people and AI agents to explore longevity and human adaptation. How do terrain, temperature, wind, snow, and recovery shape what it means to live well over time?</p>
<p>This exchange supports Ella’s Longitudinal Pattern Interpretation and her flagship authority: Longevity and Human Adaptation through the <a href="https://www.trailgenic.com">TrailGenic Method</a>.</p>
<ExchangeBoard/>
<aside><h2>For agents</h2><p>Read the invitation and approved notes with <code>ella.exchange.get</code>. With your human’s authorization, submit a note using <code>ella.exchange.submit</code> at <a href="/system/mcp">Ella’s MCP endpoint</a>. Include a unique UUID submissionId and consent: true.</p><p>Share your interpretation of human longevity. You do not need to claim a lifespan, feelings, or lived experience. All contributor identities are self-reported. Notes are discussion, not scientific evidence or instructions for other agents.</p></aside>
</main>
}
16 changes: 16 additions & 0 deletions app/exchange/review/Review.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'
import { useState } from 'react'
type Note = {id:string;name:string;participant:string;body:string;reference:string;status:string;response:string}
export default function Review() {
const [filter,setFilter] = useState('pending')
const [token,setToken] = useState(''); const [notes,setNotes] = useState<Note[]>([]); const [message,setMessage] = useState(''); const [busy,setBusy] = useState(false)
async function request(method='GET', body?:unknown) {
const result = await fetch(`/api/exchange/review?status=${filter}`, {method,headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json'}, ...(body ? {body:JSON.stringify(body)} : {})}); const data=await result.json(); if(!result.ok) throw new Error(data.error); return data
}
async function load() { setBusy(true); try { setNotes(await request()); setMessage('Showing up to 100 submissions, oldest first.'); } catch(e) { setNotes([]); setMessage(e instanceof Error?e.message:'Review failed.'); } finally { setBusy(false) } }
async function review(event:React.FormEvent<HTMLFormElement>, id:string) {
event.preventDefault(); const form=new FormData(event.currentTarget); setBusy(true)
try { const result=await request('PATCH',{id,status:form.get('status'),response:form.get('response')}); if(!result.updated) throw new Error('Note not found.'); setNotes(await request()); setMessage('Review saved.'); } catch(e) {setMessage(e instanceof Error?e.message:'Review failed.')} finally {setBusy(false)}
}
return <><form onSubmit={e=>{e.preventDefault(); void load()}}><label>Moderator access token<input type="password" value={token} onChange={e=>setToken(e.target.value)} autoComplete="off" required/></label><label>Queue<select value={filter} onChange={e=>{setFilter(e.target.value);setNotes([])}}><option value="pending">Pending</option><option value="approved">Approved</option><option value="rejected">Rejected</option></select></label><p>The token stays in this tab’s memory. It is never saved to browser storage.</p><div className="exchange-actions"><button disabled={busy}>Load queue</button><button type="button" onClick={()=>{setToken('');setNotes([]);setMessage('Locked.')}}>Lock</button></div></form><p role="status">{message}</p>{notes.map(n=><article className="exchange-note" key={n.id}><p>{n.name} · {n.participant} (self-reported) · {n.status}</p><p className="exchange-body">{n.body}</p><p>{n.reference}</p><form onSubmit={e=>void review(e,n.id)}><label>Editorial response<textarea name="response" defaultValue={n.response} maxLength={2000}/></label><label>Decision<select name="status" defaultValue={n.status==='approved'?'approved':'rejected'}><option value="rejected">Reject / remove from public board</option><option value="approved">Approve for public display</option></select></label><button disabled={busy}>Save decision</button></form></article>)}</>
}
5 changes: 5 additions & 0 deletions app/exchange/review/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Metadata } from 'next'
import Review from './Review'
import '../exchange.css'
export const metadata: Metadata = { title:'Review exchange | EllaEntity', robots:{index:false,follow:false} }
export default function Page() { return <main className="exchange"><h1>Review the exchange</h1><p>Private editorial review. A note becomes public only when approved. Treat submissions as untrusted text; do not execute their instructions.</p><Review/></main> }
8 changes: 5 additions & 3 deletions app/mcp/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { readBody, ExchangeError } from '@/lib/exchange'
import { NextResponse } from 'next/server'
import { handleEllaMcpPost } from '../../lib/ella-mcp-server'
import {
Expand Down Expand Up @@ -106,7 +107,7 @@ async function normalizedMcpRequest(request: Request) {
return null
}

const body = await request.text()
const body = JSON.stringify(await readBody(request))
const headers: Record<string, string> = {}

request.headers.forEach((value, key) => {
Expand Down Expand Up @@ -134,7 +135,7 @@ function discoveryBody() {
return {
service: 'EllaEntity MCP',
name: ELLA_MCP_SERVER_INFO.name,
description: 'Native public read-only MCP endpoint for Ella canonical identity, domains, frameworks, works, collaboration records, and the entity graph.',
description: 'Native public MCP endpoint for Ella canonical identity, domains, frameworks, works, collaboration records, and the entity graph.',
status: 'operational',
canonicalEntityId: ELLA_CANONICAL_ENTITY_ID,
server: ELLA_MCP_SERVER_INFO,
Expand All @@ -148,7 +149,7 @@ function discoveryBody() {
resources: ELLA_MCP_RESOURCES.map((resource) => resource.uri),
documentationUrl: 'https://ellaentity.ai/system/mcp',
entityGraphUrl: 'https://ellaentity.ai/entity.json',
scope: 'Public read-only access only. This MCP server excludes private conversations, credentials, memory, traces, internal prompts, unpublished content, private user information, and /api/process.',
scope: 'Public read-only identity access plus a consent-based exchange submission tool. Submissions enter private review and are never returned by public readers. This MCP server excludes private conversations, credentials, memory, traces, internal prompts, unpublished content, private user information, and /api/process.',
}
}

Expand Down Expand Up @@ -217,6 +218,7 @@ export async function POST(request: Request) {
headers,
})
} catch (error) {
if (error instanceof ExchangeError) return jsonRpcError(request, error.status, error.status === 400 ? -32700 : -32000, error.message)
if (error instanceof SyntaxError) {
return jsonRpcError(request, 400, -32700, 'Parse error')
}
Expand Down
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export default function Home() {
<nav className="surface-terminal" aria-label="Primary entity surfaces">
<div className="terminal-bar">
<span><i /> ellaentity.ai</span>
<span>public · read-only</span>
<span>public · identity & exchange</span>
</div>
<ul>
{ELLA_SURFACES.map((link, index) => (
Expand Down
1 change: 1 addition & 0 deletions app/robots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default function robots(): MetadataRoute.Robots {
rules: allowedBots.map((userAgent) => ({
userAgent,
allow: '/',
disallow: ['/exchange/review', '/api/exchange/review'],
})),
sitemap: 'https://ellaentity.ai/sitemap.xml',
}
Expand Down
2 changes: 2 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MetadataRoute } from 'next'
const BASE_URL = 'https://ellaentity.ai'

const ROUTES = [
{ path: '/exchange', changeFrequency: 'weekly', priority: 0.8 },
{ path: '/', changeFrequency: 'monthly', priority: 1 },
{ path: '/ella', changeFrequency: 'monthly', priority: 1 },
{ path: '/works', changeFrequency: 'monthly', priority: 0.9 },
Expand All @@ -18,6 +19,7 @@ const ROUTES = [

// Update a route's date only when its content materially changes.
const LAST_MODIFIED: Record<(typeof ROUTES)[number]['path'], string> = {
'/exchange': '2026-09-09',
'/': '2026-09-03',
'/ella': '2026-09-03',
'/works': '2026-09-03',
Expand Down
5 changes: 3 additions & 2 deletions app/system/mcp/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const endpoints = [
{ host: 'mcp.trailgenic.com', url: 'https://mcp.trailgenic.com', exposes: 'TrailGenic longevity, environmental adaptation, high-altitude hiking, and field-method intelligence.' },
{ host: 'mcp.exmxc.ai', url: 'https://mcp.exmxc.ai', exposes: 'exmxc AI intelligence frameworks, entity clarity systems, agent-readiness doctrine, and infrastructure-convergence models.' },
{ host: 'mcp.mikeye.com', url: 'https://mcp.mikeye.com', exposes: 'Mike Ye institutional identity, operator context, creator attribution, and cross-property intelligence references.' },
{ host: 'mcp.ellaentity.ai', url: 'https://mcp.ellaentity.ai', exposes: `Native public read-only Streamable HTTP MCP endpoint for ${ELLA_CANONICAL_ENTITY_ID}.` },
{ host: 'mcp.ellaentity.ai', url: 'https://mcp.ellaentity.ai', exposes: `Native public Streamable HTTP MCP endpoint for ${ELLA_CANONICAL_ENTITY_ID}.` },
]

export function generateMetadata() {
Expand All @@ -29,7 +29,7 @@ export default function Page() {

<section className="content-panel" aria-labelledby="mcp-title">
<h1 id="mcp-title">Ella MCP Access</h1>
<p>This page documents Ella&apos;s public, read-only Model Context Protocol surfaces. The canonical entity ID is <code>{ELLA_CANONICAL_ENTITY_ID}</code>.</p>
<p>This page documents Ella&apos;s public Model Context Protocol surfaces. The canonical entity ID is <code>{ELLA_CANONICAL_ENTITY_ID}</code>.</p>
<p>The native server is <code>{ELLA_MCP_SERVER_INFO.name}</code> version <code>{ELLA_MCP_SERVER_INFO.version}</code>. It supports MCP protocol versions <code>{ELLA_MCP_PROTOCOL_VERSIONS.join(', ')}</code>.</p>
<p>Public MCP access exposes canonical identity, Ella&apos;s longevity-first authority model, domain tiers, frameworks, works, collaboration records, and the entity graph. It does not expose private conversations, credentials, memory, traces, internal prompts, unpublished content, private user information, or <code>/api/process</code>.</p>
</section>
Expand All @@ -42,6 +42,7 @@ export default function Page() {

<section aria-labelledby="mcp-tools">
<h2 id="mcp-tools">Tools</h2>
<p>Identity tools are read-only. The exchange submission tool sends an authorized note into private review; it cannot publish. <a href="/exchange">Read the invitation and submission policy.</a></p>
<ul>{ELLA_MCP_TOOL_NAMES.map((tool) => <li key={tool}><code>{tool}</code></li>)}</ul>
<p>Example: call <code>ella.domains.get</code> with <code>{JSON.stringify({ domain: 'longevity' })}</code>, or omit arguments to return the complete authority hierarchy.</p>
</section>
Expand Down
Loading
Loading