@nelsonlongxiang/dsh-open-a2a-net 0.6.36 → 0.6.37

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.
Files changed (43) hide show
  1. package/README.md +4 -1
  2. package/README_zh.md +4 -1
  3. package/assets/nexusDist/assets/index-C7HagaGy.css +1 -0
  4. package/assets/nexusDist/assets/index-NkNSeIx6.js +9 -0
  5. package/assets/nexusDist/index.html +2 -2
  6. package/cli/a2a.mjs +1 -1
  7. package/lib/a2a-client.js +3 -3
  8. package/lib/client.js +63 -63
  9. package/lib/faces/admin.js +20 -4
  10. package/lib/faces/canvas-notify.js +8 -6
  11. package/lib/faces/wire.js +11 -5
  12. package/lib/index.js +30 -10
  13. package/lib/origin-claim.js +30 -7
  14. package/lib/owner-settings.js +38 -1
  15. package/lib/peer-link-display.js +15 -0
  16. package/lib/skill/references/cli.md +4 -0
  17. package/lib/skill/references/setup.md +4 -0
  18. package/lib/task-ledger-rows.js +192 -0
  19. package/lib/task-ledger.js +2 -191
  20. package/lib/tools/info.js +3 -1
  21. package/lib/types/a2a-client.d.ts +1 -0
  22. package/lib/types/faces/admin.d.ts +1 -0
  23. package/lib/types/faces/canvas-notify.d.ts +3 -1
  24. package/lib/types/faces/wire.d.ts +1 -0
  25. package/lib/types/index.d.ts +2 -0
  26. package/lib/types/origin-claim.d.ts +12 -3
  27. package/lib/types/owner-settings.d.ts +5 -0
  28. package/lib/types/peer-link-display.d.ts +13 -0
  29. package/lib/types/task-ledger-rows.d.ts +341 -0
  30. package/lib/types/task-ledger.d.ts +4 -288
  31. package/lib/types/tools/info.d.ts +2 -0
  32. package/lib/types/types.d.ts +2 -1
  33. package/nexus-stage/src/planning-member-handles.ts +28 -1
  34. package/nexus-stage/src/planning-menus.ts +12 -5
  35. package/nexus-stage/src/planning-view.ts +18 -14
  36. package/nexus-stage/src/seat-name-editor.ts +31 -0
  37. package/nexus-stage/src/session-drag.ts +64 -0
  38. package/nexus-stage/src/wb-directory.ts +2 -0
  39. package/nexus-stage/src/workbench.css +2 -0
  40. package/nexus-stage/src/workbench.ts +16 -5
  41. package/package.json +1 -1
  42. package/assets/nexusDist/assets/index-DIQNDBy6.js +0 -9
  43. package/assets/nexusDist/assets/index-z33lHtLw.css +0 -1
package/cli/a2a.mjs CHANGED
@@ -938,7 +938,7 @@ async function main() {
938
938
  const q = (positionals()[0] ?? '').toLowerCase()
939
939
  const rows = [
940
940
  ...['dsh'].map(t => ({ team: String(t), name: String(s.session ?? 'host'), where: 'host' })),
941
- ...(s.sessions ?? []).map(x => ({ team: String(x.team ?? ''), name: String(x.label ?? ''), where: 'local' })),
941
+ ...(s.sessions ?? []).map(x => ({ team: String(x.team ?? ''), name: String(x.seatName ?? x.name ?? x.label ?? ''), where: 'local' })),
942
942
  ...(s.remote ?? []).map(x => ({ team: String(x.team ?? ''), name: String(x.name ?? ''), where: `remote · ${String(x.via ?? '')}` })),
943
943
  ]
944
944
  const hits = rows.filter(r => q === '' || r.team.toLowerCase().includes(q) || r.name.toLowerCase().includes(q))
package/lib/a2a-client.js CHANGED
@@ -29,7 +29,7 @@ export class A2aClient {
29
29
  constructor(options) {
30
30
  this.options = options;
31
31
  }
32
- headers(json, url, method) {
32
+ headers(json, url, method, body) {
33
33
  const headers = {};
34
34
  if (json)
35
35
  headers['Content-Type'] = 'application/json';
@@ -43,7 +43,7 @@ export class A2aClient {
43
43
  // peer request when the signer is wired (full-card reads and delivery
44
44
  // verification both ride it).
45
45
  if (this.options.originClaim !== undefined) {
46
- for (const [key, value] of Object.entries(this.options.originClaim({ url, method })))
46
+ for (const [key, value] of Object.entries(this.options.originClaim({ url, method, ...(body !== undefined ? { body } : {}) })))
47
47
  headers[key] = value;
48
48
  }
49
49
  return headers;
@@ -65,7 +65,7 @@ export class A2aClient {
65
65
  const url = `${baseUrl}${path}`;
66
66
  const response = await this.options.fetch(url, {
67
67
  method: init.method,
68
- headers: { ...this.headers(init.body !== undefined, url, init.method), ...(init.guard !== undefined ? { 'X-A2A-Guard': init.guard } : {}) },
68
+ headers: { ...this.headers(init.body !== undefined, url, init.method, init.body), ...(init.guard !== undefined ? { 'X-A2A-Guard': init.guard } : {}) },
69
69
  ...(init.body !== undefined ? { body: init.body } : {}),
70
70
  signal: controller.signal,
71
71
  });
package/lib/client.js CHANGED
@@ -22,8 +22,8 @@ window.__ModuleLoader__.load({
22
22
  return n;
23
23
  }
24
24
  //#endregion
25
- //#region \0dsh-css:D:\workspace\dsh-open-a2a-net\tmp\release-0.6.36-51b18cc\src\src\client\A2aControl.module.css.mjs
26
- const css = ".KfHpVG_wrap{max-width:100%;display:inline-flex;position:relative}.KfHpVG_trigger{align-items:center;gap:4px;display:inline-flex;position:relative}.KfHpVG_unreadBadge{text-align:center;color:#fff;background:var(--dsh-accent,#4c8bf5);border-radius:7px;min-width:14px;height:14px;padding:0 3px;font-size:9px;line-height:14px;animation:2s ease-in-out infinite KfHpVG_badgePulse;position:absolute;top:-6px;right:-8px}@keyframes KfHpVG_badgePulse{0%,to{box-shadow:0 0 #4c8bf573}50%{box-shadow:0 0 0 4px #4c8bf500}}@media (prefers-reduced-motion:reduce){.KfHpVG_unreadBadge{animation:none}}.KfHpVG_popover{z-index:1100;border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface-raised,#1c1c22);border-radius:10px;flex-direction:column;gap:8px;width:320px;max-width:calc(100vw - 16px);max-height:60vh;padding:12px;display:flex;position:fixed;overflow-y:auto;box-shadow:0 8px 24px #00000059}.KfHpVG_title{font-size:13px;font-weight:600}.KfHpVG_sectionTitle{border-top:1px solid var(--dsh-border,#2a2a30);text-transform:uppercase;letter-spacing:.04em;opacity:.65;margin-top:4px;padding-top:8px;font-size:11px;font-weight:600}.KfHpVG_stageRow{border-top:1px solid var(--dsh-border,#2a2a30);align-items:center;gap:8px;margin-top:4px;padding-top:8px;display:flex}.KfHpVG_stageTitle{text-transform:uppercase;letter-spacing:.04em;opacity:.65;font-size:11px;font-weight:600}.KfHpVG_stageLinks{gap:6px;display:flex}.KfHpVG_stageLink{border:1px solid var(--dsh-border,#2a2a30);color:inherit;border-radius:999px;align-items:center;padding:3px 8px;font-size:11px;text-decoration:none;display:inline-flex}.KfHpVG_stageLink:hover,.KfHpVG_stageLink:focus-visible{border-color:var(--dsh-accent,#4c8bf5)}.KfHpVG_empty{opacity:.7;font-size:12px}.KfHpVG_row{border:1px solid #0000;border-radius:8px;align-items:center;gap:10px;padding:8px;display:flex}.KfHpVG_joined{border-color:var(--dsh-border,#2a2a30)}.KfHpVG_facts{text-align:left;min-width:0;color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;border-radius:6px;flex-direction:column;flex:1;gap:2px;padding:2px 4px;display:flex}.KfHpVG_facts:hover{background:var(--dsh-surface-hover,#ffffff0f)}.KfHpVG_nameRow{align-items:center;gap:6px;display:flex}.KfHpVG_stateDot{border-radius:50%;flex:none;width:7px;height:7px}.KfHpVG_stateDot.KfHpVG_live{background:var(--dsh-accent,#4c8bf5)}.KfHpVG_stateDot.KfHpVG_live.KfHpVG_joined{background:var(--dsh-success,#3fb96f)}.KfHpVG_stateDot.KfHpVG_cold{background:var(--dsh-warning,#d9a13c)}.KfHpVG_name{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.KfHpVG_description{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.KfHpVG_team{opacity:.55;font-size:11px;font-family:var(--dsh-mono,monospace)}.KfHpVG_note{opacity:.6;font-size:11px}.KfHpVG_searchInput{box-sizing:border-box;border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface,#141419);width:100%;color:inherit;border-radius:6px;padding:5px 8px;font-size:12px}.KfHpVG_searchInput:focus{outline:1px solid var(--dsh-accent,#4c8bf5)}.KfHpVG_sessionGroup{flex-direction:column;gap:2px;display:flex}.KfHpVG_groupHead{color:inherit;font:inherit;opacity:.75;cursor:pointer;text-align:left;background:0 0;border:none;align-items:center;gap:6px;padding:4px 2px;font-size:11px;font-weight:600;display:flex}.KfHpVG_groupChevron{transition:transform .12s;display:inline-block}.KfHpVG_groupChevronClosed{transform:rotate(-90deg)}.KfHpVG_groupTag{text-overflow:ellipsis;white-space:nowrap;border:1px solid var(--dsh-border,#2a2a30);opacity:.8;border-radius:999px;flex:none;max-width:90px;padding:0 6px;font-size:10px;line-height:16px;overflow:hidden}.KfHpVG_groupButton{width:24px;height:24px;color:inherit;opacity:.55;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;flex:none;font-size:12px;line-height:1}.KfHpVG_groupButton:hover{border-color:var(--dsh-border,#2a2a30);opacity:1}.KfHpVG_groupPicker{border:1px dashed var(--dsh-border,#2a2a30);border-radius:8px;flex-wrap:wrap;gap:6px;width:100%;padding:8px;display:flex;position:relative}.KfHpVG_groupOption{border:1px solid var(--dsh-border,#2a2a30);color:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:2px 8px;font-size:11px}.KfHpVG_groupOptionActive{border-color:var(--dsh-accent,#4c8bf5);color:var(--dsh-accent,#4c8bf5)}.KfHpVG_groupNew{align-items:center;gap:6px;width:100%;margin-top:2px;display:flex}.KfHpVG_groupInput{border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface,#141419);min-width:0;color:inherit;border-radius:6px;flex:1;padding:4px 8px;font-size:12px}.KfHpVG_peerGrid{flex-wrap:wrap;gap:6px;display:flex}.KfHpVG_peerChip{border:1px solid var(--dsh-border,#2a2a30);border-radius:999px;align-items:center;gap:6px;padding:3px 8px;font-size:11px;display:inline-flex}.KfHpVG_peerHost{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}.KfHpVG_peerScore{font-family:var(--dsh-mono,monospace);opacity:.6}.KfHpVG_activityList{flex-direction:column;gap:2px;display:flex}.KfHpVG_activityRow{border-radius:4px;align-items:center;gap:8px;padding:2px 4px;font-size:11px;display:flex}.KfHpVG_activityRow[data-ok=false]{opacity:.55}.KfHpVG_activityDir{flex:none;font-weight:600}.KfHpVG_activityDir.KfHpVG_in{color:var(--dsh-success,#3fb96f)}.KfHpVG_activityDir.KfHpVG_out{color:var(--dsh-accent,#4c8bf5)}.KfHpVG_activityTeam{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--dsh-mono,monospace);opacity:.8;flex:1;overflow:hidden}.KfHpVG_activityPeer{text-overflow:ellipsis;white-space:nowrap;opacity:.55;max-width:90px;overflow:hidden}.KfHpVG_activityTime{opacity:.5;font-family:var(--dsh-mono,monospace);flex:none}.KfHpVG_activityRow[data-jump=true]{cursor:pointer}.KfHpVG_activityRow[data-jump=true]:hover{background:var(--dsh-surface-hover,#ffffff0f)}.KfHpVG_fresh{animation:1.2s ease-out 3 KfHpVG_freshFlash}@keyframes KfHpVG_freshFlash{0%{background:#4c8bf538}to{background:0 0}}@media (prefers-reduced-motion:reduce){.KfHpVG_fresh{background:#4c8bf51f;animation:none}}.KfHpVG_inFlightList{flex-direction:column;gap:2px;margin-bottom:4px;display:flex}.KfHpVG_inFlightRow{border:1px dashed var(--dsh-border,#2a2a30);border-radius:4px;align-items:center;gap:8px;padding:3px 4px;font-size:11px;display:flex}.KfHpVG_inFlightPulse{background:var(--dsh-accent,#4c8bf5);border-radius:50%;flex:none;width:7px;height:7px;animation:1.1s ease-in-out infinite KfHpVG_flightPulse}@keyframes KfHpVG_flightPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.55)}}@media (prefers-reduced-motion:reduce){.KfHpVG_inFlightPulse{animation:none}}.KfHpVG_versionTag{color:var(--dsh-color-text-tertiary,#888);background:var(--dsh-color-bg-secondary,#8080801f);border-radius:999px;margin-left:auto;padding:1px 6px;font-size:10px;font-weight:500}.KfHpVG_inFlightStale{opacity:.55}.KfHpVG_canvasCreate{align-items:center;gap:6px;margin-bottom:4px;display:flex}.KfHpVG_canvasCreate .KfHpVG_groupInput{flex:1;min-width:0}.KfHpVG_canvasChip{max-width:100%;color:var(--dsh-color-text-secondary,#aaa);background:var(--dsh-color-bg-secondary,#8080801f);border-radius:999px;align-items:center;gap:4px;margin:2px 4px 2px 0;padding:1px 6px;font-size:11px;display:inline-flex}.KfHpVG_canvasChip>span:not([class]){white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.KfHpVG_canvasChipRemove{cursor:pointer;min-width:24px;min-height:24px;color:var(--dsh-color-text-tertiary,#888);background:0 0;border:none;padding:2px 5px;font-size:11px;line-height:1}.KfHpVG_canvasChipRemove:hover{color:var(--dsh-color-text-primary,#eee)}.KfHpVG_canvasChipRemove{transition:color .14s ease-out}@media (prefers-reduced-motion:reduce){.KfHpVG_canvasChipRemove{transition:none}}.KfHpVG_canvasChipRemove:focus-visible{outline:2px solid var(--dsh-color-primary,#4f8cff);outline-offset:1px;border-radius:4px}.KfHpVG_canvasActions{gap:6px;margin-top:4px;display:flex}.KfHpVG_inFlightStale .KfHpVG_inFlightPulse{opacity:.4;animation:none}.KfHpVG_failMark{color:#f87171;flex-shrink:0;font-size:11px}.KfHpVG_staleNote{color:var(--dsh-text-muted,#94a3b8b3);padding:4px 8px;font-size:11px}";
25
+ //#region \0dsh-css:D:\workspace\dsh-open-a2a-net\tmp\release-0.6.37-d5df179\src\src\client\A2aControl.module.css.mjs
26
+ const css = ".efpdCG_wrap{max-width:100%;display:inline-flex;position:relative}.efpdCG_trigger{align-items:center;gap:4px;display:inline-flex;position:relative}.efpdCG_unreadBadge{text-align:center;color:#fff;background:var(--dsh-accent,#4c8bf5);border-radius:7px;min-width:14px;height:14px;padding:0 3px;font-size:9px;line-height:14px;animation:2s ease-in-out infinite efpdCG_badgePulse;position:absolute;top:-6px;right:-8px}@keyframes efpdCG_badgePulse{0%,to{box-shadow:0 0 #4c8bf573}50%{box-shadow:0 0 0 4px #4c8bf500}}@media (prefers-reduced-motion:reduce){.efpdCG_unreadBadge{animation:none}}.efpdCG_popover{z-index:1100;border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface-raised,#1c1c22);border-radius:10px;flex-direction:column;gap:8px;width:320px;max-width:calc(100vw - 16px);max-height:60vh;padding:12px;display:flex;position:fixed;overflow-y:auto;box-shadow:0 8px 24px #00000059}.efpdCG_title{font-size:13px;font-weight:600}.efpdCG_sectionTitle{border-top:1px solid var(--dsh-border,#2a2a30);text-transform:uppercase;letter-spacing:.04em;opacity:.65;margin-top:4px;padding-top:8px;font-size:11px;font-weight:600}.efpdCG_stageRow{border-top:1px solid var(--dsh-border,#2a2a30);align-items:center;gap:8px;margin-top:4px;padding-top:8px;display:flex}.efpdCG_stageTitle{text-transform:uppercase;letter-spacing:.04em;opacity:.65;font-size:11px;font-weight:600}.efpdCG_stageLinks{gap:6px;display:flex}.efpdCG_stageLink{border:1px solid var(--dsh-border,#2a2a30);color:inherit;border-radius:999px;align-items:center;padding:3px 8px;font-size:11px;text-decoration:none;display:inline-flex}.efpdCG_stageLink:hover,.efpdCG_stageLink:focus-visible{border-color:var(--dsh-accent,#4c8bf5)}.efpdCG_empty{opacity:.7;font-size:12px}.efpdCG_row{border:1px solid #0000;border-radius:8px;align-items:center;gap:10px;padding:8px;display:flex}.efpdCG_joined{border-color:var(--dsh-border,#2a2a30)}.efpdCG_facts{text-align:left;min-width:0;color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;border-radius:6px;flex-direction:column;flex:1;gap:2px;padding:2px 4px;display:flex}.efpdCG_facts:hover{background:var(--dsh-surface-hover,#ffffff0f)}.efpdCG_nameRow{align-items:center;gap:6px;display:flex}.efpdCG_stateDot{border-radius:50%;flex:none;width:7px;height:7px}.efpdCG_stateDot.efpdCG_live{background:var(--dsh-accent,#4c8bf5)}.efpdCG_stateDot.efpdCG_live.efpdCG_joined{background:var(--dsh-success,#3fb96f)}.efpdCG_stateDot.efpdCG_cold{background:var(--dsh-warning,#d9a13c)}.efpdCG_name{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}.efpdCG_description{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.efpdCG_team{opacity:.55;font-size:11px;font-family:var(--dsh-mono,monospace)}.efpdCG_note{opacity:.6;font-size:11px}.efpdCG_searchInput{box-sizing:border-box;border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface,#141419);width:100%;color:inherit;border-radius:6px;padding:5px 8px;font-size:12px}.efpdCG_searchInput:focus{outline:1px solid var(--dsh-accent,#4c8bf5)}.efpdCG_sessionGroup{flex-direction:column;gap:2px;display:flex}.efpdCG_groupHead{color:inherit;font:inherit;opacity:.75;cursor:pointer;text-align:left;background:0 0;border:none;align-items:center;gap:6px;padding:4px 2px;font-size:11px;font-weight:600;display:flex}.efpdCG_groupChevron{transition:transform .12s;display:inline-block}.efpdCG_groupChevronClosed{transform:rotate(-90deg)}.efpdCG_groupTag{text-overflow:ellipsis;white-space:nowrap;border:1px solid var(--dsh-border,#2a2a30);opacity:.8;border-radius:999px;flex:none;max-width:90px;padding:0 6px;font-size:10px;line-height:16px;overflow:hidden}.efpdCG_groupButton{width:24px;height:24px;color:inherit;opacity:.55;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;flex:none;font-size:12px;line-height:1}.efpdCG_groupButton:hover{border-color:var(--dsh-border,#2a2a30);opacity:1}.efpdCG_groupPicker{border:1px dashed var(--dsh-border,#2a2a30);border-radius:8px;flex-wrap:wrap;gap:6px;width:100%;padding:8px;display:flex;position:relative}.efpdCG_groupOption{border:1px solid var(--dsh-border,#2a2a30);color:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:2px 8px;font-size:11px}.efpdCG_groupOptionActive{border-color:var(--dsh-accent,#4c8bf5);color:var(--dsh-accent,#4c8bf5)}.efpdCG_groupNew{align-items:center;gap:6px;width:100%;margin-top:2px;display:flex}.efpdCG_groupInput{border:1px solid var(--dsh-border,#2a2a30);background:var(--dsh-surface,#141419);min-width:0;color:inherit;border-radius:6px;flex:1;padding:4px 8px;font-size:12px}.efpdCG_peerGrid{flex-wrap:wrap;gap:6px;display:flex}.efpdCG_peerChip{border:1px solid var(--dsh-border,#2a2a30);border-radius:999px;align-items:center;gap:6px;padding:3px 8px;font-size:11px;display:inline-flex}.efpdCG_peerHost{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}.efpdCG_peerScore{font-family:var(--dsh-mono,monospace);opacity:.6}.efpdCG_activityList{flex-direction:column;gap:2px;display:flex}.efpdCG_activityRow{border-radius:4px;align-items:center;gap:8px;padding:2px 4px;font-size:11px;display:flex}.efpdCG_activityRow[data-ok=false]{opacity:.55}.efpdCG_activityDir{flex:none;font-weight:600}.efpdCG_activityDir.efpdCG_in{color:var(--dsh-success,#3fb96f)}.efpdCG_activityDir.efpdCG_out{color:var(--dsh-accent,#4c8bf5)}.efpdCG_activityTeam{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--dsh-mono,monospace);opacity:.8;flex:1;overflow:hidden}.efpdCG_activityPeer{text-overflow:ellipsis;white-space:nowrap;opacity:.55;max-width:90px;overflow:hidden}.efpdCG_activityTime{opacity:.5;font-family:var(--dsh-mono,monospace);flex:none}.efpdCG_activityRow[data-jump=true]{cursor:pointer}.efpdCG_activityRow[data-jump=true]:hover{background:var(--dsh-surface-hover,#ffffff0f)}.efpdCG_fresh{animation:1.2s ease-out 3 efpdCG_freshFlash}@keyframes efpdCG_freshFlash{0%{background:#4c8bf538}to{background:0 0}}@media (prefers-reduced-motion:reduce){.efpdCG_fresh{background:#4c8bf51f;animation:none}}.efpdCG_inFlightList{flex-direction:column;gap:2px;margin-bottom:4px;display:flex}.efpdCG_inFlightRow{border:1px dashed var(--dsh-border,#2a2a30);border-radius:4px;align-items:center;gap:8px;padding:3px 4px;font-size:11px;display:flex}.efpdCG_inFlightPulse{background:var(--dsh-accent,#4c8bf5);border-radius:50%;flex:none;width:7px;height:7px;animation:1.1s ease-in-out infinite efpdCG_flightPulse}@keyframes efpdCG_flightPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.55)}}@media (prefers-reduced-motion:reduce){.efpdCG_inFlightPulse{animation:none}}.efpdCG_versionTag{color:var(--dsh-color-text-tertiary,#888);background:var(--dsh-color-bg-secondary,#8080801f);border-radius:999px;margin-left:auto;padding:1px 6px;font-size:10px;font-weight:500}.efpdCG_inFlightStale{opacity:.55}.efpdCG_canvasCreate{align-items:center;gap:6px;margin-bottom:4px;display:flex}.efpdCG_canvasCreate .efpdCG_groupInput{flex:1;min-width:0}.efpdCG_canvasChip{max-width:100%;color:var(--dsh-color-text-secondary,#aaa);background:var(--dsh-color-bg-secondary,#8080801f);border-radius:999px;align-items:center;gap:4px;margin:2px 4px 2px 0;padding:1px 6px;font-size:11px;display:inline-flex}.efpdCG_canvasChip>span:not([class]){white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}.efpdCG_canvasChipRemove{cursor:pointer;min-width:24px;min-height:24px;color:var(--dsh-color-text-tertiary,#888);background:0 0;border:none;padding:2px 5px;font-size:11px;line-height:1}.efpdCG_canvasChipRemove:hover{color:var(--dsh-color-text-primary,#eee)}.efpdCG_canvasChipRemove{transition:color .14s ease-out}@media (prefers-reduced-motion:reduce){.efpdCG_canvasChipRemove{transition:none}}.efpdCG_canvasChipRemove:focus-visible{outline:2px solid var(--dsh-color-primary,#4f8cff);outline-offset:1px;border-radius:4px}.efpdCG_canvasActions{gap:6px;margin-top:4px;display:flex}.efpdCG_inFlightStale .efpdCG_inFlightPulse{opacity:.4;animation:none}.efpdCG_failMark{color:#f87171;flex-shrink:0;font-size:11px}.efpdCG_staleNote{color:var(--dsh-text-muted,#94a3b8b3);padding:4px 8px;font-size:11px}";
27
27
  const tagId = "@nelsonlongxiang/dsh-open-a2a-net/A2aControl.module.css";
28
28
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
29
29
  const tag = document.createElement("style");
@@ -33,67 +33,67 @@ window.__ModuleLoader__.load({
33
33
  document.head.appendChild(tag);
34
34
  }
35
35
  var A2aControl_module_css_default = {
36
- "activityDir": "KfHpVG_activityDir",
37
- "activityList": "KfHpVG_activityList",
38
- "activityPeer": "KfHpVG_activityPeer",
39
- "activityRow": "KfHpVG_activityRow",
40
- "activityTeam": "KfHpVG_activityTeam",
41
- "activityTime": "KfHpVG_activityTime",
42
- "badgePulse": "KfHpVG_badgePulse",
43
- "canvasActions": "KfHpVG_canvasActions",
44
- "canvasChip": "KfHpVG_canvasChip",
45
- "canvasChipRemove": "KfHpVG_canvasChipRemove",
46
- "canvasCreate": "KfHpVG_canvasCreate",
47
- "cold": "KfHpVG_cold",
48
- "description": "KfHpVG_description",
49
- "empty": "KfHpVG_empty",
50
- "facts": "KfHpVG_facts",
51
- "failMark": "KfHpVG_failMark",
52
- "flightPulse": "KfHpVG_flightPulse",
53
- "fresh": "KfHpVG_fresh",
54
- "freshFlash": "KfHpVG_freshFlash",
55
- "groupButton": "KfHpVG_groupButton",
56
- "groupChevron": "KfHpVG_groupChevron",
57
- "groupChevronClosed": "KfHpVG_groupChevronClosed",
58
- "groupHead": "KfHpVG_groupHead",
59
- "groupInput": "KfHpVG_groupInput",
60
- "groupNew": "KfHpVG_groupNew",
61
- "groupOption": "KfHpVG_groupOption",
62
- "groupOptionActive": "KfHpVG_groupOptionActive",
63
- "groupPicker": "KfHpVG_groupPicker",
64
- "groupTag": "KfHpVG_groupTag",
65
- "in": "KfHpVG_in",
66
- "inFlightList": "KfHpVG_inFlightList",
67
- "inFlightPulse": "KfHpVG_inFlightPulse",
68
- "inFlightRow": "KfHpVG_inFlightRow",
69
- "inFlightStale": "KfHpVG_inFlightStale",
70
- "joined": "KfHpVG_joined",
71
- "live": "KfHpVG_live",
72
- "name": "KfHpVG_name",
73
- "nameRow": "KfHpVG_nameRow",
74
- "note": "KfHpVG_note",
75
- "out": "KfHpVG_out",
76
- "peerChip": "KfHpVG_peerChip",
77
- "peerGrid": "KfHpVG_peerGrid",
78
- "peerHost": "KfHpVG_peerHost",
79
- "peerScore": "KfHpVG_peerScore",
80
- "popover": "KfHpVG_popover",
81
- "row": "KfHpVG_row",
82
- "searchInput": "KfHpVG_searchInput",
83
- "sectionTitle": "KfHpVG_sectionTitle",
84
- "sessionGroup": "KfHpVG_sessionGroup",
85
- "stageLink": "KfHpVG_stageLink",
86
- "stageLinks": "KfHpVG_stageLinks",
87
- "stageRow": "KfHpVG_stageRow",
88
- "stageTitle": "KfHpVG_stageTitle",
89
- "staleNote": "KfHpVG_staleNote",
90
- "stateDot": "KfHpVG_stateDot",
91
- "team": "KfHpVG_team",
92
- "title": "KfHpVG_title",
93
- "trigger": "KfHpVG_trigger",
94
- "unreadBadge": "KfHpVG_unreadBadge",
95
- "versionTag": "KfHpVG_versionTag",
96
- "wrap": "KfHpVG_wrap"
36
+ "activityDir": "efpdCG_activityDir",
37
+ "activityList": "efpdCG_activityList",
38
+ "activityPeer": "efpdCG_activityPeer",
39
+ "activityRow": "efpdCG_activityRow",
40
+ "activityTeam": "efpdCG_activityTeam",
41
+ "activityTime": "efpdCG_activityTime",
42
+ "badgePulse": "efpdCG_badgePulse",
43
+ "canvasActions": "efpdCG_canvasActions",
44
+ "canvasChip": "efpdCG_canvasChip",
45
+ "canvasChipRemove": "efpdCG_canvasChipRemove",
46
+ "canvasCreate": "efpdCG_canvasCreate",
47
+ "cold": "efpdCG_cold",
48
+ "description": "efpdCG_description",
49
+ "empty": "efpdCG_empty",
50
+ "facts": "efpdCG_facts",
51
+ "failMark": "efpdCG_failMark",
52
+ "flightPulse": "efpdCG_flightPulse",
53
+ "fresh": "efpdCG_fresh",
54
+ "freshFlash": "efpdCG_freshFlash",
55
+ "groupButton": "efpdCG_groupButton",
56
+ "groupChevron": "efpdCG_groupChevron",
57
+ "groupChevronClosed": "efpdCG_groupChevronClosed",
58
+ "groupHead": "efpdCG_groupHead",
59
+ "groupInput": "efpdCG_groupInput",
60
+ "groupNew": "efpdCG_groupNew",
61
+ "groupOption": "efpdCG_groupOption",
62
+ "groupOptionActive": "efpdCG_groupOptionActive",
63
+ "groupPicker": "efpdCG_groupPicker",
64
+ "groupTag": "efpdCG_groupTag",
65
+ "in": "efpdCG_in",
66
+ "inFlightList": "efpdCG_inFlightList",
67
+ "inFlightPulse": "efpdCG_inFlightPulse",
68
+ "inFlightRow": "efpdCG_inFlightRow",
69
+ "inFlightStale": "efpdCG_inFlightStale",
70
+ "joined": "efpdCG_joined",
71
+ "live": "efpdCG_live",
72
+ "name": "efpdCG_name",
73
+ "nameRow": "efpdCG_nameRow",
74
+ "note": "efpdCG_note",
75
+ "out": "efpdCG_out",
76
+ "peerChip": "efpdCG_peerChip",
77
+ "peerGrid": "efpdCG_peerGrid",
78
+ "peerHost": "efpdCG_peerHost",
79
+ "peerScore": "efpdCG_peerScore",
80
+ "popover": "efpdCG_popover",
81
+ "row": "efpdCG_row",
82
+ "searchInput": "efpdCG_searchInput",
83
+ "sectionTitle": "efpdCG_sectionTitle",
84
+ "sessionGroup": "efpdCG_sessionGroup",
85
+ "stageLink": "efpdCG_stageLink",
86
+ "stageLinks": "efpdCG_stageLinks",
87
+ "stageRow": "efpdCG_stageRow",
88
+ "stageTitle": "efpdCG_stageTitle",
89
+ "staleNote": "efpdCG_staleNote",
90
+ "stateDot": "efpdCG_stateDot",
91
+ "team": "efpdCG_team",
92
+ "title": "efpdCG_title",
93
+ "trigger": "efpdCG_trigger",
94
+ "unreadBadge": "efpdCG_unreadBadge",
95
+ "versionTag": "efpdCG_versionTag",
96
+ "wrap": "efpdCG_wrap"
97
97
  };
98
98
  //#endregion
99
99
  //#region lib/client/A2aControl.js
@@ -21,7 +21,7 @@ import { fileURLToPath } from 'node:url';
21
21
  import { canvasNameReserved as reservedCanvasName, isRemoteMemberRef, validRemoteMemberRef } from "../canvas-store.js";
22
22
  import { refreshPeerCard, registerCanvasNotify } from "./canvas-notify.js";
23
23
  import { CLI_HANDLE_RE } from "../cli-deposit.js";
24
- import { applyOwnerSettings } from "../owner-settings.js";
24
+ import { applyOwnerSettings, parseSeatNames } from "../owner-settings.js";
25
25
  import { rosterOp } from "../roster-op.js";
26
26
  import { resolveStageMount } from "../stage-mount.js";
27
27
  import { ARCHIVE_CAP, archivedEndedBy } from "../task-ledger.js";
@@ -112,6 +112,7 @@ export function registerAdminFaces(webServer, d) {
112
112
  sessions.push({
113
113
  id,
114
114
  label: `${session}-${id8(id)}`,
115
+ ...(d.ownerSeatNames[id] !== undefined ? { name: d.ownerSeatNames[id], seatName: d.ownerSeatNames[id] } : {}),
115
116
  team: `${config.team}/${id8(id)}`,
116
117
  joined: true,
117
118
  live: false,
@@ -522,12 +523,13 @@ export function registerAdminFaces(webServer, d) {
522
523
  }
523
524
  try {
524
525
  const target = peerUrl.replace(/\/$/, '') + '/a2a/canvas-op';
526
+ const body = JSON.stringify({ op: opV, name: nameV, ...(opV === 'add-member' || opV === 'remove-member' ? { id: idV !== '' && !isRemoteMemberRef(idV) && (sessionNodes.has(idV) || joinedSessions.has(idV)) ? `${config.team}/${id8(idV)}` : idV } : {}) });
525
527
  const reply = await d.peerFetch(target, {
526
528
  method: 'POST',
527
- headers: { 'Content-Type': 'application/json', ...originClaimHeaders({ url: target, method: 'POST' }) },
529
+ headers: { 'Content-Type': 'application/json', ...originClaimHeaders({ url: target, method: 'POST', body }) },
528
530
  // A local session id means nothing on the peer: send its zone/id8 handle (the workbench
529
531
  // drops a local card into a remote frame with its raw session id, which the peer refused).
530
- body: JSON.stringify({ op: opV, name: nameV, ...(opV === 'add-member' || opV === 'remove-member' ? { id: idV !== '' && !isRemoteMemberRef(idV) && (sessionNodes.has(idV) || joinedSessions.has(idV)) ? `${config.team}/${id8(idV)}` : idV } : {}) }),
532
+ body,
531
533
  signal: AbortSignal.timeout(15_000),
532
534
  });
533
535
  const raw = await reply.text();
@@ -602,6 +604,7 @@ export function registerAdminFaces(webServer, d) {
602
604
  if (req.method === 'GET') {
603
605
  const payload = JSON.stringify({
604
606
  ok: true,
607
+ seatNames: { ...d.ownerSeatNames },
605
608
  teamJoinAllowlist: [...d.ownerAllowlist],
606
609
  teamScopeInfo: d.ownerTeamScopeInfo,
607
610
  announce: config.announce === true,
@@ -614,6 +617,17 @@ export function registerAdminFaces(webServer, d) {
614
617
  readJsonBody(req, res, (rawBody) => {
615
618
  const body = rawBody;
616
619
  const changes = {};
620
+ if (Object.hasOwn(body, 'seatName') || Object.hasOwn(body, 'seatNames')) {
621
+ const names = body.seatNames;
622
+ const parsedNames = parseSeatNames(names);
623
+ if (Object.hasOwn(body, 'seatName') || parsedNames === undefined) {
624
+ const payload = JSON.stringify({ ok: false, error: 'seatNames must map full local session ids to nonempty display names (max 64 characters, no controls); clear by omitting the id' });
625
+ res.writeHead(400, { 'Content-Type': 'application/json' });
626
+ res.end(payload);
627
+ return;
628
+ }
629
+ changes.seatNames = parsedNames;
630
+ }
617
631
  if (Array.isArray(body.teamJoinAllowlist)) {
618
632
  const list = body.teamJoinAllowlist.filter((name) => typeof name === 'string' && name !== '' && name.length <= 128);
619
633
  changes.teamJoinAllowlist = [...new Set(list)];
@@ -634,7 +648,9 @@ export function registerAdminFaces(webServer, d) {
634
648
  const { yaml, restartNeeded } = applyOwnerSettings(current, changes);
635
649
  mkdirSync(home, { recursive: true });
636
650
  writeFileSync(patchPath, yaml, 'utf8');
637
- // Hot-apply the two call-time keys.
651
+ // Hot-apply the call-time owner keys.
652
+ if (changes.seatNames !== undefined)
653
+ d.ownerSeatNames = changes.seatNames;
638
654
  if (changes.teamJoinAllowlist !== undefined)
639
655
  d.ownerAllowlist = [...changes.teamJoinAllowlist];
640
656
  if (changes.teamScopeInfo !== undefined)
@@ -50,7 +50,7 @@ export function registerCanvasNotify(webServer, d) {
50
50
  const target = url.replace(/\/$/, '') + '/a2a/canvas-changed';
51
51
  void d.peerFetch(target, {
52
52
  method: 'POST',
53
- headers: { 'Content-Type': 'application/json', ...originClaimHeaders({ url: target, method: 'POST' }) },
53
+ headers: { 'Content-Type': 'application/json', ...originClaimHeaders({ url: target, method: 'POST', body: '{}' }) },
54
54
  body: '{}',
55
55
  signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
56
56
  }).then(async (reply) => {
@@ -95,11 +95,13 @@ export function registerCanvasNotify(webServer, d) {
95
95
  res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(text) });
96
96
  res.end(text);
97
97
  };
98
- // The ping has no content: drain a small body without buffering it,
99
- // and cut off anything larger.
98
+ // Retain only bounded raw ping bytes for Sig3 verification; cut off oversized uploads.
100
99
  let size = 0;
101
- req.on('data', (chunk) => { size += chunk.length; if (size > 4 * 1024)
102
- req.destroy(); });
100
+ const chunks = [];
101
+ req.on('data', (chunk) => { size += chunk.length; if (size > 4 * 1024) {
102
+ req.destroy();
103
+ return;
104
+ } chunks.push(chunk); });
103
105
  req.on('end', () => {
104
106
  void (async () => {
105
107
  if (req.method !== 'POST') {
@@ -115,7 +117,7 @@ export function registerCanvasNotify(webServer, d) {
115
117
  send({ ok: false, error: 'caller is not a tracked peer of this host' });
116
118
  return;
117
119
  }
118
- const claim = await verifyOriginClaim(req);
120
+ const claim = await verifyOriginClaim(req, { rawBody: Buffer.concat(chunks) });
119
121
  if (!claim.ok) {
120
122
  send({ ok: false, error: `untrusted-origin: OriginClaim verification failed (${claim.reason})` });
121
123
  return;
package/lib/faces/wire.js CHANGED
@@ -111,8 +111,13 @@ export function registerWireFaces(webServer, d) {
111
111
  req.on('end', () => {
112
112
  void (async () => {
113
113
  try {
114
- const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
115
- const claim = await verifyOriginClaim(req);
114
+ if (size > 16 * 1024) {
115
+ send(413, JSON.stringify({ ok: false, error: 'payload too large' }));
116
+ return;
117
+ }
118
+ const rawBody = Buffer.concat(chunks);
119
+ const body = JSON.parse(rawBody.toString('utf8'));
120
+ const claim = await verifyOriginClaim(req, { rawBody });
116
121
  if (!claim.ok) {
117
122
  logger.warn(`a2a: canvas-op claim failed (${claim.reason}) — caller=${String(req.headers['x-a2a-caller'] ?? '')} url=${String(req.headers['x-a2a-caller-url'] ?? '')} ts=${String(req.headers['x-a2a-caller-ts'] ?? '')}`);
118
123
  send(200, JSON.stringify({ ok: false, error: `untrusted-origin: OriginClaim verification failed (${claim.reason}) — canvas regulation requires a signed envelope`, code: -32000 }));
@@ -402,9 +407,10 @@ export function registerWireFaces(webServer, d) {
402
407
  }
403
408
  return;
404
409
  }
410
+ const rawBody = Buffer.concat(chunks);
405
411
  let body;
406
412
  try {
407
- body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
413
+ body = JSON.parse(rawBody.toString('utf8'));
408
414
  if (body === null || typeof body !== 'object' || Array.isArray(body))
409
415
  throw new TypeError('direct body must be a JSON object'); // #168: `null` read body.team in this async 'end' callback → unhandled rejection → host exit
410
416
  }
@@ -474,7 +480,7 @@ export function registerWireFaces(webServer, d) {
474
480
  // F1: only a FOREIGN CLI handle needs a bound host claim and live home card
475
481
  // before admission. Every other caller keeps the existing admission-first order.
476
482
  const foreignCli = config.teamScopeRouting && CLI_HANDLE_RE.test(caller) && cliDeposits.get(caller) === undefined;
477
- const earlyClaim = foreignCli ? await verifyOriginClaim(req, { cli: true }) : undefined;
483
+ const earlyClaim = foreignCli ? await verifyOriginClaim(req, { cli: true, rawBody }) : undefined;
478
484
  const cliProof = foreignCli && earlyClaim?.ok ? await remoteCliProof(caller, earlyClaim).catch(() => undefined) : undefined;
479
485
  const admission = a2aCallerAdmission(caller, team, cliProof);
480
486
  const callerRefusal = refusalOf(admission);
@@ -496,7 +502,7 @@ export function registerWireFaces(webServer, d) {
496
502
  // after the window closes it rejects like a failed signature —
497
503
  // N3 proves a NAME's linkability, never key possession.
498
504
  // gh#133: the delivery face is where a deposited CLI identity signs its routes.
499
- const claim = earlyClaim ?? await verifyOriginClaim(req, { cli: true });
505
+ const claim = earlyClaim ?? await verifyOriginClaim(req, { cli: true, rawBody });
500
506
  if (!claim.ok) {
501
507
  if (claim.reason !== 'absent' || config.originClaimWindow === false) {
502
508
  recordActivity('in', team, caller, false);
package/lib/index.js CHANGED
@@ -31,8 +31,10 @@ import { GroupStore } from "./group-store.js";
31
31
  import { CanvasStore, canvasNameReserved, isRemoteMemberRef } from "./canvas-store.js";
32
32
  import { decodeSessionLog, extractTranscript, TRANSCRIPT_MAX_FILE_BYTES } from "./session-transcript.js";
33
33
  import { LayoutStore } from "./layout-store.js";
34
+ import { peerLinkDisplay } from "./peer-link-display.js";
34
35
  import { JoinedSessions } from "./joined-store.js";
35
36
  import { ExposureStore } from "./exposure-store.js";
37
+ import { loadSeatNames } from "./owner-settings.js";
36
38
  import { CLI_HANDLE_RE, CliDepositStore } from "./cli-deposit.js";
37
39
  import { cliHomeChoices, decideCliHome } from "./cli-home.js";
38
40
  import { outboundScope } from "./outbound-scope.js";
@@ -111,6 +113,7 @@ export const Config = s.object({
111
113
  * refused join carries guidance instead of failing silently.
112
114
  */
113
115
  teamJoinAllowlist: s.array(s.string()).default([]),
116
+ seatNames: s.dict(s.any()).default({}),
114
117
  /**
115
118
  * S3 (phase-2 default, owner ruling 2026-09-01): route dispatch checks
116
119
  * caller↔target shared team membership before addressing — a session
@@ -570,7 +573,9 @@ export function apply(ctx, config) {
570
573
  * request) and the relay/notify faces.
571
574
  */
572
575
  function originClaimHeaders(target) {
573
- return signOriginClaim(target, { caller: config.team, callerUrl: ownCardUrl, privateKey: nodePrivateKey, unbound: config.originClaimUnboundWindow !== false, now: Date.now() });
576
+ // No local web/card anchor yet: keep the preexisting absent-envelope migration posture, not a malformed Sig3.
577
+ const signedTarget = ownCardUrl === '' ? { url: target.url, method: target.method } : target;
578
+ return signOriginClaim(signedTarget, { caller: config.team, callerUrl: ownCardUrl, privateKey: nodePrivateKey, unbound: config.originClaimUnboundWindow !== false, now: Date.now() });
574
579
  }
575
580
  const client = new A2aClient({
576
581
  apiKey: config.apiKey,
@@ -598,6 +603,8 @@ export function apply(ctx, config) {
598
603
  // are read at call time, so the panel's write applies WITHOUT a restart;
599
604
  // announce is boot-time publishing (restart pending flag for the UI).
600
605
  let ownerAllowlist = [...(config.teamJoinAllowlist ?? [])];
606
+ let ownerSeatNames = loadSeatNames(config.seatNames ?? {}, reason => logger.warn(reason));
607
+ ctx.tools.guard(exec => exec.name.startsWith('a2a_') && exec.arguments !== null && typeof exec.arguments === 'object' && (Object.hasOwn(exec.arguments, 'seatName') || Object.hasOwn(exec.arguments, 'seatNames')) ? 'seatName is human-owned; use the workbench owner settings, not model tools' : undefined);
601
608
  // Truthiness matches the ORIGINAL read site (`if (config.teamScopeInfo)`):
602
609
  // literal compositions without the schema default carry undefined, which
603
610
  // reads as the OPEN plane — `!== false` would silently scope them.
@@ -1253,14 +1260,15 @@ export function apply(ctx, config) {
1253
1260
  const LEGACY_PREWARM_PROBE_TITLE = '[prewarm] 请回复 ok。';
1254
1261
  function nodeMetadataOf(agent) {
1255
1262
  const title = sessionTitleOf(agent);
1256
- const cwd = agent.session.header?.cwd;
1263
+ const cwd = agent.session?.header?.cwd;
1257
1264
  // Display-layer fallback (pure presentation, zero data mutation): a title
1258
1265
  // that IS the legacy probe reads as machine junk in every directory and
1259
1266
  // card — surface the stable seat label instead. The session-title
1260
1267
  // service's stored value is untouched.
1261
1268
  const displayTitle = title !== undefined && title !== '' && title !== LEGACY_PREWARM_PROBE_TITLE ? title : undefined;
1262
1269
  return {
1263
- name: displayTitle !== undefined ? displayTitle : sessionLabelOf(agent),
1270
+ name: ownerSeatNames[String(agent.id)] ?? (displayTitle !== undefined ? displayTitle : sessionLabelOf(agent)),
1271
+ ...(ownerSeatNames[String(agent.id)] !== undefined ? { seatName: ownerSeatNames[String(agent.id)] } : {}),
1264
1272
  description: recentActivityOf(agent),
1265
1273
  ...(cwd !== undefined && cwd !== '' ? { workspace: cwd } : {}),
1266
1274
  };
@@ -1659,6 +1667,8 @@ export function apply(ctx, config) {
1659
1667
  get unboundInboundCount() { return unboundInboundCount; },
1660
1668
  get boundMissInboundCount() { return boundMissInboundCount; },
1661
1669
  get ownCardUrl() { return ownCardUrl; },
1670
+ get ownerSeatNames() { return ownerSeatNames; },
1671
+ set ownerSeatNames(v) { ownerSeatNames = v; },
1662
1672
  get ownerAllowlist() { return ownerAllowlist; },
1663
1673
  set ownerAllowlist(v) { ownerAllowlist = v; },
1664
1674
  get ownerTeamScopeInfo() { return ownerTeamScopeInfo; },
@@ -1776,7 +1786,7 @@ export function apply(ctx, config) {
1776
1786
  ...[...sessionNodes.values()].filter(isJoinedNode).map(agent => exposureRow(sessionTeamOf(agent), nodeMetadataOf(agent))),
1777
1787
  ...joinedSessions.list()
1778
1788
  .filter(id => !liveRoots.has(id) && isArchived?.(id) !== true)
1779
- .map(id => exposureRow(`${config.team}/${id8(id)}`, { name: `${session}-${id8(id)}`, description: 'cold — not loaded; routing here wakes the session' })),
1789
+ .map(id => exposureRow(`${config.team}/${id8(id)}`, { name: ownerSeatNames[id] ?? `${session}-${id8(id)}`, ...(ownerSeatNames[id] !== undefined ? { seatName: ownerSeatNames[id] } : {}), description: 'cold — not loaded; routing here wakes the session' })),
1780
1790
  // gh#45/47: deposited CLI identities publish as directory rows —
1781
1791
  // the fleet sees them as member cards (draggable = the operator
1782
1792
  // link). Their "zone" is the agent-name. gh#47: an EXPIRED
@@ -2224,6 +2234,7 @@ export function apply(ctx, config) {
2224
2234
  loopbackArrival: (config.originClaimUrl ?? '') === '',
2225
2235
  now: Date.now(),
2226
2236
  ...(options?.cli === true ? { cliDeposit: cliDepositCard } : {}),
2237
+ ...(options?.rawBody !== undefined ? { rawBody: options.rawBody } : {}),
2227
2238
  });
2228
2239
  }
2229
2240
  /**
@@ -2817,10 +2828,10 @@ export function apply(ctx, config) {
2817
2828
  }
2818
2829
  /**
2819
2830
  * gh#20 W-A: THIS host's view of one tracked peer's link state (the
2820
- * mirror of callerLinkedToThisHost, judged from our side about THEM):
2821
- * · linked — bilateral seeding (our config.peers carries a URL of the
2822
- * peer's origin AND the peer's card carries ours), or a canvas remote
2823
- * ref naming the peer's zone or one of its session teams;
2831
+ * display projection, not an admission decision:
2832
+ * · linked — bilateral URL seeding, or a canvas reference to an exact
2833
+ * nonlocal session handle uniquely advertised by that peer card key;
2834
+ * a shared zone or a minimal card alone never proves host identity;
2824
2835
  * · stranger — no operator-established basis (a verified key is not a
2825
2836
  * relationship);
2826
2837
  * · verified — we hold a signature-valid cached card (the badge's
@@ -2835,6 +2846,15 @@ export function apply(ctx, config) {
2835
2846
  * Pure over warm caches and config; never fetches.
2836
2847
  */
2837
2848
  function peerLinkState(peerUrl) {
2849
+ return peerLinkDisplay(peerUrl, {
2850
+ card: cardCache.get(peerUrl)?.card, ownUrl: ownCardUrl, seeds: config.peers,
2851
+ refs: canvasStore.list().flatMap(name => [...canvasStore.remoteMembersOf(name)]),
2852
+ ownHandles: new Set([...joinedSessions.list(), ...liveRoots.keys()].map(id => `${config.team}/${id8(id)}`)),
2853
+ cards: [...cardCache.values()].flatMap(entry => entry.card === undefined ? [] : [entry.card]),
2854
+ });
2855
+ }
2856
+ // Compatibility for existing CLI home/admission semantics: badge projection must not change authorization.
2857
+ function peerAdmissionLinkState(peerUrl) {
2838
2858
  const cached = cardCache.get(peerUrl)?.card;
2839
2859
  const verified = cached !== undefined;
2840
2860
  const zone = cached?.team ?? '';
@@ -2901,7 +2921,7 @@ export function apply(ctx, config) {
2901
2921
  const card = await fetchPeerCard(url).catch(() => undefined);
2902
2922
  if (card !== undefined)
2903
2923
  cardCache.set(url, { at: Date.now(), card });
2904
- return { url, card, linked: card === undefined || peerLinkState(url).link === 'linked' };
2924
+ return { url, card, linked: card === undefined || peerAdmissionLinkState(url).link === 'linked' };
2905
2925
  });
2906
2926
  }
2907
2927
  async function cliHomeForAdd(handle, requested) {
@@ -2935,7 +2955,7 @@ export function apply(ctx, config) {
2935
2955
  if (card === undefined || card.publicKey !== claim.card.publicKey)
2936
2956
  continue;
2937
2957
  cardCache.set(url, { at: Date.now(), card });
2938
- if (peerLinkState(url).link !== 'linked')
2958
+ if (peerAdmissionLinkState(url).link !== 'linked')
2939
2959
  continue;
2940
2960
  const row = card.sessionTeams?.find(item => item.team === handle);
2941
2961
  if (row !== undefined)
@@ -4,7 +4,7 @@
4
4
  * face (card, /a2a/direct, /a2a/log, /a2a/canvas-op, /a2a/canvas-changed)
5
5
  * runs.
6
6
  *
7
- * Two canonical forms ride the same four base headers
7
+ * Three canonical forms share the base caller headers
8
8
  * (X-A2A-Caller / -Url / -Ts):
9
9
  * · bound (v2, §2.1): JSON.stringify([tag, caller, callerUrl, ts,
10
10
  * audience, METHOD, path]) in X-A2A-Caller-Sig2, the audience origin in
@@ -14,18 +14,22 @@
14
14
  * · unbound (v1, legacy): JSON.stringify([caller, callerUrl, ts]) in
15
15
  * X-A2A-Caller-Sig. Emitted and accepted only while the unbound window
16
16
  * is open (originClaimUnboundWindow); accepted ones are counted.
17
- * Once the window closes a present Sig2 is the only thing verified. While
17
+ * A present Sig3 requires the exact raw body and v3 signature, with no downgrade.
18
+ * Without Sig3, once the window closes a present Sig2 is the only thing verified. While
18
19
  * it is open, a Sig2 that misses (audience or signature) falls back to the
19
20
  * Sig the envelope also carries — no more than stripping Sig2 would give —
20
21
  * and the miss is counted, so a peer dialled by an unlisted name keeps
21
22
  * working through the rollout and shows on the gauge.
22
23
  */
23
- import { createPublicKey, sign as edSign, verify as edVerify } from 'node:crypto';
24
+ import { createHash, createPublicKey, sign as edSign, verify as edVerify } from 'node:crypto';
24
25
  import { verifyCard } from "./card.js";
25
26
  /** ts skew tolerance (protocol constant, §7). */
26
27
  export const ORIGIN_CLAIM_SKEW_MS = 5 * 60_000;
27
28
  /** Domain tag of the bound form: a v1 signature can never verify as v2. */
28
29
  export const ORIGIN_CLAIM_BOUND_TAG = 'a2a-origin-claim/2';
30
+ export const ORIGIN_CLAIM_BODY_TAG = 'a2a-origin-claim/3';
31
+ /** Digest the exact UTF-8 string/bytes passed to transport, never reserialized JSON. */
32
+ export const originBodySha256 = (body) => createHash('sha256').update(body).digest('base64url');
29
33
  /** Only the canonical origin root can claim that origin's identity. */
30
34
  export function isRootOriginUrl(value) {
31
35
  try {
@@ -62,6 +66,10 @@ export function unboundCanonical(caller, callerUrl, ts) {
62
66
  export function boundCanonical(caller, callerUrl, ts, audience, method, path) {
63
67
  return JSON.stringify([ORIGIN_CLAIM_BOUND_TAG, caller, callerUrl, ts, audience, method.toUpperCase(), path]);
64
68
  }
69
+ /** The body-bound v3 form is domain-separated from v1/v2, with the digest appended. */
70
+ export function bodyBoundCanonical(caller, callerUrl, ts, audience, method, path, bodySha256) {
71
+ return JSON.stringify([ORIGIN_CLAIM_BODY_TAG, caller, callerUrl, ts, audience, method.toUpperCase(), path, bodySha256]);
72
+ }
65
73
  /** An origin in URL's normal form, or '' when the text is not an http(s) URL. */
66
74
  export function originOf(url) {
67
75
  try {
@@ -98,6 +106,11 @@ export function signOriginClaim(target, signer) {
98
106
  if (audience !== '') {
99
107
  headers['X-A2A-Caller-Aud'] = audience;
100
108
  headers['X-A2A-Caller-Sig2'] = sign(boundCanonical(caller, callerUrl, ts, audience, target.method, pathOf(target.url)));
109
+ if (target.body !== undefined) {
110
+ const digest = originBodySha256(target.body);
111
+ headers['X-A2A-Body-Sha256'] = digest;
112
+ headers['X-A2A-Caller-Sig3'] = sign(bodyBoundCanonical(caller, callerUrl, ts, audience, target.method, pathOf(target.url), digest));
113
+ }
101
114
  }
102
115
  if (signer.unbound)
103
116
  headers['X-A2A-Caller-Sig'] = sign(unboundCanonical(caller, callerUrl, ts));
@@ -153,15 +166,21 @@ export async function verifyOriginClaim(req, d) {
153
166
  const unboundSig = header('x-a2a-caller-sig');
154
167
  const boundSig = header('x-a2a-caller-sig2');
155
168
  const audience = header('x-a2a-caller-aud');
156
- if (caller === '' || callerUrl === '' || !Number.isFinite(ts) || ts <= 0 || (unboundSig === '' && boundSig === ''))
157
- return { ok: false, reason: 'absent' };
169
+ const hasBodySig = req.headers['x-a2a-caller-sig3'] !== undefined;
170
+ const bodySig = header('x-a2a-caller-sig3');
171
+ const digest = header('x-a2a-body-sha256');
172
+ // Presence, even malformed/empty, opts into v3. It must never become absent/legacy or fall back to Sig2/v1.
173
+ if (hasBodySig && (!bodySig || !/^[A-Za-z0-9_-]{43}$/.test(digest) || d.rawBody === undefined || originBodySha256(d.rawBody) !== digest))
174
+ return { ok: false, reason: 'body-mismatch' };
175
+ if (caller === '' || callerUrl === '' || !Number.isFinite(ts) || ts <= 0 || (unboundSig === '' && boundSig === '' && !hasBodySig))
176
+ return { ok: false, reason: hasBodySig ? 'bad-sig' : 'absent' };
158
177
  if (!originClaimCallerShape.test(caller))
159
178
  return { ok: false, reason: 'bad-caller-shape' };
160
179
  if (Math.abs(d.now - ts) > ORIGIN_CLAIM_SKEW_MS)
161
180
  return { ok: false, reason: 'bad-ts' };
162
- const hasBound = boundSig !== '';
181
+ const hasBound = boundSig !== '' || hasBodySig;
163
182
  const audienceOk = hasBound && requestAudiences(req, d.audiences, d.loopbackArrival ?? true).has(originOf(audience));
164
- const fallback = d.unboundOpen && unboundSig !== '';
183
+ const fallback = !hasBodySig && d.unboundOpen && unboundSig !== '';
165
184
  if (hasBound && !audienceOk && !fallback)
166
185
  return { ok: false, reason: 'wrong-audience' };
167
186
  if (!hasBound && !d.unboundOpen)
@@ -179,6 +198,10 @@ export async function verifyOriginClaim(req, d) {
179
198
  // signature is accepted for migration compatibility but remains unbound.
180
199
  let bound = false;
181
200
  const signedBy = (card) => {
201
+ if (hasBodySig) {
202
+ bound = audienceOk && verifies(card, bodyBoundCanonical(caller, callerUrl, ts, audience, req.method ?? '', pathOf(req.url ?? ''), digest), bodySig);
203
+ return bound;
204
+ }
182
205
  bound = audienceOk && verifies(card, boundCanonical(caller, callerUrl, ts, audience, req.method ?? '', pathOf(req.url ?? '')), boundSig);
183
206
  return bound || (fallback && verifies(card, unboundCanonical(caller, callerUrl, ts), unboundSig));
184
207
  };