dsh-punky-swarm 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +661 -0
  3. package/README.en.md +281 -0
  4. package/README.md +281 -0
  5. package/cordis.patch.yml +59 -0
  6. package/lib/acps/certs.js +310 -0
  7. package/lib/acps/discovery-client.js +283 -0
  8. package/lib/acps/registry-client.js +527 -0
  9. package/lib/acps/server.js +302 -0
  10. package/lib/aip/agent-descriptor.js +170 -0
  11. package/lib/aip/identity.js +469 -0
  12. package/lib/aip/tool-descriptor.js +93 -0
  13. package/lib/api.js +266 -0
  14. package/lib/artifact-types.js +48 -0
  15. package/lib/assembly/audit-blind-review.js +170 -0
  16. package/lib/assembly/schema.js +253 -0
  17. package/lib/assembly.js +41 -0
  18. package/lib/assets.js +103 -0
  19. package/lib/bridge/trajectory.js +202 -0
  20. package/lib/client.js +704 -0
  21. package/lib/comms/acps-bridge.js +219 -0
  22. package/lib/comms/aip-format.js +141 -0
  23. package/lib/comms/budget.js +124 -0
  24. package/lib/comms/mailbox.js +221 -0
  25. package/lib/comms/topic-runtime.js +76 -0
  26. package/lib/comms/topic.js +115 -0
  27. package/lib/discovery/filter.js +199 -0
  28. package/lib/discovery/schema.js +157 -0
  29. package/lib/discovery/service.js +251 -0
  30. package/lib/hot/config-watch.js +252 -0
  31. package/lib/index.js +433 -0
  32. package/lib/lock.js +69 -0
  33. package/lib/panel/batch-detail.js +163 -0
  34. package/lib/panel/batch-list.js +84 -0
  35. package/lib/panel/locales.js +93 -0
  36. package/lib/panel/main.js +270 -0
  37. package/lib/panel/stream.js +348 -0
  38. package/lib/panel/theme.js +103 -0
  39. package/lib/panel/widgets.js +63 -0
  40. package/lib/schema.d.ts +192 -0
  41. package/lib/schema.js +259 -0
  42. package/lib/schema.ts +398 -0
  43. package/lib/state/archive.js +174 -0
  44. package/lib/state/command-exec.js +148 -0
  45. package/lib/state/constants.js +37 -0
  46. package/lib/state/corrupt-registry.js +103 -0
  47. package/lib/state/event-types.js +65 -0
  48. package/lib/state/gates.d.ts +226 -0
  49. package/lib/state/gates.js +428 -0
  50. package/lib/state/gates.ts +348 -0
  51. package/lib/state/machine-rules.d.ts +14 -0
  52. package/lib/state/machine-rules.js +76 -0
  53. package/lib/state/machine-rules.ts +88 -0
  54. package/lib/state/machine.js +68 -0
  55. package/lib/state/resume.js +201 -0
  56. package/lib/state/schema-v3.d.ts +10 -0
  57. package/lib/state/schema-v3.js +57 -0
  58. package/lib/state/schema-v3.ts +80 -0
  59. package/lib/state/store.js +607 -0
  60. package/lib/state/task-utils.js +40 -0
  61. package/lib/tools/core.js +323 -0
  62. package/lib/tools/git-utils.js +44 -0
  63. package/lib/tools/lane-tools.js +455 -0
  64. package/lib/tools/log-tools.js +173 -0
  65. package/lib/tools/mailbox-tools.js +121 -0
  66. package/lib/tools/merge-agent.js +127 -0
  67. package/lib/tools/register.js +85 -0
  68. package/lib/tools/shared.js +32 -0
  69. package/lib/types/contracts.d.ts +277 -0
  70. package/lib/types/contracts.js +17 -0
  71. package/lib/types/contracts.ts +279 -0
  72. package/lib/verify/evidence.js +249 -0
  73. package/lib/verify/gate.js +161 -0
  74. package/lib/verify/mount.js +48 -0
  75. package/lib/verify/selector.js +101 -0
  76. package/lib/watch/lane-heartbeat.js +333 -0
  77. package/lib/wave-plan.d.ts +48 -0
  78. package/lib/wave-plan.js +473 -0
  79. package/lib/wave-plan.ts +465 -0
  80. package/package.json +71 -0
  81. package/presets/jiufeng/NOTICE +5 -0
  82. package/presets/jiufeng/agent.cordis.yml +275 -0
  83. package/presets/jiufeng/preset.yml +3 -0
  84. package/skills/jiufeng-team/SKILL.md +214 -0
  85. package/skills/jiufeng-team/references/constitution.md +62 -0
  86. package/skills/jiufeng-team/references/roles/coder.md +17 -0
  87. package/skills/jiufeng-team/references/roles/coordinator.md +13 -0
  88. package/skills/jiufeng-team/references/roles/designer.md +14 -0
  89. package/skills/jiufeng-team/references/roles/doc-manager.md +13 -0
  90. package/skills/jiufeng-team/references/roles/manager.md +22 -0
  91. package/skills/jiufeng-team/references/roles/reviewer.md +19 -0
  92. package/skills/jiufeng-team/references/roles/supervisor.md +13 -0
  93. package/skills/jiufeng-team/references/roles/tester.md +13 -0
  94. package/skills/jiufeng-team/references/templates/call-chain-matrix-template.md +121 -0
  95. package/skills/jiufeng-team/references/templates/endpoint-behavior-template.md +60 -0
  96. package/skills/jiufeng-team/references/templates/gap-list.json +33 -0
  97. package/skills/jiufeng-team/references/templates/leader-decision-pack.md +92 -0
  98. package/skills/jiufeng-team/references/templates/plan-template.md +61 -0
  99. package/skills/jiufeng-team/references/templates/success-pattern-seeds.md +126 -0
  100. package/skills/jiufeng-team/references/workflow.md +98 -0
  101. package/skills/jiufeng-team/scripts/check-v3-density.mjs +98 -0
@@ -0,0 +1,527 @@
1
+ /*
2
+ Copyright (C) 2025-2026 Punky
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ // registry-client:半自动注册客户端(registry-server 对接)
19
+ // EAB macKey 本地加密存证(AES-256-GCM)。审计:注册需用户凭据(EAB 要求用户拥有 active AIC),
20
+ // 人工审核不得自动化跳过。
21
+ // 约束:零新 node_modules 依赖——HTTP 用 node:http/https、加密用 node:crypto。
22
+ // 默认关:acps.registry.enabled=false;装配仅建客户端实例,不自动发起注册
23
+ // (注册需用户凭据、人工审核不自动化跳过——半自动注册,动作由用户显式触发)。
24
+ import http from 'node:http';
25
+ import https from 'node:https';
26
+ import crypto from 'node:crypto';
27
+ import { URL } from 'node:url';
28
+
29
+ // ── 配置键默认值(装配契约:acps.registry,默认关)──
30
+ // url:registry public 面 API 基址(默认 http://localhost:9001/api/v1;
31
+ // 传 {origin} 或 {origin}/api/v1 均可,本模块自动规整 apiBase/atrBase)
32
+ // username/password:registry 用户凭据(config/env 注入,不硬编码、不落仓库)
33
+ // eabKey:EAB macKey 本地加密密钥(hex 32B / urlsafe-base64 43-44 字符 / 任意字符串经 sha256 派生 32B;
34
+ // 缺省 null = 禁用加密存证能力,requestEab 仅返回明文凭据由调用方自存,不落盘)
35
+ // token:可选预置 Bearer token(跳过 login;与 username/password 二选一)
36
+ // timeoutMs / rejectUnauthorized:HTTP 层参数(默认 10s / 校验 TLS 证书)
37
+ export const REGISTRY_DEFAULTS = Object.freeze({
38
+ enabled: false,
39
+ url: null,
40
+ username: null,
41
+ password: null,
42
+ token: null,
43
+ eabKey: null,
44
+ timeoutMs: 10000,
45
+ rejectUnauthorized: true,
46
+ });
47
+
48
+ export const API_BASE_PATH = '/api/v1'; // 对齐参考实现 settings.api_v1_str 默认(config.py api_v1_str)
49
+ export const ATR_BASE_PATH = '/acps-atr-v2'; // 对齐参考实现 settings.atr_base_path 默认(config.py:277-278)
50
+
51
+ // 注册请求结构(对齐参考实现 AgentCreate schema:registry-server/app/agent/schema.py:14-30)
52
+ // AgentCreate = { name, version, description?, logo_url?, acs?, is_ontology? }
53
+ // 用户侧 upsert 载荷构造见 commands.py:382-391(name/version 取自 ACS 必填文本字段,
54
+ // description 可选、is_ontology 布尔、acs 原样嵌入)。
55
+ export function buildRegistrationPayload(acs, opts = {}) {
56
+ if (!acs || typeof acs !== 'object') {
57
+ throw new TypeError('buildRegistrationPayload: acs must be an object');
58
+ }
59
+ const name = typeof acs.name === 'string' && acs.name.trim() ? acs.name.trim() : null;
60
+ const version = typeof acs.version === 'string' && acs.version.trim() ? acs.version.trim() : null;
61
+ if (!name || !version) {
62
+ throw new Error('ACS JSON must contain non-empty string fields: name, version');
63
+ }
64
+ const payload = {
65
+ name,
66
+ version,
67
+ acs,
68
+ is_ontology: opts.is_ontology === true,
69
+ };
70
+ if (typeof opts.description === 'string' && opts.description.trim()) {
71
+ payload.description = opts.description.trim();
72
+ } else if (typeof acs.description === 'string' && acs.description.trim()) {
73
+ payload.description = acs.description.trim();
74
+ }
75
+ if (typeof opts.logo_url === 'string' && opts.logo_url.trim()) {
76
+ payload.logo_url = opts.logo_url.trim();
77
+ }
78
+ return payload;
79
+ }
80
+
81
+ // 错误类:对齐参考实现 RegistryClientError 语义(registry/client.py:116-142、170-183)——
82
+ // 携带 status_code 与 payload;消息优先取 payload.error.message / detail / title / message。
83
+ export class RegistryClientError extends Error {
84
+ constructor(message, { status = null, payload = null, errorName = null } = {}) {
85
+ super(message);
86
+ this.name = 'RegistryClientError';
87
+ this.status = status;
88
+ this.payload = payload;
89
+ this.errorName = errorName;
90
+ }
91
+ }
92
+
93
+ function extractErrorMessage(payload, defaultMessage) {
94
+ if (payload && typeof payload === 'object') {
95
+ const error = payload.error;
96
+ if (error && typeof error === 'object' && typeof error.message === 'string' && error.message.trim()) {
97
+ return error.message.trim();
98
+ }
99
+ for (const field of ['detail', 'title', 'message']) {
100
+ const v = payload[field];
101
+ if (typeof v === 'string' && v.trim()) return v.trim();
102
+ }
103
+ }
104
+ return defaultMessage;
105
+ }
106
+
107
+ function parseUrl(base) {
108
+ if (typeof base !== 'string' || !base.trim()) return null;
109
+ try {
110
+ return new URL(base);
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ // atrBase 推导(对齐 acps-cli config.py:29-35 _infer_default_atr_base_url):
117
+ // apiBase 形如 {origin}/api/v1 或 {origin} → 统一 {origin}/acps-atr-v2
118
+ export function deriveAtrBaseUrl(apiBaseUrl) {
119
+ const parsed = parseUrl(apiBaseUrl);
120
+ if (!parsed) return null;
121
+ let path = parsed.pathname.replace(/\/+$/, '');
122
+ if (path.endsWith('/api/v1')) path = path.slice(0, -'/api/v1'.length);
123
+ else if (path.endsWith('/api')) path = path.slice(0, -'/api'.length);
124
+ parsed.pathname = `${path}/acps-atr-v2`;
125
+ return parsed.toString().replace(/\/+$/, '');
126
+ }
127
+
128
+ // 配置解析:acps.registry 键(默认关)
129
+ // enabled=true 且 url 可解析 → 返回启用配置;否则 enabled=false(装配短路,零路径)。
130
+ // enabled=true 但缺 url → 返回 enabled=false 并携带 reason(供日志告警,不抛错不炸宿主)。
131
+ export function resolveRegistryConfig(config) {
132
+ const c = config?.acps?.registry ?? {};
133
+ const resolved = {
134
+ ...REGISTRY_DEFAULTS,
135
+ ...(c && typeof c === 'object' ? c : {}),
136
+ };
137
+ const url = typeof resolved.url === 'string' && resolved.url.trim() ? resolved.url.trim() : null;
138
+ resolved.url = url;
139
+ resolved.username = typeof resolved.username === 'string' && resolved.username ? resolved.username : null;
140
+ resolved.password = typeof resolved.password === 'string' && resolved.password ? resolved.password : null;
141
+ resolved.token = typeof resolved.token === 'string' && resolved.token ? resolved.token : null;
142
+ resolved.eabKey = typeof resolved.eabKey === 'string' && resolved.eabKey ? resolved.eabKey : null;
143
+ resolved.timeoutMs = Number.isFinite(Number(resolved.timeoutMs)) && Number(resolved.timeoutMs) > 0
144
+ ? Math.floor(Number(resolved.timeoutMs))
145
+ : REGISTRY_DEFAULTS.timeoutMs;
146
+
147
+ if (resolved.enabled !== true) {
148
+ return { ...resolved, enabled: false, apiBaseUrl: null, atrBaseUrl: null };
149
+ }
150
+ if (!url) {
151
+ return { ...resolved, enabled: false, apiBaseUrl: null, atrBaseUrl: null, reason: 'acps.registry.enabled=true but registry.url is missing' };
152
+ }
153
+ const apiBaseUrl = url.endsWith('/') ? url.slice(0, -1) : url;
154
+ return {
155
+ ...resolved,
156
+ enabled: true,
157
+ apiBaseUrl,
158
+ atrBaseUrl: deriveAtrBaseUrl(apiBaseUrl) ?? apiBaseUrl,
159
+ };
160
+ }
161
+
162
+ // ── EAB macKey 本地加密存证(Node 侧 AES-256-GCM)──
163
+ // 参考实现服务端用 SM4-CBC 加密 mac_key 后入库(registry-server/app/core/crypto.py:46-53 sm4_encrypt、
164
+ // app/eab/service.py:63 sm4_encrypt(mac_key, settings.sm4_encryption_key));node:crypto 无 SM4 原语,
165
+ // AES-256-GCM(node:crypto 内建,零新依赖)。差异标注:
166
+ // - 算法不同:SM4-CBC(对称分组 128bit,PKCS#7)↔ AES-256-GCM(AEAD,密文+认证标签);
167
+ // - 存储形态不同:参考实现 iv+密文 urlsafe-base64 拼接(crypto.py:53),本实现 iv+tag+密文 base64
168
+ // 信封对象;
169
+ // - 不跨节点互操作:SM4 仅 registry 服务端内部存储(消费端 acme.py 收的是明文 macKey),
170
+ // 本加密仅插件本地存证用途,不参与任何跨节点协议(README 标注见 exec/registry.md)。
171
+ // 安全语义:AES-256-GCM 随机 12B IV + 16B auth tag;keyId/macKey 均不落明文(整体加密进 ct)。
172
+
173
+ function normalizeEabKey(keyInput) {
174
+ if (typeof keyInput !== 'string' || !keyInput) {
175
+ throw new RegistryClientError('EAB encryption key is required (acps.registry.eabKey)', { errorName: 'EAB_KEY_MISSING' });
176
+ }
177
+ const trimmed = keyInput.trim();
178
+ // hex 32B(64 hex chars)
179
+ if (/^[0-9a-fA-F]{64}$/.test(trimmed)) return Buffer.from(trimmed, 'hex');
180
+ // urlsafe-base64 32B(43-44 chars,无 padding 或带 padding)
181
+ if (/^[A-Za-z0-9_-]{43}=?$/.test(trimmed)) {
182
+ try {
183
+ return Buffer.from(trimmed.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
184
+ } catch {
185
+ // fallthrough 到 sha256 派生
186
+ }
187
+ }
188
+ // 其余字符串:sha256 派生 32B(确定性,非密码学 KDF——密钥本身来自 config/env,非用户口令)
189
+ return crypto.createHash('sha256').update(trimmed, 'utf8').digest();
190
+ }
191
+
192
+ function b64url(buf) {
193
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
194
+ }
195
+
196
+ function unb64url(str) {
197
+ return Buffer.from(str.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
198
+ }
199
+
200
+ // 加密 EAB 凭据(keyId/macKey 不落明文)→ 信封对象 { v, alg, aic, expiresAt, iv, tag, ct }
201
+ export function encryptEabCredential(credential, keyInput) {
202
+ if (!credential || typeof credential !== 'object') {
203
+ throw new RegistryClientError('encryptEabCredential: credential must be an object', { errorName: 'EAB_CREDENTIAL_INVALID' });
204
+ }
205
+ const key = normalizeEabKey(keyInput);
206
+ const keyId = credential.keyId ?? credential.key_id ?? null;
207
+ const macKey = credential.macKey ?? credential.mac_key ?? null;
208
+ if (!keyId || !macKey) {
209
+ throw new RegistryClientError('EAB credential must contain keyId and macKey', { errorName: 'EAB_CREDENTIAL_INVALID' });
210
+ }
211
+ const secret = JSON.stringify({ keyId, macKey });
212
+ const iv = crypto.randomBytes(12);
213
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
214
+ const ct = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);
215
+ const tag = cipher.getAuthTag();
216
+ return {
217
+ v: 1,
218
+ alg: 'AES-256-GCM',
219
+ aic: typeof credential.aic === 'string' ? credential.aic : null,
220
+ expiresAt: typeof credential.expiresAt === 'string' ? credential.expiresAt : null,
221
+ iv: b64url(iv),
222
+ tag: b64url(tag),
223
+ ct: b64url(ct),
224
+ };
225
+ }
226
+
227
+ // 解密 EAB 凭据信封 → { keyId, macKey, aic, expiresAt }
228
+ export function decryptEabCredential(envelope, keyInput) {
229
+ if (!envelope || typeof envelope !== 'object' || envelope.alg !== 'AES-256-GCM') {
230
+ throw new RegistryClientError('Invalid EAB credential envelope', { errorName: 'EAB_ENVELOPE_INVALID' });
231
+ }
232
+ const key = normalizeEabKey(keyInput);
233
+ const iv = unb64url(envelope.iv);
234
+ const tag = unb64url(envelope.tag);
235
+ const ct = unb64url(envelope.ct);
236
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
237
+ decipher.setAuthTag(tag);
238
+ let plaintext;
239
+ try {
240
+ plaintext = Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
241
+ } catch (err) {
242
+ throw new RegistryClientError('EAB credential decryption failed (wrong key or corrupted envelope)', {
243
+ errorName: 'EAB_DECRYPT_FAILED',
244
+ });
245
+ }
246
+ let parsed;
247
+ try {
248
+ parsed = JSON.parse(plaintext);
249
+ } catch {
250
+ throw new RegistryClientError('EAB credential payload is corrupted', { errorName: 'EAB_DECRYPT_FAILED' });
251
+ }
252
+ return {
253
+ keyId: parsed.keyId ?? null,
254
+ macKey: parsed.macKey ?? null,
255
+ aic: typeof envelope.aic === 'string' ? envelope.aic : null,
256
+ expiresAt: typeof envelope.expiresAt === 'string' ? envelope.expiresAt : null,
257
+ };
258
+ }
259
+
260
+ // ── HTTP 层(node:http/https 内建,零新依赖)──
261
+ // 对齐参考实现 RegistryApiClient._request 语义(client.py:63-142):
262
+ // - 非 2xx → RegistryClientError(status + payload)
263
+ // - 204/空响应 → {}
264
+ // - JSON 解析失败 → RegistryClientError
265
+ function httpRequest(method, url, { headers = {}, body = null, timeoutMs = 10000, rejectUnauthorized = true } = {}) {
266
+ return new Promise((resolve, reject) => {
267
+ let parsed;
268
+ try {
269
+ parsed = new URL(url);
270
+ } catch {
271
+ reject(new RegistryClientError(`Invalid URL: ${url}`));
272
+ return;
273
+ }
274
+ const isHttps = parsed.protocol === 'https:';
275
+ if (!isHttps && parsed.protocol !== 'http:') {
276
+ reject(new RegistryClientError(`Unsupported protocol: ${parsed.protocol}`));
277
+ return;
278
+ }
279
+ const mod = isHttps ? https : http;
280
+ const reqOptions = {
281
+ method,
282
+ hostname: parsed.hostname,
283
+ port: parsed.port || (isHttps ? 443 : 80),
284
+ path: `${parsed.pathname}${parsed.search}`,
285
+ headers: { Accept: 'application/json', ...headers },
286
+ timeout: timeoutMs,
287
+ };
288
+ if (isHttps) {
289
+ reqOptions.rejectUnauthorized = rejectUnauthorized;
290
+ }
291
+ const req = mod.request(reqOptions, (res) => {
292
+ const chunks = [];
293
+ res.on('data', (c) => chunks.push(c));
294
+ res.on('end', () => {
295
+ const raw = Buffer.concat(chunks).toString('utf8');
296
+ let payload = null;
297
+ if (raw.length > 0) {
298
+ try {
299
+ payload = JSON.parse(raw);
300
+ } catch {
301
+ payload = raw;
302
+ }
303
+ }
304
+ const status = res.statusCode ?? 0;
305
+ if (status >= 400) {
306
+ const message = extractErrorMessage(payload, `API request failed: ${method} ${parsed.pathname}`);
307
+ reject(new RegistryClientError(message, { status, payload, errorName: (payload?.error?.code) ?? null }));
308
+ return;
309
+ }
310
+ if (status === 204 || raw.length === 0) {
311
+ resolve({ status, data: {} });
312
+ return;
313
+ }
314
+ if (typeof payload === 'string') {
315
+ reject(new RegistryClientError('Invalid JSON response from server', { status, payload }));
316
+ return;
317
+ }
318
+ resolve({ status, data: payload });
319
+ });
320
+ });
321
+ req.on('timeout', () => {
322
+ req.destroy(new RegistryClientError(`Request timeout after ${timeoutMs}ms: ${method} ${parsed.pathname}`, { errorName: 'REQUEST_TIMEOUT' }));
323
+ });
324
+ req.on('error', (err) => {
325
+ if (err instanceof RegistryClientError) reject(err);
326
+ else reject(new RegistryClientError(`Request failed: ${err.message}`, { errorName: 'REQUEST_FAILED' }));
327
+ });
328
+ if (body !== null) {
329
+ req.write(body);
330
+ }
331
+ req.end();
332
+ });
333
+ }
334
+
335
+ // ── RegistryClient:半自动注册客户端 ──
336
+ // 方法与参考实现 acps-cli RegistryApiClient / commands 编排对齐(见文件头映射)。
337
+ // 半自动语义(V3):login/upsert/submit/requestEab 均为显式动作;本体注册需人工审核
338
+ // (submit 后由 registry staff 审批,服务端在审批通过时分配权威 AIC——service_command.py:368-369),
339
+ // 本客户端不模拟、不绕过、不自动重试审核;checkAgent 供轮询审批状态。
340
+ export class RegistryClient {
341
+ constructor(config) {
342
+ // 支持两种输入形态:原始插件 config({ acps: { registry: {...} } })或
343
+ // resolveRegistryConfig 已解析配置(含 enabled/apiBaseUrl/atrBaseUrl 平铺键)
344
+ const resolved = (config && config.acps && config.acps.registry) ? resolveRegistryConfig(config) : config;
345
+ if (!resolved || resolved.enabled !== true) {
346
+ throw new RegistryClientError('acps.registry is disabled (acps.registry.enabled=true and registry.url required)', {
347
+ errorName: 'REGISTRY_DISABLED',
348
+ });
349
+ }
350
+ this.config = resolved;
351
+ this.apiBaseUrl = resolved.apiBaseUrl;
352
+ this.atrBaseUrl = resolved.atrBaseUrl;
353
+ this._token = resolved.token ?? null;
354
+ this.timeoutMs = resolved.timeoutMs;
355
+ this.rejectUnauthorized = resolved.rejectUnauthorized !== false;
356
+ }
357
+
358
+ get hasToken() {
359
+ return Boolean(this._token);
360
+ }
361
+
362
+ _authHeaders() {
363
+ if (!this._token) {
364
+ throw new RegistryClientError('No access token found, run login() first', { errorName: 'NO_TOKEN' });
365
+ }
366
+ return { Authorization: `Bearer ${this._token}` };
367
+ }
368
+
369
+ async _request(method, path, { auth = false, body = null, json = null, form = null, baseUrl = null } = {}) {
370
+ const targetBase = baseUrl ?? this.apiBaseUrl;
371
+ const url = `${targetBase}${path}`;
372
+ const headers = {};
373
+ if (auth) {
374
+ if (!this._token) {
375
+ throw new RegistryClientError('No access token found, run login() first', { errorName: 'NO_TOKEN' });
376
+ }
377
+ headers.Authorization = `Bearer ${this._token}`;
378
+ }
379
+ let payload = null;
380
+ if (json !== null) {
381
+ headers['Content-Type'] = 'application/json';
382
+ payload = JSON.stringify(json);
383
+ } else if (form !== null) {
384
+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
385
+ payload = new URLSearchParams(form).toString();
386
+ } else if (body !== null) {
387
+ payload = body;
388
+ }
389
+ const { data } = await httpRequest(method, url, {
390
+ headers,
391
+ body: payload,
392
+ timeoutMs: this.timeoutMs,
393
+ rejectUnauthorized: this.rejectUnauthorized,
394
+ });
395
+ return data;
396
+ }
397
+
398
+ // 登录(对齐 client.py:185-195:POST /auth/login,form username/password → {access_token,...})
399
+ async login() {
400
+ const { username, password } = this.config;
401
+ if (!username || !password) {
402
+ throw new RegistryClientError('login requires acps.registry.username/password (config/env injection)', {
403
+ errorName: 'CREDENTIALS_MISSING',
404
+ });
405
+ }
406
+ const result = await this._request('POST', '/auth/login', { form: { username, password } });
407
+ if (!result || typeof result !== 'object' || typeof result.access_token !== 'string') {
408
+ throw new RegistryClientError('Login response missing access_token', { payload: result });
409
+ }
410
+ this._token = result.access_token;
411
+ return result;
412
+ }
413
+
414
+ // 当前用户 Agent 列表(对齐 client.py:275-300 list_my_agents:GET /agent/client + 查询参数)
415
+ async listMyAgents({ pageNum = 1, pageSize = 100, statuses = [], name = null, version = null, aic = null, isDeleted = false } = {}) {
416
+ const params = new URLSearchParams({ page_num: String(pageNum), page_size: String(pageSize) });
417
+ if (Array.isArray(statuses) && statuses.length) {
418
+ // 对齐参考实现 list_my_agents(client.py:286-288):statuses 列表 → 重复 query 键(FastAPI list 参数)
419
+ for (const s of statuses) params.append('statuses', String(s));
420
+ }
421
+ if (name) params.append('name', name);
422
+ if (version) params.append('version', version);
423
+ if (aic) params.append('aic', aic);
424
+ if (isDeleted !== null) params.append('is_deleted', String(isDeleted).toLowerCase());
425
+ const result = await this._request('GET', `/agent/client?${params.toString()}`, { auth: true });
426
+ if (!result || typeof result !== 'object' || !Array.isArray(result.items)) {
427
+ throw new RegistryClientError('Invalid response for list_my_agents', { payload: result });
428
+ }
429
+ return result;
430
+ }
431
+
432
+ // 按 AIC / name+version 定位本人 Agent(对齐 commands.py:133-142 _resolve_agent_from_acs)
433
+ async findMyAgent({ aic = null, name = null, version = null } = {}) {
434
+ if (aic) {
435
+ const byAic = await this.listMyAgents({ aic: aic.trim().toUpperCase() });
436
+ if (byAic.items.length) return byAic.items[0];
437
+ }
438
+ if (name && version) {
439
+ const byNv = await this.listMyAgents({ name, version });
440
+ for (const item of byNv.items) {
441
+ if (item.name === name && item.version === version) return item;
442
+ }
443
+ }
444
+ return null;
445
+ }
446
+
447
+ // 创建/更新 Agent(对齐 commands.py upsert_agent 361-414 + schema.py AgentCreate 14-30):
448
+ // 按 name+version 查已有 → 有则 PUT 更新,无则 POST 创建;返回 { action:'created'|'updated', agent }
449
+ async upsertAgent(acs, opts = {}) {
450
+ const payload = buildRegistrationPayload(acs, opts);
451
+ const existing = await this.findMyAgent({ name: payload.name, version: payload.version });
452
+ if (existing && existing.id) {
453
+ const agent = await this._request('PUT', `/agent/client/${existing.id}`, { auth: true, json: payload });
454
+ return { action: 'updated', agent };
455
+ }
456
+ const agent = await this._request('POST', '/agent/client', { auth: true, json: payload });
457
+ return { action: 'created', agent };
458
+ }
459
+
460
+ // 提交人工审核(对齐 client.py:314-318 + commands.py submit 417-438;V3:审核不自动化跳过)
461
+ async submitAgent(agentId) {
462
+ const agent = await this._request('POST', `/agent/client/${agentId}/submit`, { auth: true });
463
+ return agent;
464
+ }
465
+
466
+ // 注册状态查询(对齐 commands.py check 507-541 _derive_agent_status 157-165):
467
+ // 返回 { status, agent? },status ∈ draft/pending/approved/rejected/unknown/missing
468
+ async checkAgent(acs) {
469
+ const payload = buildRegistrationPayload(acs, {});
470
+ const agent = await this.findMyAgent({ aic: typeof acs.aic === 'string' ? acs.aic : null, name: payload.name, version: payload.version });
471
+ if (!agent) return { status: 'missing', agent: null };
472
+ const approvalStatus = String(agent.approval_status ?? '').toUpperCase();
473
+ let status;
474
+ if (approvalStatus === 'APPROVED' && agent.aic) status = 'approved';
475
+ else if (approvalStatus === 'PENDING') status = 'pending';
476
+ else if (approvalStatus === 'DRAFT') status = 'draft';
477
+ else if (approvalStatus === 'REJECTED') status = 'rejected';
478
+ else status = approvalStatus.toLowerCase() || 'unknown';
479
+ return { status, agent };
480
+ }
481
+
482
+ // 获取 EAB 凭据(对齐 eab/api.py:43-59 + client.py:438-447:POST {atrBase}/eab/{aic},JWT)
483
+ // 返回 { keyId, macKey, aic, expiresAt };V3 语义:仅限本人 active AIC(service.py:51-55),
484
+ // 403 = 非本人/非 active。
485
+ async requestEab(aic) {
486
+ if (!aic || typeof aic !== 'string' || !aic.trim()) {
487
+ throw new RegistryClientError('requestEab requires aic', { errorName: 'AIC_REQUIRED' });
488
+ }
489
+ const result = await this._request('POST', `/eab/${encodeURIComponent(aic.trim().toUpperCase())}`, {
490
+ auth: true,
491
+ baseUrl: this.atrBaseUrl,
492
+ });
493
+ if (!result || typeof result !== 'object' || typeof result.keyId !== 'string' || typeof result.macKey !== 'string') {
494
+ throw new RegistryClientError('Registry server returned an invalid EAB payload', { payload: result });
495
+ }
496
+ return result;
497
+ }
498
+
499
+ // EAB 凭据 → AES-GCM 加密信封(keyId/macKey 不落明文)
500
+ encryptEab(credential) {
501
+ if (!this.config.eabKey) {
502
+ throw new RegistryClientError('acps.registry.eabKey is not configured; cannot encrypt EAB credential', {
503
+ errorName: 'EAB_KEY_MISSING',
504
+ });
505
+ }
506
+ return encryptEabCredential(credential, this.config.eabKey);
507
+ }
508
+
509
+ // 公开 ACS 查询(对齐 api_atr.py:118-188 get_agent_acs_by_aic:GET {atrBase}/acs/{aic},
510
+ // 非 active → 403;用于 V3 存证验证)
511
+ async queryAcs(aic) {
512
+ if (!aic || typeof aic !== 'string' || !aic.trim()) {
513
+ throw new RegistryClientError('queryAcs requires aic', { errorName: 'AIC_REQUIRED' });
514
+ }
515
+ const result = await this._request('GET', `/acs/${encodeURIComponent(aic.trim().toUpperCase())}`, {
516
+ baseUrl: this.atrBaseUrl,
517
+ });
518
+ return result;
519
+ }
520
+ }
521
+
522
+ // 工厂:配置 → RegistryClient(装配用;enabled=false 返回 null——短路零路径)
523
+ export function createRegistryClient(config) {
524
+ const resolved = resolveRegistryConfig(config);
525
+ if (!resolved.enabled) return null;
526
+ return new RegistryClient(resolved);
527
+ }