@nelsonlongxiang/dsh-open-a2a-net 0.6.25 → 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/index.js +636 -25
- 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/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 +214 -218
- package/assets/nexusDist/assets/index-034h1cNc.js +0 -3
- package/assets/nexusDist/assets/index-PG9EieGX.css +0 -1
package/cli/a2a.mjs
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* a2a-net CLI (gh#45) — aligned with the plugin's TEN model tools.
|
|
4
|
+
*
|
|
5
|
+
* ── CLI ↔ 插件工具对齐表(tools 命令打印同表)──────────────────────────
|
|
6
|
+
* a2a whoami ↔ a2a_whoami 身份自省:CLI 句柄/钥/户口状态/卡地址
|
|
7
|
+
* a2a teams ↔ a2a_teams 目录:本机会话队 + 远端队(可带关键词过滤)
|
|
8
|
+
* a2a route ↔ a2a_route 派发:签名信封直投 /a2a/direct
|
|
9
|
+
* a2a status ↔ a2a_status 网络健康:peers/在途/活动环 + 户口状态
|
|
10
|
+
* a2a tasks ↔ a2a_tasks 账本三层:pending/dead/archived
|
|
11
|
+
* a2a probe ↔ a2a_probe 可达性:peer 评分/链接/验证状态
|
|
12
|
+
* a2a report ↔ a2a_report 离网单向报告(CLI 形态:投给进程队信箱)
|
|
13
|
+
* a2a exposure ↔ a2a_exposure 会话暴露档 —— CLI 无会话面,见 N/A 说明
|
|
14
|
+
* a2a team join/leave ↔ a2a_team_join/leave 会话手势 —— CLI 以「拉卡入队」
|
|
15
|
+
* 等价:属主把 CLI 成员卡拖进 canvas 队
|
|
16
|
+
* a2a skills (插件面无此工具)加载本机技能目录并输出
|
|
17
|
+
* a2a init / publish / inbox / revoke / tools —— CLI 户口制自有命令
|
|
18
|
+
*
|
|
19
|
+
* ── 户口制(owner rulings 2026-10-24)──────────────────────────────────
|
|
20
|
+
* 句柄 <agent-name>/<uuid·8B=16hex>(名字自定义,后段强制)
|
|
21
|
+
* 一钥多户口;v1 挂本机宿主;在线窗口 ≤4h,到期自动注销,重新 publish 回线
|
|
22
|
+
*
|
|
23
|
+
* Runs on the SDK subpaths only — no @deepseek-ai/* imports.
|
|
24
|
+
*/
|
|
25
|
+
import { generateKeyPairSync, randomUUID, sign as edSign, createPrivateKey } from 'node:crypto'
|
|
26
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
27
|
+
import { homedir } from 'node:os'
|
|
28
|
+
import { join } from 'node:path'
|
|
29
|
+
import { signCard } from '../lib/card.js'
|
|
30
|
+
|
|
31
|
+
const IDENTITY = join(homedir(), '.a2a', 'cli.json')
|
|
32
|
+
const DEFAULT_HOME = 'http://127.0.0.1:3080'
|
|
33
|
+
const DEFAULT_SKILL_ROOTS = [join(homedir(), '.agents', 'skills')]
|
|
34
|
+
|
|
35
|
+
const args = process.argv.slice(2)
|
|
36
|
+
const cmd = args[0] ?? ''
|
|
37
|
+
const flag = (name) => {
|
|
38
|
+
const i = args.indexOf(name)
|
|
39
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined
|
|
40
|
+
}
|
|
41
|
+
const positionals = () => {
|
|
42
|
+
const out = []
|
|
43
|
+
for (let i = 1; i < args.length; i += 1) {
|
|
44
|
+
if (String(args[i]).startsWith('--')) { i += 1; continue }
|
|
45
|
+
if (i > 1 && String(args[i - 1]).startsWith('--')) continue
|
|
46
|
+
out.push(args[i])
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
const home = () => (flag('--home') ?? DEFAULT_HOME).replace(/\/$/, '')
|
|
51
|
+
const die = (msg) => { console.error(String(msg)); process.exit(1) }
|
|
52
|
+
|
|
53
|
+
function identity() {
|
|
54
|
+
if (!existsSync(IDENTITY)) die('no identity — run: a2a init <agent-name>')
|
|
55
|
+
return JSON.parse(readFileSync(IDENTITY, 'utf8'))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function originClaimFor(base) {
|
|
59
|
+
const id = identity()
|
|
60
|
+
const privateKey = createPrivateKey(id.privateKeyPem)
|
|
61
|
+
const callerUrl = `${base}/a2a/cli/${encodeURIComponent(id.handle)}`
|
|
62
|
+
return () => {
|
|
63
|
+
const ts = Date.now()
|
|
64
|
+
const signature = edSign(undefined, Buffer.from(JSON.stringify([id.name, callerUrl, ts])), privateKey).toString('base64')
|
|
65
|
+
return { 'X-A2A-Caller': id.name, 'X-A2A-Caller-Url': callerUrl, 'X-A2A-Ts': String(ts), 'X-A2A-Sig': signature }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const keyHeaders = () => {
|
|
70
|
+
const key = flag('--key')
|
|
71
|
+
return key !== undefined ? { 'X-API-Key': key } : {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function control(body) {
|
|
75
|
+
const res = await fetch(`${home()}/__dsh_a2a/cli-deposit`, {
|
|
76
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', ...keyHeaders() }, body: JSON.stringify(body),
|
|
77
|
+
})
|
|
78
|
+
return await res.json()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function state() {
|
|
82
|
+
const res = await fetch(`${home()}/__dsh_a2a/state`, { headers: keyHeaders() })
|
|
83
|
+
if (res.status !== 200) die(`state face ${String(res.status)} — ${flag('--key') === undefined ? 'pass --key <apiKey>' : 'wrong key?'}`)
|
|
84
|
+
return await res.json()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── the plugin's ten tools, one line each (mirrors src registration) ─────
|
|
88
|
+
const TOOL_CATALOG = [
|
|
89
|
+
['a2a_whoami', '身份自省:节点 id、joined 状态、可路由句柄、roster/canvas 队、allowlist 效应'],
|
|
90
|
+
['a2a_team_join', '声明会话节点为某可路由队成员(allowlist 精确名门控;人建队而非模型自组)'],
|
|
91
|
+
['a2a_team_leave', '撤销一次队成员声明(只能缩不能扩)'],
|
|
92
|
+
['a2a_report', '离网单向报告入监督读的报表环(无在场权——不带来发现/路由权)'],
|
|
93
|
+
['a2a_teams', '列/搜可达队:本机行+远端 peer 行(teamScopeInfo 开时跟随协作圈)'],
|
|
94
|
+
['a2a_route', '向远端队发消息并等回复(长任务用 async;context_id 续谈)'],
|
|
95
|
+
['a2a_status', '网络健康:peers 质量分、在途路由、近期活动环、幂等窗占用'],
|
|
96
|
+
['a2a_tasks', '账本三层对账:pending(欠回执)/dead(超期)/archived(已结算)'],
|
|
97
|
+
['a2a_probe', '探活整个 tracked fleet 或单 peer:可达性+往返延迟+失败原因'],
|
|
98
|
+
['a2a_exposure', '设本会话的网络可见档:linked(下限)/fleet(自愿上调)'],
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
function printAlignment() {
|
|
102
|
+
console.log('插件工具面(十枚)与 CLI 对齐:\n')
|
|
103
|
+
for (const [tool, desc] of TOOL_CATALOG) console.log(` ${tool.padEnd(16)} ${desc}`)
|
|
104
|
+
console.log('\nCLI 对齐映射:')
|
|
105
|
+
const map = [
|
|
106
|
+
['a2a whoami', 'a2a_whoami', '身份自省(CLI 句柄+户口+卡地址)'],
|
|
107
|
+
['a2a teams', 'a2a_teams', '目录(读宿主 state 面:本机+远端队)'],
|
|
108
|
+
['a2a route', 'a2a_route', '派发(CLI 自签信封直投 /a2a/direct)'],
|
|
109
|
+
['a2a status', 'a2a_status', '网络健康(peers/在途/活动环)+ 户口状态'],
|
|
110
|
+
['a2a tasks', 'a2a_tasks', '账本三层(pending/dead/archived)'],
|
|
111
|
+
['a2a probe', 'a2a_probe', 'peer 可达性(评分/链接/验证,读暖态)'],
|
|
112
|
+
['a2a report', 'a2a_report', '离网报告(CLI 形态:投给宿主进程队信箱)'],
|
|
113
|
+
['(N/A)', 'a2a_exposure', '暴露档是会话面概念——CLI 无会话;户口即其可见性'],
|
|
114
|
+
['(拖卡入队)', 'a2a_team_join/leave', '入队是会话手势——等价物:属主把 CLI 成员卡拖进 canvas 队(画布手势=运营者链接)'],
|
|
115
|
+
['a2a skills', '(插件无)', '加载本机技能目录输出(CLI 专有)'],
|
|
116
|
+
['a2a init/publish/inbox/revoke/tools', '(插件无)', '户口制自有:建钥/落户/收信箱/撤户/本表'],
|
|
117
|
+
]
|
|
118
|
+
for (const [cli, tool, note] of map) console.log(` ${String(cli).padEnd(28)} ↔ ${String(tool).padEnd(22)} ${note}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── skills: load the catalog, output it ─────────────────────────────────
|
|
122
|
+
function parseSkillFrontmatter(text) {
|
|
123
|
+
const fm = /^---\n([\s\S]*?)\n---/.exec(text)
|
|
124
|
+
if (fm === null) return {}
|
|
125
|
+
const pick = (key) => {
|
|
126
|
+
const m = new RegExp(`^${key}:\\s*(.+)$`, 'm').exec(fm[1])
|
|
127
|
+
return m !== null ? m[1].trim().replace(/^["']|["']$/g, '') : undefined
|
|
128
|
+
}
|
|
129
|
+
return { name: pick('name'), description: pick('description') }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function loadSkills(roots) {
|
|
133
|
+
const out = []
|
|
134
|
+
for (const root of roots) {
|
|
135
|
+
if (!existsSync(root)) continue
|
|
136
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
137
|
+
if (!entry.isDirectory()) continue
|
|
138
|
+
const md = join(root, entry.name, 'SKILL.md')
|
|
139
|
+
if (!existsSync(md)) continue
|
|
140
|
+
try {
|
|
141
|
+
const text = readFileSync(md, 'utf8')
|
|
142
|
+
const meta = parseSkillFrontmatter(text)
|
|
143
|
+
out.push({ name: meta.name ?? entry.name, description: (meta.description ?? '').split('\n')[0] ?? '', path: md, text })
|
|
144
|
+
} catch { /* unreadable: skip */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
out.sort((a, b) => a.name.localeCompare(b.name))
|
|
148
|
+
return out
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cmdSkills() {
|
|
152
|
+
const pos = positionals()
|
|
153
|
+
const roots = flag('--root') !== undefined ? [flag('--root')] : DEFAULT_SKILL_ROOTS
|
|
154
|
+
const skills = loadSkills(roots)
|
|
155
|
+
if (skills.length === 0) die(`[skills] none found under ${roots.join(', ')}`)
|
|
156
|
+
const target = pos[0]
|
|
157
|
+
if (target !== undefined) {
|
|
158
|
+
const hit = skills.find(s => s.name === target) ?? skills.find(s => s.path.includes(target))
|
|
159
|
+
if (hit === undefined) die(`[skills] "${target}" not found (bare 'a2a skills' lists)`)
|
|
160
|
+
process.stdout.write(hit.text)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (args.includes('--json')) {
|
|
164
|
+
console.log(JSON.stringify(skills.map(({ name, description, path }) => ({ name, description, path })), null, 2))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
for (const s of skills) console.log(`${s.name} — ${s.description.slice(0, 90)}`)
|
|
168
|
+
console.log(`\n(${String(skills.length)} skills · 'a2a skills <name>' prints one)`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── commands ────────────────────────────────────────────────────────────
|
|
172
|
+
async function main() {
|
|
173
|
+
if (cmd === 'init') {
|
|
174
|
+
const name = positionals()[0]
|
|
175
|
+
if (name === undefined || !/^[a-z0-9-]{1,32}$/.test(name)) die('usage: a2a init <agent-name> (name: [a-z0-9-]{1,32})')
|
|
176
|
+
let uuid = flag('--uuid')
|
|
177
|
+
if (uuid === undefined) uuid = randomUUID().replace(/-/g, '').slice(0, 16)
|
|
178
|
+
if (!/^[0-9a-f]{16}$/.test(uuid)) die('--uuid must be 16 hex chars (8 bytes)')
|
|
179
|
+
const { privateKey } = generateKeyPairSync('ed25519')
|
|
180
|
+
mkdirSync(join(homedir(), '.a2a'), { recursive: true })
|
|
181
|
+
writeFileSync(IDENTITY, JSON.stringify({ name, uuid, handle: `${name}/${uuid}`, privateKeyPem: privateKey.export({ format: 'pem', type: 'pkcs8' }) }, null, 2))
|
|
182
|
+
console.log(`[init] ${name}/${uuid} — key at ${IDENTITY}`)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if (cmd === 'publish') {
|
|
186
|
+
const id = identity()
|
|
187
|
+
const privateKey = createPrivateKey(id.privateKeyPem)
|
|
188
|
+
const card = signCard({ name: id.name, session: `cli-${id.name}`, team: id.handle, capabilities: { route: true, async: true }, expiresAt: Date.now() + 2 * 24 * 60 * 60 * 1000 }, privateKey)
|
|
189
|
+
const out = await control({ action: 'publish', card: JSON.stringify(card) })
|
|
190
|
+
if (out.ok !== true) die(`[publish] refused: ${String(out.error)}`)
|
|
191
|
+
console.log(`[publish] ${String(out.handle)} online — ${((out.expiresAt - Date.now()) / 3_600_000).toFixed(2)}h left (auto-deregister then; re-publish returns)`)
|
|
192
|
+
console.log(`[publish] card url: ${String(out.cardUrl)}`)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
if (cmd === 'route') {
|
|
196
|
+
const pos = positionals()
|
|
197
|
+
const team = pos[0]
|
|
198
|
+
const message = args.slice(args.indexOf(team) + 1).filter(a => !String(a).startsWith('--')).join(' ')
|
|
199
|
+
if (team === undefined || message === '') die('usage: a2a route <team> <message…>')
|
|
200
|
+
const res = await fetch(`${home()}/a2a/direct`, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'Content-Type': 'application/json', ...originClaimFor(home())(), ...keyHeaders() },
|
|
203
|
+
body: JSON.stringify({ team, message, caller_session: identity().handle, wait: false }),
|
|
204
|
+
})
|
|
205
|
+
console.log(JSON.stringify(await res.json(), null, 2))
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
if (cmd === 'inbox') {
|
|
209
|
+
const out = await control({ action: 'inbox', handle: identity().handle })
|
|
210
|
+
if (out.ok !== true) die(String(out.error))
|
|
211
|
+
if (out.mail.length === 0) { console.log('[inbox] empty'); return }
|
|
212
|
+
for (const m of out.mail) console.log(`[${new Date(m.at).toLocaleTimeString()}] ${String(m.from)}: ${String(m.text).slice(0, 200)}`)
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
if (cmd === 'skills') { cmdSkills(); return }
|
|
216
|
+
if (cmd === 'tools') { printAlignment(); return }
|
|
217
|
+
if (cmd === 'whoami') {
|
|
218
|
+
const id = identity()
|
|
219
|
+
const list = await control({ action: 'list' })
|
|
220
|
+
const mine = (list.deposits ?? []).find(d => d.handle === id.handle)
|
|
221
|
+
console.log(`handle ${id.handle}`)
|
|
222
|
+
console.log(`identity ~/.a2a/cli.json (own Ed25519 key — the card's team = "${id.name}" is its own zone)`)
|
|
223
|
+
console.log(`deposit ${mine === undefined ? 'OFFLINE (expired or never published — run: a2a publish)' : `ONLINE, ${((mine.expiresAt - Date.now()) / 3_600_000).toFixed(2)}h left`}`)
|
|
224
|
+
console.log(`card url ${home()}/a2a/cli/${encodeURIComponent(id.handle)}`)
|
|
225
|
+
console.log(`capability route via signed envelope; receive via mailbox (poll: a2a inbox)`)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
if (cmd === 'teams') {
|
|
229
|
+
const s = await state()
|
|
230
|
+
const q = (positionals()[0] ?? '').toLowerCase()
|
|
231
|
+
const rows = [
|
|
232
|
+
...['dsh'].map(t => ({ team: String(t), name: String(s.session ?? 'host'), where: 'host' })),
|
|
233
|
+
...(s.sessions ?? []).map(x => ({ team: String(x.team ?? ''), name: String(x.label ?? ''), where: 'local' })),
|
|
234
|
+
...(s.remote ?? []).map(x => ({ team: String(x.team ?? ''), name: String(x.name ?? ''), where: `remote · ${String(x.via ?? '')}` })),
|
|
235
|
+
]
|
|
236
|
+
for (const r of rows.filter(r => q === '' || r.team.toLowerCase().includes(q) || r.name.toLowerCase().includes(q))) {
|
|
237
|
+
console.log(`${r.team.padEnd(28)} ${r.name.slice(0, 24).padEnd(26)} [${r.where}]`)
|
|
238
|
+
}
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
if (cmd === 'probe') {
|
|
242
|
+
const s = await state()
|
|
243
|
+
for (const p of s.peers ?? []) console.log(`${String(p.url).padEnd(36)} score=${String(p.score).padEnd(9)} link=${String(p.link ?? 'stranger').padEnd(8)} verified=${p.verified === true ? '✓' : '✗'}`)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
if (cmd === 'tasks') {
|
|
247
|
+
const s = await state()
|
|
248
|
+
const pending = s.tasks ?? []
|
|
249
|
+
const dead = s.tasksDead ?? []
|
|
250
|
+
const archived = s.archived ?? []
|
|
251
|
+
console.log(`pending (${String(pending.length)}):`)
|
|
252
|
+
for (const t of pending.slice(0, 20)) console.log(` ${String(t.taskId)} → ${String(t.team)} ${String(t.direction ?? '')}`)
|
|
253
|
+
console.log(`dead (${String(dead.length)}):`)
|
|
254
|
+
for (const t of dead.slice(0, 20)) console.log(` ${String(t.taskId)} → ${String(t.team)} (${String(t.reason ?? '?')})`)
|
|
255
|
+
console.log(`archived (${String(archived.length)}):`)
|
|
256
|
+
for (const t of archived.slice(0, 20)) console.log(` ${String(t.taskId)} ${String(t.outcome ?? '?')}`)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
if (cmd === 'status') {
|
|
260
|
+
const s = await state()
|
|
261
|
+
const list = await control({ action: 'list' })
|
|
262
|
+
const mine = (list.deposits ?? []).find(d => d.handle === identity().handle)
|
|
263
|
+
console.log(`host ${home()} version=${String(s.version ?? '?')} peers=${String((s.peers ?? []).length)}`)
|
|
264
|
+
for (const p of s.peers ?? []) console.log(` peer ${String(p.url).padEnd(34)} link=${String(p.link ?? 'stranger')}`)
|
|
265
|
+
const inflight = s.inFlight ?? []
|
|
266
|
+
if (inflight.length > 0) {
|
|
267
|
+
console.log('in-flight:')
|
|
268
|
+
for (const r of inflight) console.log(` ${String(r.team)} → ${String(r.peer)}`)
|
|
269
|
+
}
|
|
270
|
+
console.log(`deposit ${mine === undefined ? 'OFFLINE' : `ONLINE, ${((mine.expiresAt - Date.now()) / 3_600_000).toFixed(2)}h left`}`)
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
if (cmd === 'report') {
|
|
274
|
+
const text = args.slice(1).filter(a => !String(a).startsWith('--')).join(' ')
|
|
275
|
+
if (text === '') die('usage: a2a report <message…> — one-way report into the host process-team mailbox')
|
|
276
|
+
const res = await fetch(`${home()}/a2a/direct`, {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: { 'Content-Type': 'application/json', ...originClaimFor(home())(), ...keyHeaders() },
|
|
279
|
+
body: JSON.stringify({ team: 'dsh', message: `[a2a-net cli report] ${text}`, caller_session: identity().handle, wait: false }),
|
|
280
|
+
})
|
|
281
|
+
const out = await res.json()
|
|
282
|
+
console.log(JSON.stringify(out, null, 2))
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
if (cmd === 'revoke') {
|
|
286
|
+
const out = await control({ action: 'revoke', handle: identity().handle })
|
|
287
|
+
console.log(`[revoke] ${out.ok === true ? 'deregistered' : 'nothing to revoke'}`)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
console.error(`a2a-net CLI — aligned with the plugin's ten model tools ('a2a tools' prints the full map)
|
|
291
|
+
|
|
292
|
+
usage: a2a <command> [args] [--home URL] [--key <apiKey>]
|
|
293
|
+
|
|
294
|
+
户口制: init <name> [--uuid 16hex] · publish · inbox · revoke
|
|
295
|
+
对齐面: whoami · teams [query] · route <team> <msg> · status · tasks · probe · report <msg>
|
|
296
|
+
CLI 专有: skills [name] [--json] [--root DIR] · tools (this alignment table)`)
|
|
297
|
+
process.exit(cmd === '' ? 1 : 0)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
void main().catch(error => { console.error(String(error)); process.exit(1) })
|
package/lib/card.js
CHANGED
|
@@ -167,6 +167,22 @@ export function verifyCard(candidate, now) {
|
|
|
167
167
|
const peers = referrals(wire);
|
|
168
168
|
const teams = sessionTeams(wire);
|
|
169
169
|
const memberships = teamMemberships(wire);
|
|
170
|
+
// gh#41: the unsigned canvas-org listing (full card only) passes through
|
|
171
|
+
// with the same drop-on-malformed posture as sessionTeams — cards from
|
|
172
|
+
// pre-0.6.26 nodes carry none.
|
|
173
|
+
const canvasOrg = [];
|
|
174
|
+
if (Array.isArray(wire.canvasOrg)) {
|
|
175
|
+
for (const entry of wire.canvasOrg) {
|
|
176
|
+
const org = entry;
|
|
177
|
+
if (typeof org?.team !== 'string' || org.team === '')
|
|
178
|
+
continue;
|
|
179
|
+
canvasOrg.push({
|
|
180
|
+
name: typeof org.name === 'string' ? org.name : '',
|
|
181
|
+
team: org.team,
|
|
182
|
+
members: Array.isArray(org.members) ? org.members.filter((m) => typeof m === 'string') : [],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
170
186
|
return {
|
|
171
187
|
ok: true,
|
|
172
188
|
card: {
|
|
@@ -175,6 +191,7 @@ export function verifyCard(candidate, now) {
|
|
|
175
191
|
...(peers !== undefined ? { peers } : {}),
|
|
176
192
|
...(teams !== undefined && teams.length > 0 ? { sessionTeams: teams } : {}),
|
|
177
193
|
...(memberships !== undefined && memberships.length > 0 ? { teamMemberships: memberships } : {}),
|
|
194
|
+
...(canvasOrg.length > 0 ? { canvasOrg } : {}),
|
|
178
195
|
...(typeof wire.lanIp === 'string' && wire.lanIp !== '' ? { lanIp: wire.lanIp } : {}),
|
|
179
196
|
...(typeof wire.version === 'string' && wire.version !== '' ? { version: wire.version } : {}),
|
|
180
197
|
publicKey: wire.publicKey,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 户口登记处 (gh#45, owner rulings 2026-10-24):
|
|
3
|
+
* ① handle = <agent-name>/<uuid·8B (16 hex)> — the name is custom, the
|
|
4
|
+
* suffix is forced 16-hex by shape gate;
|
|
5
|
+
* ② one key, multiple registrations — v1: the local home host;
|
|
6
|
+
* ③ deposit TTL: default AND max 4h — expiry auto-deregisters (the CLI
|
|
7
|
+
* vanishes from the card face and the directory; re-publish returns).
|
|
8
|
+
*
|
|
9
|
+
* The deposit is the CLI's "household registration": the home host serves
|
|
10
|
+
* the CLI's SIGNED card at /a2a/cli/<handle>/.well-known/agent-card.json
|
|
11
|
+
* (an OriginClaim fetch of the CLI envelope lands exactly there), lists
|
|
12
|
+
* the handle in its own card's sessionTeams (so the whole fleet sees the
|
|
13
|
+
* CLI as a member card — draggable into canvas teams = the operator link),
|
|
14
|
+
* and holds a mailbox for inbound deliveries (receipts land here; the CLI
|
|
15
|
+
* polls while alive).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { verifyCard } from "./card.js";
|
|
20
|
+
/** Owner ruling ③: the default AND maximum online window. */
|
|
21
|
+
export const CLI_DEPOSIT_TTL_MS = 4 * 60 * 60 * 1000;
|
|
22
|
+
/** Owner ruling ①: <agent-name>/<uuid·8B>. The name is [a-z0-9-]{1,32}. */
|
|
23
|
+
export const CLI_HANDLE_RE = /^([a-z0-9-]{1,32})\/([0-9a-f]{16})$/;
|
|
24
|
+
const MAILBOX_CAP = 64;
|
|
25
|
+
export class CliDepositStore {
|
|
26
|
+
path;
|
|
27
|
+
deposits = new Map();
|
|
28
|
+
mailbox = new Map();
|
|
29
|
+
now;
|
|
30
|
+
constructor(path, options) {
|
|
31
|
+
this.path = path;
|
|
32
|
+
this.now = options?.now ?? (() => Date.now());
|
|
33
|
+
this.restore();
|
|
34
|
+
}
|
|
35
|
+
/** Deposit (or refresh) one CLI card. TTL is clamped to the 4h ruling. */
|
|
36
|
+
deposit(cardJson) {
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(cardJson);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { ok: false, error: 'card is not JSON' };
|
|
43
|
+
}
|
|
44
|
+
const verdict = verifyCard(parsed, this.now());
|
|
45
|
+
if (!verdict.ok)
|
|
46
|
+
return { ok: false, error: `card verification failed (${verdict.reason})` };
|
|
47
|
+
if (typeof verdict.card.team !== 'string' || !CLI_HANDLE_RE.test(verdict.card.team)) {
|
|
48
|
+
return { ok: false, error: `card.team must be the CLI handle <agent-name>/<uuid16hex>, got "${String(verdict.card.team)}"` };
|
|
49
|
+
}
|
|
50
|
+
const handle = verdict.card.team;
|
|
51
|
+
const name = CLI_HANDLE_RE.exec(handle)[1];
|
|
52
|
+
if (verdict.card.team !== `${name}/${CLI_HANDLE_RE.exec(handle)[2]}`)
|
|
53
|
+
return { ok: false, error: 'handle mismatch' };
|
|
54
|
+
const ttl = Math.min(CLI_DEPOSIT_TTL_MS, CLI_DEPOSIT_TTL_MS);
|
|
55
|
+
const at = this.now();
|
|
56
|
+
const row = { handle, name, cardJson, depositedAt: at, expiresAt: at + ttl };
|
|
57
|
+
this.deposits.set(handle, row);
|
|
58
|
+
this.persist();
|
|
59
|
+
return { ok: true, handle, expiresAt: row.expiresAt };
|
|
60
|
+
}
|
|
61
|
+
revoke(handle) {
|
|
62
|
+
const had = this.deposits.delete(handle);
|
|
63
|
+
if (had)
|
|
64
|
+
this.persist();
|
|
65
|
+
return had;
|
|
66
|
+
}
|
|
67
|
+
/** Active deposits (sweeping expired ones — ruling ③ auto-deregister). */
|
|
68
|
+
list() {
|
|
69
|
+
this.sweep();
|
|
70
|
+
return [...this.deposits.values()];
|
|
71
|
+
}
|
|
72
|
+
get(handle) {
|
|
73
|
+
const row = this.deposits.get(handle);
|
|
74
|
+
if (row === undefined)
|
|
75
|
+
return undefined;
|
|
76
|
+
if (row.expiresAt <= this.now()) {
|
|
77
|
+
this.deposits.delete(handle);
|
|
78
|
+
this.persist();
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return row;
|
|
82
|
+
}
|
|
83
|
+
/** Card-serve rows: the handle + display name, marked cli (no wake). */
|
|
84
|
+
cardRows() {
|
|
85
|
+
return this.list().map(row => ({ team: row.handle, name: row.name, description: 'cli — deposited identity (no wake; mailbox)' }));
|
|
86
|
+
}
|
|
87
|
+
mail(handle, from, text) {
|
|
88
|
+
const list = this.mailbox.get(handle) ?? [];
|
|
89
|
+
list.unshift({ at: this.now(), from, text: text.slice(0, 4000) });
|
|
90
|
+
this.mailbox.set(handle, list.slice(0, MAILBOX_CAP));
|
|
91
|
+
this.persist();
|
|
92
|
+
}
|
|
93
|
+
readMail(handle) {
|
|
94
|
+
return [...(this.mailbox.get(handle) ?? [])];
|
|
95
|
+
}
|
|
96
|
+
sweep() {
|
|
97
|
+
const now = this.now();
|
|
98
|
+
let changed = false;
|
|
99
|
+
for (const [handle, row] of this.deposits) {
|
|
100
|
+
if (row.expiresAt <= now) {
|
|
101
|
+
this.deposits.delete(handle);
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (changed)
|
|
106
|
+
this.persist();
|
|
107
|
+
}
|
|
108
|
+
persist() {
|
|
109
|
+
const mailbox = {};
|
|
110
|
+
for (const [handle, list] of this.mailbox)
|
|
111
|
+
mailbox[handle] = [...list];
|
|
112
|
+
const snapshot = { deposits: [...this.deposits.values()], mailbox };
|
|
113
|
+
try {
|
|
114
|
+
mkdirSync(join(this.path, '..'), { recursive: true });
|
|
115
|
+
writeFileSync(this.path, JSON.stringify(snapshot), 'utf8');
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// persistence best-effort; the deposit re-publishes self-heal
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
restore() {
|
|
122
|
+
if (this.path === '' || !existsSync(this.path))
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
const snapshot = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
126
|
+
for (const row of snapshot.deposits ?? []) {
|
|
127
|
+
if (typeof row?.handle === 'string' && typeof row.cardJson === 'string' && typeof row.expiresAt === 'number') {
|
|
128
|
+
this.deposits.set(row.handle, row);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const [handle, list] of Object.entries(snapshot.mailbox ?? {})) {
|
|
132
|
+
if (Array.isArray(list))
|
|
133
|
+
this.mailbox.set(handle, list.filter(m => m !== null && typeof m === 'object'));
|
|
134
|
+
}
|
|
135
|
+
this.sweep();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// corrupt file: start empty (re-publish heals)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|