| 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 | }
|