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

Viewing files at commit c2393f876b9dCommit detailsView latest

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