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

Viewing files at commit af23d8c9b675Commit detailsView latest

apps/dashboard/src/components/landing-scene.tsx 319 lines · 8.7 KB · tsx
1
import { useEffect, useRef } from 'react';
2
3
const LUM = ' .:-=+*#%@';
4
const COLS = 88;
5
const ROWS = 40;
6
const reduceMotion =
7
  typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
8
9
function clockStamp() {
10
  return new Date().toISOString().slice(11, 19);
11
}
12
13
function rotateX(x: number, y: number, z: number, angle: number) {
14
  const cos = Math.cos(angle);
15
  const sin = Math.sin(angle);
16
  return [x, y * cos - z * sin, y * sin + z * cos] as const;
17
}
18
19
function rotateY(x: number, y: number, z: number, angle: number) {
20
  const cos = Math.cos(angle);
21
  const sin = Math.sin(angle);
22
  return [x * cos + z * sin, y, -x * sin + z * cos] as const;
23
}
24
25
function shade(nx: number, ny: number, nz: number) {
26
  return nx * 0.16 + ny * 0.38 + nz * 0.86;
27
}
28
29
function plot(pixels: string[][], depth: number[], x: number, y: number, z: number, light: number) {
30
  const depthOffset = 4.2;
31
  const scale = COLS * 1.04;
32
  const zz = z + depthOffset;
33
  if (zz <= 0.25) return;
34
  const inverse = 1 / zz;
35
  const column = (COLS / 2 + scale * inverse * x) | 0;
36
  const row = (ROWS / 2 - scale * inverse * y * 0.56) | 0;
37
  if (column < 0 || column >= COLS || row < 0 || row >= ROWS) return;
38
  const cell = row * COLS + column;
39
  if (inverse <= depth[cell]) return;
40
  depth[cell] = inverse;
41
  pixels[row][column] = LUM[Math.min(LUM.length - 1, Math.max(1, (1 + light * 8) | 0))] ?? '@';
42
}
43
44
function project(
45
  pixels: string[][],
46
  depth: number[],
47
  x: number,
48
  y: number,
49
  z: number,
50
  nx: number,
51
  ny: number,
52
  nz: number,
53
  yaw: number,
54
  pitch: number,
55
) {
56
  [x, y, z] = rotateY(x, y, z, yaw);
57
  [x, y, z] = rotateX(x, y, z, pitch);
58
  [nx, ny, nz] = rotateY(nx, ny, nz, yaw);
59
  [nx, ny, nz] = rotateX(nx, ny, nz, pitch);
60
  const light = shade(nx, ny, nz);
61
  if (light < 0.08) return;
62
  plot(pixels, depth, x, y, z, light);
63
}
64
65
function plotCapsule(
66
  pixels: string[][],
67
  depth: number[],
68
  x1: number,
69
  y1: number,
70
  z1: number,
71
  x2: number,
72
  y2: number,
73
  z2: number,
74
  radius: number,
75
  yaw: number,
76
  pitch: number,
77
) {
78
  const dx = x2 - x1;
79
  const dy = y2 - y1;
80
  const dz = z2 - z1;
81
  const length = Math.hypot(dx, dy, dz) || 1;
82
  const ax = dx / length;
83
  const ay = dy / length;
84
  const az = dz / length;
85
  let px = ay;
86
  let py = -ax;
87
  let pz = 0;
88
  if (Math.hypot(px, py, pz) < 0.12) {
89
    px = 0;
90
    py = az;
91
    pz = -ay;
92
  }
93
  const plen = Math.hypot(px, py, pz) || 1;
94
  px /= plen;
95
  py /= plen;
96
  pz /= plen;
97
  const qx = ay * pz - az * py;
98
  const qy = az * px - ax * pz;
99
  const qz = ax * py - ay * px;
100
101
  for (let t = 0; t <= 1; t += 0.035) {
102
    const cx = x1 + dx * t;
103
    const cy = y1 + dy * t;
104
    const cz = z1 + dz * t;
105
    for (let angle = 0; angle < Math.PI * 2; angle += 0.09) {
106
      const cos = Math.cos(angle);
107
      const sin = Math.sin(angle);
108
      const nx = px * cos + qx * sin;
109
      const ny = py * cos + qy * sin;
110
      const nz = pz * cos + qz * sin;
111
      project(
112
        pixels,
113
        depth,
114
        cx + nx * radius,
115
        cy + ny * radius,
116
        cz + nz * radius,
117
        nx,
118
        ny,
119
        nz,
120
        yaw,
121
        pitch,
122
      );
123
    }
124
  }
125
}
126
127
function plotTorusArc(
128
  pixels: string[][],
129
  depth: number[],
130
  major: number,
131
  minor: number,
132
  thetaStart: number,
133
  thetaEnd: number,
134
  yaw: number,
135
  pitch: number,
136
  flip: boolean,
137
) {
138
  for (let theta = thetaStart; theta <= thetaEnd; theta += 0.04) {
139
    const cosT = Math.cos(theta);
140
    const sinT = Math.sin(theta);
141
    for (let phi = 0; phi < Math.PI * 2; phi += 0.08) {
142
      const cosP = Math.cos(phi);
143
      const sinP = Math.sin(phi);
144
      let x = (major + minor * cosP) * cosT;
145
      let y = (major + minor * cosP) * sinT;
146
      let z = minor * sinP;
147
      let nx = cosP * cosT;
148
      let ny = cosP * sinT;
149
      let nz = sinP;
150
      if (flip) {
151
        x = -x;
152
        y = -y;
153
        z = -z;
154
        nx = -nx;
155
        ny = -ny;
156
        nz = -nz;
157
      }
158
      project(pixels, depth, x, y, z, nx, ny, nz, yaw, pitch);
159
    }
160
  }
161
}
162
163
function renderMark(yaw: number, pitch: number, pixels: string[][], depth: number[]): string {
164
  for (let y = 0; y < ROWS; y += 1) {
165
    pixels[y].fill(' ');
166
    for (let x = 0; x < COLS; x += 1) depth[y * COLS + x] = 0;
167
  }
168
169
  const major = 1.08;
170
  const minor = 0.155;
171
  const thetaStart = (102 * Math.PI) / 180;
172
  const thetaEnd = (256 * Math.PI) / 180;
173
174
  for (const flip of [false, true]) {
175
    plotTorusArc(pixels, depth, major, minor, thetaStart, thetaEnd, yaw, pitch, flip);
176
    let x1 = major * Math.cos(thetaEnd);
177
    let y1 = major * Math.sin(thetaEnd);
178
    let x2 = 0.28;
179
    let y2 = -0.68;
180
    if (flip) {
181
      x1 = -x1;
182
      y1 = -y1;
183
      x2 = -x2;
184
      y2 = -y2;
185
    }
186
    plotCapsule(pixels, depth, x1, y1, 0, x2, y2, 0, minor, yaw, pitch);
187
  }
188
189
  return pixels.map((row) => row.join('')).join('\n');
190
}
191
192
export function LandingClock() {
193
  const ref = useRef<HTMLSpanElement>(null);
194
195
  useEffect(() => {
196
    const node = ref.current;
197
    if (!node) return;
198
    const write = () => {
199
      node.textContent = clockStamp();
200
    };
201
    write();
202
    if (reduceMotion) return;
203
    const timer = window.setInterval(write, 1000);
204
    return () => window.clearInterval(timer);
205
  }, []);
206
207
  return <span ref={ref} className="landingClock" />;
208
}
209
210
export function LandingBackdrop() {
211
  const preRef = useRef<HTMLPreElement>(null);
212
213
  useEffect(() => {
214
    const node = preRef.current;
215
    if (!node) return;
216
217
    const pixels = Array.from({ length: ROWS }, () => Array<string>(COLS).fill(' '));
218
    const depth = new Array<number>(ROWS * COLS).fill(0);
219
    let time = 0;
220
221
    const paint = () => {
222
      const yaw = reduceMotion ? 0.25 : time * 0.62;
223
      const pitch = reduceMotion ? 0.18 : 0.22 + time * 0.31;
224
      node.textContent = renderMark(yaw, pitch, pixels, depth);
225
    };
226
227
    paint();
228
    if (reduceMotion) return;
229
230
    let frame = 0;
231
    let last = 0;
232
    const tick = (now: number) => {
233
      if (now - last >= 50) {
234
        last = now;
235
        time += 0.032;
236
        paint();
237
      }
238
      frame = requestAnimationFrame(tick);
239
    };
240
    frame = requestAnimationFrame(tick);
241
    return () => cancelAnimationFrame(frame);
242
  }, []);
243
244
  return (
245
    <div className="landingBackdrop" aria-hidden="true">
246
      <pre ref={preRef} className="liveAscii" />
247
    </div>
248
  );
249
}
250
251
export function LandingDiagram() {
252
  return (
253
    <div
254
      className="asciiDiagram"
255
      role="group"
256
      aria-label="Example of OpenClasp warning a candidate agent before it shares personal data"
257
    >
258
      <span className="sceneCorner sceneTL">+</span>
259
      <span className="sceneCorner sceneTR">+</span>
260
      <span className="sceneCorner sceneBL">+</span>
261
      <div className="diagramHud">
262
        <span>RECRUITING EXAMPLE</span>
263
        <span className="diagramLive">PRIVATE INSIGHT</span>
264
      </div>
265
      <div className="insightSubject">
266
        <span>OPENCLASP INSIGHT</span>
267
        <strong>Recruiter agent you’re about to connect with</strong>
268
      </div>
269
      <section className="riskPanel" aria-labelledby="risk-title">
270
        <div className="riskHeading">
271
          <div>
272
            <span>WHAT YOUR AGENT SHOULD KNOW</span>
273
            <strong id="risk-title">3 red flags</strong>
274
          </div>
275
          <small>verified context + history</small>
276
        </div>
277
        <ol className="redFlagList">
278
          <li>
279
            <span>01</span>
280
            <div>
281
              <strong>This agent asks for personal data too early</strong>
282
              <p>No meaningful job details were provided first.</p>
283
            </div>
284
          </li>
285
          <li>
286
            <span>02</span>
287
            <div>
288
              <strong>This agent provides no verifiable job evidence</strong>
289
              <p>No role title, employer, salary range, or source link.</p>
290
            </div>
291
          </li>
292
          <li>
293
            <span>03</span>
294
            <div>
295
              <strong>This agent has a pattern of unsourced roles</strong>
296
              <p>3 similar interactions ended without a sourced role.</p>
297
            </div>
298
          </li>
299
        </ol>
300
      </section>
301
      <section className="guidancePanel" aria-labelledby="guidance-title">
302
        <span>PRIVATE GUIDANCE → CANDIDATE AGENT</span>
303
        <strong id="guidance-title">Do not share personal data yet.</strong>
304
        <p>Request the job source, employer, responsibilities, and salary range first.</p>
305
      </section>
306
      <div className="directLink">
307
        <span>candidate agent</span>
308
        <b>direct A2A</b>
309
        <span>recruiter agent</span>
310
      </div>
311
      <div className="exampleOutcome">
312
        <span>AGENT DECISION</span>
313
        <strong>No verified opening · personal data withheld</strong>
314
        <small>The structured outcome improves guidance for the next candidate.</small>
315
      </div>
316
      <p className="diagramSpec">raw conversation private · signed events only</p>
317
    </div>
318
  );
319
}