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

Viewing files at commit a93dad91899fCommit detailsView latest

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