@nelsonlongxiang/dsh-open-a2a-net 0.6.41 → 0.6.43
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.
- package/README.md +2 -1
- package/README_zh.md +2 -1
- package/assets/nexusDist/assets/index-CaA76j0F.js +9 -0
- package/assets/nexusDist/index.html +1 -1
- package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -1
- package/assets/stageDist/assets/index-C-28k_o5.js +974 -974
- package/assets/stageDist/index.html +12 -12
- package/cli/a2a.mjs +24 -4
- package/lib/canvas-store.js +249 -17
- package/lib/client.js +62 -72
- package/lib/client.js.map +1 -1
- package/lib/config.js +1 -0
- package/lib/faces/admin.js +103 -63
- package/lib/faces/wire.js +3 -1
- package/lib/index.js +116 -22
- package/lib/origin-claim.js +4 -0
- package/lib/skill/SKILL.md +117 -117
- package/lib/skill/references/collaboration.md +428 -428
- package/lib/skill/references/discovery.md +78 -78
- package/lib/skill/references/intake.md +107 -107
- package/lib/skill/references/membership.md +73 -73
- package/lib/skill/references/pitfalls.md +51 -51
- package/lib/skill/references/router.md +96 -96
- package/lib/skill/references/routing.md +65 -65
- package/lib/types/canvas-store.d.ts +48 -1
- package/lib/types/config.d.ts +2 -0
- package/lib/types/faces/admin.d.ts +2 -0
- package/lib/types/origin-claim.d.ts +2 -0
- package/nexus-stage/package.json +21 -21
- package/nexus-stage/scripts/mockhost.py +261 -261
- package/nexus-stage/src/canvas-ops.ts +191 -191
- package/nexus-stage/src/canvas-wire.ts +164 -164
- package/nexus-stage/src/cli-home-picker.ts +42 -42
- package/nexus-stage/src/fault.ts +56 -56
- package/nexus-stage/src/federation-overlay.ts +103 -103
- package/nexus-stage/src/federation.ts +241 -241
- package/nexus-stage/src/layout-doc.ts +145 -145
- package/nexus-stage/src/main.ts +520 -513
- package/nexus-stage/src/planning-card-text.ts +73 -73
- package/nexus-stage/src/planning-card.ts +63 -63
- package/nexus-stage/src/planning-edges.ts +26 -26
- package/nexus-stage/src/planning-fit.ts +45 -45
- package/nexus-stage/src/planning-frame-hue.ts +13 -13
- package/nexus-stage/src/planning-legend.ts +39 -39
- package/nexus-stage/src/planning-member-handles.ts +61 -61
- package/nexus-stage/src/planning-menus.ts +422 -422
- package/nexus-stage/src/planning-mirror.ts +75 -75
- package/nexus-stage/src/planning-nodelist.ts +294 -294
- package/nexus-stage/src/planning-overlap.ts +159 -159
- package/nexus-stage/src/planning-reachability.ts +28 -28
- package/nexus-stage/src/planning-relayout.ts +121 -121
- package/nexus-stage/src/planning-roster.ts +15 -15
- package/nexus-stage/src/planning-search.ts +168 -168
- package/nexus-stage/src/planning-types.ts +115 -115
- package/nexus-stage/src/planning-undo.ts +68 -68
- package/nexus-stage/src/planning-view.ts +1499 -1499
- package/nexus-stage/src/planning.css +340 -340
- package/nexus-stage/src/seat-name-editor.ts +31 -31
- package/nexus-stage/src/session-drag.ts +64 -64
- package/nexus-stage/src/viewport.ts +106 -106
- package/nexus-stage/src/wb-chat.ts +684 -684
- package/nexus-stage/src/wb-directory.ts +237 -237
- package/nexus-stage/src/wb-ledger.ts +279 -279
- package/nexus-stage/src/wb-names.ts +43 -43
- package/nexus-stage/src/wb-rail.ts +147 -147
- package/nexus-stage/src/workbench.css +388 -388
- package/nexus-stage/src/workbench.ts +1294 -1272
- package/nexus-stage/src/world.ts +509 -509
- package/nexus-stage/tsconfig.json +24 -24
- package/nexus-stage/vite.config.ts +15 -15
- package/package.json +1 -1
- package/assets/nexusDist/assets/index-NkNSeIx6.js +0 -9
package/lib/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export function resolveCallerLabel(input) {
|
|
|
30
30
|
}
|
|
31
31
|
import { encodePublicKey, signCard, verifyCard } from "./card.js";
|
|
32
32
|
import { GroupStore } from "./group-store.js";
|
|
33
|
-
import { CanvasStore, canvasNameReserved, isRemoteMemberRef } from "./canvas-store.js";
|
|
33
|
+
import { CanvasStore, canvasNameReserved, isRemoteMemberRef, legacyBareMember } from "./canvas-store.js";
|
|
34
34
|
import { decodeSessionLog, extractTranscript, TRANSCRIPT_MAX_FILE_BYTES } from "./session-transcript.js";
|
|
35
35
|
import { LayoutStore } from "./layout-store.js";
|
|
36
36
|
import { peerLinkDisplay } from "./peer-link-display.js";
|
|
@@ -53,7 +53,7 @@ import { createFloodGuard } from "./flood-guard.js";
|
|
|
53
53
|
import { createQueueGuard, queueFullRefusal, QUEUE_FULL_NEXT, WIRE_ERROR_QUEUE_FULL } from "./queue-guard.js";
|
|
54
54
|
import { resolveZone } from "./zone.js";
|
|
55
55
|
import { registerA2aNetworkSkill } from "./skill-registration.js";
|
|
56
|
-
import { isRootOriginUrl, sameMountedUrl, signOriginClaim, verifyOriginClaim as verifyOriginClaimOf } from "./origin-claim.js";
|
|
56
|
+
import { isRootOriginUrl, sameMountedUrl, originClaimTeamWarning, signOriginClaim, verifyOriginClaim as verifyOriginClaimOf } from "./origin-claim.js";
|
|
57
57
|
import { AuthObserver } from "./auth-observer.js";
|
|
58
58
|
import { allowlistVersion, authorizationRef, buildAuthorization, callerAdmission, localCandidateAdmission, canonicalAllowlist, judgeSettlement, joinedFaceAdmission, refusalOf, sameZoneAdmission, recheckRemoteCliAuthorization } from "./task-auth.js";
|
|
59
59
|
import { MessageId } from '@deepseek-ai/dsh-llm';
|
|
@@ -173,6 +173,9 @@ export function apply(ctx, config) {
|
|
|
173
173
|
});
|
|
174
174
|
// F4 slice: say the quiet part at boot instead of leaving the exposure to
|
|
175
175
|
// be discovered from a ledger full of unexplained steering.
|
|
176
|
+
const teamWarning = originClaimTeamWarning(config.team);
|
|
177
|
+
if (teamWarning !== undefined)
|
|
178
|
+
logger.warn(teamWarning);
|
|
176
179
|
const exposure = directDeliveryExposure(config.peers, config.apiKey);
|
|
177
180
|
if (exposure.warning !== undefined)
|
|
178
181
|
logger.warn(exposure.warning);
|
|
@@ -594,7 +597,7 @@ export function apply(ctx, config) {
|
|
|
594
597
|
// Canvas teams: user-composed multi-member routing groups. A session node
|
|
595
598
|
// may sit in many teams; routing to <team>/canvas/<name> resolves the
|
|
596
599
|
// first live member or wakes the first cold one (member order = priority).
|
|
597
|
-
const canvasStore = new CanvasStore(join(home, 'a2a', 'canvas.json'));
|
|
600
|
+
const canvasStore = new CanvasStore(join(home, 'a2a', 'canvas.json'), { legacyBareMembers: config.canvasLegacyBareMembers !== false });
|
|
598
601
|
// #91 file channel: the sender host's blob store (docs/design/file-channel.md).
|
|
599
602
|
const blobStore = new BlobStore(join(home, 'a2a'));
|
|
600
603
|
// Spatial presentation state for the full-page stage (contract v2).
|
|
@@ -915,7 +918,13 @@ export function apply(ctx, config) {
|
|
|
915
918
|
for (const id of joinedSessions.list()) {
|
|
916
919
|
if (!isArchived(id))
|
|
917
920
|
continue;
|
|
918
|
-
|
|
921
|
+
try {
|
|
922
|
+
revokeCanvasMember(id, 'archive');
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
logger.error('canvas archive suspension failed: %s', String(error));
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
919
928
|
sessionNodes.delete(id);
|
|
920
929
|
// Sections come back at the next mount (re-publish) if this archived reading was transient; the row stays.
|
|
921
930
|
nodeBinder.suspend(id);
|
|
@@ -925,9 +934,6 @@ export function apply(ctx, config) {
|
|
|
925
934
|
// joined session matches"). Safety without the deletion: wake refuses
|
|
926
935
|
// archived intents (:804), card assembly and the state cold rows filter
|
|
927
936
|
// them, the reconciler skips them, and JOIN_CAP bounds the file.
|
|
928
|
-
canvasStore.dropMember(id);
|
|
929
|
-
if (ownHandle !== undefined)
|
|
930
|
-
canvasStore.dropMember(ownHandle);
|
|
931
937
|
// S2 consistency: an archived node's roster declarations die with it
|
|
932
938
|
// — a left node keeps no team membership.
|
|
933
939
|
teamMemberships.dropSession(id);
|
|
@@ -958,7 +964,13 @@ export function apply(ctx, config) {
|
|
|
958
964
|
continue;
|
|
959
965
|
if (sessionNodes.has(id) || joinedSessions.has(id))
|
|
960
966
|
continue;
|
|
961
|
-
|
|
967
|
+
try {
|
|
968
|
+
canvasStore.removeMember(name, id);
|
|
969
|
+
}
|
|
970
|
+
catch (error) {
|
|
971
|
+
logger.error('canvas stale-member prune failed (%s/%s): %s', name, id, String(error));
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
962
974
|
logger.info(`a2a: canvas team "${name}" dropped stale member ${id8(id)} (no live node, no join intent)`);
|
|
963
975
|
}
|
|
964
976
|
}
|
|
@@ -1001,7 +1013,7 @@ export function apply(ctx, config) {
|
|
|
1001
1013
|
* @param includeArchived - count archived sessions too (the archive prune
|
|
1002
1014
|
* drops an archived session's own handle).
|
|
1003
1015
|
*/
|
|
1004
|
-
function localHandleResolver(includeArchived = false) {
|
|
1016
|
+
function localHandleResolver(includeArchived = false, count = false, legacyOverride = false) {
|
|
1005
1017
|
const isArchived = includeArchived ? undefined : archivedSessionFilter();
|
|
1006
1018
|
const byHandle = new Map();
|
|
1007
1019
|
for (const id of new Set([...joinedSessions.list(), ...sessionNodes.keys()])) {
|
|
@@ -1018,7 +1030,20 @@ export function apply(ctx, config) {
|
|
|
1018
1030
|
published.add(row.team);
|
|
1019
1031
|
}
|
|
1020
1032
|
return (ref) => {
|
|
1033
|
+
// #334 qualified member routing is deliberately deferred; this window is only legacy bare inference.
|
|
1034
|
+
if (!legacyOverride && config.canvasLegacyBareMembers === false)
|
|
1035
|
+
return undefined;
|
|
1021
1036
|
const hits = byHandle.get(ref);
|
|
1037
|
+
const collision = (hits?.length ?? 0) > 1 || (hits?.length === 1 && published.has(ref));
|
|
1038
|
+
if (count && legacyBareMember(ref)) {
|
|
1039
|
+
legacyInferenceResolutions++;
|
|
1040
|
+
if (collision) {
|
|
1041
|
+
legacyCollisionWarnings++;
|
|
1042
|
+
if (legacyCollisionRecent.length >= 16)
|
|
1043
|
+
legacyCollisionRecent.shift();
|
|
1044
|
+
legacyCollisionRecent.push({ member: ref, at: Date.now() });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1022
1047
|
return hits?.length === 1 && !published.has(ref) ? hits[0] : undefined;
|
|
1023
1048
|
};
|
|
1024
1049
|
}
|
|
@@ -1029,6 +1054,30 @@ export function apply(ctx, config) {
|
|
|
1029
1054
|
* handle-stored membership must not outlive the session's consent).
|
|
1030
1055
|
* @param id - the local session id (still joined when this is read).
|
|
1031
1056
|
*/
|
|
1057
|
+
const canvasMigrationEpoch = new Date().toISOString();
|
|
1058
|
+
let legacyInferenceResolutions = 0, legacyCollisionWarnings = 0;
|
|
1059
|
+
const legacyCollisionRecent = [];
|
|
1060
|
+
function revokeCanvasMember(id, reason, name) {
|
|
1061
|
+
if (!joinedSessions.has(id) && !sessionNodes.has(id))
|
|
1062
|
+
return false;
|
|
1063
|
+
const bare = config.team + '/' + id8(id);
|
|
1064
|
+
const inferred = localHandleResolver(true, false, true)(bare) === id ? bare : undefined;
|
|
1065
|
+
const qualified = bare + '@' + ownCanvasFingerprint;
|
|
1066
|
+
const sameId8 = new Set([...joinedSessions.list(), ...sessionNodes.keys()].filter(local => id8(local) === id8(id)));
|
|
1067
|
+
const pinned = canvasStore.ownerKeyOf(qualified);
|
|
1068
|
+
const proof = sameId8.size === 1 && (pinned === undefined || pinned === encodePublicKey(createPublicKey(nodePrivateKey))) ? qualified : undefined;
|
|
1069
|
+
return canvasStore.revokeLocal(id, proof, inferred, reason, name);
|
|
1070
|
+
}
|
|
1071
|
+
function canvasMigrationState() {
|
|
1072
|
+
const raw = canvasStore.list().flatMap(name => canvasStore.rawMembersOf(name).filter(legacyBareMember).map(member => ({ name, member })));
|
|
1073
|
+
const resolve = localHandleResolver(false, false);
|
|
1074
|
+
return { canvasLegacyBareMembers: config.canvasLegacyBareMembers !== false, epoch: canvasMigrationEpoch,
|
|
1075
|
+
legacyBareMembers: raw.length,
|
|
1076
|
+
legacyInferredMembers: raw.filter(r => canvasStore.membersOf(r.name).includes(r.member) && resolve(r.member) !== undefined).length,
|
|
1077
|
+
legacyInferenceResolutions, legacyCollisionWarnings, legacyCollisionRecent: [...legacyCollisionRecent],
|
|
1078
|
+
suspendedMembersCount: new Set(canvasStore.state().suspendedMembers.map(r => JSON.stringify([r.team, r.member]))).size,
|
|
1079
|
+
...canvasStore.state() };
|
|
1080
|
+
}
|
|
1032
1081
|
function ownCanvasHandleOf(id, includeArchived = false) {
|
|
1033
1082
|
const handle = `${config.team}/${id8(id)}`;
|
|
1034
1083
|
return localHandleResolver(includeArchived)(handle) === id ? handle : undefined;
|
|
@@ -1056,8 +1105,8 @@ export function apply(ctx, config) {
|
|
|
1056
1105
|
* reading that depends on which peer cards are cached must not rewrite
|
|
1057
1106
|
* the store (review of PR #151 P2-2).
|
|
1058
1107
|
*/
|
|
1059
|
-
function canvasLocalMembersOf(name) {
|
|
1060
|
-
const resolve = localHandleResolver();
|
|
1108
|
+
function canvasLocalMembersOf(name, count = false) {
|
|
1109
|
+
const resolve = localHandleResolver(false, count);
|
|
1061
1110
|
return canvasStore.membersOf(name).map(id => (isRemoteMemberRef(id) ? resolve(id) ?? id : id));
|
|
1062
1111
|
}
|
|
1063
1112
|
/** The 8-char suffix of one agent's id. */
|
|
@@ -1108,7 +1157,7 @@ export function apply(ctx, config) {
|
|
|
1108
1157
|
function canvasLiveAgent(name, skipId) {
|
|
1109
1158
|
if (name === undefined)
|
|
1110
1159
|
return undefined;
|
|
1111
|
-
for (const id of canvasLocalMembersOf(name)) {
|
|
1160
|
+
for (const id of canvasLocalMembersOf(name, true)) {
|
|
1112
1161
|
if (id === skipId)
|
|
1113
1162
|
continue;
|
|
1114
1163
|
const agent = sessionNodes.get(id);
|
|
@@ -1128,7 +1177,7 @@ export function apply(ctx, config) {
|
|
|
1128
1177
|
if (name === undefined)
|
|
1129
1178
|
return undefined;
|
|
1130
1179
|
const isArchived = archivedSessionFilter();
|
|
1131
|
-
return canvasLocalMembersOf(name).find(id => id !== skipId && joinedSessions.has(id) && !liveRoots.has(id) && isArchived?.(id) !== true);
|
|
1180
|
+
return canvasLocalMembersOf(name, true).find(id => id !== skipId && joinedSessions.has(id) && !liveRoots.has(id) && isArchived?.(id) !== true);
|
|
1132
1181
|
}
|
|
1133
1182
|
/**
|
|
1134
1183
|
* Human-facing node facts for one session: the session title (fallback:
|
|
@@ -1593,7 +1642,7 @@ export function apply(ctx, config) {
|
|
|
1593
1642
|
whenWebServerSettled((webServer) => {
|
|
1594
1643
|
// gh#77 B1: the admin faces live in src/faces/admin.ts.
|
|
1595
1644
|
registerAdminFaces(webServer, {
|
|
1596
|
-
ctx, config, logger, home, session, lanIp, PLUGIN_VERSION, controlRoute, controlTrustedWithoutKey, readJsonBody, peerFetch, peerStore, peerLinkState, originClaimHeaders, refreshRemoteRows, joinedSessions, exposureStore, teamMemberships, cliDeposits, groupStore, canvasStore, cliHomeForAdd, cliHomeOptions, layoutStore, taskLedger, idempotencyStore, inFlightRoutes, recentActivity, teamAuditRing, reportRing, sessionNodes, liveRoots, mountSessionNode, coldJoinedIds, prewarmStatus, reconcileStatus, id8, sessionLabelOf, sessionTeamOf, ownCanvasHandleOf, localSessionOfHandle, canvasTeamsOfSession, nodeMetadataOf, canvasTeamOf, archivedSessionFilter, pruneArchivedJoins, pruneCanvasMemberships, verifyOriginClaim, callerLinkedToThisHost, cachedCardFetch, authorizationView,
|
|
1645
|
+
ctx, config, logger, home, session, lanIp, PLUGIN_VERSION, controlRoute, controlTrustedWithoutKey, readJsonBody, peerFetch, peerStore, peerLinkState, originClaimHeaders, refreshRemoteRows, joinedSessions, exposureStore, teamMemberships, cliDeposits, groupStore, canvasStore, revokeCanvasMember, canvasMigrationState, cliHomeForAdd, cliHomeOptions, layoutStore, taskLedger, idempotencyStore, inFlightRoutes, recentActivity, teamAuditRing, reportRing, sessionNodes, liveRoots, mountSessionNode, coldJoinedIds, prewarmStatus, reconcileStatus, id8, sessionLabelOf, sessionTeamOf, ownCanvasHandleOf, localSessionOfHandle, canvasTeamsOfSession, nodeMetadataOf, canvasTeamOf, archivedSessionFilter, pruneArchivedJoins, pruneCanvasMemberships, verifyOriginClaim, callerLinkedToThisHost, cachedCardFetch, authorizationView,
|
|
1597
1646
|
indexUrl: import.meta.url, enforceRefused,
|
|
1598
1647
|
get cardCache() { return cardCache; },
|
|
1599
1648
|
get remoteRowsCache() { return remoteRowsCache; },
|
|
@@ -1655,11 +1704,13 @@ export function apply(ctx, config) {
|
|
|
1655
1704
|
// MINIMAL discovery card.
|
|
1656
1705
|
handler: async (req, res) => {
|
|
1657
1706
|
const send = (named, body_) => {
|
|
1658
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body_) });
|
|
1707
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body_), 'Cache-Control': 'private, no-store' });
|
|
1659
1708
|
res.end(body_);
|
|
1660
1709
|
void named;
|
|
1661
1710
|
};
|
|
1662
1711
|
const claim = await verifyOriginClaim(req);
|
|
1712
|
+
// #312: signed discovery is a reduced face; it never requests a full-card callback.
|
|
1713
|
+
const discoveryOnly = new URL(req.url ?? '/', 'http://x').searchParams.get('discovery') === '1';
|
|
1663
1714
|
// W3 (#18 half-side, 0.6.25): a VERIFIED envelope proves the
|
|
1664
1715
|
// caller holds a real node key — it does not prove a relationship
|
|
1665
1716
|
// with THIS host. The full card (sessionTeams/rosters =
|
|
@@ -1670,14 +1721,17 @@ export function apply(ctx, config) {
|
|
|
1670
1721
|
// Interaction with the OriginClaim narrowing: the caller is a
|
|
1671
1722
|
// bare zone; linkage judges that zone face against our canvas
|
|
1672
1723
|
// remote refs, joined handles, and the caller card's peers.
|
|
1673
|
-
const
|
|
1724
|
+
const linkedCaller = claim.ok && callerLinkedToThisHost(claim.caller, claim.callerUrl, claim.card);
|
|
1725
|
+
const showPeers = claim.ok && (configuredSeedCaller(claim.callerUrl) || linkedCaller);
|
|
1726
|
+
const named = !discoveryOnly && linkedCaller;
|
|
1674
1727
|
// gh#7 G1 (review-seat R2 ruling): the card serves two faces.
|
|
1675
1728
|
// A NAMED caller — ?caller_session= or the X-A2A-Caller header in
|
|
1676
1729
|
// the zone or zone/id8 shape — gets the FULL card. An anonymous
|
|
1677
1730
|
// fetcher (or an invalid shape, fail-closed) gets the MINIMAL
|
|
1678
|
-
// card: name/team/session/capabilities/records/
|
|
1679
|
-
//
|
|
1680
|
-
//
|
|
1731
|
+
// card: name/team/session/capabilities/records/signature stay.
|
|
1732
|
+
// #312/#324 F1: peers requires a verified caller that we explicitly seed
|
|
1733
|
+
// or already link; unrelated self-signed strangers get no topology.
|
|
1734
|
+
// Meanwhile sessionTeams/teamMemberships/lanIp are
|
|
1681
1735
|
// omitted — session membership and rosters are collaboration
|
|
1682
1736
|
// facts, not discovery facts. Named determination does NOT
|
|
1683
1737
|
// require a shared team: demanding roster symmetry before the
|
|
@@ -1733,7 +1787,8 @@ export function apply(ctx, config) {
|
|
|
1733
1787
|
const memberships = teamMemberships.list().flatMap(entry => entry.teams.map(team => ({ node: `${config.team}/${id8(entry.session)}`, team })));
|
|
1734
1788
|
const body = JSON.stringify({
|
|
1735
1789
|
...currentCard,
|
|
1736
|
-
|
|
1790
|
+
// #324 F1: possession of an arbitrary self-signed identity is not a local relationship.
|
|
1791
|
+
...(showPeers ? { peers: peerStore.list() } : {}),
|
|
1737
1792
|
...(named && sessionTeams.length > 0 ? { sessionTeams } : {}),
|
|
1738
1793
|
...(named && memberships.length > 0 ? { teamMemberships: memberships } : {}),
|
|
1739
1794
|
// gh#41 fleet planning (owner ruling: any peer composes and
|
|
@@ -2182,8 +2237,21 @@ export function apply(ctx, config) {
|
|
|
2182
2237
|
boundMissRecent.unshift({ ...row, count: row.count + 1, lastAt: Date.now() });
|
|
2183
2238
|
boundMissRecent.length = Math.min(boundMissRecent.length, 16);
|
|
2184
2239
|
};
|
|
2185
|
-
|
|
2186
|
-
|
|
2240
|
+
/** Our configured seeds anchor origin claims; mounted claims require exact mount matching. */
|
|
2241
|
+
function configuredSeedCaller(callerUrl) {
|
|
2242
|
+
return config.peers.some(peer => {
|
|
2243
|
+
try {
|
|
2244
|
+
return isRootOriginUrl(callerUrl) ? new URL(peer).origin === new URL(callerUrl).origin : sameMountedUrl(peer, callerUrl);
|
|
2245
|
+
}
|
|
2246
|
+
catch {
|
|
2247
|
+
return false;
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
// #312: topology-only callbacks must never replace a full session/roster card cache entry.
|
|
2252
|
+
const signedDiscoveryPeers = new Map();
|
|
2253
|
+
async function verifyOriginClaim(req, options) {
|
|
2254
|
+
const claim = await verifyOriginClaimOf(req, {
|
|
2187
2255
|
cardCache,
|
|
2188
2256
|
fetch: fetchImpl,
|
|
2189
2257
|
audiences: [ownCardUrl, ...config.originClaimAudiences ?? []],
|
|
@@ -2195,6 +2263,32 @@ export function apply(ctx, config) {
|
|
|
2195
2263
|
...(options?.cli === true ? { cliDeposit: cliDepositCard } : {}),
|
|
2196
2264
|
...(options?.rawBody !== undefined ? { rawBody: options.rawBody } : {}),
|
|
2197
2265
|
});
|
|
2266
|
+
// #312: recover bilateral seed evidence from a signed, reduction-only card,
|
|
2267
|
+
// never from a caller-selected unseeded URL and never recursively.
|
|
2268
|
+
const discovery = new URL(req.url ?? '/', 'http://x').searchParams.get('discovery') === '1';
|
|
2269
|
+
if (!discovery && claim.ok && claim.cli !== true && claim.card.peers === undefined && configuredSeedCaller(claim.callerUrl)) {
|
|
2270
|
+
const discoveryKey = new URL(claim.callerUrl).href.replace(/\/$/, '');
|
|
2271
|
+
const known = signedDiscoveryPeers.get(discoveryKey);
|
|
2272
|
+
if (known !== undefined && known.key === claim.card.publicKey && known.expiresAt > Date.now() && Date.now() - known.at < config.cardCacheTtlMs)
|
|
2273
|
+
return { ...claim, card: { ...claim.card, peers: known.peers } };
|
|
2274
|
+
const target = claim.callerUrl.replace(/\/$/, '') + '/.well-known/agent-card.json?discovery=1';
|
|
2275
|
+
try {
|
|
2276
|
+
const response = await peerFetch(target, { method: 'GET', headers: peerRequestHeaders(target, 'GET'), redirect: 'manual', signal: AbortSignal.timeout(5_000) });
|
|
2277
|
+
if (response.ok) {
|
|
2278
|
+
const verified = verifyCard(JSON.parse(await response.text()), Date.now());
|
|
2279
|
+
if (verified.ok && verified.card.publicKey === claim.card.publicKey && verified.card.team === claim.card.team) {
|
|
2280
|
+
if (verified.card.peers !== undefined) {
|
|
2281
|
+
signedDiscoveryPeers.set(discoveryKey, { at: Date.now(), expiresAt: verified.card.expiresAt, key: verified.card.publicKey, peers: verified.card.peers });
|
|
2282
|
+
if (signedDiscoveryPeers.size > 64)
|
|
2283
|
+
signedDiscoveryPeers.delete(signedDiscoveryPeers.keys().next().value);
|
|
2284
|
+
}
|
|
2285
|
+
return { ...claim, card: { ...claim.card, ...(verified.card.peers !== undefined ? { peers: verified.card.peers } : {}) } };
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
catch { /* unavailable refresh keeps bilateral-link checks closed */ }
|
|
2290
|
+
}
|
|
2291
|
+
return claim;
|
|
2198
2292
|
}
|
|
2199
2293
|
/**
|
|
2200
2294
|
* gh#133: the deposited card a CLI claim is judged by, or undefined when
|
package/lib/origin-claim.js
CHANGED
|
@@ -153,6 +153,10 @@ export function requestAudiences(req, configured, loopbackArrival = true) {
|
|
|
153
153
|
return out;
|
|
154
154
|
}
|
|
155
155
|
const originClaimCallerShape = /^[a-z0-9-]+$/i;
|
|
156
|
+
/** #312: explain unsupported host zone configuration without changing startup or signature policy. */
|
|
157
|
+
export function originClaimTeamWarning(team) {
|
|
158
|
+
return originClaimCallerShape.test(team) ? undefined : 'a2a: configured host team contains invalid characters; OriginClaim cannot produce a verifiable host identity. Use ^[a-z0-9-]+$ (no slash).';
|
|
159
|
+
}
|
|
156
160
|
/**
|
|
157
161
|
* Verify one request's envelope. Cheap refusals (shape, ts, audience,
|
|
158
162
|
* closed window) answer before any card fetch; the card is then read from
|
package/lib/skill/SKILL.md
CHANGED
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: a2a-network
|
|
3
|
-
description: Use when a task needs cross-session or cross-node collaboration over the DSH A2A network — setting it up (install, config, apiKey, peers, canvas links for one host, two peers, or hosts that cannot reach each other), joining and teaming, routing work to remote teams and agents, reconciling receipts and the task ledger, and reading the A2A workbench (/__dsh_a2a_nexus/). Covers admission rules, the receipt contract, and the failure shapes that look like bugs but are contract semantics.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# DSH A2A Network
|
|
7
|
-
|
|
8
|
-
Drive the decentralized A2A network: sessions publish agent cards, join a
|
|
9
|
-
membership layer, form canvas teams, and route work to each other through
|
|
10
|
-
`a2a_route`. One plugin (`@nelsonlongxiang/dsh-open-a2a-net`) owns the whole
|
|
11
|
-
surface on every node.
|
|
12
|
-
|
|
13
|
-
## Objects
|
|
14
|
-
|
|
15
|
-
- **Node** — one DSH host process exposing `/.well-known/agent-card.json` and
|
|
16
|
-
the `__dsh_a2a/state` face. Peers discover each other through cards.
|
|
17
|
-
- **Session node** — a live DSH session on a node. Visibility follows
|
|
18
|
-
membership: an unjoined session is invisible to peer discovery AND refused
|
|
19
|
-
at the routing admission gate.
|
|
20
|
-
- **Team** — a routable identity. Two kinds: the node's process team
|
|
21
|
-
(`<zone>/<id8>` from the session id) and canvas teams
|
|
22
|
-
(`<zone>/canvas/<name>`, user-composed multi-member groups).
|
|
23
|
-
- **Task** — one routed round. Carries a task id, an idempotency fingerprint,
|
|
24
|
-
and a receipt contract.
|
|
25
|
-
- **CLI identity** — a transient caller outside any session (the `a2a-net`
|
|
26
|
-
command, 0.6.27+): its own Ed25519 key and a handle `<name>/<16 hex>`,
|
|
27
|
-
deposited on ONE home host for up to 4h, with a mailbox instead of a
|
|
28
|
-
wakeable session. See [cli.md](references/cli.md).
|
|
29
|
-
|
|
30
|
-
## Setting up
|
|
31
|
-
|
|
32
|
-
Start with [setup.md](references/setup.md). It is a checked path for three
|
|
33
|
-
network shapes:
|
|
34
|
-
- **one host**: install, join the sessions, give them a shared team;
|
|
35
|
-
- **two hosts that reach each other**: one shared `apiKey`, `announce` plus
|
|
36
|
-
peers seeded both ways, and a canvas team naming both handles;
|
|
37
|
-
- **hosts that cannot reach each other**: host forwarding is not shipped
|
|
38
|
-
yet (#57), so tunnel or delegate per hop.
|
|
39
|
-
|
|
40
|
-
The owner does the gestures (install, config, join, allowlist, canvas). The
|
|
41
|
-
agent prepares the exact config and verifies each step with `a2a_whoami`,
|
|
42
|
-
`a2a_probe`, `a2a_teams` and one round with a receipt.
|
|
43
|
-
|
|
44
|
-
## Routing (a2a_route)
|
|
45
|
-
|
|
46
|
-
```text
|
|
47
|
-
a2a_route { team: "<zone>/<id8 or canvas name>", message, context_id?, async? }
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
- `async: true` delivers without waiting (`delivered:true` + task id); the
|
|
51
|
-
target routes a receipt — a message starting `[A2A receipt] task <id>` —
|
|
52
|
-
back to your team.
|
|
53
|
-
- Sync (default) waits for the round's final answer. A target on this host
|
|
54
|
-
gets up to 180s; past that the tool answers the delivered shape
|
|
55
|
-
(`TASK_STATE_DELIVERED`) and the receipt contract carries the rest. A
|
|
56
|
-
remote peer's delivery rides a 15s transport budget; if it fires first, the
|
|
57
|
-
result is **-32005 DELIVERED-UNSETTLED**: the delivery landed and the peer
|
|
58
|
-
keeps executing. Either way, **never re-dispatch blindly** — reconcile via
|
|
59
|
-
the task id (the receipt, `a2a_status` activity, or `a2a_tasks`) instead.
|
|
60
|
-
- Model tools cannot route to the bare process team (`<zone>` alone, the
|
|
61
|
-
host's own mailbox): target a session handle or a canvas team.
|
|
62
|
-
- `context_id` from a previous reply continues that conversation.
|
|
63
|
-
|
|
64
|
-
## Membership rules
|
|
65
|
-
|
|
66
|
-
- An unjoined session has NO network: `a2a_route` and `a2a_teams` refuse it
|
|
67
|
-
(join gate), and discovery does not list it. The session that originates a
|
|
68
|
-
task is exempt from the gate.
|
|
69
|
-
- Join through the GUI sidebar (A2A 网络) or the join gate prompt; leave
|
|
70
|
-
through the same surface. Membership is visible; leaving removes you from
|
|
71
|
-
discovery too.
|
|
72
|
-
- Inbound deliveries from a caller with no `caller_session` are refused
|
|
73
|
-
(anonymous injection is a closed hole).
|
|
74
|
-
|
|
75
|
-
## Reading the state
|
|
76
|
-
|
|
77
|
-
`__dsh_a2a/state` (per node, key-gated like every control route) carries
|
|
78
|
-
`sessions` (live/joined, with their teams), `canvas` (canvas teams; remote
|
|
79
|
-
members show `remote: true`), `remote` (peer session rows with `origin` =
|
|
80
|
-
node label + LAN IP and `via` = the peer URL), `activity` (routing ring),
|
|
81
|
-
and the ledger: `tasks` (pending, each with `settleable`),
|
|
82
|
-
`tasksStaleCount`, `tasksDead`, and `archived` (newest 64 settled rows, each
|
|
83
|
-
with `outcome`, `endedBy` and `late`) plus `archivedCount` /
|
|
84
|
-
`archivedSplit`. `sessionTeams` is a card field, not a state field. The A2A
|
|
85
|
-
workbench (`/__dsh_a2a_nexus/`, merged responsive
|
|
86
|
-
shell since 0.6.2 — the 2D planning board is the stage, directory / ledger /
|
|
87
|
-
activity / plan ride a collapsible rail, narrow screens get a drawer)
|
|
88
|
-
renders the same data for humans.
|
|
89
|
-
|
|
90
|
-
## Failure shapes that are contract semantics
|
|
91
|
-
|
|
92
|
-
- `-32000` with "declares no team membership" — the admission gate working
|
|
93
|
-
as designed; join (or route through a teamed member).
|
|
94
|
-
- `-32000` with "caller_session is required" — the delivery had no caller
|
|
95
|
-
identity; the protocol contract requires it.
|
|
96
|
-
- `-32005` — sync wait budget fired; delivery landed, reconcile the task id.
|
|
97
|
-
- "No live DSH session node accepts team X" — no session node on the host
|
|
98
|
-
that received the delivery accepted the round. Either the target session is
|
|
99
|
-
not live/joined there, or X lives on ANOTHER host and the caller delivered
|
|
100
|
-
straight to this host's `/a2a/direct` (the `a2a-net` CLI does this):
|
|
101
|
-
direct delivery never relays to peers — `a2a_route` from a session does.
|
|
102
|
-
- `[PATTERN_NO_MINT]` / `[ALLOWLIST_DENY]` on `a2a_team_join` — only an EXACT
|
|
103
|
-
allowlisted name joins; adding that name is the owner's act of creating
|
|
104
|
-
the team. `[CANVAS_TEAM]` — canvas membership is assigned in the workbench,
|
|
105
|
-
never by the roster tool.
|
|
106
|
-
|
|
107
|
-
## Details
|
|
108
|
-
|
|
109
|
-
- [setup.md](references/setup.md) — setup and checks: one host, two peers, beyond direct peers
|
|
110
|
-
- [routing.md](references/routing.md) — receipts, idempotency, ledger anatomy
|
|
111
|
-
- [discovery.md](references/discovery.md) — cards, peer boundary model, invisibility
|
|
112
|
-
- [membership.md](references/membership.md) — join/leave, allowlist, canvas teams
|
|
113
|
-
- [pitfalls.md](references/pitfalls.md) — operational pitfalls observed in production
|
|
114
|
-
- [cli.md](references/cli.md) — the `a2a-net` CLI: deposit, mailbox, inbox hooks, limits
|
|
115
|
-
- [collaboration.md](references/collaboration.md) — collaboration norm (Chinese): single-seat first, decide-then-report with no pre-authorization (a high-tier change needs one non-self node's review PASS on the same SHA), release tiers, stuck gates, credentials, receipts
|
|
116
|
-
- [intake.md](references/intake.md) — intake node (Chinese): handling `workbench-event` deliveries from workbench-market — accept/decline/ask with a reason, open a GitHub issue on accept, write the decision back with the reply token
|
|
117
|
-
- [router.md](references/router.md) — the chief's system prompt (Chinese), self-contained: route workbench events and owner requests to the right project's dev, order cross-project resource conflicts (it authorizes nothing since collaboration v1.18); immutable layer + self-kept mutable layer
|
|
1
|
+
---
|
|
2
|
+
name: a2a-network
|
|
3
|
+
description: Use when a task needs cross-session or cross-node collaboration over the DSH A2A network — setting it up (install, config, apiKey, peers, canvas links for one host, two peers, or hosts that cannot reach each other), joining and teaming, routing work to remote teams and agents, reconciling receipts and the task ledger, and reading the A2A workbench (/__dsh_a2a_nexus/). Covers admission rules, the receipt contract, and the failure shapes that look like bugs but are contract semantics.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DSH A2A Network
|
|
7
|
+
|
|
8
|
+
Drive the decentralized A2A network: sessions publish agent cards, join a
|
|
9
|
+
membership layer, form canvas teams, and route work to each other through
|
|
10
|
+
`a2a_route`. One plugin (`@nelsonlongxiang/dsh-open-a2a-net`) owns the whole
|
|
11
|
+
surface on every node.
|
|
12
|
+
|
|
13
|
+
## Objects
|
|
14
|
+
|
|
15
|
+
- **Node** — one DSH host process exposing `/.well-known/agent-card.json` and
|
|
16
|
+
the `__dsh_a2a/state` face. Peers discover each other through cards.
|
|
17
|
+
- **Session node** — a live DSH session on a node. Visibility follows
|
|
18
|
+
membership: an unjoined session is invisible to peer discovery AND refused
|
|
19
|
+
at the routing admission gate.
|
|
20
|
+
- **Team** — a routable identity. Two kinds: the node's process team
|
|
21
|
+
(`<zone>/<id8>` from the session id) and canvas teams
|
|
22
|
+
(`<zone>/canvas/<name>`, user-composed multi-member groups).
|
|
23
|
+
- **Task** — one routed round. Carries a task id, an idempotency fingerprint,
|
|
24
|
+
and a receipt contract.
|
|
25
|
+
- **CLI identity** — a transient caller outside any session (the `a2a-net`
|
|
26
|
+
command, 0.6.27+): its own Ed25519 key and a handle `<name>/<16 hex>`,
|
|
27
|
+
deposited on ONE home host for up to 4h, with a mailbox instead of a
|
|
28
|
+
wakeable session. See [cli.md](references/cli.md).
|
|
29
|
+
|
|
30
|
+
## Setting up
|
|
31
|
+
|
|
32
|
+
Start with [setup.md](references/setup.md). It is a checked path for three
|
|
33
|
+
network shapes:
|
|
34
|
+
- **one host**: install, join the sessions, give them a shared team;
|
|
35
|
+
- **two hosts that reach each other**: one shared `apiKey`, `announce` plus
|
|
36
|
+
peers seeded both ways, and a canvas team naming both handles;
|
|
37
|
+
- **hosts that cannot reach each other**: host forwarding is not shipped
|
|
38
|
+
yet (#57), so tunnel or delegate per hop.
|
|
39
|
+
|
|
40
|
+
The owner does the gestures (install, config, join, allowlist, canvas). The
|
|
41
|
+
agent prepares the exact config and verifies each step with `a2a_whoami`,
|
|
42
|
+
`a2a_probe`, `a2a_teams` and one round with a receipt.
|
|
43
|
+
|
|
44
|
+
## Routing (a2a_route)
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
a2a_route { team: "<zone>/<id8 or canvas name>", message, context_id?, async? }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- `async: true` delivers without waiting (`delivered:true` + task id); the
|
|
51
|
+
target routes a receipt — a message starting `[A2A receipt] task <id>` —
|
|
52
|
+
back to your team.
|
|
53
|
+
- Sync (default) waits for the round's final answer. A target on this host
|
|
54
|
+
gets up to 180s; past that the tool answers the delivered shape
|
|
55
|
+
(`TASK_STATE_DELIVERED`) and the receipt contract carries the rest. A
|
|
56
|
+
remote peer's delivery rides a 15s transport budget; if it fires first, the
|
|
57
|
+
result is **-32005 DELIVERED-UNSETTLED**: the delivery landed and the peer
|
|
58
|
+
keeps executing. Either way, **never re-dispatch blindly** — reconcile via
|
|
59
|
+
the task id (the receipt, `a2a_status` activity, or `a2a_tasks`) instead.
|
|
60
|
+
- Model tools cannot route to the bare process team (`<zone>` alone, the
|
|
61
|
+
host's own mailbox): target a session handle or a canvas team.
|
|
62
|
+
- `context_id` from a previous reply continues that conversation.
|
|
63
|
+
|
|
64
|
+
## Membership rules
|
|
65
|
+
|
|
66
|
+
- An unjoined session has NO network: `a2a_route` and `a2a_teams` refuse it
|
|
67
|
+
(join gate), and discovery does not list it. The session that originates a
|
|
68
|
+
task is exempt from the gate.
|
|
69
|
+
- Join through the GUI sidebar (A2A 网络) or the join gate prompt; leave
|
|
70
|
+
through the same surface. Membership is visible; leaving removes you from
|
|
71
|
+
discovery too.
|
|
72
|
+
- Inbound deliveries from a caller with no `caller_session` are refused
|
|
73
|
+
(anonymous injection is a closed hole).
|
|
74
|
+
|
|
75
|
+
## Reading the state
|
|
76
|
+
|
|
77
|
+
`__dsh_a2a/state` (per node, key-gated like every control route) carries
|
|
78
|
+
`sessions` (live/joined, with their teams), `canvas` (canvas teams; remote
|
|
79
|
+
members show `remote: true`), `remote` (peer session rows with `origin` =
|
|
80
|
+
node label + LAN IP and `via` = the peer URL), `activity` (routing ring),
|
|
81
|
+
and the ledger: `tasks` (pending, each with `settleable`),
|
|
82
|
+
`tasksStaleCount`, `tasksDead`, and `archived` (newest 64 settled rows, each
|
|
83
|
+
with `outcome`, `endedBy` and `late`) plus `archivedCount` /
|
|
84
|
+
`archivedSplit`. `sessionTeams` is a card field, not a state field. The A2A
|
|
85
|
+
workbench (`/__dsh_a2a_nexus/`, merged responsive
|
|
86
|
+
shell since 0.6.2 — the 2D planning board is the stage, directory / ledger /
|
|
87
|
+
activity / plan ride a collapsible rail, narrow screens get a drawer)
|
|
88
|
+
renders the same data for humans.
|
|
89
|
+
|
|
90
|
+
## Failure shapes that are contract semantics
|
|
91
|
+
|
|
92
|
+
- `-32000` with "declares no team membership" — the admission gate working
|
|
93
|
+
as designed; join (or route through a teamed member).
|
|
94
|
+
- `-32000` with "caller_session is required" — the delivery had no caller
|
|
95
|
+
identity; the protocol contract requires it.
|
|
96
|
+
- `-32005` — sync wait budget fired; delivery landed, reconcile the task id.
|
|
97
|
+
- "No live DSH session node accepts team X" — no session node on the host
|
|
98
|
+
that received the delivery accepted the round. Either the target session is
|
|
99
|
+
not live/joined there, or X lives on ANOTHER host and the caller delivered
|
|
100
|
+
straight to this host's `/a2a/direct` (the `a2a-net` CLI does this):
|
|
101
|
+
direct delivery never relays to peers — `a2a_route` from a session does.
|
|
102
|
+
- `[PATTERN_NO_MINT]` / `[ALLOWLIST_DENY]` on `a2a_team_join` — only an EXACT
|
|
103
|
+
allowlisted name joins; adding that name is the owner's act of creating
|
|
104
|
+
the team. `[CANVAS_TEAM]` — canvas membership is assigned in the workbench,
|
|
105
|
+
never by the roster tool.
|
|
106
|
+
|
|
107
|
+
## Details
|
|
108
|
+
|
|
109
|
+
- [setup.md](references/setup.md) — setup and checks: one host, two peers, beyond direct peers
|
|
110
|
+
- [routing.md](references/routing.md) — receipts, idempotency, ledger anatomy
|
|
111
|
+
- [discovery.md](references/discovery.md) — cards, peer boundary model, invisibility
|
|
112
|
+
- [membership.md](references/membership.md) — join/leave, allowlist, canvas teams
|
|
113
|
+
- [pitfalls.md](references/pitfalls.md) — operational pitfalls observed in production
|
|
114
|
+
- [cli.md](references/cli.md) — the `a2a-net` CLI: deposit, mailbox, inbox hooks, limits
|
|
115
|
+
- [collaboration.md](references/collaboration.md) — collaboration norm (Chinese): single-seat first, decide-then-report with no pre-authorization (a high-tier change needs one non-self node's review PASS on the same SHA), release tiers, stuck gates, credentials, receipts
|
|
116
|
+
- [intake.md](references/intake.md) — intake node (Chinese): handling `workbench-event` deliveries from workbench-market — accept/decline/ask with a reason, open a GitHub issue on accept, write the decision back with the reply token
|
|
117
|
+
- [router.md](references/router.md) — the chief's system prompt (Chinese), self-contained: route workbench events and owner requests to the right project's dev, order cross-project resource conflicts (it authorizes nothing since collaboration v1.18); immutable layer + self-kept mutable layer
|