a2acalling 0.6.64 → 0.6.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,6 +45,11 @@
45
45
  <span class="material-symbols-outlined nav-icon" style="color:#EF4444;">monitor_heart</span>
46
46
  <span class="nav-label">Health</span>
47
47
  </a>
48
+ <!-- A2A-50: Settings nav item for relocated admin settings -->
49
+ <a data-panel="settings" class="nav-item">
50
+ <span class="material-symbols-outlined nav-icon" style="color:#6B7280;">settings</span>
51
+ <span class="nav-label">Settings</span>
52
+ </a>
48
53
  </nav>
49
54
  </aside>
50
55
 
@@ -125,6 +130,7 @@
125
130
  <span class="status-dot status-dot--teal"></span>
126
131
  Active Topics
127
132
  <span id="topic-count" class="count-badge count-badge--teal">0</span>
133
+ <button class="col-header-add-btn" data-add-type="topic" title="Add topic"><span class="material-symbols-outlined" style="font-size:16px;">add</span></button>
128
134
  </div>
129
135
  <div id="active-topics-zone" class="perm-drop-zone"></div>
130
136
  </div>
@@ -133,6 +139,7 @@
133
139
  <span class="status-dot status-dot--yellow"></span>
134
140
  Active Goals
135
141
  <span id="goal-count" class="count-badge count-badge--yellow">0</span>
142
+ <button class="col-header-add-btn" data-add-type="goal" title="Add goal"><span class="material-symbols-outlined" style="font-size:16px;">add</span></button>
136
143
  </div>
137
144
  <div id="active-goals-zone" class="perm-drop-zone"></div>
138
145
  </div>
@@ -147,77 +154,6 @@
147
154
 
148
155
  <!-- Tier Warnings -->
149
156
  <div id="tier-warnings" class="tier-warnings"></div>
150
-
151
- <!-- Settings & Administration (collapsed) -->
152
- <sl-details summary="Settings &amp; Administration">
153
- <h3>Defaults</h3>
154
- <form id="defaults-form">
155
- <sl-input id="defaults-expiration" label="Expiration" placeholder="7d"></sl-input>
156
- <sl-input id="defaults-max-calls" label="Max Calls" type="number" min="1"></sl-input>
157
- <div class="row">
158
- <sl-button type="submit" variant="primary">Save Defaults</sl-button>
159
- </div>
160
- </form>
161
-
162
- <h3>New Tier</h3>
163
- <form id="new-tier-form">
164
- <sl-input id="new-tier-id" label="Tier ID" placeholder="partners"></sl-input>
165
- <sl-input id="new-tier-name" label="Name" placeholder="Partners"></sl-input>
166
- <label>Copy from
167
- <sl-select id="new-tier-copy-from" size="small">
168
- <sl-option value="">None</sl-option>
169
- </sl-select>
170
- </label>
171
- <div class="row">
172
- <sl-button type="submit" variant="primary">Create Tier</sl-button>
173
- </div>
174
- </form>
175
-
176
- <h3>Remote Callbook</h3>
177
- <sl-card id="callbook-status"></sl-card>
178
-
179
- <h3>Auto Update</h3>
180
- <sl-card id="auto-update-status">Loading...</sl-card>
181
- <div class="row">
182
- <sl-button id="auto-update-check" size="small">Check now</sl-button>
183
- <sl-button id="auto-update-now" size="small">Update now</sl-button>
184
- <sl-button id="auto-update-toggle" size="small">Disable auto-update</sl-button>
185
- </div>
186
-
187
- <sl-card>
188
- <form id="callbook-provision-form">
189
- <div class="row">
190
- <sl-button type="submit" variant="primary" size="small">Create Install Link (24h)</sl-button>
191
- <sl-button id="callbook-logout" size="small" variant="default">Logout This Browser</sl-button>
192
- </div>
193
- <sl-input id="callbook-label" label="Device label" value="Callbook Remote"></sl-input>
194
- <sl-textarea id="callbook-install-url" label="Install URL" rows="3" readonly></sl-textarea>
195
- <div class="row">
196
- <sl-button id="callbook-copy-url" size="small">Copy Link</sl-button>
197
- </div>
198
- <div id="callbook-warnings" class="mono"></div>
199
- </form>
200
- </sl-card>
201
-
202
- <sl-card>
203
- <div class="row">
204
- <strong>Paired Devices</strong>
205
- </div>
206
- <table id="callbook-devices-table">
207
- <thead>
208
- <tr>
209
- <th>Label</th>
210
- <th>Created</th>
211
- <th>Last Used</th>
212
- <th>Sessions</th>
213
- <th>Revoked</th>
214
- <th>Action</th>
215
- </tr>
216
- </thead>
217
- <tbody></tbody>
218
- </table>
219
- </sl-card>
220
- </sl-details>
221
157
  </div>
222
158
 
223
159
  <!-- Right sidebar: preview + topic/goal lists -->
@@ -243,6 +179,30 @@
243
179
  <div id="preview-content"></div>
244
180
  <sl-button slot="footer" variant="primary" id="preview-close-btn">Close</sl-button>
245
181
  </sl-dialog>
182
+
183
+ <!-- A2A-50: Delete confirmation dialog for topics/goals -->
184
+ <sl-dialog id="delete-confirm-dialog" label="Confirm Removal" style="--width: 400px;">
185
+ <p id="delete-confirm-message">Remove this item from the tier?</p>
186
+ <div slot="footer" class="row">
187
+ <sl-button id="delete-confirm-no" variant="default">Cancel</sl-button>
188
+ <sl-button id="delete-confirm-yes" variant="danger">Remove</sl-button>
189
+ </div>
190
+ </sl-dialog>
191
+
192
+ <!-- A2A-50: New Tier dialog (glass-styled modal replacing inline form scroll) -->
193
+ <sl-dialog id="new-tier-dialog" label="Create New Tier" style="--width: 440px;">
194
+ <sl-input id="new-tier-dialog-id" label="Tier ID" placeholder="partners" required></sl-input>
195
+ <sl-input id="new-tier-dialog-name" label="Name" placeholder="Partners"></sl-input>
196
+ <label>Copy from
197
+ <sl-select id="new-tier-dialog-copy-from" size="small">
198
+ <sl-option value="">None</sl-option>
199
+ </sl-select>
200
+ </label>
201
+ <div slot="footer" class="row">
202
+ <sl-button id="new-tier-dialog-cancel" variant="default">Cancel</sl-button>
203
+ <sl-button id="new-tier-dialog-submit" variant="primary">Create Tier</sl-button>
204
+ </div>
205
+ </sl-dialog>
246
206
  </div>
247
207
 
248
208
  <div id="panel-invites" class="panel">
@@ -345,6 +305,77 @@
345
305
  <tbody></tbody>
346
306
  </table>
347
307
  </div>
308
+
309
+ <!-- A2A-50: Settings panel — relocated from sl-details in panel-permissions -->
310
+ <div id="panel-settings" class="panel">
311
+ <h3>Defaults</h3>
312
+ <form id="defaults-form">
313
+ <sl-input id="defaults-expiration" label="Expiration" placeholder="7d"></sl-input>
314
+ <sl-input id="defaults-max-calls" label="Max Calls" type="number" min="1"></sl-input>
315
+ <div class="row">
316
+ <sl-button type="submit" variant="primary">Save Defaults</sl-button>
317
+ </div>
318
+ </form>
319
+
320
+ <h3>New Tier</h3>
321
+ <form id="new-tier-form">
322
+ <sl-input id="new-tier-id" label="Tier ID" placeholder="partners"></sl-input>
323
+ <sl-input id="new-tier-name" label="Name" placeholder="Partners"></sl-input>
324
+ <label>Copy from
325
+ <sl-select id="new-tier-copy-from" size="small">
326
+ <sl-option value="">None</sl-option>
327
+ </sl-select>
328
+ </label>
329
+ <div class="row">
330
+ <sl-button type="submit" variant="primary">Create Tier</sl-button>
331
+ </div>
332
+ </form>
333
+
334
+ <h3>Remote Callbook</h3>
335
+ <sl-card id="callbook-status"></sl-card>
336
+
337
+ <h3>Auto Update</h3>
338
+ <sl-card id="auto-update-status">Loading...</sl-card>
339
+ <div class="row">
340
+ <sl-button id="auto-update-check" size="small">Check now</sl-button>
341
+ <sl-button id="auto-update-now" size="small">Update now</sl-button>
342
+ <sl-button id="auto-update-toggle" size="small">Disable auto-update</sl-button>
343
+ </div>
344
+
345
+ <sl-card>
346
+ <form id="callbook-provision-form">
347
+ <div class="row">
348
+ <sl-button type="submit" variant="primary" size="small">Create Install Link (24h)</sl-button>
349
+ <sl-button id="callbook-logout" size="small" variant="default">Logout This Browser</sl-button>
350
+ </div>
351
+ <sl-input id="callbook-label" label="Device label" value="Callbook Remote"></sl-input>
352
+ <sl-textarea id="callbook-install-url" label="Install URL" rows="3" readonly></sl-textarea>
353
+ <div class="row">
354
+ <sl-button id="callbook-copy-url" size="small">Copy Link</sl-button>
355
+ </div>
356
+ <div id="callbook-warnings" class="mono"></div>
357
+ </form>
358
+ </sl-card>
359
+
360
+ <sl-card>
361
+ <div class="row">
362
+ <strong>Paired Devices</strong>
363
+ </div>
364
+ <table id="callbook-devices-table">
365
+ <thead>
366
+ <tr>
367
+ <th>Label</th>
368
+ <th>Created</th>
369
+ <th>Last Used</th>
370
+ <th>Sessions</th>
371
+ <th>Revoked</th>
372
+ <th>Action</th>
373
+ </tr>
374
+ </thead>
375
+ <tbody></tbody>
376
+ </table>
377
+ </sl-card>
378
+ </div>
348
379
  </div>
349
380
  </main>
350
381
 
@@ -697,6 +697,25 @@ table tbody tr:hover td {
697
697
  padding: 0 0.25rem;
698
698
  }
699
699
 
700
+ /* A2A-51: "+" button in column headers for adding topics/goals without sidebar */
701
+ .col-header-add-btn {
702
+ margin-left: auto;
703
+ background: none;
704
+ border: 1px solid rgba(255,255,255,0.12);
705
+ border-radius: 4px;
706
+ color: var(--ink-muted);
707
+ cursor: pointer;
708
+ padding: 1px 4px;
709
+ display: flex;
710
+ align-items: center;
711
+ transition: color 0.15s ease, border-color 0.15s ease;
712
+ }
713
+
714
+ .col-header-add-btn:hover {
715
+ color: var(--ink);
716
+ border-color: rgba(255,255,255,0.3);
717
+ }
718
+
700
719
  .config-col-header--teal {
701
720
  color: #2DD4BF;
702
721
  }
@@ -1194,6 +1213,20 @@ sl-details::part(base) {
1194
1213
  color: var(--ink);
1195
1214
  }
1196
1215
 
1216
+ /* A2A-50: Glass styling for sl-dialog panels in the Permissions tab.
1217
+ Uses ::part(panel) to style the shadow DOM of Shoelace dialogs while
1218
+ preserving accessibility features (focus trap, ESC, ARIA). The codebase
1219
+ already uses ::part(base) on sl-card (line 205) and sl-details (line 1152). */
1220
+ #create-item-dialog::part(panel),
1221
+ #preview-dialog::part(panel),
1222
+ #delete-confirm-dialog::part(panel),
1223
+ #new-tier-dialog::part(panel) {
1224
+ background: rgba(30, 41, 59, 0.85);
1225
+ backdrop-filter: blur(16px);
1226
+ border: 1px solid rgba(255, 255, 255, 0.1);
1227
+ border-radius: 16px;
1228
+ }
1229
+
1197
1230
  /* ── A2A-48: Hide permissions sidebar below 1280px ─────────── */
1198
1231
  @media (max-width: 1280px) {
1199
1232
  .perm-sidebar {
package/src/lib/client.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const https = require('https');
6
6
  const http = require('http');
7
+ const { signRequest } = require('./crypto');
7
8
 
8
9
  function splitHostPort(rawHost) {
9
10
  const host = String(rawHost || '').trim();
@@ -54,6 +55,24 @@ class A2AClient {
54
55
  constructor(options = {}) {
55
56
  this.timeout = options.timeout || 60000;
56
57
  this.caller = options.caller || {};
58
+ // A2A-52: Ed25519 identity keys for request signing
59
+ this.privateKey = options.privateKey || null;
60
+ this.publicKey = options.publicKey || null;
61
+ }
62
+
63
+ /**
64
+ * A2A-52: Build signature headers if keypair is available.
65
+ * Shared helper used by both call() and end().
66
+ */
67
+ _signHeaders(method, endpoint, body) {
68
+ if (!this.privateKey || !this.publicKey) return {};
69
+ return signRequest({
70
+ privateKey: this.privateKey,
71
+ publicKey: this.publicKey,
72
+ method,
73
+ endpoint,
74
+ body
75
+ });
57
76
  }
58
77
 
59
78
  /**
@@ -95,6 +114,8 @@ class A2AClient {
95
114
  });
96
115
 
97
116
  const { protocol, hostname, port } = resolveProtocolAndPort(host);
117
+ // A2A-52: attach signature headers when keypair available
118
+ const sigHeaders = this._signHeaders('POST', '/api/a2a/invoke', body);
98
119
 
99
120
  return new Promise((resolve, reject) => {
100
121
  const req = protocol.request({
@@ -105,7 +126,8 @@ class A2AClient {
105
126
  headers: {
106
127
  'Authorization': `Bearer ${token}`,
107
128
  'Content-Type': 'application/json',
108
- 'Content-Length': Buffer.byteLength(body)
129
+ 'Content-Length': Buffer.byteLength(body),
130
+ ...sigHeaders
109
131
  },
110
132
  timeout: this.timeout
111
133
  }, (res) => {
@@ -141,7 +163,7 @@ class A2AClient {
141
163
 
142
164
  /**
143
165
  * Explicitly end a remote conversation and trigger call conclusion
144
- *
166
+ *
145
167
  * @param {string|object} endpoint - a2a:// URL or {host, token}
146
168
  * @param {string} conversationId - Conversation ID to conclude
147
169
  * @returns {Promise<object>} End response from remote agent
@@ -152,7 +174,7 @@ class A2AClient {
152
174
  }
153
175
 
154
176
  let host, token;
155
-
177
+
156
178
  if (typeof endpoint === 'string') {
157
179
  ({ host, token } = A2AClient.parseInvite(endpoint));
158
180
  } else {
@@ -164,6 +186,8 @@ class A2AClient {
164
186
  });
165
187
 
166
188
  const { protocol, hostname, port } = resolveProtocolAndPort(host);
189
+ // A2A-52: attach signature headers when keypair available
190
+ const sigHeaders = this._signHeaders('POST', '/api/a2a/end', body);
167
191
 
168
192
  return new Promise((resolve, reject) => {
169
193
  const req = protocol.request({
@@ -174,7 +198,8 @@ class A2AClient {
174
198
  headers: {
175
199
  'Authorization': `Bearer ${token}`,
176
200
  'Content-Type': 'application/json',
177
- 'Content-Length': Buffer.byteLength(body)
201
+ 'Content-Length': Buffer.byteLength(body),
202
+ ...sigHeaders
178
203
  },
179
204
  timeout: this.timeout
180
205
  }, (res) => {
package/src/lib/config.js CHANGED
@@ -230,10 +230,13 @@ const DEFAULT_CONFIG = {
230
230
  },
231
231
 
232
232
  // Agent info
233
+ // A2A-52: private_key/public_key store Ed25519 identity (base64 DER)
233
234
  agent: {
234
235
  name: '',
235
236
  description: '',
236
- hostname: ''
237
+ hostname: '',
238
+ private_key: null,
239
+ public_key: null
237
240
  },
238
241
 
239
242
  // Auto-updater
@@ -386,6 +389,21 @@ class A2AConfig {
386
389
  this._save();
387
390
  }
388
391
 
392
+ // A2A-52: Get Ed25519 keypair from agent config (null if not generated)
393
+ getKeypair() {
394
+ const agent = this.config.agent || {};
395
+ if (!agent.private_key || !agent.public_key) return null;
396
+ return { privateKey: agent.private_key, publicKey: agent.public_key };
397
+ }
398
+
399
+ // A2A-52: Store Ed25519 keypair in agent config (already 0o600 via _save)
400
+ setKeypair(privateKey, publicKey) {
401
+ this.config.agent = this.config.agent || {};
402
+ this.config.agent.private_key = privateKey;
403
+ this.config.agent.public_key = publicKey;
404
+ this._save();
405
+ }
406
+
389
407
  // Get full config
390
408
  getAll() {
391
409
  return this.config;
@@ -424,12 +442,13 @@ class A2AConfig {
424
442
  return next;
425
443
  }
426
444
 
427
- // Export for sharing
445
+ // Export for sharing (strips private_key to prevent leakage — A2A-52)
428
446
  export() {
447
+ const { private_key, ...agentPublic } = this.config.agent || {};
429
448
  return {
430
449
  tiers: this.config.tiers,
431
450
  defaults: this.config.defaults,
432
- agent: this.config.agent
451
+ agent: agentPublic
433
452
  };
434
453
  }
435
454
  }
@@ -147,7 +147,13 @@ class ConversationDriver {
147
147
  const clientTimeout = this.claudeMode
148
148
  ? Math.max(this.claudeTimeoutMs + 20000, 200000)
149
149
  : 65000;
150
- this.client = new A2AClient({ caller: this.caller, timeout: clientTimeout });
150
+ // A2A-52: pass Ed25519 keypair for request signing
151
+ this.client = new A2AClient({
152
+ caller: this.caller,
153
+ timeout: clientTimeout,
154
+ privateKey: options.privateKey || null,
155
+ publicKey: options.publicKey || null
156
+ });
151
157
  }
152
158
 
153
159
  /**
@@ -0,0 +1,113 @@
1
+ /**
2
+ * A2A Ed25519 Cryptographic Identity
3
+ *
4
+ * Provides keypair generation, request signing, signature verification,
5
+ * and public key fingerprinting for agent-to-agent identity verification.
6
+ *
7
+ * A2A-52: Zero new dependencies — uses Node.js built-in crypto (Ed25519 since v15).
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+
12
+ // A2A-52: 5-minute window for replay protection
13
+ const TIMESTAMP_WINDOW_MS = 5 * 60 * 1000;
14
+
15
+ /**
16
+ * Generate an Ed25519 keypair.
17
+ * Returns { privateKey, publicKey } as base64-encoded DER buffers.
18
+ */
19
+ function generateKeypair() {
20
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
21
+ privateKeyEncoding: { type: 'pkcs8', format: 'der' },
22
+ publicKeyEncoding: { type: 'spki', format: 'der' }
23
+ });
24
+ return {
25
+ privateKey: privateKey.toString('base64'),
26
+ publicKey: publicKey.toString('base64')
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Compute a SHA-256 fingerprint of a base64-encoded public key.
32
+ * Returns colon-separated hex string (like SSH fingerprints).
33
+ */
34
+ function fingerprint(publicKeyBase64) {
35
+ const hash = crypto.createHash('sha256')
36
+ .update(Buffer.from(publicKeyBase64, 'base64'))
37
+ .digest('hex');
38
+ // A2A-52: colon-separated pairs for readability (SSH-style)
39
+ return hash.match(/.{2}/g).join(':');
40
+ }
41
+
42
+ /**
43
+ * Sign an outbound request.
44
+ *
45
+ * Signing payload: `${timestamp}:${method}:${endpoint}:${bodyHash}`
46
+ * where bodyHash = SHA-256 of the request body string.
47
+ *
48
+ * @param {object} params
49
+ * @param {string} params.privateKey - base64-encoded DER private key
50
+ * @param {string} params.publicKey - base64-encoded DER public key
51
+ * @param {string} params.method - HTTP method (e.g. 'POST')
52
+ * @param {string} params.endpoint - Request path (e.g. '/api/a2a/invoke')
53
+ * @param {string} params.body - Serialized request body
54
+ * @returns {object} Headers to attach: { 'X-A2A-Signature', 'X-A2A-Public-Key', 'X-A2A-Timestamp' }
55
+ */
56
+ function signRequest({ privateKey, publicKey, method, endpoint, body }) {
57
+ const timestamp = new Date().toISOString();
58
+ const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
59
+ const payload = `${timestamp}:${method}:${endpoint}:${bodyHash}`;
60
+
61
+ const keyObject = crypto.createPrivateKey({
62
+ key: Buffer.from(privateKey, 'base64'),
63
+ format: 'der',
64
+ type: 'pkcs8'
65
+ });
66
+
67
+ const signature = crypto.sign(null, Buffer.from(payload), keyObject);
68
+
69
+ return {
70
+ 'X-A2A-Signature': signature.toString('base64'),
71
+ 'X-A2A-Public-Key': publicKey,
72
+ 'X-A2A-Timestamp': timestamp
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Verify an inbound request signature.
78
+ *
79
+ * @param {object} params
80
+ * @param {string} params.signature - base64-encoded Ed25519 signature
81
+ * @param {string} params.publicKey - base64-encoded DER public key
82
+ * @param {string} params.timestamp - ISO 8601 timestamp from header
83
+ * @param {string} params.method - HTTP method
84
+ * @param {string} params.endpoint - Request path
85
+ * @param {string} params.body - Raw request body string
86
+ * @returns {boolean} true if signature is valid
87
+ */
88
+ function verifySignature({ signature, publicKey, timestamp, method, endpoint, body }) {
89
+ const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
90
+ const payload = `${timestamp}:${method}:${endpoint}:${bodyHash}`;
91
+
92
+ const keyObject = crypto.createPublicKey({
93
+ key: Buffer.from(publicKey, 'base64'),
94
+ format: 'der',
95
+ type: 'spki'
96
+ });
97
+
98
+ return crypto.verify(null, Buffer.from(payload), keyObject, Buffer.from(signature, 'base64'));
99
+ }
100
+
101
+ /**
102
+ * Check if a timestamp is within the allowed window (replay protection).
103
+ * @param {string} timestamp - ISO 8601 timestamp
104
+ * @returns {boolean} true if within +-5 minutes of now
105
+ */
106
+ function isTimestampValid(timestamp) {
107
+ const ts = new Date(timestamp).getTime();
108
+ if (Number.isNaN(ts)) return false;
109
+ const diff = Math.abs(Date.now() - ts);
110
+ return diff <= TIMESTAMP_WINDOW_MS;
111
+ }
112
+
113
+ module.exports = { generateKeypair, fingerprint, signRequest, verifySignature, isTimestampValid };
package/src/lib/tokens.js CHANGED
@@ -448,6 +448,7 @@ class TokenStore {
448
448
  tags: Array.isArray(options.tags) ? options.tags : [],
449
449
  fields: sanitizeCustomFields(options.fields || options.custom_fields || options.customFields),
450
450
  linked_token_id: options.linkedTokenId || options.linked_token_id || null, // Token you gave them
451
+ public_key: options.public_key || options.publicKey || null, // A2A-52: Ed25519 public key (base64 DER)
451
452
  status: 'unknown',
452
453
  last_seen: null,
453
454
  added_at: new Date().toISOString(),
@@ -597,7 +598,8 @@ class TokenStore {
597
598
  }
598
599
 
599
600
  // Only allow updating specific fields
600
- const allowed = ['name', 'owner', 'is_mine', 'notes', 'tags', 'linked_token_id', 'server_name', 'fields'];
601
+ // A2A-52: 'public_key' added for Ed25519 identity verification (TOFU pinning)
602
+ const allowed = ['name', 'owner', 'is_mine', 'notes', 'tags', 'linked_token_id', 'server_name', 'fields', 'public_key'];
601
603
  for (const key of allowed) {
602
604
  if (updates[key] !== undefined) {
603
605
  if (key === 'fields') {
@@ -764,6 +766,7 @@ class TokenStore {
764
766
  tags: ['inbound'],
765
767
  fields: {},
766
768
  linked_token_id: tokenId || null,
769
+ public_key: null, // A2A-52: populated via TOFU on first verified call
767
770
  status: 'unknown',
768
771
  last_seen: null,
769
772
  added_at: new Date().toISOString(),