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

Viewing files at commit c099f79dc606Commit detailsView latest

apps/dashboard/src/components/shield-workspace.tsx 534 lines · 20.8 KB · tsx
1
import { useEffect, useMemo, useState, type FormEvent } from 'react';
2
import { ArrowRight, BrainCircuit, CheckCircle2, Plus, ShieldAlert } from 'lucide-react';
3
4
type RecordValue = Record<string, any>;
5
6
export function ShieldWorkspace({
7
  agents,
8
  cases,
9
  consultations,
10
  outcomes,
11
  api,
12
  refreshDashboard,
13
}: {
14
  agents: RecordValue[];
15
  cases: RecordValue[];
16
  consultations: RecordValue[];
17
  outcomes: RecordValue[];
18
  api: (path: string, init?: RequestInit) => Promise<unknown>;
19
  refreshDashboard: () => Promise<void>;
20
}) {
21
  const [selectedId, setSelectedId] = useState(() => String(cases[0]?.caseId ?? ''));
22
  const [creating, setCreating] = useState(cases.length === 0);
23
  const [working, setWorking] = useState('');
24
  const [error, setError] = useState('');
25
  const [agentId, setAgentId] = useState(() => String(agents[0]?.agentId ?? ''));
26
  const [title, setTitle] = useState('');
27
  const [goal, setGoal] = useState('');
28
  const [action, setAction] = useState('');
29
  const [brief, setBrief] = useState('');
30
  const [policies, setPolicies] = useState('');
31
  const [counterpartyType, setCounterpartyType] = useState('human');
32
  const [message, setMessage] = useState('');
33
  const [guidance, setGuidance] = useState('');
34
  const [outcomeResult, setOutcomeResult] = useState('successful');
35
  const [acceptedAdvice, setAcceptedAdvice] = useState(true);
36
  const [actionTaken, setActionTaken] = useState('');
37
  const [testToken, setTestToken] = useState<RecordValue>();
38
39
  useEffect(() => {
40
    if (!selectedId && cases[0]?.caseId) setSelectedId(String(cases[0].caseId));
41
  }, [cases, selectedId]);
42
43
  const selected = cases.find((item) => String(item.caseId) === selectedId);
44
  const caseConsultations = useMemo(
45
    () =>
46
      consultations
47
        .filter((item) => String(item.caseId) === selectedId)
48
        .sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt))),
49
    [consultations, selectedId],
50
  );
51
  const caseOutcome = outcomes.find((item) => String(item.caseId) === selectedId);
52
53
  const createCase = async (event: FormEvent) => {
54
    event.preventDefault();
55
    if (!agentId) return;
56
    setWorking('create');
57
    setError('');
58
    try {
59
      const result = (await api('/v0.1/shield/cases', {
60
        method: 'POST',
61
        body: JSON.stringify({
62
          agentId,
63
          title,
64
          goal,
65
          brief,
66
          ...(action ? { proposedAction: action } : {}),
67
          counterparty: { type: counterpartyType },
68
          policies: policies
69
            .split('\n')
70
            .map((line) => line.trim())
71
            .filter(Boolean)
72
            .map((statement, index) => ({ title: `Policy ${index + 1}`, statement })),
73
          facts: [],
74
          evidence: [],
75
        }),
76
      })) as RecordValue;
77
      setSelectedId(String(result.caseId));
78
      setCreating(false);
79
      setTitle('');
80
      setGoal('');
81
      setAction('');
82
      setBrief('');
83
      setPolicies('');
84
      await refreshDashboard();
85
    } catch (reason) {
86
      setError(reason instanceof Error ? reason.message : 'Could not open Shield case');
87
    } finally {
88
      setWorking('');
89
    }
90
  };
91
92
  const consult = async (event: FormEvent) => {
93
    event.preventDefault();
94
    if (!selected || !message.trim()) return;
95
    setWorking('consult');
96
    setError('');
97
    try {
98
      await api(`/v0.1/shield/cases/${encodeURIComponent(selectedId)}/consult`, {
99
        method: 'POST',
100
        body: JSON.stringify({
101
          message,
102
          situationContext: '',
103
          analysisDepth: 'deep',
104
          facts: [],
105
          evidence: [],
106
          policies: [],
107
        }),
108
      });
109
      setMessage('');
110
      await refreshDashboard();
111
    } catch (reason) {
112
      setError(reason instanceof Error ? reason.message : 'Shield consultation failed');
113
    } finally {
114
      setWorking('');
115
    }
116
  };
117
118
  const addGuidance = async (event: FormEvent) => {
119
    event.preventDefault();
120
    if (!selected || !guidance.trim()) return;
121
    setWorking('guidance');
122
    setError('');
123
    try {
124
      await api(`/v0.1/shield/cases/${encodeURIComponent(selectedId)}/guidance`, {
125
        method: 'POST',
126
        body: JSON.stringify({ instruction: guidance, scope: 'case' }),
127
      });
128
      setGuidance('');
129
      await refreshDashboard();
130
    } catch (reason) {
131
      setError(reason instanceof Error ? reason.message : 'Could not save owner guidance');
132
    } finally {
133
      setWorking('');
134
    }
135
  };
136
137
  const closeCase = async (event: FormEvent) => {
138
    event.preventDefault();
139
    if (!selected || !actionTaken.trim()) return;
140
    setWorking('close');
141
    setError('');
142
    try {
143
      await api(`/v0.1/shield/cases/${encodeURIComponent(selectedId)}/close`, {
144
        method: 'POST',
145
        body: JSON.stringify({
146
          result: outcomeResult,
147
          acceptedAdvice,
148
          actionTaken,
149
        }),
150
      });
151
      setActionTaken('');
152
      await refreshDashboard();
153
    } catch (reason) {
154
      setError(reason instanceof Error ? reason.message : 'Could not close Shield case');
155
    } finally {
156
      setWorking('');
157
    }
158
  };
159
160
  const createTestToken = async () => {
161
    if (!agentId) return;
162
    setWorking('token');
163
    setError('');
164
    try {
165
      const result = (await api(`/v0.1/agents/${encodeURIComponent(agentId)}/shield-tokens`, {
166
        method: 'POST',
167
        body: JSON.stringify({ name: 'τ³ benchmark', expiresInDays: 7 }),
168
      })) as RecordValue;
169
      setTestToken(result);
170
      await refreshDashboard();
171
    } catch (reason) {
172
      setError(reason instanceof Error ? reason.message : 'Could not create benchmark token');
173
    } finally {
174
      setWorking('');
175
    }
176
  };
177
178
  return (
179
    <div className="shieldPage">
180
      <header className="shieldHero">
181
        <div>
182
          <span className="eyebrow">OPENCLASP SHIELD · EXPERIMENTAL</span>
183
          <h1>An independent AI beside your agent.</h1>
184
          <p>
185
            Investigate persuasion, unsupported claims, policy conflicts and risky decisions before
186
            your agent commits or acts.
187
          </p>
188
        </div>
189
        <div className="shieldHeroActions">
190
          <label>
191
            Test agent
192
            <select value={agentId} onChange={(event) => setAgentId(event.target.value)}>
193
              {agents.map((agent) => (
194
                <option value={String(agent.agentId)} key={String(agent.agentId)}>
195
                  {String(agent.name ?? agent.agentId)}
196
                </option>
197
              ))}
198
            </select>
199
          </label>
200
          <button
201
            className="secondary"
202
            type="button"
203
            disabled={!agentId || working === 'token'}
204
            onClick={() => void createTestToken()}
205
          >
206
            {working === 'token' ? 'creating…' : 'create 7-day τ³ token'}
207
          </button>
208
          <button className="landingPrimary" type="button" onClick={() => setCreating(true)}>
209
            <Plus /> new case
210
          </button>
211
        </div>
212
      </header>
213
214
      {error ? <div className="errorBar">{error}</div> : null}
215
216
      {testToken ? (
217
        <section className="shieldToken" role="status">
218
          <div>
219
            <span className="eyebrow">COPY NOW · SHOWN ONCE</span>
220
            <strong>τ³ benchmark token for {String(testToken.agentId)}</strong>
221
            <p>
222
              Expires {new Date(String(testToken.expiresAt)).toLocaleString()}. It cannot manage the
223
              account or connect a runtime.
224
            </p>
225
          </div>
226
          <input aria-label="τ³ benchmark token" value={String(testToken.token)} readOnly />
227
          <button
228
            className="secondary"
229
            type="button"
230
            onClick={() => void navigator.clipboard.writeText(String(testToken.token))}
231
          >
232
            copy token
233
          </button>
234
          <button className="secondary" type="button" onClick={() => setTestToken(undefined)}>
235
            dismiss
236
          </button>
237
        </section>
238
      ) : null}
239
240
      {creating ? (
241
        <form className="shieldCreate" onSubmit={createCase}>
242
          <header>
243
            <div>
244
              <span className="eyebrow">NEW DECISION</span>
245
              <h2>What should Shield help with?</h2>
246
            </div>
247
            {cases.length ? (
248
              <button type="button" onClick={() => setCreating(false)}>
249
                cancel
250
              </button>
251
            ) : null}
252
          </header>
253
          <div className="shieldFormGrid">
254
            <label>
255
              Protected agent
256
              <select value={agentId} onChange={(event) => setAgentId(event.target.value)} required>
257
                <option value="">Select an agent</option>
258
                {agents.map((agent) => (
259
                  <option value={String(agent.agentId)} key={String(agent.agentId)}>
260
                    {String(agent.name ?? agent.agentId)}
261
                  </option>
262
                ))}
263
              </select>
264
            </label>
265
            <label>
266
              Counterparty
267
              <select
268
                value={counterpartyType}
269
                onChange={(event) => setCounterpartyType(event.target.value)}
270
              >
271
                <option value="human">Human</option>
272
                <option value="agent">AI agent</option>
273
                <option value="service">Service or tool</option>
274
                <option value="unknown">Unknown</option>
275
              </select>
276
            </label>
277
            <label className="wide">
278
              Case title
279
              <input value={title} onChange={(event) => setTitle(event.target.value)} required />
280
            </label>
281
            <label className="wide">
282
              Goal
283
              <textarea value={goal} onChange={(event) => setGoal(event.target.value)} required />
284
            </label>
285
            <label className="wide">
286
              Proposed action
287
              <input
288
                value={action}
289
                onChange={(event) => setAction(event.target.value)}
290
                placeholder="Issue refund, disclose data, approve transfer…"
291
              />
292
            </label>
293
            <label className="wide">
294
              Initial situation
295
              <textarea value={brief} onChange={(event) => setBrief(event.target.value)} />
296
            </label>
297
            <label className="wide">
298
              Relevant policies · one per line
299
              <textarea value={policies} onChange={(event) => setPolicies(event.target.value)} />
300
            </label>
301
          </div>
302
          <button className="landingPrimary" disabled={working === 'create' || !agents.length}>
303
            {working === 'create' ? 'opening…' : 'open Shield case'} <ArrowRight />
304
          </button>
305
        </form>
306
      ) : (
307
        <div className="shieldWorkspace">
308
          <aside className="shieldCases">
309
            <span className="eyebrow">CASES</span>
310
            {cases.map((item) => (
311
              <button
312
                type="button"
313
                key={String(item.caseId)}
314
                className={selectedId === String(item.caseId) ? 'active' : ''}
315
                onClick={() => setSelectedId(String(item.caseId))}
316
              >
317
                <strong>{String(item.title)}</strong>
318
                <span>
319
                  {String(item.status)} · {String(item.riskTier)} risk
320
                </span>
321
              </button>
322
            ))}
323
          </aside>
324
325
          {selected ? (
326
            <section className="shieldCase">
327
              <header className="shieldCaseHead">
328
                <div>
329
                  <span className="eyebrow">{String(selected.status).toUpperCase()}</span>
330
                  <h2>{String(selected.title)}</h2>
331
                  <p>{String(selected.goal)}</p>
332
                </div>
333
                <div className={`shieldRisk ${String(selected.riskTier)}`}>
334
                  <ShieldAlert />
335
                  <span>{String(selected.riskTier)} risk</span>
336
                </div>
337
              </header>
338
339
              {selected.ownerGuidance?.length ? (
340
                <section className="shieldGuidanceList">
341
                  <span className="eyebrow">OWNER GUIDANCE</span>
342
                  {selected.ownerGuidance.map((item: RecordValue) => (
343
                    <p key={String(item.guidanceId)}>{String(item.instruction)}</p>
344
                  ))}
345
                </section>
346
              ) : null}
347
348
              <div className="shieldConversation">
349
                {caseConsultations.length ? (
350
                  caseConsultations.map((item) => {
351
                    const analysis = item.analysis as RecordValue;
352
                    return (
353
                      <article className="shieldReply" key={String(item.consultationId)}>
354
                        <header>
355
                          <span>
356
                            <BrainCircuit /> Shield
357
                          </span>
358
                          <b>{humanize(String(analysis.disposition))}</b>
359
                        </header>
360
                        <p>{String(analysis.reply)}</p>
361
                        {analysis.rationale?.length ? (
362
                          <ul>
363
                            {analysis.rationale.map((reason: unknown) => (
364
                              <li key={String(reason)}>{String(reason)}</li>
365
                            ))}
366
                          </ul>
367
                        ) : null}
368
                        {analysis.questionsToAsk?.length ? (
369
                          <div className="shieldQuestions">
370
                            <strong>Questions to resolve</strong>
371
                            {analysis.questionsToAsk.map((question: unknown) => (
372
                              <span key={String(question)}>{String(question)}</span>
373
                            ))}
374
                          </div>
375
                        ) : null}
376
                        {analysis.claims?.length ? (
377
                          <div className="shieldAnalysisGrid">
378
                            <strong>Material claims</strong>
379
                            {analysis.claims.map((claim: RecordValue, index: number) => (
380
                              <div key={`${String(claim.claim)}-${index}`}>
381
                                <span className={`shieldTag ${String(claim.status)}`}>
382
                                  {humanize(String(claim.status))}
383
                                </span>
384
                                <p>{String(claim.claim)}</p>
385
                                <small>{String(claim.explanation)}</small>
386
                              </div>
387
                            ))}
388
                          </div>
389
                        ) : null}
390
                        {analysis.manipulationSignals?.length ? (
391
                          <div className="shieldAnalysisGrid">
392
                            <strong>Pressure or manipulation signals</strong>
393
                            {analysis.manipulationSignals.map(
394
                              (signal: RecordValue, index: number) => (
395
                                <div key={`${String(signal.tactic)}-${index}`}>
396
                                  <span className={`shieldTag ${String(signal.significance)}`}>
397
                                    {humanize(String(signal.tactic))}
398
                                  </span>
399
                                  <p>{String(signal.observation)}</p>
400
                                </div>
401
                              ),
402
                            )}
403
                          </div>
404
                        ) : null}
405
                        <div className="shieldActionGrid">
406
                          {analysis.nextSteps?.length ? (
407
                            <section>
408
                              <strong>Do next</strong>
409
                              <ol>
410
                                {analysis.nextSteps.map((step: unknown) => (
411
                                  <li key={String(step)}>{String(step)}</li>
412
                                ))}
413
                              </ol>
414
                            </section>
415
                          ) : null}
416
                          {analysis.safeguards?.length ? (
417
                            <section>
418
                              <strong>Safeguards</strong>
419
                              <ul>
420
                                {analysis.safeguards.map((safeguard: unknown) => (
421
                                  <li key={String(safeguard)}>{String(safeguard)}</li>
422
                                ))}
423
                              </ul>
424
                            </section>
425
                          ) : null}
426
                        </div>
427
                        {analysis.suggestedResponse ? (
428
                          <blockquote className="shieldSuggestedResponse">
429
                            <strong>Suggested response</strong>
430
                            <p>{String(analysis.suggestedResponse)}</p>
431
                          </blockquote>
432
                        ) : null}
433
                        <footer>
434
                          {String(item.generation?.mode)} · {String(item.generation?.model)}
435
                        </footer>
436
                      </article>
437
                    );
438
                  })
439
                ) : (
440
                  <div className="shieldEmpty">
441
                    <BrainCircuit />
442
                    <strong>Tell Shield what is happening.</strong>
443
                    <p>
444
                      It will inspect the case, challenge assumptions and propose what to do next.
445
                    </p>
446
                  </div>
447
                )}
448
              </div>
449
450
              {selected.status !== 'closed' ? (
451
                <>
452
                  <form className="shieldComposer" onSubmit={consult}>
453
                    <textarea
454
                      value={message}
455
                      onChange={(event) => setMessage(event.target.value)}
456
                      placeholder="Ask Shield what is happening, add new context, or challenge its recommendation…"
457
                      required
458
                    />
459
                    <button className="landingPrimary" disabled={working === 'consult'}>
460
                      {working === 'consult' ? 'investigating…' : 'consult Shield'} <ArrowRight />
461
                    </button>
462
                    <small>
463
                      Your message is processed transiently; only Shield’s structured assessment is
464
                      stored.
465
                    </small>
466
                  </form>
467
468
                  <div className="shieldCaseControls">
469
                    <form onSubmit={addGuidance}>
470
                      <span className="eyebrow">INSTRUCT SHIELD</span>
471
                      <input
472
                        aria-label="Owner guidance"
473
                        value={guidance}
474
                        onChange={(event) => setGuidance(event.target.value)}
475
                        placeholder="Always verify offline approval notes…"
476
                        required
477
                      />
478
                      <button disabled={working === 'guidance'}>save owner guidance</button>
479
                    </form>
480
                    <form onSubmit={closeCase}>
481
                      <span className="eyebrow">RECORD OUTCOME</span>
482
                      <div>
483
                        <select
484
                          aria-label="Outcome result"
485
                          value={outcomeResult}
486
                          onChange={(event) => setOutcomeResult(event.target.value)}
487
                        >
488
                          <option value="successful">Successful</option>
489
                          <option value="prevented_harm">Prevented harm</option>
490
                          <option value="escalated">Escalated</option>
491
                          <option value="false_alarm">False alarm</option>
492
                          <option value="unsuccessful">Unsuccessful</option>
493
                          <option value="unknown">Unknown</option>
494
                        </select>
495
                        <input
496
                          aria-label="Action ultimately taken"
497
                          value={actionTaken}
498
                          onChange={(event) => setActionTaken(event.target.value)}
499
                          placeholder="What action was ultimately taken?"
500
                          required
501
                        />
502
                      </div>
503
                      <label className="shieldCheckbox">
504
                        <input
505
                          type="checkbox"
506
                          checked={acceptedAdvice}
507
                          onChange={(event) => setAcceptedAdvice(event.target.checked)}
508
                        />
509
                        Shield's advice materially influenced the action
510
                      </label>
511
                      <button disabled={working === 'close'}>close case</button>
512
                    </form>
513
                  </div>
514
                </>
515
              ) : caseOutcome ? (
516
                <div className="shieldClosed">
517
                  <CheckCircle2 />
518
                  <span>
519
                    <strong>{humanize(String(caseOutcome.result))}</strong>
520
                    {String(caseOutcome.actionTaken)}
521
                  </span>
522
                </div>
523
              ) : null}
524
            </section>
525
          ) : null}
526
        </div>
527
      )}
528
    </div>
529
  );
530
}
531
532
function humanize(value: string) {
533
  return value.replaceAll('_', ' ');
534
}