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

Viewing files at commit 3dc3c2c6da63Commit detailsView latest

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