maka-agent 0.2.0-dev.3.20260830 → 0.2.0-dev.5.20260830
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/dist/cli-core.js +1 -0
- package/dist/eval-bundle-path.js +1 -1
- package/dist/pi-tui-runner.js +3 -1
- package/dist/runtime-host-cli.js +37 -3
- package/dist/runtime-host-onboarding.js +10 -1
- package/dist/runtime-host-peer-mesh-management-command.js +17 -0
- package/dist/runtime-host-setup-command.js +33 -15
- package/dist/runtime-host-systemd-service.js +9 -3
- package/dist/runtime-host-tui-context.js +2 -6
- package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/node_modules/@maka/eval/dist/external-subject.js +1 -1
- package/node_modules/@maka/eval/dist/harbor-external-subject.js +2 -2
- package/node_modules/@maka/runtime/dist/sandbox/macos-seatbelt.js +68 -9
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +22 -3
- package/node_modules/@maka/runtime-host/dist/operator/peer-mesh-management-frame.js +14 -0
- package/{packages/eval/dist/index.js → node_modules/@maka/runtime-host/dist/peer-mesh/display-name.js} +12 -10
- package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +32 -21
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +207 -47
- package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +2 -0
- package/node_modules/@maka/runtime-host/dist/peer-mesh/store.js +18 -5
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
- package/node_modules/@maka/runtime-host/dist/protocol/peer-mesh.js +59 -2
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +11 -7
- package/node_modules/@maka/runtime-host/dist/server/connection-session.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-service.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/peer-mesh-authority.js +19 -0
- package/package.json +1 -2
- package/packages/eval/dist/attempt-store.js +0 -138
- package/packages/eval/dist/cli.js +0 -119
- package/packages/eval/dist/experiment-directory.js +0 -59
- package/packages/eval/dist/experiment.js +0 -49
- package/packages/eval/dist/external-subject.js +0 -488
- package/packages/eval/dist/harbor-external-subject.js +0 -929
- package/packages/eval/dist/harbor-maka-subject.js +0 -83
- package/packages/eval/dist/harness-environment.js +0 -80
- package/packages/eval/dist/harness-executor.js +0 -1013
- package/packages/eval/dist/install-preflight.js +0 -201
- package/packages/eval/dist/maka-artifacts.js +0 -126
- package/packages/eval/dist/maka-runtime-policy.js +0 -42
- package/packages/eval/dist/maka-subject.js +0 -268
- package/packages/eval/dist/metering-checkpoint.js +0 -80
- package/packages/eval/dist/provider-admission.js +0 -39
- package/packages/eval/dist/provider-metering.js +0 -67
- package/packages/eval/dist/provider-web-tool-surface.js +0 -74
- package/packages/eval/dist/relay-result-frame.js +0 -38
- package/packages/eval/dist/result.js +0 -94
- package/packages/eval/dist/runner.js +0 -290
- package/packages/eval/dist/spec.js +0 -135
- package/packages/eval/dist/toolchain-verification.js +0 -130
- package/packages/eval/harbor/deepseek-codex-models.json +0 -69
- package/packages/eval/harbor/deepseek-harness-profile/cordis.patch.yml +0 -140
- package/packages/eval/harbor/deepseek-harness-profile/cordis.yml +0 -18
- package/packages/eval/harbor/deepseek-harness-profile/package.json +0 -10
- package/packages/eval/harbor/docker-compose-egress-proxy.yaml +0 -57
- package/packages/eval/harbor/egress-proxy/Dockerfile +0 -25
- package/packages/eval/harbor/egress-proxy/entrypoint.sh +0 -62
- package/packages/eval/harbor/egress-proxy/network-policy +0 -148
- package/packages/eval/harbor/egress_filter.py +0 -435
- package/packages/eval/harbor/eval_framework.py +0 -40
- package/packages/eval/harbor/relay_agent.py +0 -686
- package/packages/eval/harbor/run_trial.py +0 -223
- package/packages/eval/package.json +0 -20
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, sign, timingSafeEqual, verify, } from 'node:crypto';
|
|
20
20
|
import { decodePeerMeshInvitation as decodePeerMeshInvitationWire, } from '../protocol/peer-mesh.js';
|
|
21
21
|
import { PEER_MESH_MAX_MEMBERS, PEER_MESH_MAX_ROUTE_HINTS, PEER_MESH_ROUTE_RECORD_MAX_BYTES, } from './limits.js';
|
|
22
|
+
import { canonicalPeerMeshDisplayName } from './display-name.js';
|
|
22
23
|
export { PEER_MESH_MAX_INVITATION_RECORDS, PEER_MESH_MAX_MEMBERS, PEER_MESH_MAX_MESHES, PEER_MESH_MAX_PENDING_INVITATIONS, PEER_MESH_MAX_ROUTE_HINTS, PEER_MESH_MAX_TRANSIT_ADDRESSES_PER_RELAY, PEER_MESH_MAX_TRANSIT_RELAY_ADDRESSES, PEER_MESH_ROUTE_RECORD_MAX_BYTES, } from './limits.js';
|
|
23
24
|
export function generatePeerMeshAuthorityKeyPair() {
|
|
24
25
|
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
@@ -114,13 +115,12 @@ export function validatePeerMeshInvitation(value) {
|
|
|
114
115
|
});
|
|
115
116
|
}
|
|
116
117
|
export function canonicalPeerMeshRoster(value) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
'
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
]);
|
|
118
|
+
const keys = ['version', 'meshId', 'revision', 'members', 'closed'];
|
|
119
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
120
|
+
if (Object.hasOwn(value, 'displayName'))
|
|
121
|
+
keys.push('displayName');
|
|
122
|
+
}
|
|
123
|
+
const record = exactObject(value, 'Peer Mesh roster', keys);
|
|
124
124
|
if (record.version !== 1)
|
|
125
125
|
throw new Error('Unsupported Peer Mesh roster version');
|
|
126
126
|
const members = stringArray(record.members, 'members', PEER_MESH_MAX_MEMBERS, 256)
|
|
@@ -137,6 +137,9 @@ export function canonicalPeerMeshRoster(value) {
|
|
|
137
137
|
revision: integer(record.revision, 'revision', 1),
|
|
138
138
|
members: Object.freeze(members),
|
|
139
139
|
closed: record.closed,
|
|
140
|
+
...(record.displayName === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { displayName: canonicalPeerMeshDisplayName(record.displayName) }),
|
|
140
143
|
});
|
|
141
144
|
}
|
|
142
145
|
export function decodeAuthorityTarget(value) {
|
|
@@ -152,22 +155,23 @@ export function decodeAuthorityTarget(value) {
|
|
|
152
155
|
});
|
|
153
156
|
}
|
|
154
157
|
export function canonicalPeerMeshRouteRecord(value) {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
'
|
|
158
|
-
|
|
159
|
-
'
|
|
160
|
-
|
|
161
|
-
'
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
!Array.isArray(value) &&
|
|
166
|
-
Object.hasOwn(value, 'transitMeshId')
|
|
167
|
-
? [...baseKeys, 'transitMeshId']
|
|
168
|
-
: baseKeys);
|
|
158
|
+
const keys = ['version', 'peerId', 'sequence', 'expiresAt', 'routeHints', 'coordinationRelays'];
|
|
159
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
160
|
+
if (Object.hasOwn(value, 'endpointKind'))
|
|
161
|
+
keys.push('endpointKind');
|
|
162
|
+
if (Object.hasOwn(value, 'displayName'))
|
|
163
|
+
keys.push('displayName');
|
|
164
|
+
if (Object.hasOwn(value, 'transitMeshId'))
|
|
165
|
+
keys.push('transitMeshId');
|
|
166
|
+
}
|
|
167
|
+
const record = exactObject(value, 'Peer Mesh route record', keys);
|
|
169
168
|
if (record.version !== 1)
|
|
170
169
|
throw new Error('Unsupported Peer Mesh route record version');
|
|
170
|
+
if (record.endpointKind !== undefined &&
|
|
171
|
+
record.endpointKind !== 'client' &&
|
|
172
|
+
record.endpointKind !== 'host') {
|
|
173
|
+
throw new Error('Invalid Peer Mesh endpoint kind');
|
|
174
|
+
}
|
|
171
175
|
const route = Object.freeze({
|
|
172
176
|
version: 1,
|
|
173
177
|
peerId: token(record.peerId, 'peerId', 256),
|
|
@@ -175,6 +179,10 @@ export function canonicalPeerMeshRouteRecord(value) {
|
|
|
175
179
|
expiresAt: integer(record.expiresAt, 'route expiry', 1),
|
|
176
180
|
routeHints: Object.freeze(addressArray(record.routeHints, 'routeHints')),
|
|
177
181
|
coordinationRelays: Object.freeze(addressArray(record.coordinationRelays, 'coordinationRelays')),
|
|
182
|
+
...(record.endpointKind === undefined ? {} : { endpointKind: record.endpointKind }),
|
|
183
|
+
...(record.displayName === undefined
|
|
184
|
+
? {}
|
|
185
|
+
: { displayName: canonicalPeerMeshDisplayName(record.displayName) }),
|
|
178
186
|
...(record.transitMeshId === undefined
|
|
179
187
|
? {}
|
|
180
188
|
: { transitMeshId: string(record.transitMeshId, 'transitMeshId', 128) }),
|
|
@@ -201,6 +209,8 @@ export function decodeSignedPeerMeshRouteRecord(value) {
|
|
|
201
209
|
export function peerMeshRouteRecordSigningBytes(route) {
|
|
202
210
|
return Buffer.from(`maka.peer-mesh.route.v1\n${JSON.stringify({
|
|
203
211
|
coordinationRelays: route.coordinationRelays,
|
|
212
|
+
...(route.displayName ? { displayName: route.displayName } : {}),
|
|
213
|
+
...(route.endpointKind ? { endpointKind: route.endpointKind } : {}),
|
|
204
214
|
expiresAt: route.expiresAt,
|
|
205
215
|
peerId: route.peerId,
|
|
206
216
|
routeHints: route.routeHints,
|
|
@@ -212,6 +222,7 @@ export function peerMeshRouteRecordSigningBytes(route) {
|
|
|
212
222
|
function encodeRoster(roster) {
|
|
213
223
|
return Buffer.from(`maka.peer-mesh.roster.v1\n${JSON.stringify({
|
|
214
224
|
closed: roster.closed,
|
|
225
|
+
...(roster.displayName ? { displayName: roster.displayName } : {}),
|
|
215
226
|
members: roster.members,
|
|
216
227
|
meshId: roster.meshId,
|
|
217
228
|
revision: roster.revision,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
20
20
|
import { canonicalPeerMeshRoster, canonicalPeerMeshRouteRecord, createPeerMeshInvitationSecret, validatePeerMeshInvitation, decodeSignedPeerMeshRoster, decodeSignedPeerMeshRouteRecord, generatePeerMeshAuthorityKeyPair, matchesPeerMeshInvitationSecret, PEER_MESH_MAX_MEMBERS, PEER_MESH_MAX_MESHES, PEER_MESH_MAX_INVITATION_RECORDS, PEER_MESH_MAX_PENDING_INVITATIONS, PEER_MESH_MAX_TRANSIT_ADDRESSES_PER_RELAY, PEER_MESH_MAX_TRANSIT_RELAY_ADDRESSES, peerMeshRouteRecordSigningBytes, peerMeshId, peerMeshInvitationSecretDigest, signPeerMeshRoster, } from './model.js';
|
|
21
|
+
import { canonicalPeerMeshDisplayName } from './display-name.js';
|
|
21
22
|
import { authorityKeys, openPeerMeshStateStore, } from './store.js';
|
|
22
23
|
const CONTROL_FRAME_MAX_BYTES = 64 * 1024;
|
|
23
24
|
const DEFAULT_INVITATION_TTL_MS = 15 * 60 * 1_000;
|
|
@@ -47,6 +48,7 @@ export async function openPeerMeshNode(input) {
|
|
|
47
48
|
class PeerMeshNodeImpl {
|
|
48
49
|
#store;
|
|
49
50
|
#peer;
|
|
51
|
+
#endpointKind;
|
|
50
52
|
#now;
|
|
51
53
|
#activeControlStreams = new Set();
|
|
52
54
|
#lifetime = new AbortController();
|
|
@@ -60,6 +62,7 @@ class PeerMeshNodeImpl {
|
|
|
60
62
|
constructor(input) {
|
|
61
63
|
this.#store = input.store;
|
|
62
64
|
this.#peer = input.peer;
|
|
65
|
+
this.#endpointKind = input.endpointKind;
|
|
63
66
|
this.#now = input.now ?? Date.now;
|
|
64
67
|
}
|
|
65
68
|
async initialize() {
|
|
@@ -71,13 +74,64 @@ class PeerMeshNodeImpl {
|
|
|
71
74
|
this.#assertOpen();
|
|
72
75
|
return this.#peer.identity().peerId;
|
|
73
76
|
}
|
|
77
|
+
displayName() {
|
|
78
|
+
this.#assertOpen();
|
|
79
|
+
return this.#store.read().displayName ?? undefined;
|
|
80
|
+
}
|
|
81
|
+
setDisplayName(displayName) {
|
|
82
|
+
return this.#admitMesh(async () => {
|
|
83
|
+
const canonical = displayName === null ? null : canonicalPeerMeshDisplayName(displayName);
|
|
84
|
+
await this.#store.mutate(async (current) => {
|
|
85
|
+
if (current.displayName === canonical)
|
|
86
|
+
return { state: current, result: undefined };
|
|
87
|
+
const state = { ...current, displayName: canonical };
|
|
88
|
+
const localPeerId = this.#peer.identity().peerId;
|
|
89
|
+
if (!state.meshes.some((mesh) => isActiveMembership(mesh, localPeerId))) {
|
|
90
|
+
return { state, result: undefined };
|
|
91
|
+
}
|
|
92
|
+
const route = await this.#signLocalRoute(state);
|
|
93
|
+
return {
|
|
94
|
+
state: {
|
|
95
|
+
...state,
|
|
96
|
+
routes: mergeRoutes(state.routes, [route], this.#now()),
|
|
97
|
+
},
|
|
98
|
+
result: undefined,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
void this.reconcile().catch(() => undefined);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
setMeshDisplayName(meshId, displayName) {
|
|
105
|
+
return this.#admitMesh(async () => {
|
|
106
|
+
const canonical = displayName === null ? undefined : canonicalPeerMeshDisplayName(displayName);
|
|
107
|
+
await this.#store.mutate((current) => {
|
|
108
|
+
const state = requireAuthority(current.meshes, meshId);
|
|
109
|
+
if (state.roster.roster.closed)
|
|
110
|
+
throw new Error('Peer Mesh is closed');
|
|
111
|
+
if (state.roster.roster.displayName === canonical)
|
|
112
|
+
return { state: current, result: undefined };
|
|
113
|
+
const { displayName: _currentDisplayName, ...currentRoster } = state.roster.roster;
|
|
114
|
+
const roster = signPeerMeshRoster(canonicalPeerMeshRoster({
|
|
115
|
+
...currentRoster,
|
|
116
|
+
revision: state.roster.roster.revision + 1,
|
|
117
|
+
...(canonical ? { displayName: canonical } : {}),
|
|
118
|
+
}), authorityKeys(state));
|
|
119
|
+
return {
|
|
120
|
+
state: { ...current, meshes: replaceMesh(current.meshes, { ...state, roster }) },
|
|
121
|
+
result: undefined,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
const stored = this.#store.read();
|
|
125
|
+
return peerMeshStatus(requireAuthority(stored.meshes, meshId), this.#peer.identity(), this.#endpointKind, stored.routes, this.#now());
|
|
126
|
+
});
|
|
127
|
+
}
|
|
74
128
|
status() {
|
|
75
129
|
this.#assertOpen();
|
|
76
130
|
const identity = this.#peer.identity();
|
|
77
131
|
const stored = this.#store.read();
|
|
78
132
|
return Object.freeze(stored.meshes
|
|
79
133
|
.filter((state) => isActiveMembership(state, identity.peerId))
|
|
80
|
-
.map((state) => peerMeshStatus(state, identity, stored.routes, this.#now())));
|
|
134
|
+
.map((state) => peerMeshStatus(state, identity, this.#endpointKind, stored.routes, this.#now())));
|
|
81
135
|
}
|
|
82
136
|
create() {
|
|
83
137
|
return this.#admitMesh(async () => {
|
|
@@ -116,7 +170,7 @@ class PeerMeshNodeImpl {
|
|
|
116
170
|
};
|
|
117
171
|
});
|
|
118
172
|
const stored = this.#store.read();
|
|
119
|
-
return peerMeshStatus(findMesh(stored.meshes, state.roster.roster.meshId), identity, stored.routes, now);
|
|
173
|
+
return peerMeshStatus(findMesh(stored.meshes, state.roster.roster.meshId), identity, this.#endpointKind, stored.routes, now);
|
|
120
174
|
});
|
|
121
175
|
}
|
|
122
176
|
invite(meshId, input = {}) {
|
|
@@ -211,6 +265,7 @@ class PeerMeshNodeImpl {
|
|
|
211
265
|
throw new Error('Peer Mesh authority returned an unrelated roster');
|
|
212
266
|
}
|
|
213
267
|
const routes = await this.#validateRoutes(response.routes, roster, this.#now());
|
|
268
|
+
operationSignal.throwIfAborted();
|
|
214
269
|
await this.#store.mutate((current) => {
|
|
215
270
|
const existing = findMesh(current.meshes, invitation.meshId);
|
|
216
271
|
if (existing?.role === 'authority') {
|
|
@@ -246,7 +301,7 @@ class PeerMeshNodeImpl {
|
|
|
246
301
|
await this.#refreshLocalRoute();
|
|
247
302
|
await this.#reconcileTransit();
|
|
248
303
|
const stored = this.#store.read();
|
|
249
|
-
return peerMeshStatus(findMesh(stored.meshes, invitation.meshId), identity, stored.routes);
|
|
304
|
+
return peerMeshStatus(findMesh(stored.meshes, invitation.meshId), identity, this.#endpointKind, stored.routes);
|
|
250
305
|
}
|
|
251
306
|
finally {
|
|
252
307
|
await stream.close().catch(() => undefined);
|
|
@@ -366,12 +421,31 @@ class PeerMeshNodeImpl {
|
|
|
366
421
|
transitRelayPeerIds: Object.freeze(transitRelayPeerIds),
|
|
367
422
|
});
|
|
368
423
|
}
|
|
424
|
+
async prepareRoutes(peerId, signal) {
|
|
425
|
+
this.#assertOpen();
|
|
426
|
+
signal.throwIfAborted();
|
|
427
|
+
const localPeerId = this.#peer.identity().peerId;
|
|
428
|
+
const stored = this.#store.read();
|
|
429
|
+
const visible = stored.meshes.some((mesh) => isActiveMembership(mesh, localPeerId) && mesh.roster.roster.members.includes(peerId));
|
|
430
|
+
if (!visible)
|
|
431
|
+
return;
|
|
432
|
+
if (stored.routes.some(({ route }) => route.peerId === peerId && route.expiresAt > this.#now())) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
await this.reconcile(signal);
|
|
436
|
+
}
|
|
369
437
|
reconcile(signal) {
|
|
370
438
|
if (this.#lifetime.signal.aborted)
|
|
371
439
|
return Promise.reject(new Error('Peer Mesh node is closed'));
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
440
|
+
const previous = this.#reconcileTail;
|
|
441
|
+
let release;
|
|
442
|
+
const turn = new Promise((resolve) => {
|
|
443
|
+
release = resolve;
|
|
444
|
+
});
|
|
445
|
+
this.#reconcileTail = previous.then(() => turn);
|
|
446
|
+
return waitForTurn(previous, signal)
|
|
447
|
+
.then(() => this.#reconcile(signal))
|
|
448
|
+
.finally(release);
|
|
375
449
|
}
|
|
376
450
|
async serve() {
|
|
377
451
|
if (this.#lifetime.signal.aborted)
|
|
@@ -435,7 +509,9 @@ class PeerMeshNodeImpl {
|
|
|
435
509
|
const pending = [];
|
|
436
510
|
const gossipCursor = this.#gossipCursor;
|
|
437
511
|
this.#gossipCursor = (this.#gossipCursor + 1) % PEER_MESH_MAX_MEMBERS;
|
|
512
|
+
const now = this.#now();
|
|
438
513
|
for (const [index, state] of memberships.entries()) {
|
|
514
|
+
const authorityRoute = stored.routes.find(({ route }) => route.peerId === state.authority.peerId);
|
|
439
515
|
const targets = [currentAuthorityTarget(state, stored.routes)];
|
|
440
516
|
const gossipRoutes = state.roster.roster.members
|
|
441
517
|
.filter((peerId) => peerId !== identity.peerId && peerId !== state.authority.peerId)
|
|
@@ -446,13 +522,18 @@ class PeerMeshNodeImpl {
|
|
|
446
522
|
if (gossipRoutes.length > 0) {
|
|
447
523
|
targets.push(gossipRoutes[(gossipCursor + index) % gossipRoutes.length]);
|
|
448
524
|
}
|
|
449
|
-
pending.push({
|
|
525
|
+
pending.push({
|
|
526
|
+
meshId: state.roster.roster.meshId,
|
|
527
|
+
targets,
|
|
528
|
+
authorityRouteExpired: authorityRoute !== undefined && authorityRoute.route.expiresAt <= now,
|
|
529
|
+
});
|
|
450
530
|
}
|
|
451
531
|
if (pending.length === 0)
|
|
452
532
|
return;
|
|
453
533
|
const start = this.#reconcileCursor % pending.length;
|
|
454
534
|
const deadline = AbortSignal.timeout(RECONCILE_DEADLINE_MS);
|
|
455
535
|
const operationSignal = AbortSignal.any([lifetimeSignal, deadline]);
|
|
536
|
+
const failures = [];
|
|
456
537
|
let next = 0;
|
|
457
538
|
const worker = async () => {
|
|
458
539
|
while (!operationSignal.aborted) {
|
|
@@ -460,17 +541,14 @@ class PeerMeshNodeImpl {
|
|
|
460
541
|
next += 1;
|
|
461
542
|
if (offset >= pending.length)
|
|
462
543
|
return;
|
|
463
|
-
const { meshId, targets } = pending[(start + offset) % pending.length];
|
|
544
|
+
const { meshId, targets, authorityRouteExpired } = pending[(start + offset) % pending.length];
|
|
464
545
|
try {
|
|
465
|
-
|
|
466
|
-
if (!outcomes.some(({ status }) => status === 'fulfilled')) {
|
|
467
|
-
const failure = outcomes.find((outcome) => outcome.status === 'rejected');
|
|
468
|
-
throw failure?.reason ?? new Error('Peer Mesh synchronization failed');
|
|
469
|
-
}
|
|
546
|
+
await this.#syncTargets(meshId, targets, authorityRouteExpired, operationSignal);
|
|
470
547
|
}
|
|
471
|
-
catch {
|
|
548
|
+
catch (error) {
|
|
472
549
|
if (lifetimeSignal.aborted)
|
|
473
550
|
lifetimeSignal.throwIfAborted();
|
|
551
|
+
failures.push(error);
|
|
474
552
|
if (deadline.aborted)
|
|
475
553
|
return;
|
|
476
554
|
}
|
|
@@ -480,6 +558,46 @@ class PeerMeshNodeImpl {
|
|
|
480
558
|
this.#reconcileCursor = (start + Math.min(next, pending.length)) % pending.length;
|
|
481
559
|
await this.#reconcileTransit();
|
|
482
560
|
lifetimeSignal.throwIfAborted();
|
|
561
|
+
if (failures.length > 0) {
|
|
562
|
+
throw new AggregateError(failures, 'Peer Mesh synchronization did not reach every membership');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async #syncTargets(meshId, targets, authorityRouteExpired, signal) {
|
|
566
|
+
if (!authorityRouteExpired || targets.length === 1) {
|
|
567
|
+
try {
|
|
568
|
+
await this.#syncPeer(meshId, targets[0], signal);
|
|
569
|
+
}
|
|
570
|
+
catch (authorityFailure) {
|
|
571
|
+
const fallbackTargets = targets.slice(1);
|
|
572
|
+
if (fallbackTargets.length === 0)
|
|
573
|
+
throw authorityFailure;
|
|
574
|
+
await Promise.any(fallbackTargets.map((target) => this.#syncPeer(meshId, target, signal)));
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const controllers = targets.map(() => new AbortController());
|
|
579
|
+
const authorityPeerId = targets[0].peerId;
|
|
580
|
+
const attempts = targets.map(async (target, index) => {
|
|
581
|
+
await this.#syncPeer(meshId, target, AbortSignal.any([signal, controllers[index].signal]));
|
|
582
|
+
if (index > 0 && !this.#hasFreshRoute(meshId, authorityPeerId)) {
|
|
583
|
+
throw new Error('Peer Mesh gossip did not recover the authority route');
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
try {
|
|
587
|
+
await Promise.any(attempts);
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
for (const controller of controllers)
|
|
591
|
+
controller.abort();
|
|
592
|
+
await Promise.allSettled(attempts);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
#hasFreshRoute(meshId, peerId) {
|
|
596
|
+
const stored = this.#store.read();
|
|
597
|
+
const mesh = findMesh(stored.meshes, meshId);
|
|
598
|
+
return Boolean(mesh &&
|
|
599
|
+
isActiveMembership(mesh, this.#peer.identity().peerId) &&
|
|
600
|
+
stored.routes.some(({ route }) => route.peerId === peerId && route.expiresAt > this.#now()));
|
|
483
601
|
}
|
|
484
602
|
async #syncPeer(meshId, target, signal) {
|
|
485
603
|
for (let page = 0; page <= Math.ceil(PEER_MESH_MAX_MEMBERS / ROUTE_PAGE_SIZE); page += 1) {
|
|
@@ -492,10 +610,12 @@ class PeerMeshNodeImpl {
|
|
|
492
610
|
const route = stored.routes.find((candidate) => candidate.route.peerId === localPeerId);
|
|
493
611
|
if (!route)
|
|
494
612
|
throw new Error('Peer Mesh local route is unavailable');
|
|
613
|
+
const discovered = this.resolveRoutes(target.peerId);
|
|
495
614
|
const stream = await this.#peer.connectMeshControl({
|
|
496
615
|
peerId: target.peerId,
|
|
497
|
-
routeHints: target.routeHints,
|
|
498
|
-
coordinationRelays: target.coordinationRelays,
|
|
616
|
+
routeHints: mergeAddresses(discovered?.routeHints ?? [], target.routeHints),
|
|
617
|
+
coordinationRelays: mergeAddresses(discovered?.coordinationRelays ?? [], target.coordinationRelays),
|
|
618
|
+
transitRelayPeerIds: discovered?.transitRelayPeerIds,
|
|
499
619
|
directDeadlineMs: CONNECT_DEADLINE_MS,
|
|
500
620
|
}, signal);
|
|
501
621
|
try {
|
|
@@ -527,31 +647,32 @@ class PeerMeshNodeImpl {
|
|
|
527
647
|
}
|
|
528
648
|
async #refreshLocalRouteOnce() {
|
|
529
649
|
const identity = this.#peer.identity();
|
|
530
|
-
const current = this.#store.read();
|
|
531
|
-
const active = current.meshes.filter((state) => isActiveMembership(state, identity.peerId));
|
|
532
|
-
if (active.length === 0)
|
|
533
|
-
return undefined;
|
|
534
|
-
const existing = current.routes
|
|
535
|
-
.filter(({ route }) => route.peerId === identity.peerId)
|
|
536
|
-
.sort((left, right) => right.route.sequence - left.route.sequence)[0];
|
|
537
650
|
const now = this.#now();
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
existing
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
651
|
+
return this.#store.mutate(async (current) => {
|
|
652
|
+
if (!current.meshes.some((state) => isActiveMembership(state, identity.peerId))) {
|
|
653
|
+
return { state: current, result: undefined };
|
|
654
|
+
}
|
|
655
|
+
const existing = current.routes
|
|
656
|
+
.filter(({ route }) => route.peerId === identity.peerId)
|
|
657
|
+
.sort((left, right) => right.route.sequence - left.route.sequence)[0];
|
|
658
|
+
if (existing &&
|
|
659
|
+
existing.route.expiresAt > now + ROUTE_REFRESH_LEAD_MS &&
|
|
660
|
+
sameAddresses(existing.route.routeHints, identity.listenAddresses) &&
|
|
661
|
+
sameAddresses(existing.route.coordinationRelays, identity.coordinationRelays) &&
|
|
662
|
+
existing.route.endpointKind === this.#endpointKind &&
|
|
663
|
+
existing.route.displayName === (current.displayName ?? undefined) &&
|
|
664
|
+
existing.route.transitMeshId === current.transitMeshId) {
|
|
665
|
+
return { state: current, result: existing };
|
|
666
|
+
}
|
|
667
|
+
const route = await this.#signLocalRoute(current);
|
|
668
|
+
return {
|
|
669
|
+
state: { ...current, routes: mergeRoutes(current.routes, [route], now) },
|
|
670
|
+
result: route,
|
|
671
|
+
};
|
|
672
|
+
});
|
|
551
673
|
}
|
|
552
|
-
async #signLocalRoute() {
|
|
674
|
+
async #signLocalRoute(stored = this.#store.read()) {
|
|
553
675
|
const identity = this.#peer.identity();
|
|
554
|
-
const stored = this.#store.read();
|
|
555
676
|
const maxSequence = stored.routes
|
|
556
677
|
.filter(({ route }) => route.peerId === identity.peerId)
|
|
557
678
|
.reduce((maximum, { route }) => Math.max(maximum, route.sequence), 0);
|
|
@@ -562,6 +683,8 @@ class PeerMeshNodeImpl {
|
|
|
562
683
|
expiresAt: this.#now() + ROUTE_TTL_MS,
|
|
563
684
|
routeHints: identity.listenAddresses,
|
|
564
685
|
coordinationRelays: identity.coordinationRelays,
|
|
686
|
+
...(this.#endpointKind ? { endpointKind: this.#endpointKind } : {}),
|
|
687
|
+
...(stored.displayName ? { displayName: stored.displayName } : {}),
|
|
565
688
|
...(stored.transitMeshId ? { transitMeshId: stored.transitMeshId } : {}),
|
|
566
689
|
});
|
|
567
690
|
const proof = await this.#peer.signIdentity(peerMeshRouteRecordSigningBytes(route));
|
|
@@ -660,6 +783,9 @@ class PeerMeshNodeImpl {
|
|
|
660
783
|
revision: state.roster.roster.revision + 1,
|
|
661
784
|
members: next.members,
|
|
662
785
|
closed: next.closed,
|
|
786
|
+
...(state.roster.roster.displayName
|
|
787
|
+
? { displayName: state.roster.roster.displayName }
|
|
788
|
+
: {}),
|
|
663
789
|
}, authorityKeys(state));
|
|
664
790
|
const updated = {
|
|
665
791
|
...state,
|
|
@@ -676,7 +802,7 @@ class PeerMeshNodeImpl {
|
|
|
676
802
|
await this.#refreshLocalRoute();
|
|
677
803
|
await this.#reconcileTransit();
|
|
678
804
|
const stored = this.#store.read();
|
|
679
|
-
return peerMeshStatus(findMesh(stored.meshes, meshId), this.#peer.identity(), stored.routes);
|
|
805
|
+
return peerMeshStatus(findMesh(stored.meshes, meshId), this.#peer.identity(), this.#endpointKind, stored.routes);
|
|
680
806
|
}
|
|
681
807
|
#acceptIncoming(stream) {
|
|
682
808
|
let peerStreams = 0;
|
|
@@ -932,7 +1058,7 @@ class PeerMeshNodeImpl {
|
|
|
932
1058
|
if (route.peerId === localPeerId ||
|
|
933
1059
|
route.expiresAt <= now ||
|
|
934
1060
|
route.transitMeshId === undefined ||
|
|
935
|
-
route.routeHints.length === 0) {
|
|
1061
|
+
route.routeHints.length + route.coordinationRelays.length === 0) {
|
|
936
1062
|
return false;
|
|
937
1063
|
}
|
|
938
1064
|
return isActiveMeshMember(stored.meshes, route.transitMeshId, localPeerId, route.peerId);
|
|
@@ -953,13 +1079,18 @@ function transitRelayCandidates(routes) {
|
|
|
953
1079
|
for (const { route } of routes) {
|
|
954
1080
|
if (remaining === 0)
|
|
955
1081
|
break;
|
|
956
|
-
const
|
|
1082
|
+
const routeHints = [
|
|
957
1083
|
...new Set(route.routeHints.filter((address) => isBaseRelayFor(address, route.peerId))),
|
|
958
1084
|
].slice(0, Math.min(PEER_MESH_MAX_TRANSIT_ADDRESSES_PER_RELAY, remaining));
|
|
959
|
-
|
|
1085
|
+
const coordinationRelays = [...new Set(route.coordinationRelays)].slice(0, Math.min(PEER_MESH_MAX_TRANSIT_ADDRESSES_PER_RELAY - routeHints.length, remaining - routeHints.length));
|
|
1086
|
+
if (routeHints.length + coordinationRelays.length === 0)
|
|
960
1087
|
continue;
|
|
961
|
-
candidates.push(Object.freeze({
|
|
962
|
-
|
|
1088
|
+
candidates.push(Object.freeze({
|
|
1089
|
+
peerId: route.peerId,
|
|
1090
|
+
addresses: Object.freeze(routeHints),
|
|
1091
|
+
coordinationRelays: Object.freeze(coordinationRelays),
|
|
1092
|
+
}));
|
|
1093
|
+
remaining -= routeHints.length + coordinationRelays.length;
|
|
963
1094
|
}
|
|
964
1095
|
return Object.freeze(candidates);
|
|
965
1096
|
}
|
|
@@ -976,7 +1107,9 @@ function isActiveMeshMember(meshes, meshId, localPeerId, peerId) {
|
|
|
976
1107
|
isActiveMembership(mesh, localPeerId) &&
|
|
977
1108
|
mesh.roster.roster.members.includes(peerId));
|
|
978
1109
|
}
|
|
979
|
-
function peerMeshStatus(state, identity, routes = [], now = Date.now()) {
|
|
1110
|
+
function peerMeshStatus(state, identity, endpointKind, routes = [], now = Date.now()) {
|
|
1111
|
+
const localDisplayName = routes.find(({ route }) => route.peerId === identity.peerId)?.route
|
|
1112
|
+
.displayName;
|
|
980
1113
|
return Object.freeze({
|
|
981
1114
|
role: state.role === 'authority' ? 'authority' : 'member',
|
|
982
1115
|
authority: state.role === 'authority' ? authorityTarget(identity) : state.authority,
|
|
@@ -985,20 +1118,30 @@ function peerMeshStatus(state, identity, routes = [], now = Date.now()) {
|
|
|
985
1118
|
? state.invitations.filter((invitation) => invitation.status === 'pending' && invitation.expiresAt > now).length
|
|
986
1119
|
: 0,
|
|
987
1120
|
memberRoutes: Object.freeze(state.roster.roster.members.map((peerId) => {
|
|
988
|
-
if (peerId === identity.peerId)
|
|
989
|
-
return Object.freeze({
|
|
1121
|
+
if (peerId === identity.peerId) {
|
|
1122
|
+
return Object.freeze({
|
|
1123
|
+
peerId,
|
|
1124
|
+
...(endpointKind ? { endpointKind } : {}),
|
|
1125
|
+
...(localDisplayName ? { displayName: localDisplayName } : {}),
|
|
1126
|
+
state: 'local',
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
990
1129
|
const route = routes.find((candidate) => candidate.route.peerId === peerId)?.route;
|
|
991
1130
|
if (!route)
|
|
992
1131
|
return Object.freeze({ peerId, state: 'unknown' });
|
|
993
1132
|
if (route.expiresAt <= now) {
|
|
994
1133
|
return Object.freeze({
|
|
995
1134
|
peerId,
|
|
1135
|
+
...(route.endpointKind ? { endpointKind: route.endpointKind } : {}),
|
|
1136
|
+
...(route.displayName ? { displayName: route.displayName } : {}),
|
|
996
1137
|
state: 'stale',
|
|
997
1138
|
expiresAt: route.expiresAt,
|
|
998
1139
|
});
|
|
999
1140
|
}
|
|
1000
1141
|
return Object.freeze({
|
|
1001
1142
|
peerId,
|
|
1143
|
+
...(route.endpointKind ? { endpointKind: route.endpointKind } : {}),
|
|
1144
|
+
...(route.displayName ? { displayName: route.displayName } : {}),
|
|
1002
1145
|
state: route.routeHints.length > 0
|
|
1003
1146
|
? 'route_available'
|
|
1004
1147
|
: 'coordination_only',
|
|
@@ -1118,6 +1261,23 @@ function responseRoutes(state, routes, knownRoutes, now) {
|
|
|
1118
1261
|
function sameAddresses(left, right) {
|
|
1119
1262
|
return left.length === right.length && left.every((address, index) => address === right[index]);
|
|
1120
1263
|
}
|
|
1264
|
+
function mergeAddresses(primary, fallback) {
|
|
1265
|
+
return Object.freeze([...new Set([...primary, ...fallback])].slice(0, 32));
|
|
1266
|
+
}
|
|
1267
|
+
function waitForTurn(previous, signal) {
|
|
1268
|
+
if (!signal)
|
|
1269
|
+
return previous;
|
|
1270
|
+
if (signal.aborted)
|
|
1271
|
+
return Promise.reject(signal.reason);
|
|
1272
|
+
return new Promise((resolve, reject) => {
|
|
1273
|
+
const onAbort = () => reject(signal.reason);
|
|
1274
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1275
|
+
void previous.then(() => {
|
|
1276
|
+
signal.removeEventListener('abort', onAbort);
|
|
1277
|
+
resolve();
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1121
1281
|
function mergeTargets(primary, fallback) {
|
|
1122
1282
|
return Object.freeze({
|
|
1123
1283
|
peerId: primary.peerId,
|
|
@@ -42,6 +42,7 @@ export async function openRuntimeHostPeerMeshOwner(input) {
|
|
|
42
42
|
: { automaticRelayDiscovery: input.automaticRelayDiscovery }),
|
|
43
43
|
routeResolver: {
|
|
44
44
|
resolveRoutes: (peerId) => resolverMesh?.resolveRoutes(peerId),
|
|
45
|
+
prepareRoutes: async (peerId, signal) => resolverMesh?.prepareRoutes(peerId, signal),
|
|
45
46
|
},
|
|
46
47
|
});
|
|
47
48
|
}
|
|
@@ -54,6 +55,7 @@ export async function openRuntimeHostPeerMeshOwner(input) {
|
|
|
54
55
|
mesh = await openPeerMeshNode({
|
|
55
56
|
dataRoot: join(input.dataRoot, client.identity().peerId),
|
|
56
57
|
peer: client,
|
|
58
|
+
endpointKind: input.endpointKind,
|
|
57
59
|
});
|
|
58
60
|
resolverMesh = mesh;
|
|
59
61
|
}
|
|
@@ -20,6 +20,7 @@ import { chmod, lstat, mkdir, open, readFile, rename, unlink } from 'node:fs/pro
|
|
|
20
20
|
import { dirname, join } from 'node:path';
|
|
21
21
|
import { acquireFileLifetimeOwner, } from '@maka/storage/file-lifetime-owner';
|
|
22
22
|
import { decodeAuthorityTarget, decodeSignedPeerMeshRoster, decodeSignedPeerMeshRouteRecord, PEER_MESH_MAX_INVITATION_RECORDS, PEER_MESH_MAX_MEMBERS, PEER_MESH_MAX_MESHES, PEER_MESH_MAX_PENDING_INVITATIONS, validatePeerMeshAuthorityKeyPair, } from './model.js';
|
|
23
|
+
import { canonicalPeerMeshDisplayName } from './display-name.js';
|
|
23
24
|
const STATE_FILE = 'peer-mesh.json';
|
|
24
25
|
const LOCK_FILE = 'peer-mesh.owner';
|
|
25
26
|
const MAX_STATE_BYTES = 1024 * 1024;
|
|
@@ -94,7 +95,7 @@ class PeerMeshStateStoreImpl {
|
|
|
94
95
|
const task = this.#tail.then(async () => {
|
|
95
96
|
if (this.#failure)
|
|
96
97
|
throw this.#failure;
|
|
97
|
-
const updated = operation(this.#state);
|
|
98
|
+
const updated = await operation(this.#state);
|
|
98
99
|
if (updated.state === this.#state)
|
|
99
100
|
return updated.result;
|
|
100
101
|
const candidate = pruneUnreferencedRoutes(updated.state, this.localPeerId);
|
|
@@ -251,21 +252,30 @@ async function readState(path, expectedLocalPeerId) {
|
|
|
251
252
|
Object.hasOwn(record, 'meshes') &&
|
|
252
253
|
Object.hasOwn(record, 'routes') &&
|
|
253
254
|
Object.hasOwn(record, 'transitMeshId');
|
|
254
|
-
|
|
255
|
+
const versionFour = record.version === 4 &&
|
|
256
|
+
Object.keys(record).length === 6 &&
|
|
257
|
+
Object.hasOwn(record, 'localPeerId') &&
|
|
258
|
+
Object.hasOwn(record, 'displayName') &&
|
|
259
|
+
Object.hasOwn(record, 'meshes') &&
|
|
260
|
+
Object.hasOwn(record, 'routes') &&
|
|
261
|
+
Object.hasOwn(record, 'transitMeshId');
|
|
262
|
+
if (!versionOne && !versionTwo && !versionThree && !versionFour) {
|
|
255
263
|
throw new Error('Unsupported Peer Mesh state document');
|
|
256
264
|
}
|
|
257
265
|
if (boundedString(record.localPeerId, 'localPeerId', 256) !== expectedLocalPeerId) {
|
|
258
266
|
throw new Error('Peer Mesh state belongs to a different peer identity');
|
|
259
267
|
}
|
|
260
268
|
return decodePeerMeshStoredState({
|
|
269
|
+
displayName: versionFour ? record.displayName : null,
|
|
261
270
|
meshes: record.meshes,
|
|
262
271
|
routes: versionOne ? [] : record.routes,
|
|
263
|
-
transitMeshId: versionThree ? record.transitMeshId : null,
|
|
272
|
+
transitMeshId: versionThree || versionFour ? record.transitMeshId : null,
|
|
264
273
|
}, expectedLocalPeerId);
|
|
265
274
|
}
|
|
266
275
|
catch (error) {
|
|
267
276
|
if (isNodeError(error, 'ENOENT')) {
|
|
268
277
|
return Object.freeze({
|
|
278
|
+
displayName: null,
|
|
269
279
|
meshes: Object.freeze([]),
|
|
270
280
|
routes: Object.freeze([]),
|
|
271
281
|
transitMeshId: null,
|
|
@@ -286,7 +296,7 @@ async function pathExists(path) {
|
|
|
286
296
|
}
|
|
287
297
|
}
|
|
288
298
|
async function writeState(path, localPeerId, state) {
|
|
289
|
-
const document = `${JSON.stringify({ version:
|
|
299
|
+
const document = `${JSON.stringify({ version: 4, localPeerId, ...state }, null, 2)}\n`;
|
|
290
300
|
if (Buffer.byteLength(document) > MAX_STATE_BYTES)
|
|
291
301
|
throw new Error('Peer Mesh state is too large');
|
|
292
302
|
const temporary = `${path}.tmp`;
|
|
@@ -395,13 +405,15 @@ function decodePeerMeshStoredState(value, localPeerId) {
|
|
|
395
405
|
throw new Error('Invalid Peer Mesh state document');
|
|
396
406
|
}
|
|
397
407
|
const record = value;
|
|
398
|
-
if (Object.keys(record).length !==
|
|
408
|
+
if (Object.keys(record).length !== 4 ||
|
|
409
|
+
!Object.hasOwn(record, 'displayName') ||
|
|
399
410
|
!Object.hasOwn(record, 'meshes') ||
|
|
400
411
|
!Object.hasOwn(record, 'routes') ||
|
|
401
412
|
!Object.hasOwn(record, 'transitMeshId')) {
|
|
402
413
|
throw new Error('Invalid Peer Mesh state document');
|
|
403
414
|
}
|
|
404
415
|
const meshes = decodePeerMeshStates(record.meshes, localPeerId);
|
|
416
|
+
const displayName = record.displayName === null ? null : canonicalPeerMeshDisplayName(record.displayName);
|
|
405
417
|
const transitMeshId = record.transitMeshId === null
|
|
406
418
|
? null
|
|
407
419
|
: boundedString(record.transitMeshId, 'transitMeshId', 128);
|
|
@@ -410,6 +422,7 @@ function decodePeerMeshStoredState(value, localPeerId) {
|
|
|
410
422
|
throw new Error('Peer Mesh transit selection is not an active membership');
|
|
411
423
|
}
|
|
412
424
|
return Object.freeze({
|
|
425
|
+
displayName,
|
|
413
426
|
meshes,
|
|
414
427
|
routes: decodeRoutes(record.routes, meshes),
|
|
415
428
|
transitMeshId,
|
|
@@ -59,7 +59,11 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1;
|
|
|
59
59
|
export const RUNTIME_HOST_PROTOCOL_VERSION = 0;
|
|
60
60
|
// Increment when the same protocol version no longer guarantees safe Client-Host
|
|
61
61
|
// interoperability. Mismatches are rejected before domain commands are admitted.
|
|
62
|
-
export const RUNTIME_HOST_COMPATIBILITY_EPOCH =
|
|
62
|
+
export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 76;
|
|
63
|
+
// 76: Peer Mesh endpoint and Mesh display names are signed, persisted facts
|
|
64
|
+
// managed through Host operations rather than local-only Client labels.
|
|
65
|
+
// 75: Peer Mesh routes identify whether a peer is a Client or Runtime Host so
|
|
66
|
+
// management surfaces can present the endpoint authority boundary accurately.
|
|
63
67
|
// 74: Capability-provider credentials may carry one Host-authenticated owner
|
|
64
68
|
// identity. Older peers cannot preserve the association and could select an
|
|
65
69
|
// unrelated provider for an interactive Session.
|