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

Viewing files at commit 6e4e24f0d92fCommit detailsView latest

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