@nelsonlongxiang/dsh-open-a2a-net 0.6.24 → 0.6.26
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 +15 -11
- package/README_zh.md +3 -2
- package/assets/nexusDist/assets/index-DszoLz8i.js +5 -0
- package/assets/nexusDist/assets/index-uQf7NwjF.css +1 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +300 -0
- package/lib/card.js +17 -0
- package/lib/cli-deposit.js +141 -0
- package/lib/exposure-store.js +82 -0
- package/lib/index.js +904 -57
- package/lib/owner-settings.js +100 -0
- package/lib/peer-store.js +65 -9
- package/lib/task-ledger.js +44 -3
- package/lib/types/cli-deposit.d.ts +53 -0
- package/lib/types/exposure-store.d.ts +45 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/owner-settings.d.ts +19 -0
- package/lib/types/peer-store.d.ts +22 -3
- package/lib/types/task-ledger.d.ts +18 -0
- package/lib/types/types.d.ts +10 -0
- package/nexus-stage/src/canvas-ops.ts +2 -2
- package/nexus-stage/src/federation.ts +6 -0
- package/nexus-stage/src/main.ts +97 -4
- package/nexus-stage/src/planning-menus.ts +16 -4
- package/nexus-stage/src/planning-types.ts +3 -0
- package/nexus-stage/src/planning-view.ts +208 -18
- package/nexus-stage/src/planning.css +4 -0
- package/nexus-stage/src/wb-chat.ts +670 -0
- package/nexus-stage/src/wb-directory.ts +222 -0
- package/nexus-stage/src/wb-ledger.ts +266 -0
- package/nexus-stage/src/wb-rail.ts +147 -0
- package/nexus-stage/src/workbench.css +190 -84
- package/nexus-stage/src/workbench.ts +385 -721
- package/nexus-stage/src/world.ts +5 -1
- package/package.json +18 -22
- package/assets/nexusDist/assets/index-034h1cNc.js +0 -3
- package/assets/nexusDist/assets/index-PG9EieGX.css +0 -1
- package/lib/types/client.d.ts +0 -73
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-settings persistence (gh#34): the three unlock knobs a remote
|
|
3
|
+
* build-out needs — teamJoinAllowlist / teamScopeInfo / announce — written
|
|
4
|
+
* into the HOME patch layer (DSH_HOME/cordis.patch.yml), the authoritative
|
|
5
|
+
* config surface (the 3081 lesson: the home-level patch wins over the
|
|
6
|
+
* profile layer). A conservative line editor: it locates the a2a entry's
|
|
7
|
+
* config: block and sets exactly these three keys, never touching anything
|
|
8
|
+
* else; a missing entry is appended in the canonical short-id form.
|
|
9
|
+
* @module @nelsonlongxiang/dsh-open-a2a-net/owner-settings
|
|
10
|
+
*/
|
|
11
|
+
/** The exact keys this module may touch, in wire order. */
|
|
12
|
+
const KEYS = ['teamJoinAllowlist', 'teamScopeInfo', 'announce'];
|
|
13
|
+
export function applyOwnerSettings(patchYaml, changes) {
|
|
14
|
+
const lines = patchYaml.split('\n');
|
|
15
|
+
// Find the a2a entry: `- id: a2a` / `- id: 'a2a'` variants.
|
|
16
|
+
let entryStart = -1;
|
|
17
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
18
|
+
const m = /^(\s*)-\s*id:\s*'?(a2a)'?\s*$/.exec(lines[i] ?? '');
|
|
19
|
+
if (m !== null) {
|
|
20
|
+
entryStart = i;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let restartNeeded = false;
|
|
25
|
+
if (changes.announce !== undefined)
|
|
26
|
+
restartNeeded = true;
|
|
27
|
+
const writes = [];
|
|
28
|
+
if (changes.teamJoinAllowlist !== undefined) {
|
|
29
|
+
writes.push(changes.teamJoinAllowlist.length === 0
|
|
30
|
+
? ' teamJoinAllowlist: []'
|
|
31
|
+
: ` teamJoinAllowlist: [${changes.teamJoinAllowlist.map(name => `'${name.replace(/'/g, "''")}'`).join(', ')}]`);
|
|
32
|
+
}
|
|
33
|
+
if (changes.teamScopeInfo !== undefined)
|
|
34
|
+
writes.push(` teamScopeInfo: ${changes.teamScopeInfo ? 'true' : 'false'}`);
|
|
35
|
+
if (changes.announce !== undefined)
|
|
36
|
+
writes.push(` announce: ${changes.announce ? 'true' : 'false'}`);
|
|
37
|
+
if (writes.length === 0)
|
|
38
|
+
return { yaml: patchYaml, restartNeeded };
|
|
39
|
+
if (entryStart === -1) {
|
|
40
|
+
// Append a canonical entry (the home patch is a top-level list).
|
|
41
|
+
const trailing = patchYaml.endsWith('\n') ? '' : '\n';
|
|
42
|
+
return { yaml: `${patchYaml}${trailing}- id: a2a\n config:\n${writes.join('\n')}\n`, restartNeeded };
|
|
43
|
+
}
|
|
44
|
+
// Locate the config: block inside the entry (the entry's own indentation
|
|
45
|
+
// is two spaces under the `- id:` line).
|
|
46
|
+
let i = entryStart + 1;
|
|
47
|
+
let configAt = -1;
|
|
48
|
+
while (i < lines.length) {
|
|
49
|
+
const line = lines[i] ?? '';
|
|
50
|
+
if (/^\S/.test(line))
|
|
51
|
+
break; // next top-level entry
|
|
52
|
+
if (/^\s{2}config:\s*$/.test(line)) {
|
|
53
|
+
configAt = i;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
if (configAt === -1) {
|
|
59
|
+
// Insert a config: block right after the id line.
|
|
60
|
+
lines.splice(entryStart + 1, 0, ' config:', ...writes);
|
|
61
|
+
return { yaml: lines.join('\n'), restartNeeded };
|
|
62
|
+
}
|
|
63
|
+
// Rewrite exactly our keys inside the block; keep foreign lines verbatim.
|
|
64
|
+
const blockIndent = ' ';
|
|
65
|
+
let insertAt = configAt + 1;
|
|
66
|
+
for (const key of KEYS) {
|
|
67
|
+
let found = false;
|
|
68
|
+
let j = configAt + 1;
|
|
69
|
+
while (j < lines.length) {
|
|
70
|
+
const line = lines[j] ?? '';
|
|
71
|
+
if (/^\S/.test(line) || /^\s{2}\S/.test(line) && !/^\s{4}/.test(line) && !/^\s{2}config:/.test(lines[configAt] ?? '')) {
|
|
72
|
+
if (!line.startsWith(blockIndent) && !/^\s*$/.test(line) && j > configAt) {
|
|
73
|
+
// leaving the block when a line is less indented than the keys
|
|
74
|
+
if (!line.startsWith(' '))
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const kv = new RegExp(`^\\s*${key}:`).exec(line);
|
|
79
|
+
if (kv !== null) {
|
|
80
|
+
const write = writes.find(w => w.trim().startsWith(`${key}:`));
|
|
81
|
+
if (write !== undefined) {
|
|
82
|
+
lines[j] = write;
|
|
83
|
+
found = true;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (/^\s*-\s*id:/.test(line))
|
|
88
|
+
break;
|
|
89
|
+
j += 1;
|
|
90
|
+
}
|
|
91
|
+
if (!found) {
|
|
92
|
+
const write = writes.find(w => w.trim().startsWith(`${key}:`));
|
|
93
|
+
if (write !== undefined) {
|
|
94
|
+
lines.splice(insertAt, 0, write);
|
|
95
|
+
insertAt += 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { yaml: lines.join('\n'), restartNeeded };
|
|
100
|
+
}
|
package/lib/peer-store.js
CHANGED
|
@@ -48,8 +48,38 @@ export class PeerStore {
|
|
|
48
48
|
// The config is authoritative for a seed: it is (re)marked as one —
|
|
49
49
|
// immune to eviction — and restarts from the initial score regardless
|
|
50
50
|
// of the persisted one. Replacing the record wholesale keeps PeerRecord
|
|
51
|
-
// fields readonly.
|
|
52
|
-
|
|
51
|
+
// fields readonly. The contact stamps are observations, not config:
|
|
52
|
+
// they carry over, so a restart does not blank a seed's lamp (gh#24).
|
|
53
|
+
const prior = this.peers.get(seed);
|
|
54
|
+
this.peers.set(seed, {
|
|
55
|
+
url: seed,
|
|
56
|
+
score: INITIAL_SCORE,
|
|
57
|
+
seed: true,
|
|
58
|
+
...(prior?.lastContactAt !== undefined ? { lastContactAt: prior.lastContactAt } : {}),
|
|
59
|
+
...(prior?.lastContactOk !== undefined ? { lastContactOk: prior.lastContactOk } : {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// The config is authoritative BOTH ways (board-stale-peer fix, 0.6.26):
|
|
63
|
+
// a persisted record may carry seed:true from an OLD config generation
|
|
64
|
+
// whose operator already removed the URL — left alone, a dead URL keeps
|
|
65
|
+
// its immunity forever and the workbench board shows a peer that can
|
|
66
|
+
// never evict (owner sighting: `.157:13082` survived its config removal
|
|
67
|
+
// for days). Downgrade those to referral-learned, keeping the score so
|
|
68
|
+
// noteFailure can decay and evict them honestly.
|
|
69
|
+
const current = new Set(seeds.filter(seed => seed !== ''));
|
|
70
|
+
for (const [url, peer] of this.peers) {
|
|
71
|
+
if (peer.seed && !current.has(url)) {
|
|
72
|
+
// Downgrade keeps every learned fact (score, session, contact
|
|
73
|
+
// stamps) — only the immunity bit changes.
|
|
74
|
+
this.peers.set(url, {
|
|
75
|
+
url,
|
|
76
|
+
score: peer.score,
|
|
77
|
+
seed: false,
|
|
78
|
+
...(peer.session !== undefined ? { session: peer.session } : {}),
|
|
79
|
+
...(peer.lastContactAt !== undefined ? { lastContactAt: peer.lastContactAt } : {}),
|
|
80
|
+
...(peer.lastContactOk !== undefined ? { lastContactOk: peer.lastContactOk } : {}),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
53
83
|
}
|
|
54
84
|
}
|
|
55
85
|
/**
|
|
@@ -106,18 +136,24 @@ export class PeerStore {
|
|
|
106
136
|
this.persist();
|
|
107
137
|
}
|
|
108
138
|
/**
|
|
109
|
-
* Note a failed fetch of one peer.
|
|
110
|
-
*
|
|
111
|
-
*
|
|
139
|
+
* Note a failed fetch of one peer. Every peer records the failed attempt
|
|
140
|
+
* (the host-rail lamp turns red for a seed and a learned peer alike,
|
|
141
|
+
* gh#24); only a non-seed's score drops, and an at-or-below-floor peer is
|
|
142
|
+
* evicted, so a bad referral disappears rather than degrading forever.
|
|
143
|
+
* Seeds are config-owned and never penalized.
|
|
112
144
|
* @param url - the peer that failed.
|
|
113
145
|
*/
|
|
114
146
|
noteFailure(url) {
|
|
115
147
|
const peer = this.peers.get(url);
|
|
116
|
-
if (peer === undefined
|
|
148
|
+
if (peer === undefined)
|
|
117
149
|
return;
|
|
118
|
-
peer.
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
peer.lastContactAt = Date.now();
|
|
151
|
+
peer.lastContactOk = false;
|
|
152
|
+
if (!peer.seed) {
|
|
153
|
+
peer.score -= FAILURE_PENALTY;
|
|
154
|
+
if (peer.score <= EVICTION_FLOOR)
|
|
155
|
+
this.peers.delete(url);
|
|
156
|
+
}
|
|
121
157
|
this.persist();
|
|
122
158
|
}
|
|
123
159
|
/**
|
|
@@ -129,8 +165,24 @@ export class PeerStore {
|
|
|
129
165
|
if (peer === undefined)
|
|
130
166
|
return;
|
|
131
167
|
peer.score += SUCCESS_REWARD;
|
|
168
|
+
peer.lastContactAt = Date.now();
|
|
169
|
+
peer.lastContactOk = true;
|
|
132
170
|
this.persist();
|
|
133
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* The epoch ms of the last contact attempt with one tracked peer (#20 W-C
|
|
174
|
+
* host-rail health lamp), or undefined when the store never touched it.
|
|
175
|
+
*/
|
|
176
|
+
lastContact(url) {
|
|
177
|
+
return this.peers.get(url)?.lastContactAt;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Whether the last contact attempt succeeded (gh#24) — undefined when no
|
|
181
|
+
* attempt was ever recorded.
|
|
182
|
+
*/
|
|
183
|
+
lastContactOk(url) {
|
|
184
|
+
return this.peers.get(url)?.lastContactOk;
|
|
185
|
+
}
|
|
134
186
|
/**
|
|
135
187
|
* The current quality score of one tracked peer, for state reporting.
|
|
136
188
|
* @param url - the peer URL.
|
|
@@ -189,6 +241,10 @@ export class PeerStore {
|
|
|
189
241
|
score: typeof peer.score === 'number' && Number.isFinite(peer.score) ? peer.score : INITIAL_SCORE,
|
|
190
242
|
seed: peer.seed === true,
|
|
191
243
|
...(typeof peer.session === 'string' && peer.session !== '' ? { session: peer.session } : {}),
|
|
244
|
+
// gh#24: the contact stamp round-trips (a restart must not forget
|
|
245
|
+
// when we last reached the peer, nor whether it answered).
|
|
246
|
+
...(typeof peer.lastContactAt === 'number' && Number.isFinite(peer.lastContactAt) ? { lastContactAt: peer.lastContactAt } : {}),
|
|
247
|
+
...(typeof peer.lastContactOk === 'boolean' ? { lastContactOk: peer.lastContactOk } : {}),
|
|
192
248
|
});
|
|
193
249
|
}
|
|
194
250
|
}
|
package/lib/task-ledger.js
CHANGED
|
@@ -60,6 +60,29 @@ export function briefOf(message) {
|
|
|
60
60
|
* lazy — every read/write entry point sweeps first.
|
|
61
61
|
*/
|
|
62
62
|
export const TASK_STALE_TTL_MS = 24 * 60 * 60_000;
|
|
63
|
+
const ARCHIVE_ENDS = ['receipt', 'echo', 'abandoned', 'cancelled'];
|
|
64
|
+
/** Summary marker {@link TaskLedger.resolveEcho} writes on echo-settled rows. */
|
|
65
|
+
const ECHO_SUMMARY = '(settled by content echo)';
|
|
66
|
+
/**
|
|
67
|
+
* How one archived row ended (gh#31). The recorded field wins; legacy rows
|
|
68
|
+
* fall back to the summary markers this ledger writes itself. The fallback
|
|
69
|
+
* reads a receipt-settled row's summary, which is the peer's text, so it is
|
|
70
|
+
* only as exact as the legacy data allows.
|
|
71
|
+
* @param row - the archived record.
|
|
72
|
+
* @returns the archive path that ended it.
|
|
73
|
+
*/
|
|
74
|
+
export function archivedEndedBy(row) {
|
|
75
|
+
if (row.endedBy !== undefined)
|
|
76
|
+
return row.endedBy;
|
|
77
|
+
const summary = row.summary ?? '';
|
|
78
|
+
if (summary.startsWith('caller-cancelled'))
|
|
79
|
+
return 'cancelled';
|
|
80
|
+
if (summary.startsWith('caller-abandoned'))
|
|
81
|
+
return 'abandoned';
|
|
82
|
+
if (summary === ECHO_SUMMARY)
|
|
83
|
+
return 'echo';
|
|
84
|
+
return 'receipt';
|
|
85
|
+
}
|
|
63
86
|
/**
|
|
64
87
|
* Bounded outbound-task ledger with whole-file persistence. Pure of any
|
|
65
88
|
* routing knowledge: callers {@link track} a task when a route leaves it
|
|
@@ -247,6 +270,8 @@ export class TaskLedger {
|
|
|
247
270
|
resolvedAt,
|
|
248
271
|
summary: `caller-abandoned${cause}`,
|
|
249
272
|
outcome: 'abandoned',
|
|
273
|
+
endedBy: 'abandoned',
|
|
274
|
+
...(record.direction !== undefined ? { direction: record.direction } : {}),
|
|
250
275
|
});
|
|
251
276
|
this.persist();
|
|
252
277
|
return { outcome: 'cleared', taskId, archivedAt: resolvedAt };
|
|
@@ -286,6 +311,8 @@ export class TaskLedger {
|
|
|
286
311
|
resolvedAt,
|
|
287
312
|
summary: `caller-cancelled${cause}`,
|
|
288
313
|
outcome: 'abandoned',
|
|
314
|
+
endedBy: 'cancelled',
|
|
315
|
+
...(record.direction !== undefined ? { direction: record.direction } : {}),
|
|
289
316
|
});
|
|
290
317
|
this.persist();
|
|
291
318
|
return {
|
|
@@ -334,6 +361,8 @@ export class TaskLedger {
|
|
|
334
361
|
...(summary !== '' ? { summary } : {}),
|
|
335
362
|
...(envelopeOutcome !== undefined ? { outcome: envelopeOutcome } : {}),
|
|
336
363
|
...(elapsedMs !== undefined ? { elapsedMs } : {}),
|
|
364
|
+
endedBy: 'receipt',
|
|
365
|
+
...(record.direction !== undefined ? { direction: record.direction } : {}),
|
|
337
366
|
});
|
|
338
367
|
this.persist();
|
|
339
368
|
return {
|
|
@@ -354,7 +383,11 @@ export class TaskLedger {
|
|
|
354
383
|
// rewrite the abandonment outcome — the caller stopped waiting by choice.
|
|
355
384
|
// Record that the target did answer later; keep the decision verbatim and
|
|
356
385
|
// pin the projected verdict to 'abandoned'.
|
|
357
|
-
|
|
386
|
+
// gh#31: the recorded end wins; the summary prefix only decides legacy
|
|
387
|
+
// rows (a receipt-settled row's summary is the peer's own text).
|
|
388
|
+
const callerEnded = settled.endedBy !== undefined
|
|
389
|
+
? settled.endedBy === 'abandoned' || settled.endedBy === 'cancelled'
|
|
390
|
+
: CALLER_ENDED_PREFIX.test(settled.summary ?? '');
|
|
358
391
|
this.archived = [
|
|
359
392
|
{
|
|
360
393
|
...settled,
|
|
@@ -409,7 +442,9 @@ export class TaskLedger {
|
|
|
409
442
|
startedAt: record.startedAt,
|
|
410
443
|
...(record.contextId !== undefined ? { contextId: record.contextId } : {}),
|
|
411
444
|
resolvedAt: this.now(),
|
|
412
|
-
summary:
|
|
445
|
+
summary: ECHO_SUMMARY,
|
|
446
|
+
endedBy: 'echo',
|
|
447
|
+
...(record.direction !== undefined ? { direction: record.direction } : {}),
|
|
413
448
|
});
|
|
414
449
|
settled.push(record.taskId);
|
|
415
450
|
}
|
|
@@ -468,6 +503,8 @@ export class TaskLedger {
|
|
|
468
503
|
...(typeof entry.summary === 'string' && entry.summary !== '' ? { summary: entry.summary } : {}),
|
|
469
504
|
...(typeof entry.outcome === 'string' && RECEIPT_OUTCOMES.includes(entry.outcome) ? { outcome: entry.outcome } : {}),
|
|
470
505
|
...(typeof entry.lateReceiptAt === 'number' && Number.isFinite(entry.lateReceiptAt) ? { lateReceiptAt: entry.lateReceiptAt } : {}),
|
|
506
|
+
...(typeof entry.endedBy === 'string' && ARCHIVE_ENDS.includes(entry.endedBy) ? { endedBy: entry.endedBy } : {}),
|
|
507
|
+
...(entry.direction === 'inbound' || entry.direction === 'outbound' ? { direction: entry.direction } : {}),
|
|
471
508
|
});
|
|
472
509
|
continue;
|
|
473
510
|
}
|
|
@@ -478,7 +515,9 @@ export class TaskLedger {
|
|
|
478
515
|
startedAt: typeof entry.startedAt === 'number' && Number.isFinite(entry.startedAt) ? entry.startedAt : this.now(),
|
|
479
516
|
...(typeof entry.contextId === 'string' && entry.contextId !== '' ? { contextId: entry.contextId } : {}),
|
|
480
517
|
...(typeof entry.brief === 'string' && entry.brief !== '' ? { brief: entry.brief } : {}),
|
|
481
|
-
|
|
518
|
+
// gh#31: both sides round-trip — /state omits direction only when
|
|
519
|
+
// it is unknown, so a restored outbound row must not lose it.
|
|
520
|
+
...(entry.direction === 'inbound' || entry.direction === 'outbound' ? { direction: entry.direction } : {}),
|
|
482
521
|
...(entry.receiptExpected === false ? { receiptExpected: false } : {}),
|
|
483
522
|
status: entry.status === 'dead' ? 'dead' : 'pending',
|
|
484
523
|
...(entry.status === 'dead'
|
|
@@ -509,6 +548,8 @@ export class TaskLedger {
|
|
|
509
548
|
...(typeof entry.summary === 'string' && entry.summary !== '' ? { summary: entry.summary } : {}),
|
|
510
549
|
...(typeof entry.outcome === 'string' && RECEIPT_OUTCOMES.includes(entry.outcome) ? { outcome: entry.outcome } : {}),
|
|
511
550
|
...(typeof entry.lateReceiptAt === 'number' && Number.isFinite(entry.lateReceiptAt) ? { lateReceiptAt: entry.lateReceiptAt } : {}),
|
|
551
|
+
...(typeof entry.endedBy === 'string' && ARCHIVE_ENDS.includes(entry.endedBy) ? { endedBy: entry.endedBy } : {}),
|
|
552
|
+
...(entry.direction === 'inbound' || entry.direction === 'outbound' ? { direction: entry.direction } : {}),
|
|
512
553
|
});
|
|
513
554
|
}
|
|
514
555
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Owner ruling ③: the default AND maximum online window. */
|
|
2
|
+
export declare const CLI_DEPOSIT_TTL_MS: number;
|
|
3
|
+
/** Owner ruling ①: <agent-name>/<uuid·8B>. The name is [a-z0-9-]{1,32}. */
|
|
4
|
+
export declare const CLI_HANDLE_RE: RegExp;
|
|
5
|
+
/** One deposited CLI identity. */
|
|
6
|
+
export interface CliDeposit {
|
|
7
|
+
readonly handle: string;
|
|
8
|
+
/** The bare agent-name zone (card.team). */
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/** The CLI's signed card, verbatim wire JSON (served back as-is). */
|
|
11
|
+
readonly cardJson: string;
|
|
12
|
+
readonly depositedAt: number;
|
|
13
|
+
readonly expiresAt: number;
|
|
14
|
+
}
|
|
15
|
+
/** One mailbox entry for a deposited handle. */
|
|
16
|
+
export interface CliMail {
|
|
17
|
+
readonly at: number;
|
|
18
|
+
readonly from: string;
|
|
19
|
+
readonly text: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class CliDepositStore {
|
|
22
|
+
private readonly path;
|
|
23
|
+
private deposits;
|
|
24
|
+
private mailbox;
|
|
25
|
+
private now;
|
|
26
|
+
constructor(path: string, options?: {
|
|
27
|
+
now?: () => number;
|
|
28
|
+
});
|
|
29
|
+
/** Deposit (or refresh) one CLI card. TTL is clamped to the 4h ruling. */
|
|
30
|
+
deposit(cardJson: string): {
|
|
31
|
+
ok: true;
|
|
32
|
+
handle: string;
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
error: string;
|
|
37
|
+
};
|
|
38
|
+
revoke(handle: string): boolean;
|
|
39
|
+
/** Active deposits (sweeping expired ones — ruling ③ auto-deregister). */
|
|
40
|
+
list(): readonly CliDeposit[];
|
|
41
|
+
get(handle: string): CliDeposit | undefined;
|
|
42
|
+
/** Card-serve rows: the handle + display name, marked cli (no wake). */
|
|
43
|
+
cardRows(): Array<{
|
|
44
|
+
team: string;
|
|
45
|
+
name: string;
|
|
46
|
+
description: string;
|
|
47
|
+
}>;
|
|
48
|
+
mail(handle: string, from: string, text: string): void;
|
|
49
|
+
readMail(handle: string): readonly CliMail[];
|
|
50
|
+
private sweep;
|
|
51
|
+
private persist;
|
|
52
|
+
private restore;
|
|
53
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #19 exposure tiers (ruling final, D3/D4): per-session visibility tier on
|
|
3
|
+
* the A2A network — 'linked' (the default floor: visible to linked callers
|
|
4
|
+
* per the W3 gate) or 'fleet' (the member's voluntary opt-UP: visible to
|
|
5
|
+
* any fleet peer). v1 carries exactly these two tiers (D5: no private
|
|
6
|
+
* residue below linked).
|
|
7
|
+
*
|
|
8
|
+
* D3: joins default to linked — existing members transfer automatically
|
|
9
|
+
* (tier absence IS linked, so no migration is needed); a member may raise
|
|
10
|
+
* itself to fleet and lower itself back (fleet is an opt-up, linked is the
|
|
11
|
+
* floor, never a ceiling decision taken from the member).
|
|
12
|
+
*
|
|
13
|
+
* D4 (成文, zero group knobs): a GROUP's visibility equals its STRICTEST
|
|
14
|
+
* member tier — one linked member keeps the group linked-level. The rule
|
|
15
|
+
* binds every future group surface (canvas listing, fleet feeds); v1 has
|
|
16
|
+
* no group-level tier API by design, so the rule needs no enforcement
|
|
17
|
+
* seam yet.
|
|
18
|
+
* @module @nelsonlongxiang/dsh-open-a2a-net/exposure-store
|
|
19
|
+
*/
|
|
20
|
+
/** The two v1 tiers (D5: nothing below linked exists). */
|
|
21
|
+
export type ExposureTier = 'linked' | 'fleet';
|
|
22
|
+
/** The persisted tier document (whole-file read/write, small by design). */
|
|
23
|
+
export interface ExposureSnapshot {
|
|
24
|
+
readonly tiers?: Readonly<Record<string, ExposureTier>>;
|
|
25
|
+
}
|
|
26
|
+
export declare class ExposureStore {
|
|
27
|
+
private readonly path;
|
|
28
|
+
private tiers;
|
|
29
|
+
/**
|
|
30
|
+
* @param path - persistence file (`<dsh-home>/a2a/exposure.json`); empty = no persistence.
|
|
31
|
+
*/
|
|
32
|
+
constructor(path: string);
|
|
33
|
+
/**
|
|
34
|
+
* The session's tier — 'linked' unless explicitly raised to 'fleet'
|
|
35
|
+
* (D3: existing members transfer as linked automatically).
|
|
36
|
+
*/
|
|
37
|
+
tierOf(id: string): ExposureTier;
|
|
38
|
+
/**
|
|
39
|
+
* Set the session's tier. Members may opt up to fleet and back down to
|
|
40
|
+
* linked; there is no tier below linked (D5) and no group-level knob
|
|
41
|
+
* (D4 — group visibility derives from the strictest member).
|
|
42
|
+
*/
|
|
43
|
+
setTier(id: string, tier: ExposureTier): void;
|
|
44
|
+
private persist;
|
|
45
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -103,6 +103,11 @@ export interface Config {
|
|
|
103
103
|
* self-asserted already-linked name must not pass on shape alone).
|
|
104
104
|
*/
|
|
105
105
|
readonly originClaimWindow: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* #19 D2: card-face sessionTeams excerpt exposure — 'full' | 'masked'
|
|
108
|
+
* | 'off' (owner-level; default full). See the schema comment.
|
|
109
|
+
*/
|
|
110
|
+
readonly exposureExcerpt: 'full' | 'masked' | 'off';
|
|
106
111
|
/**
|
|
107
112
|
* Override for the OriginClaim callerUrl anchor. Loopback-bound hosts
|
|
108
113
|
* behind a pocket tunnel must name the card URL peers can actually
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-settings persistence (gh#34): the three unlock knobs a remote
|
|
3
|
+
* build-out needs — teamJoinAllowlist / teamScopeInfo / announce — written
|
|
4
|
+
* into the HOME patch layer (DSH_HOME/cordis.patch.yml), the authoritative
|
|
5
|
+
* config surface (the 3081 lesson: the home-level patch wins over the
|
|
6
|
+
* profile layer). A conservative line editor: it locates the a2a entry's
|
|
7
|
+
* config: block and sets exactly these three keys, never touching anything
|
|
8
|
+
* else; a missing entry is appended in the canonical short-id form.
|
|
9
|
+
* @module @nelsonlongxiang/dsh-open-a2a-net/owner-settings
|
|
10
|
+
*/
|
|
11
|
+
export interface OwnerSettings {
|
|
12
|
+
teamJoinAllowlist?: string[];
|
|
13
|
+
teamScopeInfo?: boolean;
|
|
14
|
+
announce?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function applyOwnerSettings(patchYaml: string, changes: OwnerSettings): {
|
|
17
|
+
yaml: string;
|
|
18
|
+
restartNeeded: boolean;
|
|
19
|
+
};
|
|
@@ -25,6 +25,13 @@ export interface PeerRecord {
|
|
|
25
25
|
readonly seed: boolean;
|
|
26
26
|
/** The signed-card session the peer served last, when a fetch revealed it. */
|
|
27
27
|
readonly session?: string;
|
|
28
|
+
/** Epoch ms of the last contact attempt (success or failure) — the host
|
|
29
|
+
* rail's health lamp reads the recency of this (#20 W-C). */
|
|
30
|
+
lastContactAt?: number;
|
|
31
|
+
/** Whether that last attempt SUCCEEDED (gh#24): the lamp distinguishes a
|
|
32
|
+
* fresh success (green) from a fresh failure (red); recency alone made
|
|
33
|
+
* a dead peer glow green. */
|
|
34
|
+
lastContactOk?: boolean;
|
|
28
35
|
}
|
|
29
36
|
/** The persisted store document (whole-file read/write, small by design). */
|
|
30
37
|
export interface PeerStoreSnapshot {
|
|
@@ -77,9 +84,11 @@ export declare class PeerStore {
|
|
|
77
84
|
*/
|
|
78
85
|
drop(url: string): void;
|
|
79
86
|
/**
|
|
80
|
-
* Note a failed fetch of one peer.
|
|
81
|
-
*
|
|
82
|
-
*
|
|
87
|
+
* Note a failed fetch of one peer. Every peer records the failed attempt
|
|
88
|
+
* (the host-rail lamp turns red for a seed and a learned peer alike,
|
|
89
|
+
* gh#24); only a non-seed's score drops, and an at-or-below-floor peer is
|
|
90
|
+
* evicted, so a bad referral disappears rather than degrading forever.
|
|
91
|
+
* Seeds are config-owned and never penalized.
|
|
83
92
|
* @param url - the peer that failed.
|
|
84
93
|
*/
|
|
85
94
|
noteFailure(url: string): void;
|
|
@@ -88,6 +97,16 @@ export declare class PeerStore {
|
|
|
88
97
|
* @param url - the peer that answered with a verified card.
|
|
89
98
|
*/
|
|
90
99
|
noteSuccess(url: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* The epoch ms of the last contact attempt with one tracked peer (#20 W-C
|
|
102
|
+
* host-rail health lamp), or undefined when the store never touched it.
|
|
103
|
+
*/
|
|
104
|
+
lastContact(url: string): number | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Whether the last contact attempt succeeded (gh#24) — undefined when no
|
|
107
|
+
* attempt was ever recorded.
|
|
108
|
+
*/
|
|
109
|
+
lastContactOk(url: string): boolean | undefined;
|
|
91
110
|
/**
|
|
92
111
|
* The current quality score of one tracked peer, for state reporting.
|
|
93
112
|
* @param url - the peer URL.
|
|
@@ -52,6 +52,8 @@ export declare const TASK_STALE_TTL_MS: number;
|
|
|
52
52
|
export type TaskStatus = 'pending' | 'dead';
|
|
53
53
|
/** F5 layer-3: why a dead row aged out (stamped by the sweep at flip time). */
|
|
54
54
|
export type DeadReason = 'fire-forget' | 'unconsumed';
|
|
55
|
+
/** gh#31: which archive path ended a row — a receipt, a content echo, or the caller. */
|
|
56
|
+
export type ArchiveEnd = 'receipt' | 'echo' | 'abandoned' | 'cancelled';
|
|
55
57
|
/** One tracked outbound task still in the owed book (unsettled). */
|
|
56
58
|
export interface TaskRecord {
|
|
57
59
|
readonly taskId: string;
|
|
@@ -111,7 +113,23 @@ export interface ArchivedRecord {
|
|
|
111
113
|
* recorded as metadata only, never promoted to resolution.
|
|
112
114
|
*/
|
|
113
115
|
readonly lateReceiptAt?: number;
|
|
116
|
+
/**
|
|
117
|
+
* gh#31: which archive path ended the row, recorded at archive time. Absent
|
|
118
|
+
* on rows archived before the field — {@link archivedEndedBy} derives it.
|
|
119
|
+
*/
|
|
120
|
+
readonly endedBy?: ArchiveEnd;
|
|
121
|
+
/** gh#31: which side of the wire the row served, copied from the owed row (absent on rows archived before the field). */
|
|
122
|
+
readonly direction?: 'inbound' | 'outbound';
|
|
114
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* How one archived row ended (gh#31). The recorded field wins; legacy rows
|
|
126
|
+
* fall back to the summary markers this ledger writes itself. The fallback
|
|
127
|
+
* reads a receipt-settled row's summary, which is the peer's text, so it is
|
|
128
|
+
* only as exact as the legacy data allows.
|
|
129
|
+
* @param row - the archived record.
|
|
130
|
+
* @returns the archive path that ended it.
|
|
131
|
+
*/
|
|
132
|
+
export declare function archivedEndedBy(row: ArchivedRecord): ArchiveEnd;
|
|
115
133
|
/** Result of an explicit caller-side abandon. Total and never-thrown. */
|
|
116
134
|
export type AbandonOutcome = 'cleared' | 'already-terminal' | 'unknown';
|
|
117
135
|
/** Structured verdict for one {@link TaskLedger.abandon} call (consumer contract: idempotent). */
|
package/lib/types/types.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export interface A2aPeerCard {
|
|
|
21
21
|
* not by this card's signature.
|
|
22
22
|
*/
|
|
23
23
|
readonly peers?: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* gh#41 fleet planning: this host's own canvas-team organization, served
|
|
26
|
+
* fresh on the FULL card (linked callers only) — each entry names a team
|
|
27
|
+
* and its member handles so any linked peer's board can render the org.
|
|
28
|
+
*/
|
|
29
|
+
readonly canvasOrg?: ReadonlyArray<{
|
|
30
|
+
name: string;
|
|
31
|
+
team: string;
|
|
32
|
+
members: readonly string[];
|
|
33
|
+
}>;
|
|
24
34
|
/**
|
|
25
35
|
* Joined session teams this peer currently dispatches. Unsigned and
|
|
26
36
|
* optional: served fresh at read time, each entry is a team name the
|
|
@@ -20,8 +20,8 @@ export type RosterOp = { op: 'remove' | 'add'; id: string }
|
|
|
20
20
|
/** The five user-facing write actions (one per design.md §3.3 row), plus the two network join/leave actions the panel-slim migration moved into the planning view. */
|
|
21
21
|
export type CanvasAction =
|
|
22
22
|
| { type: 'create-team'; name: string; ids: readonly string[]; /** true when the model had no such team before this action (a serial failure then leaves a ghost EMPTY team on the host — the wire compensates with a remove). */ created?: boolean }
|
|
23
|
-
| { type: 'add-member'; team: string; ids: readonly string[] }
|
|
24
|
-
| { type: 'remove-member'; team: string; ids: readonly string[] }
|
|
23
|
+
| { type: 'add-member'; team: string; ids: readonly string[]; /** gh#41: set when the target frame is a REMOTE peer's team — the wire routes the op through the host fleet relay. */ fleetPeer?: string }
|
|
24
|
+
| { type: 'remove-member'; team: string; ids: readonly string[]; /** gh#41: see add-member. */ fleetPeer?: string }
|
|
25
25
|
| { type: 'remove-team'; name: string }
|
|
26
26
|
/** `ops` comes from reorderOps() at emit time; the wire stays model-free. */
|
|
27
27
|
| { type: 'reorder'; team: string; ops: ReadonlyArray<RosterOp> }
|
|
@@ -26,6 +26,12 @@ export interface InFlightRow {
|
|
|
26
26
|
export interface PeerRow {
|
|
27
27
|
url: string
|
|
28
28
|
score?: number
|
|
29
|
+
/** gh#20 W-A: this host's link view — 'linked' (operator-established
|
|
30
|
+
* basis holds) | 'stranger' (no link); absent = the host predates the
|
|
31
|
+
* field (render neutral). */
|
|
32
|
+
link?: 'linked' | 'stranger'
|
|
33
|
+
/** Signature-valid cached card held (the badge tick/cross). */
|
|
34
|
+
verified?: boolean
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/** A team frame's titlebar anchor {fx + fw/2, fy + 11} (edges.ts lockstep). */
|