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

Viewing files at commit 9c0e6fd29feaCommit detailsView latest

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