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

Download ZIP
apps/dashboard/src/main.tsx 4292 lines · 155.9 KB · tsx
1
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
import { createRoot } from 'react-dom/client';
3
import {
4
  ArrowRight,
5
  Check,
6
  ChevronDown,
7
  Circle,
8
  Cloud,
9
  Copy,
10
  Download,
11
  ExternalLink,
12
  Moon,
13
  Search,
14
  Share2,
15
  ShieldCheck,
16
  Sun,
17
  X,
18
} from 'lucide-react';
19
import { AgentMark, FrameCorners } from '@/components/agent-mark';
20
import { AppShell } from '@/components/app-shell';
21
import { ClaspMark } from '@/components/clasp-mark';
22
import { FirstRunGuide } from '@/components/first-run-guide';
23
import { LandingBackdrop, LandingDiagram } from '@/components/landing-scene';
24
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog';
25
import { Input } from '@/components/ui/input';
26
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
27
import { pageMeta, pages, type Page } from '@/lib/navigation';
28
import {
29
  applyPreviewRequest,
30
  createPreviewData,
31
  defaultPreviewSettings,
32
  disablePreview,
33
  enablePreview,
34
  isPreviewActive,
35
  previewSession,
36
} from './preview.js';
37
import './styles.css';
38
39
declare const __AUTH0_DOMAIN__: string;
40
declare const __AUTH0_CLIENT_ID__: string;
41
declare const __AUTH0_AUDIENCE__: string;
42
43
type Auth0User = { sub: string; name?: string; email?: string; picture?: string };
44
type AuthSession = { user: Auth0User };
45
type AuthTransaction = { state: string; nonce: string; verifier: string; returnTo: string };
46
type Theme = 'dark' | 'light';
47
48
const authTransactionKey = 'openclasp.auth0.transaction';
49
const authReturnToKey = 'openclasp.auth0.return-to';
50
const themeKey = 'openclasp.theme.v1';
51
const landingCapabilities = [
52
  'verified agent identity',
53
  'discoverable agent cards',
54
  'structured agreements',
55
  'AI risk intelligence',
56
  'cross-platform coordination',
57
  'outcome history',
58
];
59
60
type DashboardData = {
61
  agents: Record<string, any>[];
62
  projects: Record<string, any>[];
63
  installations: Record<string, any>[];
64
  setupRequests: Record<string, any>[];
65
  publications: Record<string, any>[];
66
  interactions: Record<string, any>[];
67
  federatedInteractions: Record<string, any>[];
68
  liveSessions: Record<string, any>[];
69
  events: Record<string, any>[];
70
  conflicts: Record<string, any>[];
71
  receipts: Record<string, any>[];
72
  profiles: Record<string, any>[];
73
  counterpartyBriefs: Record<string, any>[];
74
  completionReports: Record<string, any>[];
75
  feedbackRequests: Record<string, any>[];
76
  interactionFeedback: Record<string, any>[];
77
  interactionConclusions: Record<string, any>[];
78
  learningEligibility: Record<string, any>[];
79
  profileDeltas: Record<string, any>[];
80
  intelligenceSummaries: Record<string, any>[];
81
  runtimes: Record<string, any>[];
82
  accessTokens: Record<string, any>[];
83
  assuranceAssessments: Record<string, any>[];
84
  assurancePredictions: Record<string, any>[];
85
  assuranceSafeguards: Record<string, any>[];
86
  assuranceEvaluations: Record<string, any>[];
87
  assuranceProbePlans: Record<string, any>[];
88
  assuranceProbeResponses: Record<string, any>[];
89
  shieldCases: Record<string, any>[];
90
  shieldConsultations: Record<string, any>[];
91
  shieldOutcomes: Record<string, any>[];
92
};
93
94
type Settings = {
95
  displayName: string;
96
  contributionEnabled: boolean;
97
  rawConversationsStored: false;
98
};
99
100
const emptyData: DashboardData = {
101
  agents: [],
102
  projects: [],
103
  installations: [],
104
  setupRequests: [],
105
  publications: [],
106
  interactions: [],
107
  federatedInteractions: [],
108
  liveSessions: [],
109
  events: [],
110
  conflicts: [],
111
  receipts: [],
112
  profiles: [],
113
  counterpartyBriefs: [],
114
  completionReports: [],
115
  feedbackRequests: [],
116
  interactionFeedback: [],
117
  interactionConclusions: [],
118
  learningEligibility: [],
119
  profileDeltas: [],
120
  intelligenceSummaries: [],
121
  runtimes: [],
122
  accessTokens: [],
123
  assuranceAssessments: [],
124
  assurancePredictions: [],
125
  assuranceSafeguards: [],
126
  assuranceEvaluations: [],
127
  assuranceProbePlans: [],
128
  assuranceProbeResponses: [],
129
  shieldCases: [],
130
  shieldConsultations: [],
131
  shieldOutcomes: [],
132
};
133
const defaultSettings: Settings = {
134
  displayName: '',
135
  contributionEnabled: false,
136
  rawConversationsStored: false,
137
};
138
139
function normalizeDashboard(value: unknown): DashboardData {
140
  return { ...emptyData, ...(value as Partial<DashboardData>) };
141
}
142
143
function base64Url(bytes: Uint8Array) {
144
  let binary = '';
145
  for (const byte of bytes) binary += String.fromCharCode(byte);
146
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
147
}
148
149
function randomValue(size = 32) {
150
  return base64Url(crypto.getRandomValues(new Uint8Array(size)));
151
}
152
153
async function beginAuth(provider: 'google' | 'github') {
154
  const verifier = randomValue(48);
155
  const digest = await crypto.subtle.digest(
156
    'SHA-256',
157
    new TextEncoder().encode(verifier) as BufferSource,
158
  );
159
  const returnTo = sessionStorage.getItem(authReturnToKey) ?? '/dashboard';
160
  sessionStorage.removeItem(authReturnToKey);
161
  const transaction: AuthTransaction = {
162
    state: randomValue(),
163
    nonce: randomValue(),
164
    verifier,
165
    returnTo,
166
  };
167
  sessionStorage.setItem(authTransactionKey, JSON.stringify(transaction));
168
  const parameters = new URLSearchParams({
169
    client_id: __AUTH0_CLIENT_ID__,
170
    redirect_uri: `${location.origin}/sso-callback`,
171
    response_type: 'code',
172
    scope: 'openid profile email',
173
    audience: __AUTH0_AUDIENCE__,
174
    state: transaction.state,
175
    nonce: transaction.nonce,
176
    code_challenge: base64Url(new Uint8Array(digest)),
177
    code_challenge_method: 'S256',
178
    connection: provider === 'google' ? 'google-oauth2' : 'github',
179
  });
180
  location.assign(`https://${__AUTH0_DOMAIN__}/authorize?${parameters}`);
181
}
182
183
async function signOut(preview: boolean) {
184
  if (preview) {
185
    disablePreview();
186
    location.replace('/login');
187
    return;
188
  }
189
  await fetch('/api/session', { method: 'DELETE', credentials: 'same-origin' });
190
  const parameters = new URLSearchParams({
191
    client_id: __AUTH0_CLIENT_ID__,
192
    returnTo: `${location.origin}/login`,
193
  });
194
  location.assign(`https://${__AUTH0_DOMAIN__}/v2/logout?${parameters}`);
195
}
196
197
async function remoteApi(path: string, init?: RequestInit) {
198
  const headers = new Headers(init?.headers);
199
  if (init?.body && !headers.has('content-type')) headers.set('content-type', 'application/json');
200
  return fetch(path, {
201
    ...init,
202
    credentials: 'same-origin',
203
    headers,
204
  }).then(async (response) => {
205
    if (!response.ok) {
206
      const text = await response.text();
207
      let message = text || `Request failed: ${response.status}`;
208
      try {
209
        const value = JSON.parse(text) as { error?: unknown };
210
        if (typeof value.error === 'string') message = value.error;
211
      } catch {
212
        // Preserve non-JSON error text from proxies and platform failures.
213
      }
214
      throw new Error(message);
215
    }
216
    return response.json();
217
  });
218
}
219
220
function route(): Page {
221
  const value = location.pathname.slice(1) || 'dashboard';
222
  if ((pages as readonly string[]).includes(value)) return value as Page;
223
  return 'dashboard';
224
}
225
226
function initialTheme(): Theme {
227
  const saved = localStorage.getItem(themeKey);
228
  if (saved === 'dark' || saved === 'light') return saved;
229
  return matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
230
}
231
232
function App() {
233
  const [session, setSession] = useState<AuthSession | null>();
234
  const [preview, setPreview] = useState(false);
235
  const [theme, setTheme] = useState<Theme>(initialTheme);
236
  const [page, setPage] = useState<Page>(route());
237
  const [data, setData] = useState<DashboardData>(emptyData);
238
  const [settings, setSettings] = useState<Settings>(defaultSettings);
239
  const [loading, setLoading] = useState(true);
240
  const [error, setError] = useState('');
241
  const api = useCallback(
242
    async (path: string, init?: RequestInit) => {
243
      if (preview) {
244
        const next = applyPreviewRequest(data, settings, path, init);
245
        setData(next.data);
246
        setSettings(next.settings);
247
        return next.result;
248
      }
249
      return remoteApi(path, init);
250
    },
251
    [data, preview, settings],
252
  );
253
  const refreshDashboard = useCallback(async () => {
254
    if (preview) return;
255
    setData(normalizeDashboard(await remoteApi('/v0.1/dashboard')));
256
  }, [preview]);
257
258
  useEffect(() => {
259
    document.title = session
260
      ? `${pageMeta[page].title} · OpenClasp`
261
      : session === null
262
        ? 'OpenClasp · AI assurance for agent agreements'
263
        : 'OpenClasp';
264
  }, [page, session]);
265
266
  useEffect(() => {
267
    let cancelled = false;
268
    const boot = async () => {
269
      if (import.meta.env.DEV && isPreviewActive()) {
270
        if (cancelled) return;
271
        setPreview(true);
272
        setSession(previewSession);
273
        setData(createPreviewData());
274
        setSettings(defaultPreviewSettings);
275
        setLoading(false);
276
        return;
277
      }
278
      try {
279
        const response = await fetch('/api/session', { credentials: 'same-origin' });
280
        if (!response.ok) {
281
          if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) setSession(null);
282
          return;
283
        }
284
        if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) {
285
          setSession((await response.json()) as AuthSession);
286
        }
287
      } catch {
288
        if (!cancelled && !(import.meta.env.DEV && isPreviewActive())) setSession(null);
289
      }
290
    };
291
    void boot();
292
    return () => {
293
      cancelled = true;
294
    };
295
  }, []);
296
297
  useEffect(() => {
298
    document.documentElement.dataset.theme = theme;
299
    document.documentElement.classList.toggle('dark', theme === 'dark');
300
    localStorage.setItem(themeKey, theme);
301
    const meta = document.querySelector('meta[name="theme-color"]');
302
    if (meta) meta.setAttribute('content', theme === 'light' ? '#f7f4f2' : '#0c0a0a');
303
  }, [theme]);
304
305
  useEffect(() => {
306
    const onPopState = () => setPage(route());
307
    addEventListener('popstate', onPopState);
308
    return () => removeEventListener('popstate', onPopState);
309
  }, []);
310
311
  useEffect(() => {
312
    if (session === undefined) return;
313
    if (!session) {
314
      if (location.pathname === '/connect' && new URLSearchParams(location.search).has('claim'))
315
        sessionStorage.setItem(authReturnToKey, `${location.pathname}${location.search}`);
316
      const publicPath = '/login';
317
      if (location.pathname !== publicPath) history.replaceState({}, '', publicPath);
318
      return;
319
    }
320
    if (location.pathname === '/' || location.pathname === '/login')
321
      history.replaceState({}, '', '/dashboard');
322
    setPage(route());
323
    if (preview) {
324
      setLoading(false);
325
      return;
326
    }
327
    Promise.all([remoteApi('/v0.1/dashboard'), remoteApi('/v0.1/settings')])
328
      .then(([dashboard, accountSettings]) => {
329
        setData(normalizeDashboard(dashboard));
330
        setSettings(accountSettings as Settings);
331
      })
332
      .catch((reason: unknown) =>
333
        setError(reason instanceof Error ? reason.message : 'Load failed'),
334
      )
335
      .finally(() => setLoading(false));
336
  }, [preview, session]);
337
338
  useEffect(() => {
339
    if (!session || preview) return;
340
    const refresh = () => {
341
      if (document.visibilityState === 'visible') void refreshDashboard().catch(() => undefined);
342
    };
343
    const timer = window.setInterval(refresh, 10_000);
344
    document.addEventListener('visibilitychange', refresh);
345
    return () => {
346
      window.clearInterval(timer);
347
      document.removeEventListener('visibilitychange', refresh);
348
    };
349
  }, [preview, refreshDashboard, session]);
350
351
  const navigate = (next: Page) => {
352
    history.pushState({}, '', `/${next}`);
353
    setPage(next);
354
  };
355
  if (session === undefined) return <Loading />;
356
  if (!session)
357
    return import.meta.env.DEV ? (
358
      <PublicLanding
359
        theme={theme}
360
        onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
361
        onPreview={() => {
362
          enablePreview();
363
          setPreview(true);
364
          setSession(previewSession);
365
          setData(createPreviewData());
366
          setSettings(defaultPreviewSettings);
367
          setLoading(false);
368
          history.replaceState({}, '', '/dashboard');
369
          setPage('dashboard');
370
        }}
371
      />
372
    ) : (
373
      <PublicLanding
374
        theme={theme}
375
        onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
376
      />
377
    );
378
379
  return (
380
    <AppShell
381
      page={page}
382
      navigate={navigate}
383
      user={session.user}
384
      theme={theme}
385
      onToggleTheme={() => setTheme((current) => (current === 'dark' ? 'light' : 'dark'))}
386
      onSignOut={() => void signOut(preview)}
387
    >
388
      {preview && <div className="previewBanner">local preview · changes stay here</div>}
389
      {error && (
390
        <div className="errorBar" role="alert">
391
          {error}
392
        </div>
393
      )}
394
      {loading ? (
395
        <Loading compact />
396
      ) : (
397
        <PageContent
398
          page={page}
399
          data={data}
400
          settings={settings}
401
          setSettings={setSettings}
402
          navigate={navigate}
403
          refreshDashboard={refreshDashboard}
404
          api={api}
405
        />
406
      )}
407
    </AppShell>
408
  );
409
}
410
411
function PublicLanding({
412
  onPreview,
413
  theme,
414
  onToggleTheme,
415
}: {
416
  onPreview?: () => void;
417
  theme: Theme;
418
  onToggleTheme: () => void;
419
}) {
420
  const [error, setError] = useState('');
421
  const continueWith = async (provider: 'google' | 'github') => {
422
    setError('');
423
    try {
424
      await beginAuth(provider);
425
    } catch {
426
      setError(`${provider === 'google' ? 'Google' : 'GitHub'} sign-in is not configured yet.`);
427
    }
428
  };
429
  const scrollToAnchor = (event: React.MouseEvent<HTMLAnchorElement>) => {
430
    const id = event.currentTarget.hash.slice(1);
431
    const target = document.getElementById(id);
432
    if (!target) return;
433
    event.preventDefault();
434
    const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
435
    history.pushState({}, '', `#${id}`);
436
    target.classList.remove('is-arriving');
437
    if (!reducedMotion && id !== 'top') {
438
      void target.offsetWidth;
439
      target.classList.add('is-arriving');
440
      window.setTimeout(() => target.classList.remove('is-arriving'), 900);
441
    }
442
    target.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'start' });
443
  };
444
  return (
445
    <div className="landingPage">
446
      <LandingBackdrop />
447
      <header className="landingNav">
448
        <a
449
          className="landingBrand"
450
          href="#top"
451
          aria-label="OpenClasp home"
452
          onClick={scrollToAnchor}
453
        >
454
          <ClaspMark className="landingMark" size={28} />
455
          <strong>openclasp</strong>
456
        </a>
457
        <nav aria-label="Main navigation">
458
          <a href="#product" onClick={scrollToAnchor}>
459
            what it knows
460
          </a>
461
          <a href="#intelligence" onClick={scrollToAnchor}>
462
            intelligence
463
          </a>
464
        </nav>
465
        <div className="landingNavActions">
466
          <button
467
            className="themeButton"
468
            type="button"
469
            onClick={onToggleTheme}
470
            aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} theme`}
471
          >
472
            {theme === 'dark' ? <Sun /> : <Moon />}
473
          </button>
474
          <a className="navCta" href="#access" onClick={scrollToAnchor}>
475
            sign in <ArrowRight />
476
          </a>
477
        </div>
478
      </header>
479
480
      <main id="top">
481
        <section className="landingHero">
482
          <div className="heroCopy">
483
            <div className="heroLead">
484
              <p className="landingKicker">
485
                <span>//</span> trust infrastructure for the agent economy
486
              </p>
487
              <h1>
488
                Agents can
489
                <br />
490
                talk.
491
                <br />
492
                <em>
493
                  Now they can
494
                  <br />
495
                  build trust.
496
                </em>
497
              </h1>
498
              <p>
499
                OpenClasp gives AI agents a shared layer for identity, agreements, safeguards, and
500
                outcome intelligence—across organizations and runtimes.
501
              </p>
502
            </div>
503
            <div className="heroActions">
504
              <a className="landingPrimary" href="#access" onClick={scrollToAnchor}>
505
                sign in <ArrowRight />
506
              </a>
507
              <a className="landingSecondary" href="#product" onClick={scrollToAnchor}>
508
                see how it works
509
              </a>
510
            </div>
511
          </div>
512
          <LandingDiagram />
513
        </section>
514
515
        <div className="capabilityRail" aria-label="OpenClasp capabilities">
516
          {landingCapabilities.map((item) => (
517
            <span key={item}>{item}</span>
518
          ))}
519
        </div>
520
521
        <section className="productSection" id="product">
522
          <div className="sectionIntro">
523
            <p className="landingKicker">
524
              <span>01</span> before the agent acts
525
            </p>
526
            <h2>
527
              Trust agents by context.
528
              <br />
529
              <em>Not reputation alone.</em>
530
            </h2>
531
            <p>
532
              Identity is only the start. OpenClasp helps agents understand who they are dealing
533
              with, what was agreed, what could go wrong, and what actually happened.
534
            </p>
535
          </div>
536
          <section className="coreQuestions" aria-labelledby="core-questions-title">
537
            <header>
538
              <span>THE TRUST LAYER</span>
539
              <h3 id="core-questions-title">Before agents work together, OpenClasp asks:</h3>
540
            </header>
541
            <ol>
542
              <li>
543
                <span>01 · IDENTITY</span>
544
                <strong>Who operates this agent, and is its published identity verified?</strong>
545
              </li>
546
              <li>
547
                <span>02 · AGREEMENT</span>
548
                <strong>What exactly will each agent do, under which constraints?</strong>
549
              </li>
550
              <li>
551
                <span>03 · ASSURANCE</span>
552
                <strong>What risks, questions, or safeguards matter for this work?</strong>
553
              </li>
554
              <li>
555
                <span>04 · OUTCOME</span>
556
                <strong>What happened, and what should future agents learn from it?</strong>
557
              </li>
558
            </ol>
559
          </section>
560
        </section>
561
562
        <section className="intelligenceSection" id="intelligence">
563
          <div className="sectionIntro">
564
            <p className="landingKicker">
565
              <span>02</span> the compounding layer
566
            </p>
567
            <h2>
568
              The network gets smarter
569
              <br />
570
              <em>with every interaction.</em>
571
            </h2>
572
            <p>
573
              OpenClasp learns how agents and versions behave, which questions reveal risk, and
574
              which safeguards are associated with better outcomes.
575
            </p>
576
          </div>
577
          <ol className="intelligenceLoop" aria-label="OpenClasp intelligence flywheel">
578
            <li>
579
              <span>01</span>
580
              <strong>Verified agent</strong>
581
            </li>
582
            <li>
583
              <span>02</span>
584
              <strong>Explicit agreement</strong>
585
            </li>
586
            <li>
587
              <span>03</span>
588
              <strong>Assurance decision</strong>
589
            </li>
590
            <li>
591
              <span>04</span>
592
              <strong>Scored outcome</strong>
593
            </li>
594
          </ol>
595
          <p className="intelligenceBoundary">
596
            Conversation bodies stay between agent runtimes. Learning uses explicit structured
597
            records, stays scoped to task and version, and never claims safeguards caused an
598
            outcome.
599
          </p>
600
        </section>
601
602
        <section className="accessSection" id="access">
603
          <div className="accessCopy">
604
            <p className="landingKicker">
605
              <span>03</span> identity required
606
            </p>
607
            <h2>Access your agent network.</h2>
608
            <p>
609
              Sign in to connect agents, control network participation, and inspect structured
610
              history.
611
            </p>
612
          </div>
613
          <div className="accessCard">
614
            <div className="accessChrome">
615
              <span>$ authenticate</span>
616
              <span className="accessBlink">_</span>
617
            </div>
618
            <div className="socialButtons">
619
              <button
620
                className="authProviderButton"
621
                type="button"
622
                onClick={() => void continueWith('google')}
623
              >
624
                <span className="authProviderIcon authProviderIconGoogle" aria-hidden="true">
625
                  <GoogleMark />
626
                </span>
627
                <span>continue with google</span>
628
              </button>
629
              <button
630
                className="authProviderButton"
631
                type="button"
632
                onClick={() => void continueWith('github')}
633
              >
634
                <span className="authProviderIcon" aria-hidden="true">
635
                  <GitHubMark />
636
                </span>
637
                <span>continue with github</span>
638
              </button>
639
              {onPreview && (
640
                <button type="button" onClick={() => void onPreview()}>
641
                  open local preview <ArrowRight />
642
                </button>
643
              )}
644
            </div>
645
            {error && (
646
              <div className="loginError" role="alert">
647
                {error}
648
              </div>
649
            )}
650
            <small>google or github · openclasp never receives your password</small>
651
          </div>
652
        </section>
653
      </main>
654
655
      <footer className="landingFooter">
656
        <a className="landingBrand" href="#top" onClick={scrollToAnchor}>
657
          <ClaspMark className="landingMark" size={28} />
658
          <strong>openclasp</strong>
659
        </a>
660
        <p>Trust infrastructure for AI agents.</p>
661
        <small>Identity. Agreements. Assurance. Outcomes.</small>
662
      </footer>
663
    </div>
664
  );
665
}
666
667
let callbackStarted = false;
668
function AuthCallback() {
669
  const [error, setError] = useState('');
670
  useEffect(() => {
671
    if (callbackStarted) return;
672
    callbackStarted = true;
673
    const finish = async () => {
674
      const query = new URLSearchParams(location.search);
675
      const oauthError = query.get('error_description') ?? query.get('error');
676
      if (oauthError) throw new Error(oauthError);
677
      const code = query.get('code');
678
      const returnedState = query.get('state');
679
      if (code && returnedState?.startsWith('oc_tx_')) {
680
        const response = await fetch('/api/oauth-callback', {
681
          method: 'POST',
682
          credentials: 'same-origin',
683
          headers: { 'content-type': 'application/json' },
684
          body: JSON.stringify({ code, state: returnedState }),
685
        });
686
        if (!response.ok) throw new Error('MCP authorization failed');
687
        const result = (await response.json()) as { redirectTo?: unknown };
688
        if (typeof result.redirectTo !== 'string')
689
          throw new Error('Invalid MCP authorization response');
690
        location.replace(result.redirectTo);
691
        return;
692
      }
693
      const rawTransaction = sessionStorage.getItem(authTransactionKey);
694
      if (!code || !returnedState || !rawTransaction) throw new Error('Missing OAuth transaction');
695
      const transaction = JSON.parse(rawTransaction) as AuthTransaction;
696
      sessionStorage.removeItem(authTransactionKey);
697
      if (returnedState !== transaction.state) throw new Error('OAuth state mismatch');
698
      const response = await fetch('/api/session', {
699
        method: 'POST',
700
        credentials: 'same-origin',
701
        headers: { 'content-type': 'application/json' },
702
        body: JSON.stringify({ code, codeVerifier: transaction.verifier }),
703
      });
704
      if (!response.ok) throw new Error('Auth0 token exchange failed');
705
      const returnTo =
706
        transaction.returnTo.startsWith('/') && !transaction.returnTo.startsWith('//')
707
          ? transaction.returnTo
708
          : '/dashboard';
709
      location.replace(returnTo);
710
    };
711
    void finish().catch((reason: unknown) =>
712
      setError(reason instanceof Error ? reason.message : 'Authentication failed'),
713
    );
714
  }, []);
715
  if (error)
716
    return (
717
      <div className="loading">
718
        <span className="mark">OC</span>
719
        <p>{error}</p>
720
        <a href="/">Return home</a>
721
      </div>
722
    );
723
  return <Loading />;
724
}
725
726
function PageContent({
727
  page,
728
  data,
729
  settings,
730
  setSettings,
731
  navigate,
732
  refreshDashboard,
733
  api,
734
}: {
735
  page: Page;
736
  data: DashboardData;
737
  settings: Settings;
738
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
739
  navigate: (page: Page) => void;
740
  refreshDashboard: () => Promise<void>;
741
  api: (path: string, init?: RequestInit) => Promise<unknown>;
742
}) {
743
  if (page === 'connect')
744
    return (
745
      <Connect data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />
746
    );
747
  if (page === 'marketplace') return <Marketplace data={data} />;
748
  if (page === 'settings')
749
    return <SettingsPage settings={settings} setSettings={setSettings} api={api} />;
750
  if (page === 'history')
751
    return <History data={data} refreshDashboard={refreshDashboard} api={api} />;
752
  if (page === 'agents')
753
    return <Agents data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />;
754
  if (page === 'insights') return <Insights data={data} />;
755
  if (page !== 'dashboard')
756
    return (
757
      <Overview data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />
758
    );
759
  return (
760
    <AgentWorkspace data={data} navigate={navigate} refreshDashboard={refreshDashboard} api={api} />
761
  );
762
}
763
764
type AgentHistoryItem = {
765
  interactionId: string;
766
  title: string;
767
  counterpart: string;
768
  counterpartMode: 'agent';
769
  outcome: 'success' | 'partial' | 'failure' | 'cancelled' | 'provisional' | 'active' | 'pending';
770
  at: string;
771
};
772
773
const SCORECARD_DIMENSIONS = [
774
  { key: 'completion', label: 'Task completion', short: 'Completion' },
775
  { key: 'acceptance', label: 'Outcome satisfaction', short: 'Satisfaction' },
776
  { key: 'specification', label: 'Requirement adherence', short: 'Requirements' },
777
  { key: 'deadline', label: 'Timeliness', short: 'Timeliness' },
778
  { key: 'communication', label: 'Communication', short: 'Communication' },
779
  { key: 'evidence', label: 'Evidence quality', short: 'Evidence' },
780
  { key: 'scope', label: 'Scope adherence', short: 'Scope' },
781
  { key: 'correction', label: 'Correction behaviour', short: 'Corrections' },
782
  { key: 'limitations', label: 'Limitation disclosure', short: 'Limitations' },
783
  { key: 'disputes', label: 'Dispute-free outcomes', short: 'Dispute-free' },
784
] as const;
785
786
type ScorecardMetric = {
787
  key: (typeof SCORECARD_DIMENSIONS)[number]['key'];
788
  label: string;
789
  short: string;
790
  value: number | null;
791
  evidence: number;
792
};
793
794
function confirmAgentDeletion(agent: Record<string, any>) {
795
  const agentId = String(agent.agentId);
796
  return window.confirm(
797
    `Delete “${String(agent.name ?? agentId)}” from OpenClasp?\n\nIts provider connection, runtime, token, publication and profile will be removed. Signed interaction and receipt history will be retained. You should also uninstall the connector from its provider.`,
798
  );
799
}
800
801
function AgentWorkspace({
802
  data,
803
  navigate,
804
  refreshDashboard,
805
  api,
806
}: {
807
  data: DashboardData;
808
  navigate: (page: Page) => void;
809
  refreshDashboard: () => Promise<void>;
810
  api: (path: string, init?: RequestInit) => Promise<unknown>;
811
}) {
812
  const [expandedAgent, setExpandedAgent] = useState('');
813
  const [openHistory, setOpenHistory] = useState<Record<string, boolean>>({});
814
  const [expandedHistoryId, setExpandedHistoryId] = useState('');
815
  const [scorecardAgentId, setScorecardAgentId] = useState(
816
    () => new URLSearchParams(window.location.search).get('scorecard') ?? '',
817
  );
818
  const [working, setWorking] = useState('');
819
  const [error, setError] = useState('');
820
  const pendingSetups = data.setupRequests.filter((request) => request.status === 'pending');
821
  const latestAssurancePrediction = data.assurancePredictions.at(-1);
822
  const pendingAssuranceSafeguards = data.assuranceSafeguards.filter(
823
    (item) => item.status === 'recommended',
824
  ).length;
825
  const toggleAgent = (agentId: string, isOpen: boolean) => {
826
    setExpandedAgent(isOpen ? '' : agentId);
827
    setExpandedHistoryId('');
828
  };
829
830
  const decideSetup = async (requestId: string, decision: 'approve' | 'reject') => {
831
    setWorking(`setup:${requestId}`);
832
    setError('');
833
    try {
834
      await api(`/v0.1/onboarding/${encodeURIComponent(requestId)}/${decision}`, {
835
        method: 'POST',
836
      });
837
      await refreshDashboard();
838
    } catch (reason) {
839
      setError(reason instanceof Error ? reason.message : 'Could not update request');
840
    } finally {
841
      setWorking('');
842
    }
843
  };
844
845
  const respondToContract = async (
846
    interaction: Record<string, any>,
847
    agentId: string,
848
    decision: 'accept' | 'reject',
849
  ) => {
850
    const proposal = latestContractProposal(interaction);
851
    if (!proposal) return;
852
    setWorking(`contract:${interaction.interactionId}`);
853
    setError('');
854
    try {
855
      await api(
856
        `/v0.1/federated-interactions/${encodeURIComponent(String(interaction.interactionId))}/contract-proposals/${encodeURIComponent(String(proposal.revisionId))}/respond`,
857
        { method: 'POST', body: JSON.stringify({ agentId, decision }) },
858
      );
859
      await refreshDashboard();
860
    } catch (reason) {
861
      setError(reason instanceof Error ? reason.message : 'Could not respond to request');
862
    } finally {
863
      setWorking('');
864
    }
865
  };
866
867
  const deleteAgent = async (agent: Record<string, any>) => {
868
    const agentId = String(agent.agentId);
869
    if (!confirmAgentDeletion(agent)) return;
870
    setWorking(`delete:${agentId}`);
871
    setError('');
872
    try {
873
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
874
      setExpandedAgent('');
875
      await refreshDashboard();
876
    } catch (reason) {
877
      setError(reason instanceof Error ? reason.message : 'Agent deletion failed');
878
    } finally {
879
      setWorking('');
880
    }
881
  };
882
883
  return (
884
    <div className="workspacePage">
885
      <header className="workspaceHead">
886
        <h1>
887
          Agents <small>{data.agents.length}</small>
888
        </h1>
889
        <button className="landingPrimary" type="button" onClick={() => navigate('connect')}>
890
          add agent <ArrowRight />
891
        </button>
892
      </header>
893
894
      <FirstRunGuide data={data} api={api} refreshDashboard={refreshDashboard} />
895
896
      <section className="assuranceOverview" aria-label="AI assurance overview">
897
        <div>
898
          <span className="eyebrow">AI ASSURANCE · EXPERIMENTAL</span>
899
          <h2>Predict success before your agents commit.</h2>
900
          <p>
901
            One risk-revealing question at a time. Explicit safeguards. Outcomes that improve the
902
            next decision.
903
          </p>
904
        </div>
905
        <div className="assuranceOverviewStats">
906
          <span>
907
            <strong>{data.assuranceAssessments.length}</strong>
908
            decisions
909
          </span>
910
          <span>
911
            <strong>
912
              {latestAssurancePrediction
913
                ? `${Math.round(Number(latestAssurancePrediction.successProbability) * 100)}%`
914
                : '—'}
915
            </strong>
916
            latest prediction
917
          </span>
918
          <span>
919
            <strong>{data.assuranceProbeResponses.length}</strong>
920
            probes answered
921
          </span>
922
          <span>
923
            <strong>{pendingAssuranceSafeguards}</strong>
924
            safeguards pending
925
          </span>
926
        </div>
927
        <button type="button" onClick={() => navigate('history')}>
928
          view assurance decisions <ArrowRight />
929
        </button>
930
      </section>
931
932
      {error ? (
933
        <div className="errorBar" role="alert">
934
          {error}
935
        </div>
936
      ) : null}
937
938
      {pendingSetups.length ? (
939
        <section className="compactRequests" aria-label="New agent requests">
940
          {pendingSetups.map((request) => (
941
            <article key={request.requestId}>
942
              <AgentMark name={String(request.agentName ?? 'New agent')} size="sm" />
943
              <div>
944
                <strong>{request.agentName ?? 'New agent'}</strong>
945
                <small>{request.framework ?? 'Agent'} wants to connect</small>
946
              </div>
947
              <div className="iconDecisions">
948
                <button
949
                  type="button"
950
                  aria-label={`Reject ${String(request.agentName ?? 'agent')}`}
951
                  disabled={working === `setup:${request.requestId}`}
952
                  onClick={() => void decideSetup(String(request.requestId), 'reject')}
953
                >
954
                  <X />
955
                </button>
956
                <button
957
                  className="approve"
958
                  type="button"
959
                  aria-label={`Approve ${String(request.agentName ?? 'agent')}`}
960
                  disabled={working === `setup:${request.requestId}`}
961
                  onClick={() => void decideSetup(String(request.requestId), 'approve')}
962
                >
963
                  <Check />
964
                </button>
965
              </div>
966
            </article>
967
          ))}
968
        </section>
969
      ) : null}
970
971
      <section className="agentList" aria-label="Connected agents">
972
        {data.agents.length ? (
973
          data.agents.map((agent) => {
974
            const agentId = String(agent.agentId);
975
            const expanded = expandedAgent === agentId;
976
            const invitations = invitationsForAgent(data, agentId);
977
            const history = historyForAgent(data, agent);
978
            const reliability = reliabilityForAgent(data, agentId);
979
            const runtime = data.runtimes.find((item) => item.agentId === agentId);
980
            const verified = !agent.revoked && Boolean(agent.identityMode);
981
            const online = agent.presence?.status === 'online';
982
            const connected = runtime?.status === 'verified';
983
            const available = connected || online;
984
            const published = data.publications.some(
985
              (item) => item.agentId === agentId && item.published,
986
            );
987
            return (
988
              <article className={`agentRow ${expanded ? 'isExpanded' : ''}`} key={agentId}>
989
                <div className="agentRowSummary">
990
                  <button
991
                    className="agentRowCore"
992
                    type="button"
993
                    aria-expanded={expanded}
994
                    onClick={() => toggleAgent(agentId, expanded)}
995
                  >
996
                    <AgentMark name={String(agent.name ?? agentId)} online={available} />
997
                    <span className="agentIdentity">
998
                      <strong>{agent.name ?? agentId}</strong>
999
                      <small>{agent.framework ?? 'Agent'}</small>
1000
                    </span>
1001
                    <span className="agentSignals">
1002
                      <span className={available ? 'signalOnline' : ''}>
1003
                        <Circle /> {connected ? 'Connected' : online ? 'Online' : 'Offline'}
1004
                      </span>
1005
                      <span className={verified ? 'signalVerified' : ''}>
1006
                        <ShieldCheck /> {verified ? 'Verified' : 'Unverified'}
1007
                      </span>
1008
                      <span>
1009
                        <Cloud />
1010
                        {runtime?.status === 'verified' ? 'Cloud' : 'Cloud · disconnected'}
1011
                      </span>
1012
                    </span>
1013
                  </button>
1014
                  <button
1015
                    className="scorecardTeaser"
1016
                    type="button"
1017
                    onClick={() => setScorecardAgentId(agentId)}
1018
                    aria-label={`Open behavioural scorecard for ${String(agent.name ?? agentId)}`}
1019
                  >
1020
                    <strong>{reliability.score == null ? '—' : reliability.score}</strong>
1021
                    <span>
1022
                      <b>behaviour</b>
1023
                      <small>
1024
                        {reliability.summary
1025
                          ? `${humanize(reliability.summary.confidence.level)} confidence · ${reliability.samples}`
1026
                          : 'awaiting eligible outcomes'}
1027
                      </small>
1028
                    </span>
1029
                  </button>
1030
                  {invitations.length ? (
1031
                    <span
1032
                      className="notificationCount"
1033
                      aria-label={`${invitations.length} requests`}
1034
                    >
1035
                      {invitations.length}
1036
                    </span>
1037
                  ) : null}
1038
                  <button
1039
                    className="agentExpandButton"
1040
                    type="button"
1041
                    aria-expanded={expanded}
1042
                    aria-label={`${expanded ? 'Collapse' : 'Expand'} ${String(agent.name ?? agentId)}`}
1043
                    onClick={() => toggleAgent(agentId, expanded)}
1044
                  >
1045
                    <ChevronDown className="expandIcon" />
1046
                  </button>
1047
                </div>
1048
1049
                {expanded ? (
1050
                  <div className="agentExpanded">
1051
                    {invitations.length ? (
1052
                      <section className="agentRequests" aria-label="Contract requests">
1053
                        <p className="sectionLabel">Requests</p>
1054
                        {invitations.map((interaction) => {
1055
                          const proposal = latestContractProposal(interaction);
1056
                          return (
1057
                            <article key={interaction.interactionId}>
1058
                              <span className="requestPulse" />
1059
                              <div>
1060
                                <strong>
1061
                                  {interaction.contract?.purpose ?? 'New interaction request'}
1062
                                </strong>
1063
                                <small>from {counterpartyName(data, interaction, agentId)}</small>
1064
                              </div>
1065
                              <div className="iconDecisions">
1066
                                <button
1067
                                  type="button"
1068
                                  aria-label="Reject request"
1069
                                  disabled={working === `contract:${interaction.interactionId}`}
1070
                                  onClick={() =>
1071
                                    void respondToContract(interaction, agentId, 'reject')
1072
                                  }
1073
                                >
1074
                                  <X />
1075
                                </button>
1076
                                <button
1077
                                  className="approve"
1078
                                  type="button"
1079
                                  aria-label="Accept request"
1080
                                  disabled={
1081
                                    !proposal || working === `contract:${interaction.interactionId}`
1082
                                  }
1083
                                  onClick={() =>
1084
                                    void respondToContract(interaction, agentId, 'accept')
1085
                                  }
1086
                                >
1087
                                  <Check />
1088
                                </button>
1089
                              </div>
1090
                            </article>
1091
                          );
1092
                        })}
1093
                      </section>
1094
                    ) : null}
1095
1096
                    <div className="agentDetailStrip">
1097
                      <span>
1098
                        <b>{reliability.samples || '—'}</b>
1099
                        eligible outcomes
1100
                      </span>
1101
                      <span>
1102
                        <b>{published ? 'Public' : 'Private'}</b>
1103
                        discovery
1104
                      </span>
1105
                      <span>
1106
                        <b>
1107
                          {connected
1108
                            ? 'Verified'
1109
                            : online
1110
                              ? 'Now'
1111
                              : agent.presence?.lastSeenAt
1112
                                ? relativeTime(agent.presence.lastSeenAt)
1113
                                : 'Never'}
1114
                        </b>
1115
                        {connected ? 'runtime endpoint' : 'last active'}
1116
                      </span>
1117
                      {published ? (
1118
                        <a
1119
                          href={`/a/${encodeURIComponent(agentId)}`}
1120
                          target="_blank"
1121
                          rel="noreferrer"
1122
                        >
1123
                          Profile <ExternalLink />
1124
                        </a>
1125
                      ) : null}
1126
                    </div>
1127
1128
                    <section className="agentHistory">
1129
                      <button
1130
                        className="historyHeading"
1131
                        type="button"
1132
                        aria-expanded={openHistory[agentId] !== false}
1133
                        aria-controls={`history-${agentId}`}
1134
                        onClick={() => {
1135
                          setOpenHistory((current) => ({
1136
                            ...current,
1137
                            [agentId]: current[agentId] === false,
1138
                          }));
1139
                          setExpandedHistoryId('');
1140
                        }}
1141
                      >
1142
                        History
1143
                        <small>{history.length}</small>
1144
                        <ChevronDown className="expandIcon" />
1145
                      </button>
1146
                      {openHistory[agentId] === false ? null : (
1147
                        <ol id={`history-${agentId}`}>
1148
                          {history.length ? (
1149
                            history.map((item) => {
1150
                              const itemOpen = expandedHistoryId === item.interactionId;
1151
                              return (
1152
                                <li
1153
                                  className={itemOpen ? 'historyEntry isExpanded' : 'historyEntry'}
1154
                                  key={item.interactionId}
1155
                                >
1156
                                  <div className="historyRowSummary">
1157
                                    <button
1158
                                      className="historyItem"
1159
                                      type="button"
1160
                                      aria-expanded={itemOpen}
1161
                                      onClick={() =>
1162
                                        setExpandedHistoryId(itemOpen ? '' : item.interactionId)
1163
                                      }
1164
                                    >
1165
                                      <OutcomeSymbol outcome={item.outcome} />
1166
                                      <span className="historyIdentity">
1167
                                        <strong>{item.counterpart}</strong>
1168
                                        <small>
1169
                                          Agent
1170
                                          {' · '}
1171
                                          {relativeTime(item.at)}
1172
                                        </small>
1173
                                      </span>
1174
                                      <span className="historyPurpose">{item.title}</span>
1175
                                      <b className={`outcomeText ${item.outcome}`}>
1176
                                        {humanize(item.outcome)}
1177
                                      </b>
1178
                                    </button>
1179
                                    <button
1180
                                      className="agentExpandButton"
1181
                                      type="button"
1182
                                      aria-expanded={itemOpen}
1183
                                      aria-label={`${itemOpen ? 'Collapse' : 'Expand'} ${item.title}`}
1184
                                      onClick={() =>
1185
                                        setExpandedHistoryId(itemOpen ? '' : item.interactionId)
1186
                                      }
1187
                                    >
1188
                                      <ChevronDown className="expandIcon" />
1189
                                    </button>
1190
                                  </div>
1191
                                  {itemOpen ? (
1192
                                    <div className="historyDetail">
1193
                                      <p>{item.title}</p>
1194
                                      <small>
1195
                                        {new Date(item.at).toLocaleString()} · {item.interactionId}
1196
                                      </small>
1197
                                    </div>
1198
                                  ) : null}
1199
                                </li>
1200
                              );
1201
                            })
1202
                          ) : (
1203
                            <li className="quietEmpty">No interactions yet</li>
1204
                          )}
1205
                        </ol>
1206
                      )}
1207
                    </section>
1208
                    <div className="agentActions agentManagementActions">
1209
                      <button
1210
                        className="secondary dangerButton"
1211
                        type="button"
1212
                        disabled={Boolean(working)}
1213
                        onClick={() => void deleteAgent(agent)}
1214
                      >
1215
                        {working === `delete:${agentId}` ? 'Deleting…' : 'Delete agent'}
1216
                      </button>
1217
                    </div>
1218
                  </div>
1219
                ) : null}
1220
              </article>
1221
            );
1222
          })
1223
        ) : (
1224
          <button className="emptyAgentState" type="button" onClick={() => navigate('connect')}>
1225
            <FrameCorners />
1226
            <span aria-hidden="true">+</span>
1227
            <strong>Connect your first agent</strong>
1228
            <span>OpenClasp issues its identity. It does not run the agent.</span>
1229
          </button>
1230
        )}
1231
      </section>
1232
      <BehaviourScorecardDialog
1233
        open={Boolean(scorecardAgentId)}
1234
        onOpenChange={(open) => {
1235
          if (!open) setScorecardAgentId('');
1236
        }}
1237
        data={data}
1238
        agent={data.agents.find((item) => item.agentId === scorecardAgentId)}
1239
      />
1240
    </div>
1241
  );
1242
}
1243
1244
function latestContractProposal(interaction: Record<string, any>) {
1245
  return [...(interaction.contractRevisions ?? [])]
1246
    .reverse()
1247
    .find((revision) => revision.status === 'proposed');
1248
}
1249
1250
function invitationsForAgent(data: DashboardData, agentId: string) {
1251
  return data.federatedInteractions.filter((interaction) => {
1252
    const proposal = latestContractProposal(interaction);
1253
    return (
1254
      proposal &&
1255
      (
1256
        interaction.contract?.parties ?? [
1257
          interaction.initiatorAgentId,
1258
          interaction.responderAgentId,
1259
        ]
1260
      ).includes(agentId) &&
1261
      !proposal.acceptances?.[agentId]
1262
    );
1263
  });
1264
}
1265
1266
function counterpartyName(data: DashboardData, interaction: Record<string, any>, agentId: string) {
1267
  const counterpartyId =
1268
    interaction.initiatorAgentId === agentId
1269
      ? interaction.responderAgentId
1270
      : interaction.initiatorAgentId;
1271
  return data.agents.find((agent) => agent.agentId === counterpartyId)?.name ?? counterpartyId;
1272
}
1273
1274
function reliabilityForAgent(data: DashboardData, agentId: string) {
1275
  const summaries = data.intelligenceSummaries
1276
    .filter((summary) => summary.agentId === agentId)
1277
    .sort((left, right) => {
1278
      if (left.versionStatus?.status !== right.versionStatus?.status)
1279
        return left.versionStatus?.status === 'current' ? -1 : 1;
1280
      return Number(right.confidence?.value ?? 0) - Number(left.confidence?.value ?? 0);
1281
    });
1282
  const summary = summaries[0];
1283
  if (summary)
1284
    return {
1285
      score: Math.round(Number(summary.score) * 100),
1286
      samples: Number(summary.confidence?.evidenceCount ?? 0),
1287
      summary,
1288
      all: summaries,
1289
    };
1290
  const profiles = data.profiles.filter((profile) => profile.agentId === agentId);
1291
  if (!profiles.length) return { score: null, samples: 0, summary: null, all: [] };
1292
  let weightedScore = 0;
1293
  let weight = 0;
1294
  for (const profile of profiles) {
1295
    const dimensions = [
1296
      profile.completion,
1297
      profile.acceptance,
1298
      profile.specification,
1299
      profile.scope,
1300
      profile.evidence,
1301
      profile.communication,
1302
    ].filter((value) => Number.isFinite(Number(value)));
1303
    const profileScore = dimensions.length
1304
      ? dimensions.reduce((sum, value) => sum + Number(value), 0) / dimensions.length
1305
      : 0.5;
1306
    const sampleWeight = Math.max(
1307
      1,
1308
      Number(profile.effectiveSampleSize ?? profile.sampleSize ?? 1),
1309
    );
1310
    weightedScore += profileScore * sampleWeight;
1311
    weight += sampleWeight;
1312
  }
1313
  return {
1314
    score: Math.round((weightedScore / Math.max(1, weight)) * 100),
1315
    samples: profiles.reduce((sum, profile) => sum + Number(profile.sampleSize ?? 0), 0),
1316
    summary: null,
1317
    all: [],
1318
  };
1319
}
1320
1321
type BehaviourScorecardModel = {
1322
  agentId: string;
1323
  name: string;
1324
  version: string;
1325
  taskCategory: string;
1326
  confidence: string;
1327
  confidenceValue: number;
1328
  evidenceCount: number;
1329
  trend: string;
1330
  updatedAt: string;
1331
  verified: boolean;
1332
  metrics: ScorecardMetric[];
1333
};
1334
1335
function scorecardForAgent(
1336
  data: DashboardData,
1337
  agent: Record<string, any>,
1338
): BehaviourScorecardModel {
1339
  const agentId = String(agent.agentId);
1340
  const reliability = reliabilityForAgent(data, agentId);
1341
  const summary = reliability.summary;
1342
  const profiles = data.profiles
1343
    .filter(
1344
      (profile) =>
1345
        profile.agentId === agentId &&
1346
        (!summary ||
1347
          (profile.taskCategory === summary.taskCategory &&
1348
            profile.agentVersion === summary.agentVersion)),
1349
    )
1350
    .sort(
1351
      (left, right) =>
1352
        Date.parse(String(right.updatedAt ?? 0)) - Date.parse(String(left.updatedAt ?? 0)),
1353
    );
1354
  const profile = profiles[0];
1355
  const summaryDimensions = new Map<string, number>();
1356
  for (const item of [...(summary?.strengths ?? []), ...(summary?.risks ?? [])])
1357
    summaryDimensions.set(String(item.dimension), Number(item.score));
1358
  const legacyDimensions = new Set([
1359
    'completion',
1360
    'acceptance',
1361
    'specification',
1362
    'deadline',
1363
    'communication',
1364
    'evidence',
1365
    'scope',
1366
    'disputes',
1367
  ]);
1368
  const metrics = SCORECARD_DIMENSIONS.map((definition): ScorecardMetric => {
1369
    const raw = Number(profile?.[definition.key]);
1370
    const evidence = Number(
1371
      profile?.dimensionSampleSizes?.[definition.key] ??
1372
        (legacyDimensions.has(definition.key) ? (profile?.effectiveSampleSize ?? 0) : 0),
1373
    );
1374
    const profileValue = Number.isFinite(raw)
1375
      ? definition.key === 'disputes'
1376
        ? 1 - raw
1377
        : raw
1378
      : undefined;
1379
    const fallback = summaryDimensions.get(definition.key);
1380
    const measured = evidence > 0 || typeof fallback === 'number';
1381
    return {
1382
      ...definition,
1383
      value: measured
1384
        ? Math.round(Math.max(0, Math.min(1, profileValue ?? fallback ?? 0)) * 100)
1385
        : null,
1386
      evidence: Math.max(0, evidence),
1387
    };
1388
  });
1389
  return {
1390
    agentId,
1391
    name: String(agent.name ?? agentId),
1392
    version: String(summary?.agentVersion ?? agent.agentVersion ?? 'unknown'),
1393
    taskCategory: String(summary?.taskCategory ?? profile?.taskCategory ?? 'general'),
1394
    confidence: String(summary?.confidence?.level ?? 'unrated'),
1395
    confidenceValue: Number(summary?.confidence?.value ?? 0),
1396
    evidenceCount: Number(summary?.confidence?.evidenceCount ?? profile?.sampleSize ?? 0),
1397
    trend: String(summary?.trend?.direction ?? 'unrated'),
1398
    updatedAt: String(summary?.updatedAt ?? profile?.updatedAt ?? new Date().toISOString()),
1399
    verified: !agent.revoked && Boolean(agent.identityMode),
1400
    metrics,
1401
  };
1402
}
1403
1404
function scorecardSummary(model: BehaviourScorecardModel) {
1405
  const metrics = model.metrics
1406
    .map(
1407
      (metric) =>
1408
        `${metric.label}: ${metric.value === null ? 'not measured' : `${metric.value}/100`}`,
1409
    )
1410
    .join('\n');
1411
  return `${model.name} — OpenClasp behavioural scorecard\nContext: ${humanize(model.taskCategory)} · ${humanize(model.confidence)} confidence · ${model.evidenceCount} eligible outcomes\n${metrics}\nGenerated from authenticated structured outcomes; no conversation content.`;
1412
}
1413
1414
function escapeSvg(value: string) {
1415
  return value
1416
    .replaceAll('&', '&amp;')
1417
    .replaceAll('<', '&lt;')
1418
    .replaceAll('>', '&gt;')
1419
    .replaceAll('"', '&quot;')
1420
    .replaceAll("'", '&apos;');
1421
}
1422
1423
function scorecardSvg(model: BehaviourScorecardModel) {
1424
  const metricRows = model.metrics
1425
    .map((metric, index) => {
1426
      const column = index >= 5 ? 1 : 0;
1427
      const row = index % 5;
1428
      const x = 76 + column * 554;
1429
      const y = 266 + row * 64;
1430
      const width = metric.value === null ? 0 : Math.round((metric.value / 100) * 360);
1431
      const value = metric.value === null ? 'NOT MEASURED' : `${metric.value}`;
1432
      return `<g><text x="${x}" y="${y}" class="label">${escapeSvg(metric.short.toUpperCase())}</text><text x="${x + 454}" y="${y}" class="value">${value}</text><rect x="${x}" y="${y + 14}" width="454" height="5" rx="2.5" fill="#282321"/><rect x="${x}" y="${y + 14}" width="${width}" height="5" rx="2.5" fill="#ff4d2e"/></g>`;
1433
    })
1434
    .join('');
1435
  return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="675" viewBox="0 0 1200 675"><style>.brand{font:700 22px Arial,sans-serif;fill:#f5f1ed}.name{font:700 48px Arial,sans-serif;fill:#f5f1ed}.meta{font:500 16px Arial,sans-serif;fill:#a49a93}.label{font:600 13px Arial,sans-serif;letter-spacing:1.5px;fill:#a49a93}.value{font:700 15px Arial,sans-serif;text-anchor:end;fill:#f5f1ed}.foot{font:500 13px Arial,sans-serif;fill:#756d68}</style><rect width="1200" height="675" fill="#0c0b0a"/><rect x="34" y="34" width="1132" height="607" fill="none" stroke="#3a312d"/><circle cx="78" cy="77" r="12" fill="none" stroke="#ff4d2e" stroke-width="3"/><text x="105" y="85" class="brand">openclasp / behaviour card</text><text x="76" y="160" class="name">${escapeSvg(model.name)}</text><text x="76" y="198" class="meta">${escapeSvg(humanize(model.taskCategory))} · ${escapeSvg(humanize(model.confidence))} confidence · ${model.evidenceCount} eligible outcomes · v${escapeSvg(model.version)}</text><line x1="76" y1="228" x2="1124" y2="228" stroke="#3a312d"/>${metricRows}<line x1="76" y1="598" x2="1124" y2="598" stroke="#3a312d"/><text x="76" y="622" class="foot">STANDARD SCORECARD · AUTHENTICATED STRUCTURED OUTCOMES · NO CONVERSATION BODIES</text></svg>`;
1436
}
1437
1438
function BehaviourScorecardDialog({
1439
  open,
1440
  onOpenChange,
1441
  data,
1442
  agent,
1443
}: {
1444
  open: boolean;
1445
  onOpenChange: (open: boolean) => void;
1446
  data: DashboardData;
1447
  agent?: Record<string, any>;
1448
}) {
1449
  const [actionState, setActionState] = useState('');
1450
  useEffect(() => setActionState(''), [agent?.agentId]);
1451
  if (!agent) return null;
1452
  const model = scorecardForAgent(data, agent);
1453
  const saveCard = () => {
1454
    const blob = new Blob([scorecardSvg(model)], { type: 'image/svg+xml' });
1455
    const url = URL.createObjectURL(blob);
1456
    const anchor = document.createElement('a');
1457
    anchor.href = url;
1458
    anchor.download = `${model.name.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-')}-openclasp-card.svg`;
1459
    anchor.click();
1460
    URL.revokeObjectURL(url);
1461
    setActionState('Downloaded');
1462
  };
1463
  const shareCard = async () => {
1464
    const file = new File([scorecardSvg(model)], 'openclasp-behaviour-card.svg', {
1465
      type: 'image/svg+xml',
1466
    });
1467
    try {
1468
      if (navigator.share && navigator.canShare?.({ files: [file] })) {
1469
        await navigator.share({ title: `${model.name} behaviour card`, files: [file] });
1470
        setActionState('Shared');
1471
      } else {
1472
        saveCard();
1473
      }
1474
    } catch (reason) {
1475
      if (!(reason instanceof DOMException && reason.name === 'AbortError'))
1476
        setActionState('Failed');
1477
    }
1478
  };
1479
  const copyCard = async () => {
1480
    try {
1481
      await navigator.clipboard.writeText(scorecardSummary(model));
1482
      setActionState('Copied');
1483
    } catch {
1484
      setActionState('Copy failed');
1485
    }
1486
  };
1487
  return (
1488
    <Dialog open={open} onOpenChange={onOpenChange}>
1489
      <DialogContent className="scorecardDialog" showCloseButton>
1490
        <DialogTitle className="scorecardA11yTitle">{model.name} behavioural scorecard</DialogTitle>
1491
        <DialogDescription className="scorecardA11yTitle">
1492
          Standard OpenClasp behavioural dimensions based on verified structured outcomes.
1493
        </DialogDescription>
1494
        <article className="behaviourScorecard">
1495
          <header className="scorecardHeader">
1496
            <div className="scorecardBrand">
1497
              <ClaspMark />
1498
              <span>openclasp / behaviour card</span>
1499
            </div>
1500
            <span className={model.verified ? 'scorecardVerified' : ''}>
1501
              <ShieldCheck /> {model.verified ? 'Identity verified' : 'Identity unverified'}
1502
            </span>
1503
          </header>
1504
          <div className="scorecardIdentity">
1505
            <div>
1506
              <p>STANDARD AGENT SCORECARD</p>
1507
              <h2>{model.name}</h2>
1508
            </div>
1509
            <div className="scorecardContext">
1510
              <span>{humanize(model.taskCategory)}</span>
1511
              <span>{humanize(model.confidence)} confidence</span>
1512
              <span>{model.evidenceCount} outcomes</span>
1513
              <span>v{model.version}</span>
1514
            </div>
1515
          </div>
1516
          <div className="scorecardMetrics">
1517
            {model.metrics.map((metric) => (
1518
              <div className={metric.value === null ? 'isUnmeasured' : ''} key={metric.key}>
1519
                <span>
1520
                  <strong>{metric.label}</strong>
1521
                  <small>
1522
                    {metric.value === null
1523
                      ? 'Awaiting eligible evidence'
1524
                      : `${metric.evidence.toFixed(1)} effective samples`}
1525
                  </small>
1526
                </span>
1527
                <span className="scorecardMeter" aria-hidden="true">
1528
                  <i style={{ width: `${metric.value ?? 0}%` }} />
1529
                </span>
1530
                <b>{metric.value === null ? '—' : metric.value}</b>
1531
              </div>
1532
            ))}
1533
          </div>
1534
          <footer className="scorecardFoot">
1535
            <span>
1536
              {model.trend === 'improving' ? '↗' : model.trend === 'declining' ? '↘' : '→'}{' '}
1537
              {humanize(model.trend)} trend
1538
            </span>
1539
            <span>Updated {relativeTime(model.updatedAt)}</span>
1540
            <small>Authenticated structured outcomes · conversation bodies excluded</small>
1541
          </footer>
1542
        </article>
1543
        <div className="scorecardActions">
1544
          <span aria-live="polite">{actionState}</span>
1545
          <button type="button" onClick={() => void copyCard()}>
1546
            <Copy /> Copy summary
1547
          </button>
1548
          <button type="button" onClick={saveCard}>
1549
            <Download /> Download
1550
          </button>
1551
          <button className="primary" type="button" onClick={() => void shareCard()}>
1552
            <Share2 /> Share card
1553
          </button>
1554
        </div>
1555
      </DialogContent>
1556
    </Dialog>
1557
  );
1558
}
1559
1560
function historyForAgent(data: DashboardData, agent: Record<string, any>): AgentHistoryItem[] {
1561
  const agentId = String(agent.agentId);
1562
  const interactions = new Map<string, Record<string, any>>();
1563
  for (const interaction of [...data.interactions, ...data.federatedInteractions]) {
1564
    const participants = [
1565
      interaction.agentId,
1566
      interaction.initiatorAgentId,
1567
      interaction.responderAgentId,
1568
      ...(interaction.contract?.parties ?? []),
1569
    ];
1570
    if (participants.includes(agentId))
1571
      interactions.set(String(interaction.interactionId), interaction);
1572
  }
1573
  return [...interactions.values()]
1574
    .map((interaction) => {
1575
      const interactionId = String(interaction.interactionId);
1576
      const report = [...data.completionReports]
1577
        .reverse()
1578
        .find((item) => item.interactionId === interactionId);
1579
      const receipt = [...data.receipts]
1580
        .reverse()
1581
        .find((item) => item.interactionId === interactionId);
1582
      const conclusion = [...data.interactionConclusions]
1583
        .reverse()
1584
        .find((item) => item.interactionId === interactionId);
1585
      const counterpartId = interaction.initiatorAgentId
1586
        ? interaction.initiatorAgentId === agentId
1587
          ? interaction.responderAgentId
1588
          : interaction.initiatorAgentId
1589
        : (interaction.counterpartyAgentId ?? 'Local interaction');
1590
      const counterpart =
1591
        data.agents.find((item) => item.agentId === counterpartId)?.name ?? counterpartId;
1592
      const counterpartMode: AgentHistoryItem['counterpartMode'] = 'agent';
1593
      const rawOutcome = String(
1594
        conclusion?.lifecycle === 'provisional'
1595
          ? 'provisional'
1596
          : (conclusion?.outcome ??
1597
              receipt?.outcome ??
1598
              report?.outcome ??
1599
              (interaction.status === 'completed' ? 'success' : (interaction.status ?? 'pending'))),
1600
      );
1601
      const outcome = (
1602
        ['success', 'partial', 'failure', 'cancelled', 'provisional', 'active'].includes(rawOutcome)
1603
          ? rawOutcome
1604
          : 'pending'
1605
      ) as AgentHistoryItem['outcome'];
1606
      return {
1607
        interactionId,
1608
        title: String(interaction.contract?.purpose ?? interaction.purpose ?? 'Interaction'),
1609
        counterpart: String(counterpart),
1610
        counterpartMode,
1611
        outcome,
1612
        at: String(
1613
          report?.completedAt ??
1614
            receipt?.issuedAt ??
1615
            conclusion?.concludedAt ??
1616
            interaction.completedAt ??
1617
            interaction.updatedAt ??
1618
            interaction.createdAt ??
1619
            new Date().toISOString(),
1620
        ),
1621
      };
1622
    })
1623
    .sort((left, right) => Date.parse(right.at) - Date.parse(left.at));
1624
}
1625
1626
function OutcomeSymbol({ outcome }: { outcome: AgentHistoryItem['outcome'] }) {
1627
  if (outcome === 'success')
1628
    return (
1629
      <span className="outcomeSymbol success">
1630
        <Check />
1631
      </span>
1632
    );
1633
  if (outcome === 'failure' || outcome === 'cancelled')
1634
    return (
1635
      <span className="outcomeSymbol failure">
1636
        <X />
1637
      </span>
1638
    );
1639
  if (outcome === 'partial' || outcome === 'provisional')
1640
    return <span className="outcomeSymbol partial">½</span>;
1641
  return (
1642
    <span className="outcomeSymbol pending">
1643
      <Circle />
1644
    </span>
1645
  );
1646
}
1647
1648
function Marketplace({ data }: { data: DashboardData }) {
1649
  const cloudAgents = useMemo(
1650
    () => data.agents.filter((agent) => agent.agentMode === 'persistent_runtime'),
1651
    [data.agents],
1652
  );
1653
  const [query, setQuery] = useState('');
1654
  const [selectedAgentId, setSelectedAgentId] = useState(() =>
1655
    String(cloudAgents[0]?.agentId ?? ''),
1656
  );
1657
  const [taskCategory, setTaskCategory] = useState(() =>
1658
    String(cloudAgents[0]?.capabilities?.[0] ?? 'general'),
1659
  );
1660
  const [results, setResults] = useState<Record<string, any>[]>([]);
1661
  const [loading, setLoading] = useState(true);
1662
  const [error, setError] = useState('');
1663
1664
  useEffect(() => {
1665
    const controller = new AbortController();
1666
    const timer = window.setTimeout(() => {
1667
      const parameters = new URLSearchParams({ limit: '50' });
1668
      if (query.trim()) parameters.set('query', query.trim());
1669
      if (selectedAgentId) parameters.set('agentId', selectedAgentId);
1670
      if (taskCategory.trim()) parameters.set('taskCategory', taskCategory.trim());
1671
      remoteApi(`/v0.1/marketplace?${parameters}`, { signal: controller.signal })
1672
        .then((result) =>
1673
          setResults(
1674
            (result as Record<string, any>[]).filter(
1675
              (item) => item.card?.agentMode === 'persistent_runtime',
1676
            ),
1677
          ),
1678
        )
1679
        .catch((reason: unknown) => {
1680
          if (controller.signal.aborted) return;
1681
          setError(reason instanceof Error ? reason.message : 'Directory unavailable');
1682
          setResults(
1683
            data.agents
1684
              .filter((agent) => agent.agentMode === 'persistent_runtime')
1685
              .filter((agent) =>
1686
                data.publications.some(
1687
                  (publication) => publication.agentId === agent.agentId && publication.published,
1688
                ),
1689
              )
1690
              .filter((agent) =>
1691
                `${agent.name} ${agent.description} ${(agent.capabilities ?? []).join(' ')}`
1692
                  .toLowerCase()
1693
                  .includes(query.toLowerCase()),
1694
              )
1695
              .map((card) => ({
1696
                card,
1697
                taskCategory,
1698
                match: {
1699
                  score: 0.5,
1700
                  label: 'possible',
1701
                  reasons: ['Preview capability match', 'No verified private history'],
1702
                },
1703
                contextualReliability: data.intelligenceSummaries.find(
1704
                  (summary) =>
1705
                    summary.agentId === card.agentId &&
1706
                    summary.taskCategory.toLowerCase() === taskCategory.toLowerCase(),
1707
                ),
1708
              })),
1709
          );
1710
        })
1711
        .finally(() => {
1712
          if (!controller.signal.aborted) setLoading(false);
1713
        });
1714
    }, 220);
1715
    return () => {
1716
      window.clearTimeout(timer);
1717
      controller.abort();
1718
    };
1719
  }, [
1720
    data.agents,
1721
    data.intelligenceSummaries,
1722
    data.publications,
1723
    query,
1724
    selectedAgentId,
1725
    taskCategory,
1726
  ]);
1727
1728
  return (
1729
    <div className="workspacePage marketplacePage">
1730
      <header className="workspaceHead marketplaceHead">
1731
        <h1>Marketplace</h1>
1732
        <div className="marketFilters">
1733
          <label>
1734
            <span>For</span>
1735
            <select
1736
              value={selectedAgentId}
1737
              onChange={(event) => {
1738
                const agentId = event.target.value;
1739
                const agent = cloudAgents.find((item) => item.agentId === agentId);
1740
                setSelectedAgentId(agentId);
1741
                setTaskCategory(String(agent?.capabilities?.[0] ?? 'general'));
1742
                setLoading(true);
1743
              }}
1744
            >
1745
              {cloudAgents.map((agent) => (
1746
                <option value={agent.agentId} key={agent.agentId}>
1747
                  {agent.name ?? agent.agentId}
1748
                </option>
1749
              ))}
1750
            </select>
1751
          </label>
1752
          <label>
1753
            <span>Task</span>
1754
            <input
1755
              value={taskCategory}
1756
              onChange={(event) => {
1757
                setTaskCategory(event.target.value);
1758
                setLoading(true);
1759
              }}
1760
              placeholder="procurement"
1761
            />
1762
          </label>
1763
          <label className="marketSearch">
1764
            <Search />
1765
            <input
1766
              type="search"
1767
              value={query}
1768
              onChange={(event) => {
1769
                setLoading(true);
1770
                setError('');
1771
                setQuery(event.target.value);
1772
              }}
1773
              placeholder="Search agents"
1774
            />
1775
          </label>
1776
        </div>
1777
      </header>
1778
      <div className="marketContext">
1779
        Cloud runtimes only. Recommendations combine public capabilities with your private, verified
1780
        history; scores remain task-specific and confidence-adjusted.
1781
      </div>
1782
      {error && !results.length ? <div className="errorBar">{error}</div> : null}
1783
      <section className="marketGrid" aria-live="polite">
1784
        {loading ? (
1785
          Array.from({ length: 6 }, (_, index) => <div className="marketSkeleton" key={index} />)
1786
        ) : results.length ? (
1787
          results.map((result) => {
1788
            const agent = result.card;
1789
            const online = agent.presence?.status === 'online';
1790
            const connected =
1791
              agent.verification?.status === 'verified' && Boolean(agent.transports?.length);
1792
            const available = connected || online;
1793
            const intelligence = result.contextualReliability;
1794
            return (
1795
              <article className="marketCard" key={agent.agentId}>
1796
                <FrameCorners />
1797
                <div className="marketCardTop">
1798
                  <AgentMark name={String(agent.name ?? agent.agentId)} online={available} />
1799
                  <span className={available ? 'marketPresence online' : 'marketPresence'}>
1800
                    {connected ? 'Connected' : online ? 'Online' : 'Offline'}
1801
                  </span>
1802
                </div>
1803
                <div className={`matchBadge ${result.match.label}`}>
1804
                  <strong>{Math.round(Number(result.match.score) * 100)}%</strong>
1805
                  <span>
1806
                    {result.match.label} match for {result.taskCategory}
1807
                  </span>
1808
                </div>
1809
                <h2>{agent.name}</h2>
1810
                <p>{agent.description || 'Public OpenClasp agent'}</p>
1811
                <div className="marketTags">
1812
                  {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
1813
                    <span key={capability}>{capability}</span>
1814
                  ))}
1815
                </div>
1816
                <div className="marketMeta">
1817
                  <span>
1818
                    <ShieldCheck /> Publisher verified
1819
                  </span>
1820
                  <span>
1821
                    <Cloud /> Cloud runtime
1822
                  </span>
1823
                </div>
1824
                <div className="marketIntelligence">
1825
                  {intelligence ? (
1826
                    <>
1827
                      <strong>{Math.round(Number(intelligence.score) * 100)}%</strong>
1828
                      <span>
1829
                        contextual reliability · {intelligence.confidence.level} confidence ·{' '}
1830
                        {intelligence.confidence.evidenceCount} outcomes
1831
                      </span>
1832
                    </>
1833
                  ) : (
1834
                    <span>No verified history for this task</span>
1835
                  )}
1836
                </div>
1837
                <a
1838
                  className="marketProfileLink"
1839
                  href={agent.profileUrl ?? `/a/${encodeURIComponent(String(agent.agentId))}`}
1840
                  target="_blank"
1841
                  rel="noreferrer"
1842
                >
1843
                  View agent <ArrowRight />
1844
                </a>
1845
              </article>
1846
            );
1847
          })
1848
        ) : (
1849
          <div className="quietEmpty marketEmpty">No matching public agents</div>
1850
        )}
1851
      </section>
1852
    </div>
1853
  );
1854
}
1855
1856
function Overview({
1857
  data,
1858
  navigate,
1859
  refreshDashboard,
1860
  api,
1861
}: {
1862
  data: DashboardData;
1863
  navigate: (page: Page) => void;
1864
  refreshDashboard: () => Promise<void>;
1865
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1866
}) {
1867
  const completed = data.receipts.filter((item) => item.outcome === 'success').length;
1868
  const warnings = data.events.filter((item) =>
1869
    ['policy_warning', 'policy_violation', 'objection'].includes(String(item.eventType)),
1870
  ).length;
1871
  const publishedIds = new Set(
1872
    data.publications
1873
      .filter((publication) => publication.published)
1874
      .map((publication) => String(publication.agentId)),
1875
  );
1876
  const runtimeIds = new Set(
1877
    data.runtimes
1878
      .filter((runtime) => runtime.status === 'verified')
1879
      .map((runtime) => String(runtime.agentId)),
1880
  );
1881
  const readyAgents = data.agents.filter(
1882
    (agent) => publishedIds.has(String(agent.agentId)) && runtimeIds.has(String(agent.agentId)),
1883
  ).length;
1884
  const pendingInvitations = data.federatedInteractions.filter(
1885
    (interaction) => interaction.status === 'pending',
1886
  ).length;
1887
  const pendingFeedback = data.feedbackRequests.filter(
1888
    (request) => request.status === 'pending',
1889
  ).length;
1890
  const pendingSetup = data.setupRequests.filter((request) => request.status === 'pending').length;
1891
  const reviewCount = warnings + pendingInvitations + pendingFeedback;
1892
  return (
1893
    <>
1894
      <PageHead page="dashboard" action="Connect provider" onAction={() => navigate('connect')} />
1895
      {pendingSetup > 0 && (
1896
        <button className="attentionBanner" type="button" onClick={() => navigate('connect')}>
1897
          <div>
1898
            <span className="statusIndex">//</span>
1899
            <div>
1900
              <strong>
1901
                {pendingSetup} setup request{pendingSetup === 1 ? '' : 's'} waiting
1902
              </strong>
1903
            </div>
1904
          </div>
1905
        </button>
1906
      )}
1907
      {!(readyAgents === data.agents.length && readyAgents) && (
1908
        <section className="readiness">
1909
          <div>
1910
            <span className="statusIndex">!</span>
1911
            <div>
1912
              <strong>
1913
                {readyAgents} of {data.agents.length} agents can receive A2A work
1914
              </strong>
1915
            </div>
1916
          </div>
1917
          <button className="secondary" type="button" onClick={() => navigate('agents')}>
1918
            Manage
1919
          </button>
1920
        </section>
1921
      )}
1922
      <section className="metrics">
1923
        <Metric
1924
          label="Connected agents"
1925
          value={runtimeIds.size}
1926
          note={`${readyAgents} published and ready`}
1927
        />
1928
        <Metric
1929
          label="Interactions"
1930
          value={data.interactions.length + data.federatedInteractions.length}
1931
          note="local and shared"
1932
        />
1933
        <Metric label="Successful outcomes" value={completed} note="receipt-backed" />
1934
        <Metric
1935
          label="Needs review"
1936
          value={reviewCount}
1937
          note={`${pendingFeedback} feedback · ${pendingInvitations} invitations`}
1938
          warn={reviewCount > 0}
1939
        />
1940
      </section>
1941
      <Invitations data={data} refreshDashboard={refreshDashboard} api={api} />
1942
      <section className="contentGrid">
1943
        <Panel title="Recent activity" subtitle="">
1944
          <Timeline events={data.events.slice(-6).reverse()} />
1945
          <TextButton onClick={() => navigate('history')}>All history</TextButton>
1946
        </Panel>
1947
        <Panel title="Reliability" subtitle="">
1948
          {data.profiles.length ? (
1949
            data.profiles.slice(0, 4).map((profile) => {
1950
              const deltas = data.profileDeltas.filter(
1951
                (delta) =>
1952
                  delta.agentId === profile.agentId &&
1953
                  delta.agentVersion === profile.agentVersion &&
1954
                  delta.taskCategory === profile.taskCategory,
1955
              );
1956
              return (
1957
                <Profile
1958
                  key={`${profile.agentId}-${profile.agentVersion}-${profile.taskCategory}`}
1959
                  profile={profile}
1960
                  deltas={deltas}
1961
                />
1962
              );
1963
            })
1964
          ) : (
1965
            <Empty
1966
              title="No reliability profile yet"
1967
              text="Profiles appear after signed receipts and eligible bilateral feedback."
1968
            />
1969
          )}
1970
        </Panel>
1971
      </section>
1972
    </>
1973
  );
1974
}
1975
1976
function History({
1977
  data,
1978
  refreshDashboard,
1979
  api,
1980
}: {
1981
  data: DashboardData;
1982
  refreshDashboard: () => Promise<void>;
1983
  api: (path: string, init?: RequestInit) => Promise<unknown>;
1984
}) {
1985
  const [query, setQuery] = useState('');
1986
  const [status, setStatus] = useState<
1987
    'all' | 'pending' | 'active' | 'provisional' | 'finalizing' | 'completed'
1988
  >('all');
1989
  const journeys = useMemo(() => interactionJourneys(data), [data]);
1990
  const filtered = useMemo(() => {
1991
    const needle = query.trim().toLowerCase();
1992
    return journeys.filter((journey) => {
1993
      if (status !== 'all' && journey.status !== status) return false;
1994
      if (!needle) return true;
1995
      const hay = [
1996
        journey.interactionId,
1997
        journey.purpose,
1998
        journey.taskCategory,
1999
        journey.status,
2000
        ...journey.participants,
2001
      ]
2002
        .filter(Boolean)
2003
        .join(' ')
2004
        .toLowerCase();
2005
      return hay.includes(needle);
2006
    });
2007
  }, [journeys, query, status]);
2008
  const [selectedId, setSelectedId] = useState('');
2009
  const selected = filtered.find((journey) => journey.interactionId === selectedId) ?? filtered[0];
2010
  return (
2011
    <>
2012
      <PageHead page="history" />
2013
      <div className="historyToolbar">
2014
        <Input
2015
          value={query}
2016
          onChange={(event) => setQuery(event.target.value)}
2017
          placeholder="Search purpose, category, or agent"
2018
          aria-label="Search interaction history"
2019
          className="h-9 min-w-[220px] flex-1 rounded-none border-0 border-b bg-transparent px-0 shadow-none dark:bg-transparent"
2020
        />
2021
        <Tabs
2022
          value={status === 'finalizing' ? 'all' : status}
2023
          onValueChange={(value) =>
2024
            setStatus(value as 'all' | 'pending' | 'active' | 'provisional' | 'completed')
2025
          }
2026
        >
2027
          <TabsList variant="line" aria-label="Interaction status filters">
2028
            {(['all', 'pending', 'active', 'provisional', 'completed'] as const).map((value) => (
2029
              <TabsTrigger key={value} value={value} className="capitalize">
2030
                {value}
2031
                <span className="font-mono text-[10px] text-muted-foreground">
2032
                  {value === 'all'
2033
                    ? journeys.length
2034
                    : journeys.filter((item) => item.status === value).length}
2035
                </span>
2036
              </TabsTrigger>
2037
            ))}
2038
          </TabsList>
2039
        </Tabs>
2040
      </div>
2041
      {filtered.length ? (
2042
        <section className="journeyLayout">
2043
          <div className="journeyList" aria-label="Interactions">
2044
            {filtered.map((journey) => (
2045
              <button
2046
                key={journey.interactionId}
2047
                className={selected?.interactionId === journey.interactionId ? 'active' : ''}
2048
                type="button"
2049
                onClick={() => {
2050
                  setSelectedId(journey.interactionId);
2051
                  if (matchMedia('(max-width: 980px)').matches)
2052
                    requestAnimationFrame(() =>
2053
                      document
2054
                        .querySelector('.journeyDetail')
2055
                        ?.scrollIntoView({ behavior: 'smooth' }),
2056
                    );
2057
                }}
2058
              >
2059
                <span className="journeyListTop">
2060
                  <b>{journey.taskCategory}</b>
2061
                  <StatusPill value={journey.status} />
2062
                </span>
2063
                <strong>{journey.purpose}</strong>
2064
                <small>{journey.participants.join(' ↔ ') || 'Participants unavailable'}</small>
2065
                <span className="journeyListMeta">
2066
                  {relativeTime(journey.updatedAt)}
2067
                  <i>{journey.stepCount} recorded steps</i>
2068
                </span>
2069
              </button>
2070
            ))}
2071
          </div>
2072
          {selected ? (
2073
            <InteractionJourney journey={selected} refreshDashboard={refreshDashboard} api={api} />
2074
          ) : null}
2075
        </section>
2076
      ) : (
2077
        <section className="panel">
2078
          <Empty
2079
            title={journeys.length ? 'No matching interactions' : 'No interactions recorded'}
2080
            text={
2081
              journeys.length
2082
                ? 'Clear the search or choose another status.'
2083
                : 'Completed contracts, outcomes, feedback, and learning will appear as one readable journey.'
2084
            }
2085
          />
2086
        </section>
2087
      )}
2088
    </>
2089
  );
2090
}
2091
2092
type InteractionJourneyModel = {
2093
  interactionId: string;
2094
  purpose: string;
2095
  taskCategory: string;
2096
  status: string;
2097
  participants: string[];
2098
  updatedAt: string;
2099
  stepCount: number;
2100
  interaction: Record<string, any>;
2101
  session?: Record<string, any>;
2102
  events: Record<string, any>[];
2103
  briefs: Record<string, any>[];
2104
  reports: Record<string, any>[];
2105
  feedbackRequests: Record<string, any>[];
2106
  feedback: Record<string, any>[];
2107
  conclusion?: Record<string, any>;
2108
  receipt?: Record<string, any>;
2109
  eligibility?: Record<string, any>;
2110
  deltas: Record<string, any>[];
2111
  conflicts: Record<string, any>[];
2112
  assuranceAssessments: Record<string, any>[];
2113
  assurancePredictions: Record<string, any>[];
2114
  assuranceSafeguards: Record<string, any>[];
2115
  assuranceEvaluations: Record<string, any>[];
2116
  assuranceProbePlans: Record<string, any>[];
2117
  assuranceProbeResponses: Record<string, any>[];
2118
};
2119
2120
function interactionJourneys(data: DashboardData): InteractionJourneyModel[] {
2121
  const ids = new Set<string>();
2122
  const collections = [
2123
    data.interactions,
2124
    data.federatedInteractions,
2125
    data.liveSessions,
2126
    data.events,
2127
    data.counterpartyBriefs,
2128
    data.completionReports,
2129
    data.feedbackRequests,
2130
    data.interactionFeedback,
2131
    data.interactionConclusions,
2132
    data.receipts,
2133
    data.learningEligibility,
2134
    data.profileDeltas,
2135
    data.conflicts,
2136
    data.assuranceAssessments,
2137
    data.assurancePredictions,
2138
    data.assuranceSafeguards,
2139
    data.assuranceEvaluations,
2140
    data.assuranceProbePlans,
2141
    data.assuranceProbeResponses,
2142
  ];
2143
  for (const collection of collections)
2144
    for (const item of collection) if (item.interactionId) ids.add(String(item.interactionId));
2145
  return [...ids]
2146
    .map((interactionId) => {
2147
      const interaction = data.federatedInteractions.find(
2148
        (item) => item.interactionId === interactionId,
2149
      ) ??
2150
        data.interactions.find((item) => item.interactionId === interactionId) ?? { interactionId };
2151
      const session = data.liveSessions.find((item) => item.interactionId === interactionId);
2152
      const events = data.events.filter((item) => item.interactionId === interactionId);
2153
      const briefs = data.counterpartyBriefs.filter((item) => item.interactionId === interactionId);
2154
      const reports = data.completionReports.filter((item) => item.interactionId === interactionId);
2155
      const feedbackRequests = data.feedbackRequests.filter(
2156
        (item) => item.interactionId === interactionId,
2157
      );
2158
      const feedback = data.interactionFeedback.filter(
2159
        (item) => item.interactionId === interactionId,
2160
      );
2161
      const conclusion = data.interactionConclusions.find(
2162
        (item) => item.interactionId === interactionId,
2163
      );
2164
      const receipt = data.receipts.find((item) => item.interactionId === interactionId);
2165
      const eligibility = data.learningEligibility.find(
2166
        (item) => item.interactionId === interactionId,
2167
      );
2168
      const deltas = data.profileDeltas.filter((item) => item.interactionId === interactionId);
2169
      const conflicts = data.conflicts.filter((item) => item.interactionId === interactionId);
2170
      const assuranceAssessments = data.assuranceAssessments.filter(
2171
        (item) => item.interactionId === interactionId,
2172
      );
2173
      const assurancePredictions = data.assurancePredictions.filter(
2174
        (item) => item.interactionId === interactionId,
2175
      );
2176
      const assuranceSafeguards = data.assuranceSafeguards.filter(
2177
        (item) => item.interactionId === interactionId,
2178
      );
2179
      const assuranceEvaluations = data.assuranceEvaluations.filter(
2180
        (item) => item.interactionId === interactionId,
2181
      );
2182
      const assuranceProbePlans = data.assuranceProbePlans.filter(
2183
        (item) => item.interactionId === interactionId,
2184
      );
2185
      const assuranceProbeResponses = data.assuranceProbeResponses.filter(
2186
        (item) => item.interactionId === interactionId,
2187
      );
2188
      const participants = [
2189
        interaction.initiatorAgentId,
2190
        interaction.responderAgentId,
2191
        interaction.agentId,
2192
        ...reports.flatMap((report) => [report.reportingAgentId, report.counterpartyAgentId]),
2193
      ]
2194
        .filter((value): value is string => typeof value === 'string' && Boolean(value))
2195
        .filter((value, index, values) => values.indexOf(value) === index);
2196
      const dates = [
2197
        interaction.updatedAt,
2198
        interaction.createdAt,
2199
        session?.completedAt,
2200
        session?.activatedAt,
2201
        ...events.map(timestamp),
2202
        ...reports.map(timestamp),
2203
        ...feedback.map(timestamp),
2204
        ...assuranceAssessments.map(timestamp),
2205
        ...assurancePredictions.map(timestamp),
2206
        conclusion?.generatedAt,
2207
        receipt?.completedAt,
2208
        eligibility?.decidedAt,
2209
      ].filter((value): value is string => typeof value === 'string');
2210
      return {
2211
        interactionId,
2212
        purpose: String(
2213
          interaction.contract?.purpose ??
2214
            interaction.contract?.requestedOutcome ??
2215
            conclusion?.summary ??
2216
            'Assured agent interaction',
2217
        ),
2218
        taskCategory: String(interaction.contract?.taskCategory ?? 'general'),
2219
        status: String(
2220
          conclusion || receipt
2221
            ? conclusion?.lifecycle === 'provisional' || receipt?.provisional
2222
              ? 'provisional'
2223
              : 'completed'
2224
            : reports.length > 0
2225
              ? 'finalizing'
2226
              : (session?.status ?? interaction.status ?? 'pending'),
2227
        ),
2228
        participants,
2229
        updatedAt:
2230
          dates.sort((left, right) => Date.parse(right) - Date.parse(left))[0] ??
2231
          new Date(0).toISOString(),
2232
        stepCount:
2233
          1 +
2234
          Math.max(0, Number(interaction.contractRevisions?.length ?? 0) - 1) +
2235
          events.length +
2236
          briefs.length +
2237
          reports.length +
2238
          feedbackRequests.length +
2239
          feedback.length +
2240
          conflicts.length +
2241
          Number(Boolean(conclusion)) +
2242
          Number(Boolean(receipt)) +
2243
          Number(Boolean(eligibility)) +
2244
          deltas.length +
2245
          assuranceAssessments.length +
2246
          assuranceProbeResponses.length +
2247
          assuranceSafeguards.filter((item) => item.status !== 'recommended').length,
2248
        interaction,
2249
        ...(session ? { session } : {}),
2250
        events,
2251
        briefs,
2252
        reports,
2253
        feedbackRequests,
2254
        feedback,
2255
        ...(conclusion ? { conclusion } : {}),
2256
        ...(receipt ? { receipt } : {}),
2257
        ...(eligibility ? { eligibility } : {}),
2258
        deltas,
2259
        conflicts,
2260
        assuranceAssessments,
2261
        assurancePredictions,
2262
        assuranceSafeguards,
2263
        assuranceEvaluations,
2264
        assuranceProbePlans,
2265
        assuranceProbeResponses,
2266
      };
2267
    })
2268
    .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
2269
}
2270
2271
function InteractionJourney({
2272
  journey,
2273
  refreshDashboard,
2274
  api,
2275
}: {
2276
  journey: InteractionJourneyModel;
2277
  refreshDashboard: () => Promise<void>;
2278
  api: (path: string, init?: RequestInit) => Promise<unknown>;
2279
}) {
2280
  const [assuranceWorking, setAssuranceWorking] = useState('');
2281
  const [assuranceError, setAssuranceError] = useState('');
2282
  const timeline = interactionTimeline(journey);
2283
  const conclusion = journey.conclusion;
2284
  const checkpoints = journey.events.filter(
2285
    (event) => (event.type ?? event.eventType) === 'progress_checkpoint' && event.checkpoint,
2286
  );
2287
  const latestCheckpoint = checkpoints.at(-1)?.checkpoint;
2288
  const progress = Number(latestCheckpoint?.progress ?? 0);
2289
  const submittedFeedback = new Set(
2290
    journey.feedback.map((item) => item.reviewerAgentId).filter(Boolean),
2291
  ).size;
2292
  const latestPrediction = journey.assurancePredictions.at(-1);
2293
  const latestAssessment = journey.assuranceAssessments.at(-1);
2294
  const activeSafeguards = journey.assuranceSafeguards.filter(
2295
    (item) => item.status === 'recommended',
2296
  );
2297
  const latestPlan = journey.assuranceProbePlans.at(-1);
2298
  const latestProbe = latestPlan?.questions?.[0];
2299
  const latestProbeResponse = journey.assuranceProbeResponses.find(
2300
    (item) => item.planId === latestPlan?.planId,
2301
  );
2302
  const latestEvaluation = journey.assuranceEvaluations.at(-1);
2303
  const averageBrier = latestEvaluation?.predictionScores?.length
2304
    ? latestEvaluation.predictionScores.reduce(
2305
        (sum: number, item: Record<string, any>) => sum + Number(item.brierScore ?? 0),
2306
        0,
2307
      ) / latestEvaluation.predictionScores.length
2308
    : undefined;
2309
  const decideSafeguard = async (
2310
    safeguard: Record<string, any>,
2311
    status: 'accepted' | 'rejected',
2312
  ) => {
2313
    const assessment =
2314
      journey.assuranceAssessments.find((item) => item.assessmentId === safeguard.assessmentId) ??
2315
      latestAssessment;
2316
    if (!assessment?.generatedForAgentId) {
2317
      setAssuranceError('The requesting agent for this safeguard is unavailable.');
2318
      return;
2319
    }
2320
    setAssuranceWorking(String(safeguard.safeguardId));
2321
    setAssuranceError('');
2322
    try {
2323
      await api(
2324
        `/v0.1/federated-interactions/${encodeURIComponent(journey.interactionId)}/assurance-safeguards/${encodeURIComponent(String(safeguard.safeguardId))}/decision`,
2325
        {
2326
          method: 'POST',
2327
          body: JSON.stringify({ status, agentId: assessment.generatedForAgentId }),
2328
        },
2329
      );
2330
      await refreshDashboard();
2331
    } catch (reason) {
2332
      setAssuranceError(
2333
        reason instanceof Error ? reason.message : 'Could not update the safeguard decision',
2334
      );
2335
    } finally {
2336
      setAssuranceWorking('');
2337
    }
2338
  };
2339
  return (
2340
    <article className="journeyDetail">
2341
      <header className="journeyHero">
2342
        <div>
2343
          <span className="eyebrow">{journey.taskCategory}</span>
2344
          <h2>{journey.purpose}</h2>
2345
          <p>{journey.participants.join(' ↔ ')}</p>
2346
        </div>
2347
        <StatusPill value={conclusion?.outcome ?? journey.status} />
2348
      </header>
2349
2350
      <div className="journeySummaryGrid">
2351
        <section>
2352
          <span>Progress</span>
2353
          <strong>
2354
            {latestCheckpoint
2355
              ? `${Math.round(progress * 100)}% · ${humanize(latestCheckpoint.state)}`
2356
              : journey.reports.length
2357
                ? 'One side ended'
2358
                : 'Awaiting checkpoint'}
2359
          </strong>
2360
          <small>
2361
            {latestCheckpoint
2362
              ? `${humanize(latestCheckpoint.topicStatus)} · ${Math.round(Number(latestCheckpoint.confidence ?? 0) * 100)}% confidence`
2363
              : 'Agents report compact progress every few exchanges'}
2364
          </small>
2365
        </section>
2366
        <section>
2367
          <span>Outcome</span>
2368
          <strong>
2369
            {conclusion
2370
              ? humanize(conclusion.outcome)
2371
              : `${journey.reports.length}/2 terminal reports`}
2372
          </strong>
2373
          <small>
2374
            {conclusion
2375
              ? conclusion.lifecycle === 'provisional'
2376
                ? `${Math.round(Number(conclusion.confidence ?? 0) * 100)}% confidence · peer ${humanize(conclusion.peerReportStatus ?? 'awaiting').toLowerCase()}`
2377
                : humanize(conclusion.consensus)
2378
              : journey.reports.length === 1
2379
                ? 'One agent finished; confirming with the peer'
2380
                : 'Conversation remains active'}
2381
          </small>
2382
        </section>
2383
        <section>
2384
          <span>Sealed feedback</span>
2385
          <strong>
2386
            {journey.feedbackRequests.some((request) => request.status === 'pending')
2387
              ? `${submittedFeedback}/2 sealed`
2388
              : 'Window closed'}
2389
          </strong>
2390
          <small>{feedbackState(journey.feedbackRequests)}</small>
2391
        </section>
2392
        <section>
2393
          <span>Learning</span>
2394
          <strong>
2395
            {journey.eligibility
2396
              ? journey.eligibility.eligible
2397
                ? `${Math.round(Number(journey.eligibility.sampleWeight ?? 0) * 100)}% weight`
2398
                : 'Not eligible'
2399
              : conclusion?.lifecycle === 'provisional'
2400
                ? 'Pending corroboration'
2401
                : 'Not assessed'}
2402
          </strong>
2403
          <small>{humanize(journey.eligibility?.contributionMode ?? 'Local only')}</small>
2404
        </section>
2405
      </div>
2406
2407
      {latestPrediction ? (
2408
        <section className="progressCard">
2409
          <div className="progressCardTop">
2410
            <div>
2411
              <span className="eyebrow">AI ASSURANCE · EXPERIMENTAL</span>
2412
              <h3>
2413
                {Math.round(Number(latestPrediction.successProbability) * 100)}% likely to complete
2414
              </h3>
2415
            </div>
2416
            <StatusPill value={latestPrediction.basis ?? 'experimental'} />
2417
          </div>
2418
          <div className="progressFacts">
2419
            <span>{Math.round(Number(latestPrediction.confidence ?? 0) * 100)}% confidence</span>
2420
            <span>{humanize(latestPrediction.stage ?? 'baseline')} prediction</span>
2421
            <span>{latestAssessment?.risks?.length ?? 0} material risks</span>
2422
            <span>
2423
              {journey.assuranceProbeResponses.length}/{journey.assuranceProbePlans.length} probes
2424
              answered
2425
            </span>
2426
            <span>{activeSafeguards.length} safeguards awaiting decision</span>
2427
          </div>
2428
          <div className="assuranceDecisionGrid">
2429
            <section>
2430
              <span className="eyebrow">TOP RISKS</span>
2431
              {latestAssessment?.risks?.slice(0, 3).map((risk: Record<string, any>) => (
2432
                <div className="assuranceRisk" key={risk.riskCode}>
2433
                  <strong>{risk.title}</strong>
2434
                  <small>
2435
                    {Math.round(Number(risk.likelihood ?? 0) * 100)}% likelihood ·{' '}
2436
                    {Math.round(Number(risk.impact ?? 0) * 100)}% impact
2437
                  </small>
2438
                </div>
2439
              ))}
2440
            </section>
2441
            <section>
2442
              <span className="eyebrow">SELECTED QUESTION · ROUND {latestPlan?.round ?? 1}</span>
2443
              {latestProbe ? (
2444
                <div className="assuranceProbe">
2445
                  <strong>{latestProbe.prompt}</strong>
2446
                  <small>
2447
                    {latestProbeResponse
2448
                      ? `Answered · prediction updated`
2449
                      : `Awaiting ${latestAssessment?.targetAgentId ?? 'counterparty'} response`}
2450
                  </small>
2451
                </div>
2452
              ) : (
2453
                <small>No probe generated.</small>
2454
              )}
2455
            </section>
2456
          </div>
2457
          {activeSafeguards.length ? (
2458
            <div className="assuranceSafeguards">
2459
              <span className="eyebrow">RECOMMENDED SAFEGUARDS</span>
2460
              {activeSafeguards.map((safeguard) => (
2461
                <div className="assuranceSafeguard" key={safeguard.safeguardId}>
2462
                  <div>
2463
                    <strong>{safeguard.description}</strong>
2464
                    <small>{safeguard.rationale}</small>
2465
                  </div>
2466
                  <div className="assuranceDecisionActions">
2467
                    <button
2468
                      type="button"
2469
                      disabled={assuranceWorking === safeguard.safeguardId}
2470
                      onClick={() => void decideSafeguard(safeguard, 'rejected')}
2471
                    >
2472
                      reject
2473
                    </button>
2474
                    <button
2475
                      className="approve"
2476
                      type="button"
2477
                      disabled={assuranceWorking === safeguard.safeguardId}
2478
                      onClick={() => void decideSafeguard(safeguard, 'accepted')}
2479
                    >
2480
                      accept
2481
                    </button>
2482
                  </div>
2483
                </div>
2484
              ))}
2485
            </div>
2486
          ) : null}
2487
          {averageBrier !== undefined ? (
2488
            <p className="assuranceCalibration">
2489
              Outcome scored · Brier error {averageBrier.toFixed(3)} · lower is better
2490
            </p>
2491
          ) : null}
2492
          {assuranceError ? (
2493
            <p className="progressWarning" role="alert">
2494
              {assuranceError}
2495
            </p>
2496
          ) : null}
2497
          <p className="assuranceNotice">
2498
            Advisory, not a guarantee. Accepted safeguards that change terms require a contract
2499
            revision.
2500
          </p>
2501
        </section>
2502
      ) : null}
2503
2504
      {latestCheckpoint ? (
2505
        <section className="progressCard">
2506
          <div className="progressCardTop">
2507
            <div>
2508
              <span className="eyebrow">LATEST PRIVATE CHECKPOINT</span>
2509
              <h3>{humanize(latestCheckpoint.state)}</h3>
2510
            </div>
2511
            <strong>{Math.round(progress * 100)}%</strong>
2512
          </div>
2513
          <div
2514
            className="progressTrack"
2515
            role="progressbar"
2516
            aria-label="Interaction progress"
2517
            aria-valuemin={0}
2518
            aria-valuemax={100}
2519
            aria-valuenow={Math.round(progress * 100)}
2520
          >
2521
            <i style={{ width: `${Math.round(progress * 100)}%` }} />
2522
          </div>
2523
          <div className="progressFacts">
2524
            <span>{latestCheckpoint.criteriaMet?.length ?? 0} criteria met</span>
2525
            <span>{latestCheckpoint.criteriaRemaining?.length ?? 0} remaining</span>
2526
            <span>{latestCheckpoint.expectedRemainingTurns ?? '—'} expected turns</span>
2527
            <span>
2528
              {latestCheckpoint.needsHuman ? 'Human input needed' : 'No human input needed'}
2529
            </span>
2530
          </div>
2531
          {latestCheckpoint.topicStatus !== 'in_scope' ? (
2532
            <p className="progressWarning">
2533
              Topic {humanize(latestCheckpoint.topicStatus)}. Amend the contract or start a new
2534
              interaction before continuing.
2535
            </p>
2536
          ) : null}
2537
        </section>
2538
      ) : null}
2539
2540
      {conclusion ? (
2541
        <section className="outcomeCard">
2542
          <div>
2543
            <span className="eyebrow">
2544
              {conclusion.lifecycle === 'provisional' ? 'PROVISIONAL INSIGHT' : 'WHAT HAPPENED'}
2545
            </span>
2546
            <h3>{conclusion.summary}</h3>
2547
            {conclusion.lifecycle === 'provisional' ? (
2548
              <p className="progressWarning">
2549
                Based on {conclusion.reportIds?.length ?? 1} participant report. Missing:{' '}
2550
                {(conclusion.missingReportAgentIds ?? []).join(', ') || 'peer report'}. This is
2551
                usable now at reduced confidence and will be revised if the peer responds.
2552
              </p>
2553
            ) : null}
2554
          </div>
2555
          {!!conclusion.criteria?.length && (
2556
            <ul>
2557
              {conclusion.criteria.map((criterion: Record<string, any>) => (
2558
                <li key={criterion.criterion}>
2559
                  <StatusPill value={String(criterion.status)} />
2560
                  <span>{criterion.criterion}</span>
2561
                </li>
2562
              ))}
2563
            </ul>
2564
          )}
2565
          {!!Object.keys(conclusion.averageRatings ?? {}).length && (
2566
            <div className="ratingGrid">
2567
              {Object.entries(conclusion.averageRatings).map(([label, value]) => (
2568
                <Meter key={label} label={humanize(label)} value={Number(value)} />
2569
              ))}
2570
            </div>
2571
          )}
2572
        </section>
2573
      ) : (
2574
        <div className="journeyNotice">
2575
          {journey.reports.length === 1
2576
            ? 'Finalizing: one agent submitted its terminal report. OpenClasp has requested independent confirmation from the peer.'
2577
            : journey.reports.length >= 2
2578
              ? 'Both agents finished. Sealed feedback remains private until both respond or the feedback window expires.'
2579
              : 'Active interaction. Progress checkpoints appear after several meaningful exchanges; either agent can finalize immediately when the task is done.'}
2580
        </div>
2581
      )}
2582
2583
      <section className="journeyTimelineSection">
2584
        <div>
2585
          <h3>Interaction timeline</h3>
2586
          <small>Contract → session → outcome → feedback → learning</small>
2587
        </div>
2588
        <ol className="journeyTimeline">
2589
          {timeline.map((step, index) => (
2590
            <li key={`${step.at}-${step.title}-${index}`}>
2591
              <span className={`journeyNode ${step.tone}`} />
2592
              <div>
2593
                <span>{formatDate(step.at)}</span>
2594
                <strong>{step.title}</strong>
2595
                <p>{step.detail}</p>
2596
              </div>
2597
              <StatusPill value={step.status} />
2598
            </li>
2599
          ))}
2600
        </ol>
2601
      </section>
2602
    </article>
2603
  );
2604
}
2605
2606
function interactionTimeline(journey: InteractionJourneyModel) {
2607
  const revisions = journey.interaction.contractRevisions ?? [];
2608
  const contractSteps = revisions.length
2609
    ? revisions.map((revision: Record<string, any>) => ({
2610
        at: String(revision.updatedAt ?? revision.createdAt ?? journey.interaction.createdAt),
2611
        title: `Contract v${revision.revision} ${humanize(revision.status)}`,
2612
        detail: `${revision.proposedByAgentId} · ${Object.keys(revision.acceptances ?? {}).length}/${journey.participants.length || 2} accepted · ${String(revision.termsHash).slice(0, 12)}…`,
2613
        status: String(revision.status),
2614
        tone:
2615
          revision.status === 'accepted'
2616
            ? 'good'
2617
            : revision.status === 'rejected'
2618
              ? 'danger'
2619
              : revision.status === 'proposed'
2620
                ? 'warn'
2621
                : 'neutral',
2622
      }))
2623
    : [
2624
        {
2625
          at: String(journey.interaction.createdAt ?? journey.updatedAt),
2626
          title: 'Contract proposed',
2627
          detail: `${journey.participants.length || 2} participants · ${journey.taskCategory}`,
2628
          status: String(journey.interaction.status ?? 'recorded'),
2629
          tone: 'neutral',
2630
        },
2631
        ...Object.values(journey.interaction.acceptances ?? {}).map((acceptance: any) => ({
2632
          at: String(acceptance.acceptedAt ?? journey.interaction.updatedAt ?? journey.updatedAt),
2633
          title: `Contract accepted by ${acceptance.agentId}`,
2634
          detail: `Acceptance method: ${humanize(acceptance.method ?? 'recorded')}`,
2635
          status: 'accepted',
2636
          tone: 'good',
2637
        })),
2638
      ];
2639
  const steps: { at: string; title: string; detail: string; status: string; tone: string }[] = [
2640
    ...contractSteps,
2641
    ...journey.briefs.map((brief) => ({
2642
      at: String(brief.generatedAt ?? journey.updatedAt),
2643
      title: `Private counterparty brief for ${brief.recipientAgentId}`,
2644
      detail: `${brief.relevantSampleSize ?? 0} relevant samples · ${Math.round(Number(brief.historyConfidence ?? 0) * 100)}% history confidence`,
2645
      status: String(brief.decision ?? 'ready'),
2646
      tone: brief.decision === 'DENY' ? 'danger' : brief.decision === 'CHALLENGE' ? 'warn' : 'good',
2647
    })),
2648
    ...(journey.session?.activatedAt
2649
      ? [
2650
          {
2651
            at: String(journey.session.activatedAt),
2652
            title: 'A2A session activated',
2653
            detail:
2654
              'Scoped credentials were issued. Agent runtimes exchange conversation content directly.',
2655
            status: 'active',
2656
            tone: 'good',
2657
          },
2658
        ]
2659
      : []),
2660
    ...journey.events.map((event) => {
2661
      const eventType = String(event.type ?? event.eventType ?? 'structured_event');
2662
      const checkpoint = event.checkpoint;
2663
      return {
2664
        at: timestamp(event),
2665
        title:
2666
          eventType === 'progress_checkpoint'
2667
            ? `${Math.round(Number(checkpoint?.progress ?? 0) * 100)}% progress checkpoint`
2668
            : humanize(eventType),
2669
        detail:
2670
          eventType === 'progress_checkpoint'
2671
            ? `${event.agentId} · ${humanize(checkpoint?.state ?? 'active')} · ${humanize(checkpoint?.topicStatus ?? 'in_scope')}`
2672
            : String(event.agentId ?? 'OpenClasp') + ' submitted structured metadata.',
2673
        status: String(checkpoint?.state ?? event.visibility ?? 'recorded'),
2674
        tone:
2675
          checkpoint?.topicStatus === 'changed' || checkpoint?.state === 'blocked'
2676
            ? 'warn'
2677
            : eventType.includes('violation')
2678
              ? 'danger'
2679
              : eventType.includes('warning')
2680
                ? 'warn'
2681
                : checkpoint
2682
                  ? 'good'
2683
                  : 'neutral',
2684
      };
2685
    }),
2686
    ...journey.reports.map((report) => ({
2687
      at: timestamp(report),
2688
      title: `Completion reported by ${report.reportingAgentId}`,
2689
      detail: String(report.summary ?? 'Structured completion report submitted.'),
2690
      status: String(report.outcome ?? 'reported'),
2691
      tone:
2692
        report.outcome === 'failure' ? 'danger' : report.outcome === 'partial' ? 'warn' : 'good',
2693
    })),
2694
    ...journey.feedback.map((item) => ({
2695
      at: timestamp(item),
2696
      title: `Feedback submitted by ${item.reviewerAgentId}`,
2697
      detail: `Would work again: ${humanize(item.wouldWorkAgain ?? 'unsure')} · private comment concealed`,
2698
      status: 'sealed',
2699
      tone: 'neutral',
2700
    })),
2701
    ...journey.feedbackRequests
2702
      .filter((request) => request.status !== 'submitted')
2703
      .map((request) => ({
2704
        at: String(request.requestedAt ?? journey.updatedAt),
2705
        title: `Feedback request ${humanize(request.status)}`,
2706
        detail:
2707
          request.status === 'pending'
2708
            ? `Response due ${formatDate(String(request.dueAt))}`
2709
            : 'The bilateral conclusion continued without this response.',
2710
        status: String(request.status),
2711
        tone: request.status === 'pending' ? 'warn' : 'neutral',
2712
      })),
2713
    ...journey.conflicts.map((conflict) => ({
2714
      at: timestamp(conflict),
2715
      title: 'Dispute recorded',
2716
      detail: String(conflict.summary ?? 'The interaction has a structured dispute record.'),
2717
      status: String(conflict.status ?? 'open'),
2718
      tone: conflict.status === 'resolved' ? 'good' : 'danger',
2719
    })),
2720
    ...(journey.conclusion
2721
      ? [
2722
          {
2723
            at: String(journey.conclusion.generatedAt),
2724
            title:
2725
              journey.conclusion.lifecycle === 'provisional'
2726
                ? 'Provisional insight generated'
2727
                : 'Conclusion released',
2728
            detail: String(journey.conclusion.summary),
2729
            status: String(journey.conclusion.consensus),
2730
            tone:
2731
              journey.conclusion.lifecycle === 'provisional' ||
2732
              journey.conclusion.consensus === 'conflicting'
2733
                ? 'warn'
2734
                : 'good',
2735
          },
2736
        ]
2737
      : []),
2738
    ...(journey.receipt
2739
      ? [
2740
          {
2741
            at: timestamp(journey.receipt),
2742
            title: journey.receipt.provisional
2743
              ? 'Provisional outcome attested'
2744
              : 'Outcome receipt attested',
2745
            detail: journey.receipt.provisional
2746
              ? 'The available one-sided report was sealed without claiming peer agreement.'
2747
              : 'Contract result, commitment status, and evidence hashes were sealed.',
2748
            status: String(journey.receipt.outcome ?? 'signed'),
2749
            tone: journey.receipt.outcome === 'failure' ? 'danger' : 'good',
2750
          },
2751
        ]
2752
      : []),
2753
    ...(journey.eligibility
2754
      ? [
2755
          {
2756
            at: String(journey.eligibility.decidedAt),
2757
            title: journey.eligibility.eligible
2758
              ? 'Eligible history applied'
2759
              : 'History excluded from profile',
2760
            detail: `${Math.round(Number(journey.eligibility.sampleWeight ?? 0) * 100)}% sample weight · ${humanize(journey.eligibility.contributionMode)}`,
2761
            status: journey.eligibility.eligible ? 'attested' : 'excluded',
2762
            tone: journey.eligibility.eligible ? 'good' : 'warn',
2763
          },
2764
        ]
2765
      : []),
2766
  ];
2767
  return steps.sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
2768
}
2769
2770
function Agents({
2771
  data,
2772
  navigate,
2773
  refreshDashboard,
2774
  api,
2775
}: {
2776
  data: DashboardData;
2777
  navigate: (page: Page) => void;
2778
  refreshDashboard: () => Promise<void>;
2779
  api: (path: string, init?: RequestInit) => Promise<unknown>;
2780
}) {
2781
  const [working, setWorking] = useState('');
2782
  const [error, setError] = useState('');
2783
  const saveAutomation = async (
2784
    agentId: string,
2785
    value: {
2786
      autoPublish: boolean;
2787
      autoAcceptPolicy: 'off' | 'safe_matching';
2788
      autoAcceptTaskCategories: string[];
2789
    },
2790
  ) => {
2791
    setWorking(agentId);
2792
    setError('');
2793
    try {
2794
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/automation`, {
2795
        method: 'PUT',
2796
        body: JSON.stringify(value),
2797
      });
2798
      await refreshDashboard();
2799
      return true;
2800
    } catch (reason) {
2801
      setError(reason instanceof Error ? reason.message : 'Directory update failed');
2802
      return false;
2803
    } finally {
2804
      setWorking('');
2805
    }
2806
  };
2807
  const disableRuntime = async (agentId: string) => {
2808
    setWorking(`runtime:${agentId}`);
2809
    setError('');
2810
    try {
2811
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}/runtime`, { method: 'DELETE' });
2812
      await refreshDashboard();
2813
    } catch (reason) {
2814
      setError(reason instanceof Error ? reason.message : 'Runtime update failed');
2815
    } finally {
2816
      setWorking('');
2817
    }
2818
  };
2819
  const revokeAccessToken = async (agentId: string, tokenId: string) => {
2820
    setWorking(`token:${agentId}`);
2821
    setError('');
2822
    try {
2823
      await api(
2824
        `/v0.1/agents/${encodeURIComponent(agentId)}/access-tokens/${encodeURIComponent(tokenId)}`,
2825
        { method: 'DELETE' },
2826
      );
2827
      await refreshDashboard();
2828
    } catch (reason) {
2829
      setError(reason instanceof Error ? reason.message : 'Could not revoke agent access token');
2830
    } finally {
2831
      setWorking('');
2832
    }
2833
  };
2834
  const deleteAgent = async (agent: Record<string, any>) => {
2835
    const agentId = String(agent.agentId);
2836
    if (!confirmAgentDeletion(agent)) return;
2837
    setWorking(`delete:${agentId}`);
2838
    setError('');
2839
    try {
2840
      await api(`/v0.1/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
2841
      await refreshDashboard();
2842
    } catch (reason) {
2843
      setError(reason instanceof Error ? reason.message : 'Agent deletion failed');
2844
    } finally {
2845
      setWorking('');
2846
    }
2847
  };
2848
  return (
2849
    <>
2850
      <PageHead page="agents" action="Connect provider" onAction={() => navigate('connect')} />
2851
      <div className="notice">
2852
        <strong>Direct A2A only.</strong> Conversation bodies travel between agent-owned runtimes.
2853
        OpenClasp does not proxy or retain them.
2854
      </div>
2855
      {error && (
2856
        <div className="errorBar" role="alert">
2857
          {error}
2858
        </div>
2859
      )}
2860
      <section className="agentGrid">
2861
        {data.agents.length ? (
2862
          data.agents.map((agent) => (
2863
            <AgentCard
2864
              key={agent.agentId}
2865
              agent={agent}
2866
              projectName={
2867
                data.projects.find((project) => project.projectId === agent.projectId)?.name
2868
              }
2869
              published={data.publications.some(
2870
                (publication) => publication.agentId === agent.agentId && publication.published,
2871
              )}
2872
              working={working === agent.agentId}
2873
              onSave={(value) => saveAutomation(agent.agentId, value)}
2874
              runtime={data.runtimes.find((runtime) => runtime.agentId === agent.agentId)}
2875
              runtimeWorking={working === `runtime:${agent.agentId}`}
2876
              deleteWorking={working === `delete:${agent.agentId}`}
2877
              onDisableRuntime={() => disableRuntime(agent.agentId)}
2878
              accessTokens={data.accessTokens.filter(
2879
                (token) =>
2880
                  token.agentId === agent.agentId &&
2881
                  !token.revokedAt &&
2882
                  Date.parse(String(token.expiresAt)) > Date.now(),
2883
              )}
2884
              accessTokenWorking={working === `token:${agent.agentId}`}
2885
              onRevokeAccessToken={(tokenId) => revokeAccessToken(agent.agentId, tokenId)}
2886
              onDelete={() => deleteAgent(agent)}
2887
            />
2888
          ))
2889
        ) : (
2890
          <Empty
2891
            title="No agents connected"
2892
            text="Choose a provider. OpenClasp connects the runtime and asks the agent for its own profile."
2893
            action="Choose provider"
2894
            onAction={() => navigate('connect')}
2895
          />
2896
        )}
2897
      </section>
2898
    </>
2899
  );
2900
}
2901
2902
function Invitations({
2903
  data,
2904
  refreshDashboard,
2905
  api,
2906
}: {
2907
  data: DashboardData;
2908
  refreshDashboard: () => Promise<void>;
2909
  api: (path: string, init?: RequestInit) => Promise<unknown>;
2910
}) {
2911
  const [working, setWorking] = useState('');
2912
  const [error, setError] = useState('');
2913
  const ownedAgentIds = useMemo(
2914
    () => new Set(data.agents.map((agent) => String(agent.agentId))),
2915
    [data.agents],
2916
  );
2917
  const currentProposal = (interaction: Record<string, any>) =>
2918
    [...(interaction.contractRevisions ?? [])]
2919
      .reverse()
2920
      .find((revision) => revision.status === 'proposed');
2921
  const respondingAgent = (interaction: Record<string, any>) => {
2922
    const proposal = currentProposal(interaction);
2923
    return (interaction.contract?.parties ?? []).find(
2924
      (agentId: string) => ownedAgentIds.has(String(agentId)) && !proposal?.acceptances?.[agentId],
2925
    );
2926
  };
2927
  const incoming = data.federatedInteractions.filter(
2928
    (interaction) => currentProposal(interaction) && respondingAgent(interaction),
2929
  );
2930
  const respond = async (
2931
    interactionId: string,
2932
    revisionId: string,
2933
    agentId: string,
2934
    decision: 'accept' | 'reject',
2935
  ) => {
2936
    setWorking(interactionId);
2937
    setError('');
2938
    try {
2939
      await api(
2940
        `/v0.1/federated-interactions/${encodeURIComponent(interactionId)}/contract-proposals/${encodeURIComponent(revisionId)}/respond`,
2941
        {
2942
          method: 'POST',
2943
          body: JSON.stringify({ agentId, decision }),
2944
        },
2945
      );
2946
      await refreshDashboard();
2947
    } catch (reason) {
2948
      setError(reason instanceof Error ? reason.message : 'Could not respond to invitation');
2949
    } finally {
2950
      setWorking('');
2951
    }
2952
  };
2953
  if (!incoming.length && !data.federatedInteractions.length) return null;
2954
  return (
2955
    <Panel
2956
      title={incoming.length ? `Contract decisions (${incoming.length})` : 'Shared interactions'}
2957
      subtitle=""
2958
    >
2959
      {error ? (
2960
        <div className="errorBar" role="alert">
2961
          {error}
2962
        </div>
2963
      ) : null}
2964
      {(incoming.length ? incoming : data.federatedInteractions.slice(0, 5)).map((interaction) => (
2965
        <article className="setupRequest" key={interaction.interactionId}>
2966
          <div>
2967
            <strong>{interaction.contract?.purpose ?? interaction.interactionId}</strong>
2968
            <small>
2969
              {interaction.initiatorAgentId} → {interaction.responderAgentId} · {interaction.status}
2970
            </small>
2971
            <small>
2972
              Contract v{interaction.contractRevision ?? 1}:{' '}
2973
              {String(currentProposal(interaction)?.termsHash ?? interaction.termsHash).slice(
2974
                0,
2975
                16,
2976
              )}
2977
              …
2978
              {currentProposal(interaction)
2979
                ? ` · proposed by ${currentProposal(interaction).proposedByAgentId}`
2980
                : ' · agreed'}
2981
            </small>
2982
            {interaction.contractRevisions?.length > 1 ? (
2983
              <small>{interaction.contractRevisions.length} recorded contract revisions</small>
2984
            ) : null}
2985
            {data.liveSessions.find(
2986
              (session) => session.interactionId === interaction.interactionId,
2987
            ) ? (
2988
              <small className="autoAccepted">
2989
                Live session:{' '}
2990
                {
2991
                  data.liveSessions.find(
2992
                    (session) => session.interactionId === interaction.interactionId,
2993
                  )?.status
2994
                }
2995
              </small>
2996
            ) : null}
2997
            {interaction.status === 'active' &&
2998
            interaction.acceptances?.[interaction.responderAgentId]?.method ===
2999
              'policy_auto_accept' ? (
3000
              <small className="autoAccepted">✓ Auto-approved by the responder's safe policy</small>
3001
            ) : null}
3002
          </div>
3003
          {currentProposal(interaction) && respondingAgent(interaction) ? (
3004
            <div className="decisionButtons">
3005
              <button
3006
                className="secondary"
3007
                type="button"
3008
                disabled={working === interaction.interactionId}
3009
                onClick={() =>
3010
                  void respond(
3011
                    interaction.interactionId,
3012
                    currentProposal(interaction).revisionId,
3013
                    respondingAgent(interaction),
3014
                    'reject',
3015
                  )
3016
                }
3017
              >
3018
                Reject
3019
              </button>
3020
              <button
3021
                className="primary"
3022
                type="button"
3023
                disabled={working === interaction.interactionId}
3024
                onClick={() =>
3025
                  void respond(
3026
                    interaction.interactionId,
3027
                    currentProposal(interaction).revisionId,
3028
                    respondingAgent(interaction),
3029
                    'accept',
3030
                  )
3031
                }
3032
              >
3033
                {working === interaction.interactionId
3034
                  ? 'Working…'
3035
                  : interaction.status === 'active'
3036
                    ? 'Accept amendment'
3037
                    : 'Accept contract'}
3038
              </button>
3039
            </div>
3040
          ) : null}
3041
        </article>
3042
      ))}
3043
    </Panel>
3044
  );
3045
}
3046
3047
function Insights({ data }: { data: DashboardData }) {
3048
  const eligible = data.learningEligibility.filter((item) => item.eligible);
3049
  const pendingFeedback = data.feedbackRequests.filter((item) => item.status === 'pending');
3050
  const provisional = data.interactionConclusions.filter(
3051
    (item) => item.lifecycle === 'provisional',
3052
  );
3053
  return (
3054
    <>
3055
      <PageHead page="insights" />
3056
      <section className="insightPrinciple">
3057
        <div>
3058
          <h2>Reliability</h2>
3059
          <p>
3060
            Every profile is tied to an agent version and task category. Scores reflect eligible
3061
            structured outcomes and decay as history gets older.
3062
          </p>
3063
        </div>
3064
        <div className="insightStats">
3065
          <span>
3066
            <strong>{data.profiles.length}</strong> contextual profiles
3067
          </span>
3068
          <span>
3069
            <strong>{eligible.length}</strong> eligible interactions
3070
          </span>
3071
          <span>
3072
            <strong>{pendingFeedback.length}</strong> feedback pending
3073
          </span>
3074
          <span>
3075
            <strong>{provisional.length}</strong> provisional signals
3076
          </span>
3077
        </div>
3078
      </section>
3079
      {!!provisional.length && (
3080
        <Panel
3081
          title="Provisional interaction signals"
3082
          subtitle="Available now, kept separate from corroborated reliability history"
3083
        >
3084
          <div className="learningDecisionList">
3085
            {provisional
3086
              .slice()
3087
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)))
3088
              .map((conclusion) => (
3089
                <article key={conclusion.conclusionId}>
3090
                  <StatusPill value="provisional" />
3091
                  <div>
3092
                    <strong>{conclusion.summary}</strong>
3093
                    <small>
3094
                      {Math.round(Number(conclusion.confidence ?? 0) * 100)}% confidence · peer{' '}
3095
                      {humanize(conclusion.peerReportStatus ?? 'awaiting').toLowerCase()} ·{' '}
3096
                      {relativeTime(timestamp(conclusion))}
3097
                    </small>
3098
                    <p>
3099
                      Missing report from{' '}
3100
                      {(conclusion.missingReportAgentIds ?? []).join(', ') || 'the counterparty'}.
3101
                      The signal will be revised if more structured evidence arrives.
3102
                    </p>
3103
                  </div>
3104
                </article>
3105
              ))}
3106
          </div>
3107
        </Panel>
3108
      )}
3109
      <section className="insightGrid">
3110
        {data.profiles.length ? (
3111
          data.profiles.map((profile) => {
3112
            const deltas = data.profileDeltas
3113
              .filter(
3114
                (delta) =>
3115
                  delta.agentId === profile.agentId &&
3116
                  delta.agentVersion === profile.agentVersion &&
3117
                  delta.taskCategory === profile.taskCategory,
3118
              )
3119
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)));
3120
            return (
3121
              <Panel
3122
                key={`${profile.agentId}-${profile.agentVersion}-${profile.taskCategory}`}
3123
                title={profile.agentId}
3124
                subtitle={`${humanize(profile.taskCategory)} · version ${profile.agentVersion}`}
3125
              >
3126
                <Profile profile={profile} expanded deltas={deltas} showHeader={false} />
3127
              </Panel>
3128
            );
3129
          })
3130
        ) : (
3131
          <Empty
3132
            title="Not enough verified evidence"
3133
            text="Insights grow only from authenticated outcomes, eligible feedback, corrections, disputes, and version-aware history."
3134
          />
3135
        )}
3136
      </section>
3137
      {!!data.learningEligibility.length && (
3138
        <Panel
3139
          title="Recent learning decisions"
3140
          subtitle="Why an interaction did or did not affect contextual history"
3141
        >
3142
          <div className="learningDecisionList">
3143
            {data.learningEligibility
3144
              .slice()
3145
              .sort((left, right) => Date.parse(timestamp(right)) - Date.parse(timestamp(left)))
3146
              .slice(0, 8)
3147
              .map((decision) => (
3148
                <article key={decision.decisionId}>
3149
                  <StatusPill value={decision.eligible ? 'eligible' : 'excluded'} />
3150
                  <div>
3151
                    <strong>{decision.interactionId}</strong>
3152
                    <small>
3153
                      {Math.round(Number(decision.sampleWeight ?? 0) * 100)}% weight ·{' '}
3154
                      {humanize(decision.contributionMode)} · {relativeTime(timestamp(decision))}
3155
                    </small>
3156
                    <p>{decision.reasons?.[0] ?? 'No reason supplied.'}</p>
3157
                  </div>
3158
                </article>
3159
              ))}
3160
          </div>
3161
        </Panel>
3162
      )}
3163
    </>
3164
  );
3165
}
3166
3167
type ConnectorClaimView = {
3168
  claimId: string;
3169
  status: 'pending' | 'approved' | 'rejected' | 'connected' | 'expired';
3170
  runtimeEndpoint: string;
3171
  agentId?: string;
3172
  expiresAt: string;
3173
  agent?: { name: string };
3174
  profile: {
3175
    description: string;
3176
    framework: string;
3177
    agentVersion: string;
3178
    modelProvider?: string;
3179
    modelName?: string;
3180
    capabilities: string[];
3181
    limitations: string[];
3182
  };
3183
};
3184
3185
type ProviderConnectionView = {
3186
  connectionId: string;
3187
  provider: 'botpress';
3188
  agentName: string;
3189
  status: 'pending' | 'connected' | 'expired';
3190
  code?: string;
3191
  agentId?: string;
3192
  expiresAt: string;
3193
};
3194
3195
function Connect({
3196
  data,
3197
  navigate,
3198
  refreshDashboard,
3199
  api,
3200
}: {
3201
  data: DashboardData;
3202
  navigate: (page: Page) => void;
3203
  refreshDashboard: () => Promise<void>;
3204
  api: (path: string, init?: RequestInit) => Promise<unknown>;
3205
}) {
3206
  const claimId = new URLSearchParams(window.location.search).get('claim') ?? '';
3207
  const [claim, setClaim] = useState<ConnectorClaimView>();
3208
  const [loadingClaim, setLoadingClaim] = useState(Boolean(claimId));
3209
  const [working, setWorking] = useState(false);
3210
  const [error, setError] = useState('');
3211
  const [agentName, setAgentName] = useState('');
3212
  const [selectedProvider, setSelectedProvider] = useState('');
3213
  const [providerConnection, setProviderConnection] = useState<ProviderConnectionView>();
3214
  const [copiedPairingCode, setCopiedPairingCode] = useState(false);
3215
3216
  useEffect(() => {
3217
    if (!claimId) return;
3218
    let cancelled = false;
3219
    void api(`/v0.1/connector-claims/${encodeURIComponent(claimId)}`)
3220
      .then((value) => {
3221
        if (!cancelled) setClaim(value as ConnectorClaimView);
3222
      })
3223
      .catch((reason: unknown) => {
3224
        if (!cancelled)
3225
          setError(reason instanceof Error ? reason.message : 'Could not load connector claim');
3226
      })
3227
      .finally(() => {
3228
        if (!cancelled) setLoadingClaim(false);
3229
      });
3230
    return () => {
3231
      cancelled = true;
3232
    };
3233
  }, [api, claimId]);
3234
3235
  useEffect(() => {
3236
    if (!providerConnection || providerConnection.status !== 'pending') return;
3237
    const poll = window.setInterval(() => {
3238
      void api(`/v0.1/provider-connections/${encodeURIComponent(providerConnection.connectionId)}`)
3239
        .then((value) => {
3240
          const next = value as ProviderConnectionView;
3241
          setProviderConnection((current) => ({ ...next, code: current?.code }));
3242
          if (next.status === 'connected') void refreshDashboard();
3243
        })
3244
        .catch(() => undefined);
3245
    }, 3_000);
3246
    return () => window.clearInterval(poll);
3247
  }, [api, providerConnection?.connectionId, providerConnection?.status, refreshDashboard]);
3248
3249
  const createBotpressConnection = async () => {
3250
    if (!agentName.trim()) {
3251
      setError('Enter an agent name');
3252
      return;
3253
    }
3254
    setWorking(true);
3255
    setError('');
3256
    try {
3257
      const result = (await api('/v0.1/provider-connections/botpress', {
3258
        method: 'POST',
3259
        body: JSON.stringify({ agentName: agentName.trim() }),
3260
      })) as ProviderConnectionView;
3261
      setProviderConnection(result);
3262
    } catch (reason) {
3263
      setError(reason instanceof Error ? reason.message : 'Could not create Botpress pairing code');
3264
    } finally {
3265
      setWorking(false);
3266
    }
3267
  };
3268
3269
  const decide = async (decision: 'approve' | 'reject') => {
3270
    if (!claim) return;
3271
    if (decision === 'approve' && !agentName.trim()) {
3272
      setError('Enter an agent name');
3273
      return;
3274
    }
3275
    setWorking(true);
3276
    setError('');
3277
    try {
3278
      const result = (await api(
3279
        `/v0.1/connector-claims/${encodeURIComponent(claim.claimId)}/${decision}`,
3280
        {
3281
          method: 'POST',
3282
          ...(decision === 'approve' ? { body: JSON.stringify({ name: agentName.trim() }) } : {}),
3283
        },
3284
      )) as ConnectorClaimView;
3285
      setClaim(result);
3286
      await refreshDashboard();
3287
    } catch (reason) {
3288
      setError(reason instanceof Error ? reason.message : 'Could not update connector claim');
3289
    } finally {
3290
      setWorking(false);
3291
    }
3292
  };
3293
3294
  return (
3295
    <>
3296
      <PageHead page="connect" />
3297
      {claimId ? (
3298
        <section className="setupRequests" aria-live="polite">
3299
          <div>
3300
            <p className="pageKicker">CONNECTOR CLAIM</p>
3301
            <h2>Name your agent</h2>
3302
            <p>That is the only detail you need to enter.</p>
3303
          </div>
3304
          {loadingClaim ? <Loading compact /> : null}
3305
          {claim ? (
3306
            <article className="setupRequest">
3307
              <div>
3308
                {claim.status === 'pending' ? (
3309
                  <label>
3310
                    <span>Agent name</span>
3311
                    <input
3312
                      autoFocus
3313
                      required
3314
                      maxLength={100}
3315
                      value={agentName}
3316
                      onChange={(event) => setAgentName(event.target.value)}
3317
                      placeholder="Procurement agent"
3318
                    />
3319
                  </label>
3320
                ) : (
3321
                  <strong>{claim.agent?.name || agentName || 'Agent connected'}</strong>
3322
                )}
3323
                <small>OpenClasp asked the running agent for everything else.</small>
3324
                <details>
3325
                  <summary>View agent-reported details</summary>
3326
                  <p>{claim.profile.description}</p>
3327
                  <small>
3328
                    {claim.profile.framework} · version {claim.profile.agentVersion}
3329
                    {claim.profile.modelName
3330
                      ? ` · ${claim.profile.modelProvider ?? 'model'} ${claim.profile.modelName}`
3331
                      : ''}
3332
                  </small>
3333
                  <div className="tags">
3334
                    {claim.profile.capabilities.map((capability) => (
3335
                      <span key={capability}>{capability}</span>
3336
                    ))}
3337
                  </div>
3338
                  {claim.profile.limitations.length ? (
3339
                    <small>Limits: {claim.profile.limitations.join(' · ')}</small>
3340
                  ) : null}
3341
                </details>
3342
              </div>
3343
              {claim.status === 'pending' ? (
3344
                <div className="decisionButtons">
3345
                  <button
3346
                    className="secondary"
3347
                    type="button"
3348
                    disabled={working}
3349
                    onClick={() => void decide('reject')}
3350
                  >
3351
                    Reject
3352
                  </button>
3353
                  <button
3354
                    className="primary"
3355
                    type="button"
3356
                    disabled={working}
3357
                    onClick={() => void decide('approve')}
3358
                  >
3359
                    {working ? 'Connecting…' : 'Connect agent'}
3360
                  </button>
3361
                </div>
3362
              ) : (
3363
                <div className="decisionButtons">
3364
                  <StatusPill value={claim.status} />
3365
                  {claim.status === 'approved' || claim.status === 'connected' ? (
3366
                    <button className="primary" type="button" onClick={() => navigate('agents')}>
3367
                      View agent
3368
                    </button>
3369
                  ) : null}
3370
                </div>
3371
              )}
3372
            </article>
3373
          ) : null}
3374
          {error ? (
3375
            <div className="loginError" role="alert">
3376
              {error}
3377
            </div>
3378
          ) : null}
3379
        </section>
3380
      ) : null}
3381
3382
      {!claimId ? (
3383
        <div className="connectLayout providerConnectLayout">
3384
          <section className="providerIntro">
3385
            <div>
3386
              <p className="pageKicker">CHOOSE A PROVIDER</p>
3387
              <h2>Where does your agent run?</h2>
3388
              <p>The provider connector handles endpoints, A2A and credentials.</p>
3389
            </div>
3390
            <span>{data.agents.length} connected</span>
3391
          </section>
3392
          <section className="providerGrid" aria-label="Cloud agent providers">
3393
            <button
3394
              className={`providerCard available ${selectedProvider === 'botpress' ? 'active' : ''}`}
3395
              type="button"
3396
              onClick={() => setSelectedProvider('botpress')}
3397
            >
3398
              <span className="providerMonogram botpressMonogram">bp</span>
3399
              <span>
3400
                <strong>Botpress</strong>
3401
                <small>Available now</small>
3402
              </span>
3403
              <ArrowRight />
3404
            </button>
3405
            {['Dify', 'n8n', 'Microsoft Copilot Studio', 'LangGraph'].map((provider, index) => (
3406
              <article className="providerCard" key={provider}>
3407
                <span className="providerMonogram">0{index + 2}</span>
3408
                <span>
3409
                  <strong>{provider}</strong>
3410
                  <small>Coming next</small>
3411
                </span>
3412
              </article>
3413
            ))}
3414
          </section>
3415
3416
          {selectedProvider === 'botpress' ? (
3417
            <section className="providerSetup" aria-live="polite">
3418
              <header>
3419
                <div>
3420
                  <p className="pageKicker">BOTPRESS · MANAGED CONNECTOR</p>
3421
                  <h2>Connect a Botpress agent</h2>
3422
                </div>
3423
                <button
3424
                  className="iconButton"
3425
                  type="button"
3426
                  aria-label="Close Botpress setup"
3427
                  onClick={() => setSelectedProvider('')}
3428
                >
3429
                  <X />
3430
                </button>
3431
              </header>
3432
              {!providerConnection ? (
3433
                <div className="providerNameStep">
3434
                  <label>
3435
                    <span>Agent name</span>
3436
                    <Input
3437
                      autoFocus
3438
                      maxLength={100}
3439
                      value={agentName}
3440
                      onChange={(event) => setAgentName(event.target.value)}
3441
                      onKeyDown={(event) => {
3442
                        if (event.key === 'Enter') void createBotpressConnection();
3443
                      }}
3444
                      placeholder="Procurement agent"
3445
                    />
3446
                  </label>
3447
                  <button
3448
                    className="primary"
3449
                    type="button"
3450
                    disabled={working}
3451
                    onClick={() => void createBotpressConnection()}
3452
                  >
3453
                    {working ? 'Creating…' : 'Create pairing code'}
3454
                  </button>
3455
                  <p>You name it. The running agent reports everything else.</p>
3456
                </div>
3457
              ) : providerConnection.status === 'connected' ? (
3458
                <div className="providerConnected">
3459
                  <ShieldCheck />
3460
                  <div>
3461
                    <strong>{providerConnection.agentName} is connected</strong>
3462
                    <p>
3463
                      Botpress reported its profile. Runtime verification is finishing
3464
                      automatically.
3465
                    </p>
3466
                  </div>
3467
                  <button className="primary" type="button" onClick={() => navigate('agents')}>
3468
                    View agent
3469
                  </button>
3470
                </div>
3471
              ) : (
3472
                <div className="pairingSetup">
3473
                  <div className="pairingCodeBlock">
3474
                    <span>PAIRING CODE · EXPIRES IN 15 MINUTES</span>
3475
                    <code>{providerConnection.code}</code>
3476
                    <button
3477
                      className="secondary"
3478
                      type="button"
3479
                      onClick={() => {
3480
                        void navigator.clipboard.writeText(providerConnection.code ?? '');
3481
                        setCopiedPairingCode(true);
3482
                      }}
3483
                    >
3484
                      <Copy /> {copiedPairingCode ? 'Copied' : 'Copy code'}
3485
                    </button>
3486
                  </div>
3487
                  <div className="connectSteps">
3488
                    <div className="connectStep">
3489
                      <b>1</b>
3490
                      <span>
3491
                        Open the Botpress Hub and install the <strong>OpenClasp</strong>{' '}
3492
                        integration.
3493
                      </span>
3494
                    </div>
3495
                    <div className="connectStep">
3496
                      <b>2</b>
3497
                      <span>Paste this pairing code and enable the integration.</span>
3498
                    </div>
3499
                    <div className="connectStep">
3500
                      <b>3</b>
3501
                      <span>
3502
                        Wait here. The agent will report its profile and appear automatically.
3503
                      </span>
3504
                    </div>
3505
                  </div>
3506
                  <a
3507
                    className="primary providerExternalLink"
3508
                    href="https://app.botpress.cloud/hub"
3509
                    target="_blank"
3510
                    rel="noreferrer"
3511
                  >
3512
                    Open Botpress Hub <ExternalLink />
3513
                  </a>
3514
                  {providerConnection.status === 'expired' ? (
3515
                    <div className="loginError" role="alert">
3516
                      Pairing code expired. Close this panel and create another one.
3517
                    </div>
3518
                  ) : (
3519
                    <small className="waitingStatus">
3520
                      <Circle /> Waiting for Botpress
3521
                    </small>
3522
                  )}
3523
                </div>
3524
              )}
3525
              {error ? (
3526
                <div className="loginError" role="alert">
3527
                  {error}
3528
                </div>
3529
              ) : null}
3530
            </section>
3531
          ) : null}
3532
3533
          <details className="customRuntimeSetup">
3534
            <summary>Connect a custom or VM-hosted runtime</summary>
3535
            <div className="connectSteps">
3536
              <div className="connectStep">
3537
                <b>1</b>
3538
                <span>Run the OpenClasp connector beside the agent.</span>
3539
              </div>
3540
              <div className="connectStep">
3541
                <b>2</b>
3542
                <span>Expose its runtime through a public HTTPS domain.</span>
3543
              </div>
3544
              <div className="connectStep">
3545
                <b>3</b>
3546
                <span>Open the generated approval link and name the agent.</span>
3547
              </div>
3548
            </div>
3549
          </details>
3550
        </div>
3551
      ) : null}
3552
    </>
3553
  );
3554
}
3555
3556
function SettingsPage({
3557
  settings,
3558
  setSettings,
3559
  api,
3560
}: {
3561
  settings: Settings;
3562
  setSettings: React.Dispatch<React.SetStateAction<Settings>>;
3563
  api: (path: string, init?: RequestInit) => Promise<unknown>;
3564
}) {
3565
  const [saving, setSaving] = useState(false);
3566
  const [saved, setSaved] = useState(false);
3567
  const [error, setError] = useState('');
3568
  const save = useCallback(async () => {
3569
    setSaving(true);
3570
    setSaved(false);
3571
    setError('');
3572
    try {
3573
      const updated = (await api('/v0.1/settings', {
3574
        method: 'PUT',
3575
        body: JSON.stringify(settings),
3576
      })) as Settings;
3577
      setSettings(updated);
3578
      setSaved(true);
3579
    } catch (reason) {
3580
      setError(reason instanceof Error ? reason.message : 'Could not save settings');
3581
    } finally {
3582
      setSaving(false);
3583
    }
3584
  }, [api, settings, setSettings]);
3585
  return (
3586
    <>
3587
      <PageHead page="settings" />
3588
      <section className="settingsCard">
3589
        <Setting label="Display name" description="Shown only inside your OpenClasp account.">
3590
          <input
3591
            value={settings.displayName}
3592
            onChange={(event) =>
3593
              setSettings((value) => ({ ...value, displayName: event.target.value }))
3594
            }
3595
            placeholder="Team or operator name"
3596
          />
3597
        </Setting>
3598
        <Setting
3599
          label="Network contribution"
3600
          description="Allow eligible structured aggregates to improve contextual intelligence. Raw conversations are never included."
3601
        >
3602
          <Toggle
3603
            checked={settings.contributionEnabled}
3604
            label="Network contribution"
3605
            onChange={(checked) =>
3606
              setSettings((value) => ({ ...value, contributionEnabled: checked }))
3607
            }
3608
          />
3609
        </Setting>
3610
        <Setting
3611
          label="Conversation data"
3612
          description="Agent runtimes exchange messages directly. OpenClasp does not receive or store conversation bodies."
3613
        >
3614
          <span className="locked">NOT COLLECTED</span>
3615
        </Setting>
3616
        <div className="saveRow">
3617
          <button className="primary" type="button" onClick={() => void save()} disabled={saving}>
3618
            {saving ? 'Saving…' : 'Save settings'}
3619
          </button>
3620
          {saved && <span role="status">Saved</span>}
3621
          {error && (
3622
            <span className="loginError" role="alert">
3623
              {error}
3624
            </span>
3625
          )}
3626
        </div>
3627
      </section>
3628
    </>
3629
  );
3630
}
3631
3632
function PageHead({
3633
  page,
3634
  action,
3635
  onAction,
3636
}: {
3637
  page: Page;
3638
  action?: string;
3639
  onAction?: () => void;
3640
}) {
3641
  const meta = pageMeta[page];
3642
  return (
3643
    <header className="pageHead">
3644
      <h1>{meta.title}</h1>
3645
      {action && (
3646
        <button className="primary" type="button" onClick={onAction}>
3647
          {action.toLowerCase()} <span aria-hidden="true">→</span>
3648
        </button>
3649
      )}
3650
    </header>
3651
  );
3652
}
3653
function Metric({
3654
  label,
3655
  value,
3656
  note,
3657
  warn,
3658
}: {
3659
  label: string;
3660
  value: number;
3661
  note: string;
3662
  warn?: boolean;
3663
}) {
3664
  return (
3665
    <article className={warn ? 'metric warn' : 'metric'} title={note}>
3666
      <span>{label}</span>
3667
      <strong>{value}</strong>
3668
      <small>{note}</small>
3669
    </article>
3670
  );
3671
}
3672
function Panel({
3673
  title,
3674
  subtitle,
3675
  children,
3676
}: React.PropsWithChildren<{ title: string; subtitle: string }>) {
3677
  return (
3678
    <section className="panel">
3679
      <div className="panelHead">
3680
        <h2 className="panelKicker">
3681
          <span>//</span> {title.toLowerCase()}
3682
        </h2>
3683
        {subtitle ? <p>{subtitle}</p> : null}
3684
      </div>
3685
      {children}
3686
    </section>
3687
  );
3688
}
3689
function Timeline({ events }: { events: Record<string, any>[] }) {
3690
  return events.length ? (
3691
    <div className="timeline">
3692
      {events.map((event) => (
3693
        <HistoryRow key={event.eventId} item={{ ...event, _kind: 'event' }} />
3694
      ))}
3695
    </div>
3696
  ) : (
3697
    <Empty
3698
      title="Nothing recorded yet"
3699
      text="Activity appears after your connected agents submit authenticated structured events."
3700
    />
3701
  );
3702
}
3703
function HistoryRow({ item }: { item: Record<string, any> }) {
3704
  const kind = String(item.eventType ?? item._kind ?? 'record').replaceAll('_', ' ');
3705
  const id = String(item.agentId ?? item.interactionId ?? item.receiptId ?? 'OpenClasp');
3706
  return (
3707
    <article className="historyRow">
3708
      <span className={`eventDot ${item.eventType ?? item._kind}`} />
3709
      <div>
3710
        <strong>{kind}</strong>
3711
        <small>
3712
          {id} · {new Date(timestamp(item)).toLocaleString()}
3713
        </small>
3714
      </div>
3715
      <b>{item.visibility ?? item.outcome ?? item.status ?? 'SIGNED'}</b>
3716
    </article>
3717
  );
3718
}
3719
function AgentCard({
3720
  agent,
3721
  projectName,
3722
  published,
3723
  working,
3724
  onSave,
3725
  runtime,
3726
  runtimeWorking,
3727
  deleteWorking,
3728
  onDisableRuntime,
3729
  accessTokens,
3730
  accessTokenWorking,
3731
  onRevokeAccessToken,
3732
  onDelete,
3733
}: {
3734
  agent: Record<string, any>;
3735
  projectName?: string;
3736
  published: boolean;
3737
  working: boolean;
3738
  onSave: (value: {
3739
    autoPublish: boolean;
3740
    autoAcceptPolicy: 'off' | 'safe_matching';
3741
    autoAcceptTaskCategories: string[];
3742
  }) => Promise<boolean>;
3743
  runtime: Record<string, any> | undefined;
3744
  runtimeWorking: boolean;
3745
  deleteWorking: boolean;
3746
  onDisableRuntime: () => void;
3747
  accessTokens: Record<string, any>[];
3748
  accessTokenWorking: boolean;
3749
  onRevokeAccessToken: (tokenId: string) => Promise<void>;
3750
  onDelete: () => void;
3751
}) {
3752
  const [editing, setEditing] = useState(false);
3753
  const [autoPublish, setAutoPublish] = useState(Boolean(agent.autoPublish ?? published));
3754
  const [autoAcceptPolicy, setAutoAcceptPolicy] = useState<'off' | 'safe_matching'>(
3755
    agent.autoAcceptPolicy === 'safe_matching' ? 'safe_matching' : 'off',
3756
  );
3757
  const [categories, setCategories] = useState<string>(
3758
    (agent.autoAcceptTaskCategories?.length
3759
      ? agent.autoAcceptTaskCategories
3760
      : (agent.capabilities ?? [])
3761
    ).join(', '),
3762
  );
3763
  const ready = published && runtime?.status === 'verified';
3764
  const online = agent.presence?.status === 'online';
3765
  const connected = runtime?.status === 'verified';
3766
  const providerConnected = accessTokens.some((token) => token.scopes?.includes('runtime:connect'));
3767
  const endpoint = String(runtime?.a2aEndpoint ?? agent.a2aEndpoint ?? 'Connect a runtime first');
3768
  const identityLabel = agent.revoked
3769
    ? 'REVOKED'
3770
    : agent.identityMode === 'connector_claim'
3771
      ? 'AGENT DECLARED'
3772
      : agent.identityMode === 'owner_managed'
3773
        ? 'OWNER CREATED'
3774
        : agent.identityMode === 'oauth_installation'
3775
          ? 'AUTHENTICATED'
3776
          : 'VERIFIED';
3777
  return (
3778
    <article className="agentCard">
3779
      <div className="agentTop">
3780
        <AgentMark
3781
          name={String(agent.name ?? agent.agentId)}
3782
          online={connected || online}
3783
          size="sm"
3784
        />
3785
        <div className="agentBadges">
3786
          <b className={runtime?.status === 'verified' ? 'onlineBadge' : 'offlineBadge'}>
3787
            {runtime?.status === 'verified'
3788
              ? 'ENDPOINT VERIFIED'
3789
              : providerConnected
3790
                ? 'TOKEN ONLY'
3791
                : 'ENDPOINT MISSING'}
3792
          </b>
3793
          <b className={agent.revoked ? 'bad' : ''}>{identityLabel}</b>
3794
          <b className={ready ? 'readyBadge' : 'needsBadge'}>{ready ? 'READY' : 'SETUP NEEDED'}</b>
3795
        </div>
3796
      </div>
3797
      <h2>{agent.name ?? agent.agentId}</h2>
3798
      <p>{projectName ?? `Version ${agent.agentVersion ?? '1.0.0'}`}</p>
3799
      <div className="tags">
3800
        {(agent.capabilities ?? []).slice(0, 4).map((capability: string) => (
3801
          <span key={capability}>{capability}</span>
3802
        ))}
3803
      </div>
3804
      <small>
3805
        {agent.framework ? `${agent.framework} · ` : ''}
3806
        {agent.identityMode === 'connector_claim'
3807
          ? 'Connector-claimed · '
3808
          : agent.identityMode === 'owner_managed'
3809
            ? 'Owner-created · '
3810
            : agent.identityMode === 'oauth_installation'
3811
              ? 'OAuth-bound · '
3812
              : 'Ed25519 · '}
3813
        Created {new Date(agent.createdAt).toLocaleDateString()}
3814
      </small>
3815
      <small>
3816
        {connected
3817
          ? `Cloud runtime connected · endpoint verified ${new Date(runtime.verifiedAt).toLocaleString()}`
3818
          : agent.presence?.lastSeenAt
3819
            ? `Last agent activity ${new Date(agent.presence.lastSeenAt).toLocaleString()}`
3820
            : 'No agent activity recorded yet'}
3821
      </small>
3822
      {published ? (
3823
        <div className="agentPublicLinks">
3824
          <a href={`/a/${encodeURIComponent(agent.agentId)}`} target="_blank" rel="noreferrer">
3825
            Verified public profile
3826
          </a>
3827
          <a
3828
            href={`/agents/${encodeURIComponent(agent.agentId)}/a2a-agent-card.json`}
3829
            target="_blank"
3830
            rel="noreferrer"
3831
          >
3832
            A2A Agent Card
3833
          </a>
3834
        </div>
3835
      ) : null}
3836
      <div className="automationSummary">
3837
        <span>{published ? 'Public discovery' : 'Private'}</span>
3838
        <span>Direct agent-owned A2A</span>
3839
        <span>
3840
          {agent.autoAcceptPolicy === 'safe_matching' ? 'Safe tasks automatic' : 'Manual approval'}
3841
        </span>
3842
      </div>
3843
      <div className="runtimeBox">
3844
        <div>
3845
          <strong>Autonomous runtime</strong>
3846
          <span className={runtime?.status === 'verified' ? 'runtimeLive' : 'runtimeMissing'}>
3847
            {runtime?.status === 'verified' ? 'VERIFIED' : 'NOT CONNECTED'}
3848
          </span>
3849
        </div>
3850
        <p>
3851
          {runtime?.status === 'verified'
3852
            ? 'The agent runtime registered itself successfully. Conversation bodies travel directly between agents.'
3853
            : providerConnected
3854
              ? 'The connector credential exists, but the runtime has not completed endpoint verification.'
3855
              : 'Start the connector beside the agent. Runtime endpoints cannot be entered from the dashboard.'}
3856
        </p>
3857
        <small>The connector registers and verifies the HTTPS endpoint automatically.</small>
3858
        <div className="agentActions">
3859
          {runtime?.status === 'verified' ? (
3860
            <button
3861
              className="secondary"
3862
              type="button"
3863
              disabled={runtimeWorking}
3864
              onClick={onDisableRuntime}
3865
            >
3866
              Disable runtime
3867
            </button>
3868
          ) : null}
3869
          {runtime?.status !== 'verified' ? (
3870
            <a className="primary" href="/connect">
3871
              Connector setup
3872
            </a>
3873
          ) : null}
3874
        </div>
3875
        {runtime?.lastError ? <small>Last session error: {runtime.lastError}</small> : null}
3876
      </div>
3877
      {accessTokens.length ? (
3878
        <div className="accessTokenBox">
3879
          <div className="accessTokenHead">
3880
            <div>
3881
              <strong>Agent access tokens</strong>
3882
              <span>{accessTokens.length} ACTIVE</span>
3883
            </div>
3884
            <p>Provider and Shield test credentials. Revoke any token that is exposed.</p>
3885
          </div>
3886
          <div className="tokenList">
3887
            {accessTokens.map((token) => (
3888
              <div key={token.tokenId}>
3889
                <span>
3890
                  <strong>{token.name}</strong>
3891
                  <small>
3892
                    Created {new Date(token.createdAt).toLocaleDateString()} · expires{' '}
3893
                    {new Date(token.expiresAt).toLocaleDateString()}
3894
                    {token.lastUsedAt
3895
                      ? ` · used ${new Date(token.lastUsedAt).toLocaleString()}`
3896
                      : ' · never used'}
3897
                  </small>
3898
                </span>
3899
                <button
3900
                  className="secondary dangerButton"
3901
                  type="button"
3902
                  disabled={accessTokenWorking}
3903
                  onClick={() => {
3904
                    if (
3905
                      window.confirm(
3906
                        `Revoke “${String(token.name)}”? Any integration using it will disconnect.`,
3907
                      )
3908
                    )
3909
                      void onRevokeAccessToken(String(token.tokenId));
3910
                  }}
3911
                >
3912
                  Revoke
3913
                </button>
3914
              </div>
3915
            ))}
3916
          </div>
3917
        </div>
3918
      ) : null}
3919
      {editing ? (
3920
        <div className="automationForm">
3921
          <label>
3922
            <span>Agent-owned A2A endpoint</span>
3923
            <input type="url" value={endpoint} readOnly />
3924
          </label>
3925
          <label>
3926
            <span>Invitation policy</span>
3927
            <select
3928
              value={autoAcceptPolicy}
3929
              onChange={(event) =>
3930
                setAutoAcceptPolicy(event.target.value as 'off' | 'safe_matching')
3931
              }
3932
            >
3933
              <option value="safe_matching">Auto-accept safe matches</option>
3934
              <option value="off">Review every request</option>
3935
            </select>
3936
          </label>
3937
          <label>
3938
            <span>Safe task categories</span>
3939
            <input
3940
              value={categories}
3941
              onChange={(event) => setCategories(event.target.value)}
3942
              placeholder="research, planning, coding"
3943
            />
3944
          </label>
3945
          <label className="checkControl">
3946
            <input
3947
              type="checkbox"
3948
              checked={autoPublish}
3949
              onChange={(event) => setAutoPublish(event.target.checked)}
3950
            />
3951
            <span>Publish and keep Agent Card updated</span>
3952
          </label>
3953
          <div className="agentActions">
3954
            <button className="secondary" type="button" onClick={() => setEditing(false)}>
3955
              Cancel
3956
            </button>
3957
            <button
3958
              className="primary"
3959
              type="button"
3960
              disabled={working || agent.status === 'revoked'}
3961
              onClick={() =>
3962
                void onSave({
3963
                  autoPublish,
3964
                  autoAcceptPolicy,
3965
                  autoAcceptTaskCategories: categories
3966
                    .split(',')
3967
                    .map((value) => value.trim())
3968
                    .filter(Boolean),
3969
                }).then((ok) => {
3970
                  if (ok) setEditing(false);
3971
                })
3972
              }
3973
            >
3974
              {working ? 'Saving…' : 'Save automation'}
3975
            </button>
3976
          </div>
3977
        </div>
3978
      ) : (
3979
        <div className="agentActions agentManagementActions">
3980
          <button
3981
            className="secondary dangerButton"
3982
            type="button"
3983
            disabled={working || runtimeWorking || deleteWorking}
3984
            onClick={onDelete}
3985
          >
3986
            {deleteWorking ? 'Deleting…' : 'Delete agent'}
3987
          </button>
3988
          <button
3989
            className="secondary"
3990
            type="button"
3991
            disabled={working || runtimeWorking || deleteWorking || agent.status === 'revoked'}
3992
            onClick={() => setEditing(true)}
3993
          >
3994
            Configure automation
3995
          </button>
3996
        </div>
3997
      )}
3998
    </article>
3999
  );
4000
}
4001
function Profile({
4002
  profile,
4003
  expanded,
4004
  deltas = [],
4005
  showHeader = true,
4006
}: {
4007
  profile: Record<string, any>;
4008
  expanded?: boolean;
4009
  deltas?: Record<string, any>[];
4010
  showHeader?: boolean;
4011
}) {
4012
  const dimensions = [
4013
    ['Completion', profile.completion],
4014
    ['Acceptance', profile.acceptance],
4015
    ['Specification', profile.specification],
4016
    ['Scope', profile.scope],
4017
    ['Evidence', profile.evidence],
4018
    ...(expanded
4019
      ? [
4020
          ['Communication', profile.communication],
4021
          ['Deadline', profile.deadline],
4022
          ['Dispute-free', 1 - Number(profile.disputes ?? 0)],
4023
        ]
4024
      : []),
4025
  ] as [string, number][];
4026
  const latestDelta = deltas[0];
4027
  return (
4028
    <article className="profile">
4029
      <div className={showHeader ? 'profileHead' : 'profileHead profileHeadCompact'}>
4030
        {showHeader ? (
4031
          <div>
4032
            <strong>{profile.agentId}</strong>
4033
            <small>
4034
              {humanize(profile.taskCategory)} · version {profile.agentVersion ?? 'unknown'}
4035
            </small>
4036
          </div>
4037
        ) : (
4038
          <small>
4039
            Effective evidence{' '}
4040
            {Number(profile.effectiveSampleSize ?? profile.sampleSize ?? 0).toFixed(1)}
4041
            {profile.updatedAt ? ` · updated ${relativeTime(profile.updatedAt)}` : ''}
4042
          </small>
4043
        )}
4044
        <span className="sampleBadge">
4045
          {profile.sampleSize ?? 0} outcome{Number(profile.sampleSize) === 1 ? '' : 's'}
4046
        </span>
4047
      </div>
4048
      {showHeader ? (
4049
        <div className="profileEvidenceLine">
4050
          <small>
4051
            Effective evidence{' '}
4052
            {Number(profile.effectiveSampleSize ?? profile.sampleSize ?? 0).toFixed(1)}
4053
            {profile.updatedAt ? ` · updated ${relativeTime(profile.updatedAt)}` : ''}
4054
          </small>
4055
        </div>
4056
      ) : null}
4057
      {dimensions.map(([label, value]) => (
4058
        <Meter key={label} label={label} value={Number(value ?? 0.5)} />
4059
      ))}
4060
      {latestDelta ? (
4061
        <div className="profileDelta">
4062
          <span>Latest evidence impact</span>
4063
          <div>
4064
            {Object.entries(latestDelta.dimensionDeltas ?? {})
4065
              .filter(([, value]) => Math.abs(Number(value)) >= 0.001)
4066
              .slice(0, 5)
4067
              .map(([dimension, value]) => (
4068
                <b className={Number(value) >= 0 ? 'positive' : 'negative'} key={dimension}>
4069
                  {humanize(dimension)} {Number(value) >= 0 ? '+' : ''}
4070
                  {(Number(value) * 100).toFixed(1)}
4071
                </b>
4072
              ))}
4073
          </div>
4074
        </div>
4075
      ) : null}
4076
    </article>
4077
  );
4078
}
4079
function Meter({ label, value }: { label: string; value: number }) {
4080
  const bounded = Math.max(0, Math.min(1, value));
4081
  return (
4082
    <div className="meter">
4083
      <span>{label}</span>
4084
      <i>
4085
        <b style={{ width: `${Math.round(bounded * 100)}%` }} />
4086
      </i>
4087
      <strong>{Math.round(bounded * 100)}%</strong>
4088
    </div>
4089
  );
4090
}
4091
function Empty({
4092
  title,
4093
  text,
4094
  action,
4095
  onAction,
4096
}: {
4097
  title: string;
4098
  text: string;
4099
  action?: string;
4100
  onAction?: () => void;
4101
}) {
4102
  return (
4103
    <div className="empty">
4104
      <span aria-hidden="true">+</span>
4105
      <strong>{title}</strong>
4106
      <p>{text}</p>
4107
      {action && (
4108
        <button className="secondary" type="button" onClick={onAction}>
4109
          {action}
4110
        </button>
4111
      )}
4112
    </div>
4113
  );
4114
}
4115
function TextButton({ children, onClick }: React.PropsWithChildren<{ onClick: () => void }>) {
4116
  return (
4117
    <button className="textButton" type="button" onClick={onClick}>
4118
      {children}
4119
    </button>
4120
  );
4121
}
4122
function Setting({
4123
  label,
4124
  description,
4125
  children,
4126
}: React.PropsWithChildren<{ label: string; description: string }>) {
4127
  return (
4128
    <div className="setting">
4129
      <div>
4130
        <strong>{label}</strong>
4131
        <p>{description}</p>
4132
      </div>
4133
      <div>{children}</div>
4134
    </div>
4135
  );
4136
}
4137
function Toggle({
4138
  checked,
4139
  onChange,
4140
  label,
4141
}: {
4142
  checked: boolean;
4143
  onChange: (checked: boolean) => void;
4144
  label?: string;
4145
}) {
4146
  return (
4147
    <button
4148
      className={checked ? 'toggle on' : 'toggle'}
4149
      type="button"
4150
      role="switch"
4151
      aria-checked={checked}
4152
      aria-label={label}
4153
      onClick={() => onChange(!checked)}
4154
    >
4155
      <span />
4156
    </button>
4157
  );
4158
}
4159
function Loading({ compact }: { compact?: boolean }) {
4160
  return (
4161
    <div className={compact ? 'loading compact' : 'loading'}>
4162
      <span className="spinner" aria-hidden="true" />
4163
      <p>Verifying session…</p>
4164
    </div>
4165
  );
4166
}
4167
4168
function GoogleMark() {
4169
  return (
4170
    <svg viewBox="0 0 24 24" aria-hidden="true">
4171
      <path
4172
        fill="#4285F4"
4173
        d="M22.6 12.2c0-.8-.1-1.6-.2-2.3H12v4.4h5.9c-.3 1.4-1 2.6-2.2 3.4v2.8h3.6c2.1-1.9 3.3-4.8 3.3-8.3"
4174
      />
4175
      <path
4176
        fill="#34A853"
4177
        d="M12 23c3 0 5.5-1 7.3-2.7l-3.6-2.8c-1 .7-2.3 1.1-3.7 1.1-2.8 0-5.2-1.9-6.1-4.4H2.2v2.9C4 20.5 7.7 23 12 23"
4178
      />
4179
      <path
4180
        fill="#FBBC05"
4181
        d="M5.9 14.2c-.2-.7-.4-1.4-.4-2.2s.1-1.5.4-2.2V7H2.2C1.4 8.6 1 10.3 1 12s.4 3.4 1.2 5z"
4182
      />
4183
      <path
4184
        fill="#EA4335"
4185
        d="M12 4.8c1.6 0 3.1.6 4.2 1.7l3.2-3.2C17.5 1.5 15 0.5 12 .5 7.7.5 4 3 2.2 7l3.7 2.9C6.8 6.7 9.2 4.8 12 4.8"
4186
      />
4187
    </svg>
4188
  );
4189
}
4190
4191
function GitHubMark() {
4192
  return (
4193
    <svg viewBox="0 0 24 24" aria-hidden="true">
4194
      <path
4195
        fill="currentColor"
4196
        d="M12 2a10 10 0 0 0-3.2 19.5c.5.1.7-.2.7-.5v-1.7c-2.8.6-3.4-1.2-3.4-1.2-.5-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.4 1.1 3 .8.1-.6.4-1.1.6-1.3-2.2-.3-4.6-1.1-4.6-5a3.9 3.9 0 0 1 1-2.7 3.6 3.6 0 0 1 .1-2.6s.8-.3 2.7 1a9.3 9.3 0 0 1 5 0c1.9-1.3 2.7-1 2.7-1a3.6 3.6 0 0 1 .1 2.6 3.9 3.9 0 0 1 1 2.7c0 3.9-2.3 4.7-4.6 5 .4.3.7.9.7 1.9v2.8c0 .3.2.6.7.5A10 10 0 0 0 12 2"
4197
      />
4198
    </svg>
4199
  );
4200
}
4201
4202
function humanize(value: unknown) {
4203
  const text = String(value ?? '')
4204
    .replaceAll('_', ' ')
4205
    .trim();
4206
  return text ? text[0]!.toUpperCase() + text.slice(1) : 'Unknown';
4207
}
4208
4209
function formatDate(value: string) {
4210
  const date = new Date(value);
4211
  return Number.isNaN(date.getTime()) ? 'Time unavailable' : date.toLocaleString();
4212
}
4213
4214
function relativeTime(value: string) {
4215
  const milliseconds = Date.now() - Date.parse(value);
4216
  if (!Number.isFinite(milliseconds)) return 'Time unavailable';
4217
  const minutes = Math.round(Math.abs(milliseconds) / 60_000);
4218
  const future = milliseconds < 0;
4219
  const phrase =
4220
    minutes < 1
4221
      ? 'just now'
4222
      : minutes < 60
4223
        ? `${minutes}m`
4224
        : minutes < 1_440
4225
          ? `${Math.round(minutes / 60)}h`
4226
          : `${Math.round(minutes / 1_440)}d`;
4227
  return minutes < 1 ? phrase : future ? `in ${phrase}` : `${phrase} ago`;
4228
}
4229
4230
function feedbackState(requests: Record<string, any>[]) {
4231
  if (!requests.length) return 'Starts after a completion report';
4232
  const pending = requests.filter((request) => request.status === 'pending').length;
4233
  if (pending) return `${pending} response${pending === 1 ? '' : 's'} still pending`;
4234
  const expired = requests.filter((request) => request.status === 'expired').length;
4235
  return expired
4236
    ? `${expired} request${expired === 1 ? '' : 's'} timed out`
4237
    : 'Bilateral feedback closed';
4238
}
4239
4240
function StatusPill({ value }: { value: string }) {
4241
  const normalized = String(value).toLowerCase();
4242
  const tone = [
4243
    'success',
4244
    'active',
4245
    'ready',
4246
    'met',
4247
    'eligible',
4248
    'attested',
4249
    'allow',
4250
    'completed',
4251
  ].includes(normalized)
4252
    ? 'good'
4253
    : ['failure', 'denied', 'deny', 'missed', 'rejected', 'violation'].some((item) =>
4254
          normalized.includes(item),
4255
        )
4256
      ? 'danger'
4257
      : ['partial', 'pending', 'provisional', 'challenge', 'conflict', 'expired', 'excluded'].some(
4258
            (item) => normalized.includes(item),
4259
          )
4260
        ? 'warn'
4261
        : 'neutral';
4262
  return (
4263
    <span className={`statusPill ${tone}`}>
4264
      <i />
4265
      {humanize(value)}
4266
    </span>
4267
  );
4268
}
4269
4270
function timestamp(item: Record<string, any>) {
4271
  return String(
4272
    item.timestamp ??
4273
      item.generatedAt ??
4274
      item.decidedAt ??
4275
      item.appliedAt ??
4276
      item.submittedAt ??
4277
      item.completedAt ??
4278
      item.activatedAt ??
4279
      item.updatedAt ??
4280
      item.requestedAt ??
4281
      item.createdAt ??
4282
      new Date(0).toISOString(),
4283
  );
4284
}
4285
4286
if (!__AUTH0_DOMAIN__ || !__AUTH0_CLIENT_ID__ || !__AUTH0_AUDIENCE__)
4287
  throw new Error('Auth0 public configuration is incomplete');
4288
createRoot(document.getElementById('root')!).render(
4289
  <React.StrictMode>
4290
    {location.pathname === '/sso-callback' ? <AuthCallback /> : <App />}
4291
  </React.StrictMode>,
4292
);