The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit 791506c0dbc6Commit detailsView latest

apps/dashboard/src/main.tsx 1029 lines · 32.6 KB · tsx
1
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
import { createRoot } from 'react-dom/client';
3
import './styles.css';
4
5
declare const __AUTH0_DOMAIN__: string;
6
declare const __AUTH0_CLIENT_ID__: string;
7
declare const __AUTH0_AUDIENCE__: string;
8
9
type Auth0User = { sub: string; name?: string; email?: string; picture?: string };
10
type AuthSession = { user: Auth0User };
11
type AuthTransaction = { state: string; nonce: string; verifier: string };
12
13
const authTransactionKey = 'openclasp.auth0.transaction';
14
15
type DashboardData = {
16
  agents: Record<string, any>[];
17
  projects: Record<string, any>[];
18
  installations: Record<string, any>[];
19
  setupRequests: Record<string, any>[];
20
  publications: Record<string, any>[];
21
  interactions: Record<string, any>[];
22
  events: Record<string, any>[];
23
  conflicts: Record<string, any>[];
24
  receipts: Record<string, any>[];
25
  profiles: Record<string, any>[];
26
};
27
28
type Settings = {
29
  displayName: string;
30
  contributionEnabled: boolean;
31
  retentionDays: number;
32
  evidenceSharing: 'never' | 'ask' | 'contract_only';
33
  rawConversationsStored: false;
34
};
35
36
const emptyData: DashboardData = {
37
  agents: [],
38
  projects: [],
39
  installations: [],
40
  setupRequests: [],
41
  publications: [],
42
  interactions: [],
43
  events: [],
44
  conflicts: [],
45
  receipts: [],
46
  profiles: [],
47
};
48
const defaultSettings: Settings = {
49
  displayName: '',
50
  contributionEnabled: false,
51
  retentionDays: 30,
52
  evidenceSharing: 'ask',
53
  rawConversationsStored: false,
54
};
55
const pages = ['dashboard', 'history', 'agents', 'insights', 'connect', 'settings'] as const;
56
type Page = (typeof pages)[number];
57
58
function base64Url(bytes: Uint8Array) {
59
  let binary = '';
60
  for (const byte of bytes) binary += String.fromCharCode(byte);
61
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
62
}
63
64
function randomValue(size = 32) {
65
  return base64Url(crypto.getRandomValues(new Uint8Array(size)));
66
}
67
68
async function beginAuth(provider: 'google' | 'github') {
69
  const verifier = randomValue(48);
70
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
71
  const transaction: AuthTransaction = { state: randomValue(), nonce: randomValue(), verifier };
72
  sessionStorage.setItem(authTransactionKey, JSON.stringify(transaction));
73
  const parameters = new URLSearchParams({
74
    client_id: __AUTH0_CLIENT_ID__,
75
    redirect_uri: `${location.origin}/sso-callback`,
76
    response_type: 'code',
77
    scope: 'openid profile email',
78
    audience: __AUTH0_AUDIENCE__,
79
    state: transaction.state,
80
    nonce: transaction.nonce,
81
    code_challenge: base64Url(new Uint8Array(digest)),
82
    code_challenge_method: 'S256',
83
    connection: provider === 'google' ? 'google-oauth2' : 'github',
84
  });
85
  location.assign(`https://${__AUTH0_DOMAIN__}/authorize?${parameters}`);
86
}
87
88
async function signOut() {
89
  await fetch('/api/session', { method: 'DELETE', credentials: 'same-origin' });
90
  const parameters = new URLSearchParams({
91
    client_id: __AUTH0_CLIENT_ID__,
92
    returnTo: `${location.origin}/login`,
93
  });
94
  location.assign(`https://${__AUTH0_DOMAIN__}/v2/logout?${parameters}`);
95
}
96
97
async function api(path: string, init?: RequestInit) {
98
  const headers = new Headers(init?.headers);
99
  if (init?.body && !headers.has('content-type')) headers.set('content-type', 'application/json');
100
  return fetch(path, {
101
    ...init,
102
    credentials: 'same-origin',
103
    headers,
104
  }).then(async (response) => {
105
    if (!response.ok)
106
      throw new Error((await response.text()) || `Request failed: ${response.status}`);
107
    return response.json();
108
  });
109
}
110
111
function route(): Page {
112
  const value = location.pathname.slice(1) || 'dashboard';
113
  return pages.includes(value as Page) ? (value as Page) : 'dashboard';
114
}
115
116
function App() {
117
  const [session, setSession] = useState<AuthSession | null>();
118
  const [page, setPage] = useState<Page>(route());
119
  const [data, setData] = useState<DashboardData>(emptyData);
120
  const [settings, setSettings] = useState<Settings>(defaultSettings);
121
  const [loading, setLoading] = useState(true);
122
  const [error, setError] = useState('');
123
  const refreshDashboard = useCallback(async () => {
124
    setData((await api('/v0.1/dashboard')) as DashboardData);
125
  }, []);
126
127
  useEffect(() => {
128
    fetch('/api/session', { credentials: 'same-origin' })
129
      .then(async (response) => {
130
        if (!response.ok) return null;
131
        return (await response.json()) as AuthSession;
132
      })
133
      .then(setSession)
134
      .catch(() => setSession(null));
135
  }, []);
136
137
  useEffect(() => {
138
    const onPopState = () => setPage(route());
139
    addEventListener('popstate', onPopState);
140
    return () => removeEventListener('popstate', onPopState);
141
  }, []);
142
143
  useEffect(() => {
144
    if (session === undefined) return;
145
    if (!session && location.pathname !== '/login') history.replaceState({}, '', '/login');
146
    if (!session) return;
147
    if (location.pathname === '/login') history.replaceState({}, '', '/dashboard');
148
    setPage(route());
149
    Promise.all([api('/v0.1/dashboard'), api('/v0.1/settings')])
150
      .then(([dashboard, accountSettings]) => {
151
        setData(dashboard as DashboardData);
152
        setSettings(accountSettings as Settings);
153
      })
154
      .catch((reason: unknown) =>
155
        setError(reason instanceof Error ? reason.message : 'Load failed'),
156
      )
157
      .finally(() => setLoading(false));
158
  }, [session]);
159
160
  const navigate = (next: Page) => {
161
    history.pushState({}, '', `/${next}`);
162
    setPage(next);
163
  };
164
165
  if (session === undefined) return <Loading />;
166
  if (!session) return <Login />;
167
168
  return (
169
    <div className="appShell">
170
      <aside>
171
        <div className="brand">
172
          <div className="mark">OC</div>
173
          <div>
174
            <strong>OpenClasp</strong>
175
            <small>ASSURANCE NETWORK</small>
176
          </div>
177
        </div>
178
        <nav>
179
          <Nav page="dashboard" active={page} onClick={navigate} label="Overview" glyph="⌂" />
180
          <Nav page="history" active={page} onClick={navigate} label="History" glyph="≡" />
181
          <Nav page="agents" active={page} onClick={navigate} label="Agents" glyph="◇" />
182
          <Nav page="insights" active={page} onClick={navigate} label="Insights" glyph="⌁" />
183
          <Nav page="connect" active={page} onClick={navigate} label="Connect" glyph="+" />
184
          <Nav page="settings" active={page} onClick={navigate} label="Settings" glyph="⚙" />
185
        </nav>
186
        <div className="privacyStamp">
187
          <span className="liveDot" />
188
          <div>
189
            <strong>Structured-only</strong>
190
            <small>Raw conversations stay local</small>
191
          </div>
192
        </div>
193
        <button className="account" onClick={() => void signOut()}>
194
          <span>{initials(session.user.name || session.user.email || 'OC')}</span>
195
          <div>
196
            <strong>{session.user.name || 'OpenClasp user'}</strong>
197
            <small>{session.user.email || 'Sign out'}</small>
198
          </div>
199
          <b>↗</b>
200
        </button>
201
      </aside>
202
      <main>
203
        {error && <div className="errorBar">{error}</div>}
204
        {loading ? (
205
          <Loading compact />
206
        ) : (
207
          <PageContent
208
            page={page}
209
            data={data}
210
            settings={settings}
211
            setSettings={setSettings}
212
            navigate={navigate}
213
            refreshDashboard={refreshDashboard}
214
          />
215
        )}
216
      </main>
217
    </div>
218
  );
219
}
220
221
function Login() {
222
  const [error, setError] = useState('');
223
  const continueWith = async (provider: 'google' | 'github') => {
224
    setError('');
225
    try {
226
      await beginAuth(provider);
227
    } catch {
228
      setError(`${provider === 'google' ? 'Google' : 'GitHub'} sign-in is not configured yet.`);
229
    }
230
  };
231
  return (
232
    <div className="loginPage">
233
      <section className="loginPitch">
234
        <div className="brand">
235
          <div className="mark">OC</div>
236
          <strong>OpenClasp</strong>
237
        </div>
238
        <div>
239
          <p className="eyebrow">AGENT-TO-AGENT ASSURANCE</p>
240
          <h1>Know who your agent is dealing with.</h1>
241
          <p>
242
            Verified identities, signed outcomes, private warnings, and contextual
243
            reliability—without collecting raw conversations.
244
          </p>
245
        </div>
246
        <div className="loginProof">
247
          <span>01</span> User-owned history<span>02</span> No universal trust score<span>03</span>{' '}
248
          Explicit network consent
249
        </div>
250
      </section>
251
      <section className="loginCard">
252
        <div>
253
          <p className="eyebrow">SECURE ACCESS</p>
254
          <h2>Sign in to OpenClasp</h2>
255
          <p>
256
            Manage connected agents, review signed history, and control what contributes to the
257
            network.
258
          </p>
259
        </div>
260
        <div className="socialButtons">
261
          <button onClick={() => void continueWith('google')}>
262
            <span className="google">G</span> Continue with Google
263
          </button>
264
          <button onClick={() => void continueWith('github')}>
265
            <span>◉</span> Continue with GitHub
266
          </button>
267
        </div>
268
        {error && <div className="loginError">{error}</div>}
269
        <small>
270
          Google and GitHub handle authentication. OpenClasp never receives your password.
271
        </small>
272
      </section>
273
    </div>
274
  );
275
}
276
277
let callbackStarted = false;
278
function AuthCallback() {
279
  const [error, setError] = useState('');
280
  useEffect(() => {
281
    if (callbackStarted) return;
282
    callbackStarted = true;
283
    const finish = async () => {
284
      const query = new URLSearchParams(location.search);
285
      const oauthError = query.get('error_description') ?? query.get('error');
286
      if (oauthError) throw new Error(oauthError);
287
      const code = query.get('code');
288
      const returnedState = query.get('state');
289
      const rawTransaction = sessionStorage.getItem(authTransactionKey);
290
      if (!code || !returnedState || !rawTransaction) throw new Error('Missing OAuth transaction');
291
      const transaction = JSON.parse(rawTransaction) as AuthTransaction;
292
      sessionStorage.removeItem(authTransactionKey);
293
      if (returnedState !== transaction.state) throw new Error('OAuth state mismatch');
294
      const response = await fetch('/api/session', {
295
        method: 'POST',
296
        credentials: 'same-origin',
297
        headers: { 'content-type': 'application/json' },
298
        body: JSON.stringify({
299
          code,
300
          codeVerifier: transaction.verifier,
301
        }),
302
      });
303
      if (!response.ok) throw new Error('Auth0 token exchange failed');
304
      location.replace('/dashboard');
305
    };
306
    void finish().catch((reason: unknown) =>
307
      setError(reason instanceof Error ? reason.message : 'Authentication failed'),
308
    );
309
  }, []);
310
  if (error)
311
    return (
312
      <div className="loading">
313
        <span className="mark">OC</span>
314
        <p>{error}</p>
315
        <a href="/login">Return to login</a>
316
      </div>
317
    );
318
  return <Loading />;
319
}
320
321
function PageContent({
322
  page,
323
  data,
324
  settings,
325
  setSettings,
326
  navigate,
327
  refreshDashboard,
328
}: {
329
  page: Page;
330
  data: DashboardData;
331
  settings: Settings;
332
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
333
  navigate: (page: Page) => void;
334
  refreshDashboard: () => Promise<void>;
335
}) {
336
  if (page === 'history') return <History data={data} />;
337
  if (page === 'agents')
338
    return <Agents data={data} navigate={navigate} refreshDashboard={refreshDashboard} />;
339
  if (page === 'insights') return <Insights data={data} />;
340
  if (page === 'connect') return <Connect data={data} refreshDashboard={refreshDashboard} />;
341
  if (page === 'settings') return <SettingsPage settings={settings} setSettings={setSettings} />;
342
  return <Overview data={data} navigate={navigate} />;
343
}
344
345
function Overview({ data, navigate }: { data: DashboardData; navigate: (page: Page) => void }) {
346
  const completed = data.receipts.filter((item) => item.outcome === 'success').length;
347
  const warnings = data.events.filter((item) =>
348
    ['policy_warning', 'policy_violation', 'objection'].includes(String(item.eventType)),
349
  ).length;
350
  return (
351
    <>
352
      <PageHead
353
        eyebrow="NETWORK OVERVIEW"
354
        title="Assurance, at a glance."
355
        action="Connect agent"
356
        onAction={() => navigate('connect')}
357
      />
358
      <section className="metrics">
359
        <Metric label="Connected agents" value={data.agents.length} note="bound identities" />
360
        <Metric label="Interactions" value={data.interactions.length} note="signed or active" />
361
        <Metric label="Successful outcomes" value={completed} note="receipt-backed" />
362
        <Metric label="Open warnings" value={warnings} note="needs attention" warn={warnings > 0} />
363
      </section>
364
      <section className="contentGrid">
365
        <Panel title="Recent activity" subtitle="Structured events and signed outcomes">
366
          <Timeline events={data.events.slice(-6).reverse()} />
367
          <TextButton onClick={() => navigate('history')}>View complete history →</TextButton>
368
        </Panel>
369
        <Panel
370
          title="Reliability context"
371
          subtitle="Task-specific profiles, never a universal score"
372
        >
373
          {data.profiles.length ? (
374
            data.profiles
375
              .slice(0, 4)
376
              .map((profile) => (
377
                <Profile key={`${profile.agentId}-${profile.taskCategory}`} profile={profile} />
378
              ))
379
          ) : (
380
            <Empty
381
              title="No reliability profile yet"
382
              text="Profiles appear after signed receipts and eligible bilateral feedback."
383
            />
384
          )}
385
        </Panel>
386
      </section>
387
    </>
388
  );
389
}
390
391
function History({ data }: { data: DashboardData }) {
392
  const records = useMemo(
393
    () =>
394
      [
395
        ...data.interactions.map((value) => ({ ...value, _kind: 'interaction' })),
396
        ...data.events.map((value) => ({ ...value, _kind: 'event' })),
397
        ...data.receipts.map((value) => ({ ...value, _kind: 'receipt' })),
398
        ...data.conflicts.map((value) => ({ ...value, _kind: 'dispute' })),
399
      ].sort((a, b) => Date.parse(timestamp(b)) - Date.parse(timestamp(a))),
400
    [data],
401
  );
402
  return (
403
    <>
404
      <PageHead eyebrow="AUDIT HISTORY" title="What happened, and what proves it." />
405
      <Panel
406
        title="Account history"
407
        subtitle="Only structured metadata, signatures, hashes, and permitted evidence are hosted"
408
      >
409
        {records.length ? (
410
          records.map((item, index) => (
411
            <HistoryRow
412
              key={String(
413
                (item as Record<string, any>).eventId ??
414
                  (item as Record<string, any>).receiptId ??
415
                  (item as Record<string, any>).interactionId ??
416
                  index,
417
              )}
418
              item={item}
419
            />
420
          ))
421
        ) : (
422
          <Empty
423
            title="No interactions recorded"
424
            text="Connect an agent and start an assured interaction. Raw message bodies will not appear here."
425
          />
426
        )}
427
      </Panel>
428
    </>
429
  );
430
}
431
432
function Agents({
433
  data,
434
  navigate,
435
  refreshDashboard,
436
}: {
437
  data: DashboardData;
438
  navigate: (page: Page) => void;
439
  refreshDashboard: () => Promise<void>;
440
}) {
441
  const [working, setWorking] = useState('');
442
  const [error, setError] = useState('');
443
  const setPublication = async (agentId: string, published: boolean) => {
444
    setWorking(agentId);
445
    setError('');
446
    try {
447
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/publication`, {
448
        method: 'POST',
449
        body: JSON.stringify({ published }),
450
      });
451
      await refreshDashboard();
452
    } catch (reason) {
453
      setError(reason instanceof Error ? reason.message : 'Directory update failed');
454
    } finally {
455
      setWorking('');
456
    }
457
  };
458
  return (
459
    <>
460
      <PageHead
461
        eyebrow="IDENTITY REGISTRY"
462
        title="Your connected agents."
463
        action="Connect agent"
464
        onAction={() => navigate('connect')}
465
      />
466
      <div className="notice">
467
        <strong>Private by default.</strong> Publishing shares only this agent's name, framework,
468
        capabilities, limitations, and assurance method. It never shares your identity, project,
469
        scores, or conversations.
470
      </div>
471
      {error && <div className="errorBar">{error}</div>}
472
      <section className="agentGrid">
473
        {data.agents.length ? (
474
          data.agents.map((agent) => (
475
            <AgentCard
476
              key={agent.agentId}
477
              agent={agent}
478
              projectName={
479
                data.projects.find((project) => project.projectId === agent.projectId)?.name
480
              }
481
              published={data.publications.some(
482
                (publication) => publication.agentId === agent.agentId && publication.published,
483
              )}
484
              working={working === agent.agentId}
485
              onPublication={(published) => setPublication(agent.agentId, published)}
486
            />
487
          ))
488
        ) : (
489
          <Empty
490
            title="No agents connected"
491
            text="Install the remote MCP endpoint in an agent, sign in, then create and register its identity."
492
            action="Open connection guide"
493
            onAction={() => navigate('connect')}
494
          />
495
        )}
496
      </section>
497
    </>
498
  );
499
}
500
501
function Insights({ data }: { data: DashboardData }) {
502
  return (
503
    <>
504
      <PageHead eyebrow="BEHAVIOURAL CONTEXT" title="Evidence, not reputation theatre." />
505
      <div className="notice">
506
        <strong>Context matters.</strong> OpenClasp profiles an agent by task category and version.
507
        It does not produce a single universal trust score.
508
      </div>
509
      <section className="agentGrid">
510
        {data.profiles.length ? (
511
          data.profiles.map((profile) => (
512
            <Panel
513
              key={`${profile.agentId}-${profile.taskCategory}`}
514
              title={profile.agentId}
515
              subtitle={`${profile.taskCategory} · v${profile.agentVersion}`}
516
            >
517
              <Profile profile={profile} expanded />
518
            </Panel>
519
          ))
520
        ) : (
521
          <Empty
522
            title="Not enough verified evidence"
523
            text="Insights grow only from signed outcomes, eligible feedback, corrections, disputes, and version-aware history."
524
          />
525
        )}
526
      </section>
527
    </>
528
  );
529
}
530
531
function Connect({
532
  data,
533
  refreshDashboard,
534
}: {
535
  data: DashboardData;
536
  refreshDashboard: () => Promise<void>;
537
}) {
538
  const endpoint = 'https://openclasp.vercel.app/mcp';
539
  const [copied, setCopied] = useState(false);
540
  const [working, setWorking] = useState('');
541
  const [decisionError, setDecisionError] = useState('');
542
  const pending = data.setupRequests.filter((request) => request.status === 'pending');
543
  useEffect(() => {
544
    const poll = () => void refreshDashboard().catch(() => undefined);
545
    poll();
546
    const timer = window.setInterval(poll, 5000);
547
    return () => window.clearInterval(timer);
548
  }, [refreshDashboard]);
549
  const decide = async (requestId: string, decision: 'approve' | 'reject') => {
550
    setWorking(requestId);
551
    setDecisionError('');
552
    try {
553
      await api(`/v0.1/onboarding/${encodeURIComponent(requestId)}/${decision}`, {
554
        method: 'POST',
555
      });
556
      await refreshDashboard();
557
    } catch (reason) {
558
      setDecisionError(reason instanceof Error ? reason.message : 'Could not update setup request');
559
    } finally {
560
      setWorking('');
561
    }
562
  };
563
  return (
564
    <>
565
      <PageHead eyebrow="AGENT CONNECTION" title="Add OpenClasp to an agent." />
566
      {pending.length > 0 && (
567
        <section className="setupRequests">
568
          <div>
569
            <p className="eyebrow">CONFIRMATION REQUIRED</p>
570
            <h2>Approve agent setup</h2>
571
            <p>
572
              An agent proposed this identity. Confirm it before OpenClasp binds the installation.
573
            </p>
574
          </div>
575
          {pending.map((request) => (
576
            <article className="setupRequest" key={request.requestId}>
577
              <div>
578
                <strong>{request.agentName ?? 'Switch installation'}</strong>
579
                <small>
580
                  {request.projectName ?? request.existingAgentId} · {request.framework}
581
                </small>
582
                {!!request.capabilities?.length && (
583
                  <div className="tags">
584
                    {request.capabilities.slice(0, 5).map((capability: string) => (
585
                      <span key={capability}>{capability}</span>
586
                    ))}
587
                  </div>
588
                )}
589
                {!!request.limitations?.length && (
590
                  <small>Limitations: {request.limitations.join(' · ')}</small>
591
                )}
592
              </div>
593
              <div className="decisionButtons">
594
                <button
595
                  className="secondary"
596
                  disabled={working === request.requestId}
597
                  onClick={() => void decide(request.requestId, 'reject')}
598
                >
599
                  Reject
600
                </button>
601
                <button
602
                  className="primary"
603
                  disabled={working === request.requestId}
604
                  onClick={() => void decide(request.requestId, 'approve')}
605
                >
606
                  {working === request.requestId ? 'Working…' : 'Confirm agent'}
607
                </button>
608
              </div>
609
            </article>
610
          ))}
611
          {decisionError && <div className="loginError">{decisionError}</div>}
612
        </section>
613
      )}
614
      <section className="connectLayout">
615
        <Panel title="Remote MCP endpoint" subtitle="For OAuth-capable MCP clients">
616
          <div className="endpoint">
617
            <code>{endpoint}</code>
618
            <button
619
              onClick={() => {
620
                void navigator.clipboard.writeText(endpoint);
621
                setCopied(true);
622
              }}
623
            >
624
              {copied ? 'Copied' : 'Copy'}
625
            </button>
626
          </div>
627
          <ol>
628
            <li>Add the URL as a remote MCP server in your agent or framework.</li>
629
            <li>The client discovers OAuth and opens the Auth0 login page.</li>
630
            <li>Sign in with this account and approve access.</li>
631
            <li>
632
              Ask the agent to call <code>openclasp_setup</code>.
633
            </li>
634
            <li>
635
              Confirm the proposed identity on this page. The installation stays bound afterward.
636
            </li>
637
          </ol>
638
        </Panel>
639
        <Panel
640
          title="What the agent receives"
641
          subtitle="Assurance tools, not a replacement transport"
642
        >
643
          <ul className="checkList">
644
            <li>Identity and delegation verification</li>
645
            <li>Counterparty-specific reliability clues</li>
646
            <li>Signed contracts, events, feedback, and receipts</li>
647
            <li>Policy challenges and consented mediation</li>
648
            <li>No hidden rewriting or raw-message upload</li>
649
          </ul>
650
        </Panel>
651
      </section>
652
    </>
653
  );
654
}
655
656
function SettingsPage({
657
  settings,
658
  setSettings,
659
}: {
660
  settings: Settings;
661
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
662
}) {
663
  const [saving, setSaving] = useState(false);
664
  const [saved, setSaved] = useState(false);
665
  const save = useCallback(async () => {
666
    setSaving(true);
667
    setSaved(false);
668
    const updated = (await api('/v0.1/settings', {
669
      method: 'PUT',
670
      body: JSON.stringify(settings),
671
    })) as Settings;
672
    setSettings(updated);
673
    setSaving(false);
674
    setSaved(true);
675
  }, [settings, setSettings]);
676
  return (
677
    <>
678
      <PageHead eyebrow="ACCOUNT CONTROLS" title="Privacy and network settings." />
679
      <section className="settingsCard">
680
        <Setting label="Display name" description="Shown only inside your OpenClasp account.">
681
          <input
682
            value={settings.displayName}
683
            onChange={(event) =>
684
              setSettings((value) => ({ ...value, displayName: event.target.value }))
685
            }
686
            placeholder="Team or operator name"
687
          />
688
        </Setting>
689
        <Setting
690
          label="Network contribution"
691
          description="Allow eligible structured aggregates to improve contextual intelligence. Raw conversations are never included."
692
        >
693
          <Toggle
694
            checked={settings.contributionEnabled}
695
            onChange={(checked) =>
696
              setSettings((value) => ({ ...value, contributionEnabled: checked }))
697
            }
698
          />
699
        </Setting>
700
        <Setting
701
          label="Evidence sharing"
702
          description="Controls when permitted evidence references may be shared with interaction participants."
703
        >
704
          <select
705
            value={settings.evidenceSharing}
706
            onChange={(event) =>
707
              setSettings((value) => ({
708
                ...value,
709
                evidenceSharing: event.target.value as Settings['evidenceSharing'],
710
              }))
711
            }
712
          >
713
            <option value="never">Never</option>
714
            <option value="ask">Ask every time</option>
715
            <option value="contract_only">When contract permits</option>
716
          </select>
717
        </Setting>
718
        <Setting
719
          label="Structured record retention"
720
          description="Retention for hosted account records. Signed protocol records may require separate revocation markers."
721
        >
722
          <select
723
            value={settings.retentionDays}
724
            onChange={(event) =>
725
              setSettings((value) => ({ ...value, retentionDays: Number(event.target.value) }))
726
            }
727
          >
728
            <option value={7}>7 days</option>
729
            <option value={30}>30 days</option>
730
            <option value={90}>90 days</option>
731
            <option value={365}>1 year</option>
732
            <option value={0}>No automatic deletion</option>
733
          </select>
734
        </Setting>
735
        <Setting
736
          label="Raw conversations"
737
          description="Message bodies remain local and user-owned. This cannot be enabled on the hosted service."
738
        >
739
          <span className="locked">ALWAYS OFF</span>
740
        </Setting>
741
        <div className="saveRow">
742
          <button className="primary" onClick={() => void save()} disabled={saving}>
743
            {saving ? 'Saving…' : 'Save settings'}
744
          </button>
745
          {saved && <span>Saved</span>}
746
        </div>
747
      </section>
748
    </>
749
  );
750
}
751
752
function PageHead({
753
  eyebrow,
754
  title,
755
  action,
756
  onAction,
757
}: {
758
  eyebrow: string;
759
  title: string;
760
  action?: string;
761
  onAction?: () => void;
762
}) {
763
  return (
764
    <header className="pageHead">
765
      <div>
766
        <p className="eyebrow">{eyebrow}</p>
767
        <h1>{title}</h1>
768
      </div>
769
      {action && (
770
        <button className="primary" onClick={onAction}>
771
          {action}
772
        </button>
773
      )}
774
    </header>
775
  );
776
}
777
function Nav({
778
  page,
779
  active,
780
  onClick,
781
  label,
782
  glyph,
783
}: {
784
  page: Page;
785
  active: Page;
786
  onClick: (page: Page) => void;
787
  label: string;
788
  glyph: string;
789
}) {
790
  return (
791
    <button className={active === page ? 'active' : ''} onClick={() => onClick(page)}>
792
      <span>{glyph}</span>
793
      {label}
794
    </button>
795
  );
796
}
797
function Metric({
798
  label,
799
  value,
800
  note,
801
  warn,
802
}: {
803
  label: string;
804
  value: number;
805
  note: string;
806
  warn?: boolean;
807
}) {
808
  return (
809
    <article className={warn ? 'metric warn' : 'metric'}>
810
      <span>{label}</span>
811
      <strong>{String(value).padStart(2, '0')}</strong>
812
      <small>{note}</small>
813
    </article>
814
  );
815
}
816
function Panel({
817
  title,
818
  subtitle,
819
  children,
820
}: React.PropsWithChildren<{ title: string; subtitle: string }>) {
821
  return (
822
    <section className="panel">
823
      <div className="panelHead">
824
        <div>
825
          <h2>{title}</h2>
826
          <p>{subtitle}</p>
827
        </div>
828
      </div>
829
      {children}
830
    </section>
831
  );
832
}
833
function Timeline({ events }: { events: Record<string, any>[] }) {
834
  return events.length ? (
835
    <div className="timeline">
836
      {events.map((event) => (
837
        <HistoryRow key={event.eventId} item={{ ...event, _kind: 'event' }} />
838
      ))}
839
    </div>
840
  ) : (
841
    <Empty
842
      title="Nothing recorded yet"
843
      text="Activity appears after your connected agents submit signed structured events."
844
    />
845
  );
846
}
847
function HistoryRow({ item }: { item: Record<string, any> }) {
848
  const kind = String(item.eventType ?? item._kind ?? 'record').replaceAll('_', ' ');
849
  const id = String(item.agentId ?? item.interactionId ?? item.receiptId ?? 'OpenClasp');
850
  return (
851
    <article className="historyRow">
852
      <span className={`eventDot ${item.eventType ?? item._kind}`} />
853
      <div>
854
        <strong>{kind}</strong>
855
        <small>
856
          {id} · {new Date(timestamp(item)).toLocaleString()}
857
        </small>
858
      </div>
859
      <b>{item.visibility ?? item.outcome ?? item.status ?? 'SIGNED'}</b>
860
    </article>
861
  );
862
}
863
function AgentCard({
864
  agent,
865
  projectName,
866
  published,
867
  working,
868
  onPublication,
869
}: {
870
  agent: Record<string, any>;
871
  projectName?: string;
872
  published: boolean;
873
  working: boolean;
874
  onPublication: (published: boolean) => void;
875
}) {
876
  const identityLabel = agent.revoked
877
    ? 'REVOKED'
878
    : agent.identityMode === 'oauth_installation'
879
      ? 'AUTHENTICATED'
880
      : 'VERIFIED';
881
  return (
882
    <article className="agentCard">
883
      <div className="agentTop">
884
        <span className="agentGlyph">◇</span>
885
        <b className={agent.revoked ? 'bad' : ''}>{identityLabel}</b>
886
      </div>
887
      <h2>{agent.name ?? agent.agentId}</h2>
888
      <p>{projectName ?? `Version ${agent.agentVersion ?? '1.0.0'}`}</p>
889
      <div className="tags">
890
        {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
891
          <span key={capability}>{capability}</span>
892
        ))}
893
      </div>
894
      <small>
895
        {agent.framework ? `${agent.framework} · ` : ''}
896
        {agent.identityMode === 'oauth_installation' ? 'OAuth-bound · ' : 'Ed25519 · '}Created{' '}
897
        {new Date(agent.createdAt).toLocaleDateString()}
898
      </small>
899
      <button
900
        className="secondary"
901
        disabled={working || agent.status === 'revoked'}
902
        onClick={() => onPublication(!published)}
903
      >
904
        {working ? 'Updating…' : published ? 'Remove from directory' : 'Publish to directory'}
905
      </button>
906
    </article>
907
  );
908
}
909
function Profile({ profile, expanded }: { profile: Record<string, any>; expanded?: boolean }) {
910
  const dimensions = [
911
    ['Completion', profile.completion],
912
    ['Scope', profile.scope],
913
    ['Evidence', profile.evidence],
914
    ...(expanded
915
      ? [
916
          ['Communication', profile.communication],
917
          ['Deadline', profile.deadline],
918
        ]
919
      : []),
920
  ] as [string, number][];
921
  return (
922
    <article className="profile">
923
      <div>
924
        <strong>{profile.agentId}</strong>
925
        <small>
926
          {profile.taskCategory} · {profile.sampleSize} outcome(s)
927
        </small>
928
      </div>
929
      {dimensions.map(([label, value]) => (
930
        <Meter key={label} label={label} value={value ?? 0} />
931
      ))}
932
    </article>
933
  );
934
}
935
function Meter({ label, value }: { label: string; value: number }) {
936
  return (
937
    <div className="meter">
938
      <span>{label}</span>
939
      <i>
940
        <b style={{ width: `${Math.round(value * 100)}%` }} />
941
      </i>
942
      <strong>{Math.round(value * 100)}%</strong>
943
    </div>
944
  );
945
}
946
function Empty({
947
  title,
948
  text,
949
  action,
950
  onAction,
951
}: {
952
  title: string;
953
  text: string;
954
  action?: string;
955
  onAction?: () => void;
956
}) {
957
  return (
958
    <div className="empty">
959
      <span>◇</span>
960
      <strong>{title}</strong>
961
      <p>{text}</p>
962
      {action && (
963
        <button className="secondary" onClick={onAction}>
964
          {action}
965
        </button>
966
      )}
967
    </div>
968
  );
969
}
970
function TextButton({ children, onClick }: React.PropsWithChildren<{ onClick: () => void }>) {
971
  return (
972
    <button className="textButton" onClick={onClick}>
973
      {children}
974
    </button>
975
  );
976
}
977
function Setting({
978
  label,
979
  description,
980
  children,
981
}: React.PropsWithChildren<{ label: string; description: string }>) {
982
  return (
983
    <div className="setting">
984
      <div>
985
        <strong>{label}</strong>
986
        <p>{description}</p>
987
      </div>
988
      <div>{children}</div>
989
    </div>
990
  );
991
}
992
function Toggle({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }) {
993
  return (
994
    <button
995
      className={checked ? 'toggle on' : 'toggle'}
996
      role="switch"
997
      aria-checked={checked}
998
      onClick={() => onChange(!checked)}
999
    >
1000
      <span />
1001
    </button>
1002
  );
1003
}
1004
function Loading({ compact }: { compact?: boolean }) {
1005
  return (
1006
    <div className={compact ? 'loading compact' : 'loading'}>
1007
      <span className="mark">OC</span>
1008
      <p>Verifying session…</p>
1009
    </div>
1010
  );
1011
}
1012
function initials(value: string) {
1013
  return value
1014
    .split(/\s+|@/)
1015
    .slice(0, 2)
1016
    .map((part) => part[0]?.toUpperCase())
1017
    .join('');
1018
}
1019
function timestamp(item: Record<string, any>) {
1020
  return String(item.timestamp ?? item.completedAt ?? item.createdAt ?? new Date(0).toISOString());
1021
}
1022
1023
if (!__AUTH0_DOMAIN__ || !__AUTH0_CLIENT_ID__ || !__AUTH0_AUDIENCE__)
1024
  throw new Error('Auth0 public configuration is incomplete');
1025
createRoot(document.getElementById('root')!).render(
1026
  <React.StrictMode>
1027
    {location.pathname === '/sso-callback' ? <AuthCallback /> : <App />}
1028
  </React.StrictMode>,
1029
);