@nelsonlongxiang/dsh-open-a2a-net 0.6.35 → 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 (48) hide show
  1. package/README.md +5 -2
  2. package/README_zh.md +5 -2
  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 +23 -5
  10. package/lib/faces/canvas-notify.js +8 -6
  11. package/lib/faces/wire.js +11 -5
  12. package/lib/index.js +35 -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/queue-guard.js +3 -3
  17. package/lib/skill/references/cli.md +4 -0
  18. package/lib/skill/references/routing.md +4 -0
  19. package/lib/skill/references/setup.md +4 -0
  20. package/lib/task-ledger-rows.js +192 -0
  21. package/lib/task-ledger.js +11 -189
  22. package/lib/tools/info.js +7 -3
  23. package/lib/types/a2a-client.d.ts +1 -0
  24. package/lib/types/faces/admin.d.ts +1 -0
  25. package/lib/types/faces/canvas-notify.d.ts +3 -1
  26. package/lib/types/faces/wire.d.ts +1 -0
  27. package/lib/types/index.d.ts +2 -0
  28. package/lib/types/origin-claim.d.ts +12 -3
  29. package/lib/types/owner-settings.d.ts +5 -0
  30. package/lib/types/peer-link-display.d.ts +13 -0
  31. package/lib/types/queue-guard.d.ts +3 -2
  32. package/lib/types/task-ledger-rows.d.ts +341 -0
  33. package/lib/types/task-ledger.d.ts +6 -284
  34. package/lib/types/tools/info.d.ts +2 -0
  35. package/lib/types/types.d.ts +2 -1
  36. package/nexus-stage/scripts/mockhost.py +14 -6
  37. package/nexus-stage/src/planning-card.ts +5 -3
  38. package/nexus-stage/src/planning-member-handles.ts +28 -1
  39. package/nexus-stage/src/planning-menus.ts +12 -5
  40. package/nexus-stage/src/planning-view.ts +26 -16
  41. package/nexus-stage/src/seat-name-editor.ts +31 -0
  42. package/nexus-stage/src/session-drag.ts +64 -0
  43. package/nexus-stage/src/wb-directory.ts +2 -0
  44. package/nexus-stage/src/workbench.css +2 -0
  45. package/nexus-stage/src/workbench.ts +16 -5
  46. package/package.json +1 -1
  47. package/assets/nexusDist/assets/index-Du2E6DJL.js +0 -9
  48. package/assets/nexusDist/assets/index-z33lHtLw.css +0 -1
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.35-d5a66d9\src\src\client\A2aControl.module.css.mjs
26
- const css = "._9zk42q_wrap{max-width:100%;display:inline-flex;position:relative}._9zk42q_trigger{align-items:center;gap:4px;display:inline-flex;position:relative}._9zk42q_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 _9zk42q_badgePulse;position:absolute;top:-6px;right:-8px}@keyframes _9zk42q_badgePulse{0%,to{box-shadow:0 0 #4c8bf573}50%{box-shadow:0 0 0 4px #4c8bf500}}@media (prefers-reduced-motion:reduce){._9zk42q_unreadBadge{animation:none}}._9zk42q_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}._9zk42q_title{font-size:13px;font-weight:600}._9zk42q_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}._9zk42q_stageRow{border-top:1px solid var(--dsh-border,#2a2a30);align-items:center;gap:8px;margin-top:4px;padding-top:8px;display:flex}._9zk42q_stageTitle{text-transform:uppercase;letter-spacing:.04em;opacity:.65;font-size:11px;font-weight:600}._9zk42q_stageLinks{gap:6px;display:flex}._9zk42q_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}._9zk42q_stageLink:hover,._9zk42q_stageLink:focus-visible{border-color:var(--dsh-accent,#4c8bf5)}._9zk42q_empty{opacity:.7;font-size:12px}._9zk42q_row{border:1px solid #0000;border-radius:8px;align-items:center;gap:10px;padding:8px;display:flex}._9zk42q_joined{border-color:var(--dsh-border,#2a2a30)}._9zk42q_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}._9zk42q_facts:hover{background:var(--dsh-surface-hover,#ffffff0f)}._9zk42q_nameRow{align-items:center;gap:6px;display:flex}._9zk42q_stateDot{border-radius:50%;flex:none;width:7px;height:7px}._9zk42q_stateDot._9zk42q_live{background:var(--dsh-accent,#4c8bf5)}._9zk42q_stateDot._9zk42q_live._9zk42q_joined{background:var(--dsh-success,#3fb96f)}._9zk42q_stateDot._9zk42q_cold{background:var(--dsh-warning,#d9a13c)}._9zk42q_name{text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:500;overflow:hidden}._9zk42q_description{opacity:.7;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._9zk42q_team{opacity:.55;font-size:11px;font-family:var(--dsh-mono,monospace)}._9zk42q_note{opacity:.6;font-size:11px}._9zk42q_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}._9zk42q_searchInput:focus{outline:1px solid var(--dsh-accent,#4c8bf5)}._9zk42q_sessionGroup{flex-direction:column;gap:2px;display:flex}._9zk42q_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}._9zk42q_groupChevron{transition:transform .12s;display:inline-block}._9zk42q_groupChevronClosed{transform:rotate(-90deg)}._9zk42q_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}._9zk42q_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}._9zk42q_groupButton:hover{border-color:var(--dsh-border,#2a2a30);opacity:1}._9zk42q_groupPicker{border:1px dashed var(--dsh-border,#2a2a30);border-radius:8px;flex-wrap:wrap;gap:6px;width:100%;padding:8px;display:flex;position:relative}._9zk42q_groupOption{border:1px solid var(--dsh-border,#2a2a30);color:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:2px 8px;font-size:11px}._9zk42q_groupOptionActive{border-color:var(--dsh-accent,#4c8bf5);color:var(--dsh-accent,#4c8bf5)}._9zk42q_groupNew{align-items:center;gap:6px;width:100%;margin-top:2px;display:flex}._9zk42q_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}._9zk42q_peerGrid{flex-wrap:wrap;gap:6px;display:flex}._9zk42q_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}._9zk42q_peerHost{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}._9zk42q_peerScore{font-family:var(--dsh-mono,monospace);opacity:.6}._9zk42q_activityList{flex-direction:column;gap:2px;display:flex}._9zk42q_activityRow{border-radius:4px;align-items:center;gap:8px;padding:2px 4px;font-size:11px;display:flex}._9zk42q_activityRow[data-ok=false]{opacity:.55}._9zk42q_activityDir{flex:none;font-weight:600}._9zk42q_activityDir._9zk42q_in{color:var(--dsh-success,#3fb96f)}._9zk42q_activityDir._9zk42q_out{color:var(--dsh-accent,#4c8bf5)}._9zk42q_activityTeam{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:var(--dsh-mono,monospace);opacity:.8;flex:1;overflow:hidden}._9zk42q_activityPeer{text-overflow:ellipsis;white-space:nowrap;opacity:.55;max-width:90px;overflow:hidden}._9zk42q_activityTime{opacity:.5;font-family:var(--dsh-mono,monospace);flex:none}._9zk42q_activityRow[data-jump=true]{cursor:pointer}._9zk42q_activityRow[data-jump=true]:hover{background:var(--dsh-surface-hover,#ffffff0f)}._9zk42q_fresh{animation:1.2s ease-out 3 _9zk42q_freshFlash}@keyframes _9zk42q_freshFlash{0%{background:#4c8bf538}to{background:0 0}}@media (prefers-reduced-motion:reduce){._9zk42q_fresh{background:#4c8bf51f;animation:none}}._9zk42q_inFlightList{flex-direction:column;gap:2px;margin-bottom:4px;display:flex}._9zk42q_inFlightRow{border:1px dashed var(--dsh-border,#2a2a30);border-radius:4px;align-items:center;gap:8px;padding:3px 4px;font-size:11px;display:flex}._9zk42q_inFlightPulse{background:var(--dsh-accent,#4c8bf5);border-radius:50%;flex:none;width:7px;height:7px;animation:1.1s ease-in-out infinite _9zk42q_flightPulse}@keyframes _9zk42q_flightPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.55)}}@media (prefers-reduced-motion:reduce){._9zk42q_inFlightPulse{animation:none}}._9zk42q_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}._9zk42q_inFlightStale{opacity:.55}._9zk42q_canvasCreate{align-items:center;gap:6px;margin-bottom:4px;display:flex}._9zk42q_canvasCreate ._9zk42q_groupInput{flex:1;min-width:0}._9zk42q_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}._9zk42q_canvasChip>span:not([class]){white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}._9zk42q_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}._9zk42q_canvasChipRemove:hover{color:var(--dsh-color-text-primary,#eee)}._9zk42q_canvasChipRemove{transition:color .14s ease-out}@media (prefers-reduced-motion:reduce){._9zk42q_canvasChipRemove{transition:none}}._9zk42q_canvasChipRemove:focus-visible{outline:2px solid var(--dsh-color-primary,#4f8cff);outline-offset:1px;border-radius:4px}._9zk42q_canvasActions{gap:6px;margin-top:4px;display:flex}._9zk42q_inFlightStale ._9zk42q_inFlightPulse{opacity:.4;animation:none}._9zk42q_failMark{color:#f87171;flex-shrink:0;font-size:11px}._9zk42q_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": "_9zk42q_activityDir",
37
- "activityList": "_9zk42q_activityList",
38
- "activityPeer": "_9zk42q_activityPeer",
39
- "activityRow": "_9zk42q_activityRow",
40
- "activityTeam": "_9zk42q_activityTeam",
41
- "activityTime": "_9zk42q_activityTime",
42
- "badgePulse": "_9zk42q_badgePulse",
43
- "canvasActions": "_9zk42q_canvasActions",
44
- "canvasChip": "_9zk42q_canvasChip",
45
- "canvasChipRemove": "_9zk42q_canvasChipRemove",
46
- "canvasCreate": "_9zk42q_canvasCreate",
47
- "cold": "_9zk42q_cold",
48
- "description": "_9zk42q_description",
49
- "empty": "_9zk42q_empty",
50
- "facts": "_9zk42q_facts",
51
- "failMark": "_9zk42q_failMark",
52
- "flightPulse": "_9zk42q_flightPulse",
53
- "fresh": "_9zk42q_fresh",
54
- "freshFlash": "_9zk42q_freshFlash",
55
- "groupButton": "_9zk42q_groupButton",
56
- "groupChevron": "_9zk42q_groupChevron",
57
- "groupChevronClosed": "_9zk42q_groupChevronClosed",
58
- "groupHead": "_9zk42q_groupHead",
59
- "groupInput": "_9zk42q_groupInput",
60
- "groupNew": "_9zk42q_groupNew",
61
- "groupOption": "_9zk42q_groupOption",
62
- "groupOptionActive": "_9zk42q_groupOptionActive",
63
- "groupPicker": "_9zk42q_groupPicker",
64
- "groupTag": "_9zk42q_groupTag",
65
- "in": "_9zk42q_in",
66
- "inFlightList": "_9zk42q_inFlightList",
67
- "inFlightPulse": "_9zk42q_inFlightPulse",
68
- "inFlightRow": "_9zk42q_inFlightRow",
69
- "inFlightStale": "_9zk42q_inFlightStale",
70
- "joined": "_9zk42q_joined",
71
- "live": "_9zk42q_live",
72
- "name": "_9zk42q_name",
73
- "nameRow": "_9zk42q_nameRow",
74
- "note": "_9zk42q_note",
75
- "out": "_9zk42q_out",
76
- "peerChip": "_9zk42q_peerChip",
77
- "peerGrid": "_9zk42q_peerGrid",
78
- "peerHost": "_9zk42q_peerHost",
79
- "peerScore": "_9zk42q_peerScore",
80
- "popover": "_9zk42q_popover",
81
- "row": "_9zk42q_row",
82
- "searchInput": "_9zk42q_searchInput",
83
- "sectionTitle": "_9zk42q_sectionTitle",
84
- "sessionGroup": "_9zk42q_sessionGroup",
85
- "stageLink": "_9zk42q_stageLink",
86
- "stageLinks": "_9zk42q_stageLinks",
87
- "stageRow": "_9zk42q_stageRow",
88
- "stageTitle": "_9zk42q_stageTitle",
89
- "staleNote": "_9zk42q_staleNote",
90
- "stateDot": "_9zk42q_stateDot",
91
- "team": "_9zk42q_team",
92
- "title": "_9zk42q_title",
93
- "trigger": "_9zk42q_trigger",
94
- "unreadBadge": "_9zk42q_unreadBadge",
95
- "versionTag": "_9zk42q_versionTag",
96
- "wrap": "_9zk42q_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,
@@ -202,6 +203,7 @@ export function registerAdminFaces(webServer, d) {
202
203
  .map(task => ({
203
204
  taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, status: task.status,
204
205
  direction: task.direction ?? 'outbound', receiptExpected: task.receiptExpected !== false,
206
+ ...(task.answeredAt !== undefined ? { answeredAt: task.answeredAt } : {}),
205
207
  // The dispatch brief rides every row: a task id and a team
206
208
  // say nothing about WHAT is owed — supervision cannot tell
207
209
  // a nudge from an abandon without it.
@@ -220,7 +222,7 @@ export function registerAdminFaces(webServer, d) {
220
222
  tasksStaleCount: taskLedger.list().filter(task => task.status === 'pending' && (task.receiptExpected === false || Date.now() - task.startedAt >= 60 * 60_000)).length,
221
223
  tasksDead: taskLedger.list()
222
224
  .filter(task => task.status === 'dead')
223
- .map(task => ({ taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, deadAt: task.deadAt ?? task.startedAt, reason: task.reason ?? 'unspecified', ...(task.brief !== undefined ? { brief: task.brief } : {}), ...(task.direction !== undefined ? { direction: task.direction } : {}), ...(task.sender !== undefined ? { sender: task.sender } : {}), ...(task.auth !== undefined ? { auth: task.auth } : task.direction === 'inbound' ? { authLegacy: true } : {}) })),
225
+ .map(task => ({ taskId: task.taskId, team: task.team, peer: task.peer, startedAt: task.startedAt, deadAt: task.deadAt ?? task.startedAt, reason: task.reason ?? 'unspecified', ...(task.answeredAt !== undefined ? { answeredAt: task.answeredAt } : {}), ...(task.brief !== undefined ? { brief: task.brief } : {}), ...(task.direction !== undefined ? { direction: task.direction } : {}), ...(task.sender !== undefined ? { sender: task.sender } : {}), ...(task.auth !== undefined ? { auth: task.auth } : task.direction === 'inbound' ? { authLegacy: true } : {}) })),
224
226
  // gh#31: per-task outcomes for rows that LEFT the ledger — the
225
227
  // chat drawer used to guess (settled, abandoned or cancelled).
226
228
  // Bounded to the archive (≤64), newest first, with the
@@ -243,6 +245,7 @@ export function registerAdminFaces(webServer, d) {
243
245
  peer: row.peer,
244
246
  startedAt: row.startedAt,
245
247
  resolvedAt: row.resolvedAt,
248
+ ...(row.answeredAt !== undefined ? { answeredAt: row.answeredAt } : {}),
246
249
  outcome: row.outcome ?? 'unclassified',
247
250
  endedBy: archivedEndedBy(row),
248
251
  ...(row.direction !== undefined ? { direction: row.direction } : {}),
@@ -520,12 +523,13 @@ export function registerAdminFaces(webServer, d) {
520
523
  }
521
524
  try {
522
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 } : {}) });
523
527
  const reply = await d.peerFetch(target, {
524
528
  method: 'POST',
525
- headers: { 'Content-Type': 'application/json', ...originClaimHeaders({ url: target, method: 'POST' }) },
529
+ headers: { 'Content-Type': 'application/json', ...originClaimHeaders({ url: target, method: 'POST', body }) },
526
530
  // A local session id means nothing on the peer: send its zone/id8 handle (the workbench
527
531
  // drops a local card into a remote frame with its raw session id, which the peer refused).
528
- 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,
529
533
  signal: AbortSignal.timeout(15_000),
530
534
  });
531
535
  const raw = await reply.text();
@@ -600,6 +604,7 @@ export function registerAdminFaces(webServer, d) {
600
604
  if (req.method === 'GET') {
601
605
  const payload = JSON.stringify({
602
606
  ok: true,
607
+ seatNames: { ...d.ownerSeatNames },
603
608
  teamJoinAllowlist: [...d.ownerAllowlist],
604
609
  teamScopeInfo: d.ownerTeamScopeInfo,
605
610
  announce: config.announce === true,
@@ -612,6 +617,17 @@ export function registerAdminFaces(webServer, d) {
612
617
  readJsonBody(req, res, (rawBody) => {
613
618
  const body = rawBody;
614
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
+ }
615
631
  if (Array.isArray(body.teamJoinAllowlist)) {
616
632
  const list = body.teamJoinAllowlist.filter((name) => typeof name === 'string' && name !== '' && name.length <= 128);
617
633
  changes.teamJoinAllowlist = [...new Set(list)];
@@ -632,7 +648,9 @@ export function registerAdminFaces(webServer, d) {
632
648
  const { yaml, restartNeeded } = applyOwnerSettings(current, changes);
633
649
  mkdirSync(home, { recursive: true });
634
650
  writeFileSync(patchPath, yaml, 'utf8');
635
- // 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;
636
654
  if (changes.teamJoinAllowlist !== undefined)
637
655
  d.ownerAllowlist = [...changes.teamJoinAllowlist];
638
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
@@ -2025,6 +2035,7 @@ export function apply(ctx, config) {
2025
2035
  // placeholder as one on the same-node loop-back.
2026
2036
  if (placeholder === true)
2027
2037
  return;
2038
+ taskLedger.markAnswered(taskId, sessionTeamOf(target));
2028
2039
  // Idempotency: a row THIS ledger tracks must still be pending
2029
2040
  // (settled/dead rows never re-send). A foreign task id (an inbound
2030
2041
  // route the caller tracks on ITS node) has no local row — the final
@@ -2223,6 +2234,7 @@ export function apply(ctx, config) {
2223
2234
  loopbackArrival: (config.originClaimUrl ?? '') === '',
2224
2235
  now: Date.now(),
2225
2236
  ...(options?.cli === true ? { cliDeposit: cliDepositCard } : {}),
2237
+ ...(options?.rawBody !== undefined ? { rawBody: options.rawBody } : {}),
2226
2238
  });
2227
2239
  }
2228
2240
  /**
@@ -2816,10 +2828,10 @@ export function apply(ctx, config) {
2816
2828
  }
2817
2829
  /**
2818
2830
  * gh#20 W-A: THIS host's view of one tracked peer's link state (the
2819
- * mirror of callerLinkedToThisHost, judged from our side about THEM):
2820
- * · linked — bilateral seeding (our config.peers carries a URL of the
2821
- * peer's origin AND the peer's card carries ours), or a canvas remote
2822
- * 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;
2823
2835
  * · stranger — no operator-established basis (a verified key is not a
2824
2836
  * relationship);
2825
2837
  * · verified — we hold a signature-valid cached card (the badge's
@@ -2834,6 +2846,15 @@ export function apply(ctx, config) {
2834
2846
  * Pure over warm caches and config; never fetches.
2835
2847
  */
2836
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) {
2837
2858
  const cached = cardCache.get(peerUrl)?.card;
2838
2859
  const verified = cached !== undefined;
2839
2860
  const zone = cached?.team ?? '';
@@ -2900,7 +2921,7 @@ export function apply(ctx, config) {
2900
2921
  const card = await fetchPeerCard(url).catch(() => undefined);
2901
2922
  if (card !== undefined)
2902
2923
  cardCache.set(url, { at: Date.now(), card });
2903
- return { url, card, linked: card === undefined || peerLinkState(url).link === 'linked' };
2924
+ return { url, card, linked: card === undefined || peerAdmissionLinkState(url).link === 'linked' };
2904
2925
  });
2905
2926
  }
2906
2927
  async function cliHomeForAdd(handle, requested) {
@@ -2934,7 +2955,7 @@ export function apply(ctx, config) {
2934
2955
  if (card === undefined || card.publicKey !== claim.card.publicKey)
2935
2956
  continue;
2936
2957
  cardCache.set(url, { at: Date.now(), card });
2937
- if (peerLinkState(url).link !== 'linked')
2958
+ if (peerAdmissionLinkState(url).link !== 'linked')
2938
2959
  continue;
2939
2960
  const row = card.sessionTeams?.find(item => item.team === handle);
2940
2961
  if (row !== undefined)
@@ -3317,6 +3338,10 @@ export function apply(ctx, config) {
3317
3338
  // node team when it has joined, else the node label), so the receiver
3318
3339
  // can answer with one a2a_route instead of an unroutable display label.
3319
3340
  a2aJoinGateRefusal(agent === undefined ? undefined : { agent });
3341
+ // A target-authored receipt attempt answers its own work even if scope/transport later refuses delivery.
3342
+ const answer = parseReceipt(args.message);
3343
+ if (agent !== undefined && answer !== null)
3344
+ taskLedger.markAnswered(answer.taskId, sessionTeamOf(agent));
3320
3345
  // S3 (team-scoped routing, phase-2 default ON per the 2026-09-01 owner
3321
3346
  // ruling; set false to restore the open outbound seam): a session
3322
3347
  // caller must have declared membership in the target team before it
@@ -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
  };