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

Viewing files at commit 867f032d6ce0Commit detailsView latest

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