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.
- package/CHANGELOG.md +120 -0
- package/LICENSE +661 -0
- package/README.en.md +281 -0
- package/README.md +281 -0
- package/cordis.patch.yml +59 -0
- package/lib/acps/certs.js +310 -0
- package/lib/acps/discovery-client.js +283 -0
- package/lib/acps/registry-client.js +527 -0
- package/lib/acps/server.js +302 -0
- package/lib/aip/agent-descriptor.js +170 -0
- package/lib/aip/identity.js +469 -0
- package/lib/aip/tool-descriptor.js +93 -0
- package/lib/api.js +266 -0
- package/lib/artifact-types.js +48 -0
- package/lib/assembly/audit-blind-review.js +170 -0
- package/lib/assembly/schema.js +253 -0
- package/lib/assembly.js +41 -0
- package/lib/assets.js +103 -0
- package/lib/bridge/trajectory.js +202 -0
- package/lib/client.js +704 -0
- package/lib/comms/acps-bridge.js +219 -0
- package/lib/comms/aip-format.js +141 -0
- package/lib/comms/budget.js +124 -0
- package/lib/comms/mailbox.js +221 -0
- package/lib/comms/topic-runtime.js +76 -0
- package/lib/comms/topic.js +115 -0
- package/lib/discovery/filter.js +199 -0
- package/lib/discovery/schema.js +157 -0
- package/lib/discovery/service.js +251 -0
- package/lib/hot/config-watch.js +252 -0
- package/lib/index.js +433 -0
- package/lib/lock.js +69 -0
- package/lib/panel/batch-detail.js +163 -0
- package/lib/panel/batch-list.js +84 -0
- package/lib/panel/locales.js +93 -0
- package/lib/panel/main.js +270 -0
- package/lib/panel/stream.js +348 -0
- package/lib/panel/theme.js +103 -0
- package/lib/panel/widgets.js +63 -0
- package/lib/schema.d.ts +192 -0
- package/lib/schema.js +259 -0
- package/lib/schema.ts +398 -0
- package/lib/state/archive.js +174 -0
- package/lib/state/command-exec.js +148 -0
- package/lib/state/constants.js +37 -0
- package/lib/state/corrupt-registry.js +103 -0
- package/lib/state/event-types.js +65 -0
- package/lib/state/gates.d.ts +226 -0
- package/lib/state/gates.js +428 -0
- package/lib/state/gates.ts +348 -0
- package/lib/state/machine-rules.d.ts +14 -0
- package/lib/state/machine-rules.js +76 -0
- package/lib/state/machine-rules.ts +88 -0
- package/lib/state/machine.js +68 -0
- package/lib/state/resume.js +201 -0
- package/lib/state/schema-v3.d.ts +10 -0
- package/lib/state/schema-v3.js +57 -0
- package/lib/state/schema-v3.ts +80 -0
- package/lib/state/store.js +607 -0
- package/lib/state/task-utils.js +40 -0
- package/lib/tools/core.js +323 -0
- package/lib/tools/git-utils.js +44 -0
- package/lib/tools/lane-tools.js +455 -0
- package/lib/tools/log-tools.js +173 -0
- package/lib/tools/mailbox-tools.js +121 -0
- package/lib/tools/merge-agent.js +127 -0
- package/lib/tools/register.js +85 -0
- package/lib/tools/shared.js +32 -0
- package/lib/types/contracts.d.ts +277 -0
- package/lib/types/contracts.js +17 -0
- package/lib/types/contracts.ts +279 -0
- package/lib/verify/evidence.js +249 -0
- package/lib/verify/gate.js +161 -0
- package/lib/verify/mount.js +48 -0
- package/lib/verify/selector.js +101 -0
- package/lib/watch/lane-heartbeat.js +333 -0
- package/lib/wave-plan.d.ts +48 -0
- package/lib/wave-plan.js +473 -0
- package/lib/wave-plan.ts +465 -0
- package/package.json +71 -0
- package/presets/jiufeng/NOTICE +5 -0
- package/presets/jiufeng/agent.cordis.yml +275 -0
- package/presets/jiufeng/preset.yml +3 -0
- package/skills/jiufeng-team/SKILL.md +214 -0
- package/skills/jiufeng-team/references/constitution.md +62 -0
- package/skills/jiufeng-team/references/roles/coder.md +17 -0
- package/skills/jiufeng-team/references/roles/coordinator.md +13 -0
- package/skills/jiufeng-team/references/roles/designer.md +14 -0
- package/skills/jiufeng-team/references/roles/doc-manager.md +13 -0
- package/skills/jiufeng-team/references/roles/manager.md +22 -0
- package/skills/jiufeng-team/references/roles/reviewer.md +19 -0
- package/skills/jiufeng-team/references/roles/supervisor.md +13 -0
- package/skills/jiufeng-team/references/roles/tester.md +13 -0
- package/skills/jiufeng-team/references/templates/call-chain-matrix-template.md +121 -0
- package/skills/jiufeng-team/references/templates/endpoint-behavior-template.md +60 -0
- package/skills/jiufeng-team/references/templates/gap-list.json +33 -0
- package/skills/jiufeng-team/references/templates/leader-decision-pack.md +92 -0
- package/skills/jiufeng-team/references/templates/plan-template.md +61 -0
- package/skills/jiufeng-team/references/templates/success-pattern-seeds.md +126 -0
- package/skills/jiufeng-team/references/workflow.md +98 -0
- package/skills/jiufeng-team/scripts/check-v3-density.mjs +98 -0
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
// 文件 certs:ACPs mTLS 证书材料(lib/acps 域,P1 lane exec-acps-server)
|
|
19
|
+
// 契约:CAI 自签(node:crypto 自签 X.509,零新依赖);文件三路径(cert/key/ca 三路径
|
|
20
|
+
// config 可配,默认 acps 数据目录);TLS 语义对齐参考实现
|
|
21
|
+
// registry-server/app/main_mtls.py:14-30(cert/key/ca 三件套 + CERT_REQUIRED + TLSv1_3)。
|
|
22
|
+
// 说明:lib/aip/identity.js 的 issueCredential 产出 JSON CAI(cn/san/签名载荷),非 X.509 DER/PEM,
|
|
23
|
+
// 不能直接喂 node:https TLS 上下文——本模块用 node:crypto(generateKeyPairSync + createSign)
|
|
24
|
+
// 直接编码 X.509 证书(ASN.1 DER 手工编码,纯内建能力),签名算法 ECDSA P-256
|
|
25
|
+
// (对齐 identity.js DEFAULT_SIGN_ALGORITHM = 'ecdsa-p256')。CAI 语义对齐(identity.js:24-27):
|
|
26
|
+
// CN=AIC、SAN=URI:acps://{AIC}、extendedKeyUsage 分用途(serverAuth/clientAuth)。
|
|
27
|
+
// 私钥文件权限注意:写文件 mode 0o600;数据目录默认 <root>/acps/certs(引擎根内,不落工作区根)。
|
|
28
|
+
|
|
29
|
+
import crypto from 'node:crypto';
|
|
30
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
// ── ASN.1 DER 最小编码器(X.509 证书构造用,纯内建)──
|
|
34
|
+
|
|
35
|
+
function derLen(n) {
|
|
36
|
+
if (n < 0x80) return [n];
|
|
37
|
+
const bytes = [];
|
|
38
|
+
let v = n;
|
|
39
|
+
while (v > 0) { bytes.unshift(v & 0xff); v >>= 8; }
|
|
40
|
+
return [0x80 | bytes.length, ...bytes];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function derWrap(tag, body) {
|
|
44
|
+
return Buffer.from([tag, ...derLen(body.length), ...body]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function derSeq(...parts) {
|
|
48
|
+
return derWrap(0x30, Buffer.concat(parts));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function derSet(...parts) {
|
|
52
|
+
return derWrap(0x31, Buffer.concat(parts));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function derInt(n) {
|
|
56
|
+
let hex = n.toString(16);
|
|
57
|
+
if (hex.length % 2) hex = '0' + hex;
|
|
58
|
+
let bytes = Buffer.from(hex, 'hex');
|
|
59
|
+
if (bytes[0] & 0x80) bytes = Buffer.concat([Buffer.from([0]), bytes]);
|
|
60
|
+
return derWrap(0x02, bytes);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function derOid(oid) {
|
|
64
|
+
const parts = oid.split('.').map(Number);
|
|
65
|
+
const body = [40 * parts[0] + parts[1]];
|
|
66
|
+
for (const p of parts.slice(2)) {
|
|
67
|
+
let v = p;
|
|
68
|
+
const chunk = [v & 0x7f];
|
|
69
|
+
v >>= 7;
|
|
70
|
+
while (v > 0) { chunk.unshift((v & 0x7f) | 0x80); v >>= 7; }
|
|
71
|
+
body.push(...chunk);
|
|
72
|
+
}
|
|
73
|
+
return derWrap(0x06, Buffer.from(body));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function derOctets(buf) {
|
|
77
|
+
return derWrap(0x04, Buffer.from(buf));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function derPrintable(s) {
|
|
81
|
+
return derWrap(0x13, Buffer.from(s, 'ascii'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function derNull() {
|
|
85
|
+
return Buffer.from([0x05, 0x00]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function derBool(v) {
|
|
89
|
+
return Buffer.from([0x01, 0x01, v ? 0xff : 0x00]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function derBitString(bytes) {
|
|
93
|
+
return derWrap(0x03, Buffer.concat([Buffer.from([0]), bytes]));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function derTime(date) {
|
|
97
|
+
if (date.getUTCFullYear() >= 2050) {
|
|
98
|
+
const s = date.getUTCFullYear().toString().padStart(4, '0')
|
|
99
|
+
+ String(date.getUTCMonth() + 1).padStart(2, '0')
|
|
100
|
+
+ String(date.getUTCDate()).padStart(2, '0')
|
|
101
|
+
+ String(date.getUTCHours()).padStart(2, '0')
|
|
102
|
+
+ String(date.getUTCMinutes()).padStart(2, '0')
|
|
103
|
+
+ String(date.getUTCSeconds()).padStart(2, '0') + 'Z';
|
|
104
|
+
return derWrap(0x18, Buffer.from(s, 'ascii'));
|
|
105
|
+
}
|
|
106
|
+
const s = String(date.getUTCFullYear() % 100).padStart(2, '0')
|
|
107
|
+
+ String(date.getUTCMonth() + 1).padStart(2, '0')
|
|
108
|
+
+ String(date.getUTCDate()).padStart(2, '0')
|
|
109
|
+
+ String(date.getUTCHours()).padStart(2, '0')
|
|
110
|
+
+ String(date.getUTCMinutes()).padStart(2, '0')
|
|
111
|
+
+ String(date.getUTCSeconds()).padStart(2, '0') + 'Z';
|
|
112
|
+
return derWrap(0x17, Buffer.from(s, 'ascii'));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Name(RDNSequence):CN 单 RDN(X.509 客户端/服务端识别用;CN=AIC 对齐 CAI 语义)
|
|
116
|
+
function derName(cn) {
|
|
117
|
+
const rdn = derSet(derSeq(derOid('2.5.4.3'), derPrintable(cn)));
|
|
118
|
+
return derSeq(rdn);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// AlgorithmIdentifier:ecdsa-with-SHA256(1.2.840.10045.4.3.2)
|
|
122
|
+
function derEcdsaSha256() {
|
|
123
|
+
return derSeq(derOid('1.2.840.10045.4.3.2'));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// SubjectPublicKeyInfo:ecPublicKey(1.2.840.10045.2.1)+ prime256v1(1.2.840.10045.3.1.7)
|
|
127
|
+
function derSpki(publicKey) {
|
|
128
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
129
|
+
// EC 公钥点(未压缩 0x04||X||Y,65 字节)位于 spki 尾
|
|
130
|
+
const point = spki.subarray(spki.length - 65, spki.length);
|
|
131
|
+
return derSeq(
|
|
132
|
+
derSeq(derOid('1.2.840.10045.2.1'), derOid('1.2.840.10045.3.1.7')),
|
|
133
|
+
derBitString(point),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function derExtension(oid, critical, value) {
|
|
138
|
+
// RFC 5280:Extension.critical 为 BOOLEAN DEFAULT FALSE——false 时省略字段(不编码 NULL)
|
|
139
|
+
return derSeq(derOid(oid), ...(critical ? [derBool(true)] : []), derOctets(value));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function derBasicConstraints(ca) {
|
|
143
|
+
return derSeq(derBool(ca));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function derKeyUsage(ca) {
|
|
147
|
+
// RFC 5280 KeyUsage 位序(BIT STRING 字节:bit0=0x80 … bit5=0x04 keyCertSign, bit6=0x02 cRLSign):
|
|
148
|
+
// CA: keyCertSign(0x04)|cRLSign(0x02) = 0x06;leaf: digitalSignature(0x80)
|
|
149
|
+
return derBitString(Buffer.from([ca ? 0x06 : 0x80]));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function derAltNames(entries) {
|
|
153
|
+
const parts = [];
|
|
154
|
+
for (const e of entries) {
|
|
155
|
+
if (e.type === 'dns') parts.push(derWrap(0x82, Buffer.from(e.value, 'ascii')));
|
|
156
|
+
else if (e.type === 'ip') parts.push(derWrap(0x87, Buffer.from(e.value.split('.').map(Number))));
|
|
157
|
+
else if (e.type === 'uri') parts.push(derWrap(0x86, Buffer.from(e.value, 'utf8')));
|
|
158
|
+
}
|
|
159
|
+
return derSeq(...parts);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function derExtendedKeyUsage(usages) {
|
|
163
|
+
const oids = { serverAuth: '1.3.6.1.5.5.7.3.1', clientAuth: '1.3.6.1.5.5.7.3.2' };
|
|
164
|
+
return derSeq(...usages.map((u) => derOid(oids[u])));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── 密钥对 ──
|
|
168
|
+
|
|
169
|
+
function generateKeyPair() {
|
|
170
|
+
return crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── X.509 证书生成(node:crypto 原生:手动 ASN.1 DER 编码 + ECDSA P-256 签名)──
|
|
174
|
+
// opts: { cn, issuerCn, signerPrivateKey, altNames, days, isCa, usages, serial }
|
|
175
|
+
// issuerCn/signerPrivateKey 缺省 = 自签(issuer=subject,用自身私钥);
|
|
176
|
+
// 提供时 = CA 签发(issuer=CA CN,用 CA 私钥签名)——X.509 标准签发结构。
|
|
177
|
+
function createX509({
|
|
178
|
+
cn, issuerCn, signerPrivateKey, altNames = [], days = 365, isCa = false, usages = ['serverAuth'], serial,
|
|
179
|
+
} = {}) {
|
|
180
|
+
if (typeof cn !== 'string' || cn.length === 0) throw new Error('createX509: cn is required');
|
|
181
|
+
const { publicKey, privateKey } = generateKeyPair();
|
|
182
|
+
const notBefore = new Date(Date.now() - 60_000); // 1 分钟回拨容忍
|
|
183
|
+
const notAfter = new Date(notBefore.getTime() + days * 24 * 60 * 60 * 1000);
|
|
184
|
+
const serialNum = serial ?? crypto.randomBytes(16).readBigUInt64BE(0);
|
|
185
|
+
|
|
186
|
+
const alg = derEcdsaSha256();
|
|
187
|
+
const extensions = [
|
|
188
|
+
derExtension('2.5.29.19', true, derBasicConstraints(isCa)),
|
|
189
|
+
derExtension('2.5.29.15', true, derKeyUsage(isCa)),
|
|
190
|
+
];
|
|
191
|
+
if (altNames.length > 0) extensions.push(derExtension('2.5.29.17', false, derAltNames(altNames)));
|
|
192
|
+
if (!isCa && usages.length > 0) extensions.push(derExtension('2.5.29.37', false, derExtendedKeyUsage(usages)));
|
|
193
|
+
|
|
194
|
+
const tbs = derSeq(
|
|
195
|
+
derWrap(0xa0, derInt(2)), // version [0] EXPLICIT INTEGER v3(RFC 5280 上下文标签)
|
|
196
|
+
derInt(serialNum), // serialNumber
|
|
197
|
+
alg, // signature algorithm
|
|
198
|
+
derName(issuerCn ?? cn), // issuer
|
|
199
|
+
derSeq(derTime(notBefore), derTime(notAfter)), // validity
|
|
200
|
+
derName(cn), // subject
|
|
201
|
+
derSpki(publicKey), // subjectPublicKeyInfo
|
|
202
|
+
derWrap(0xa3, derSeq(...extensions)), // extensions [3]
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const signKey = signerPrivateKey ?? privateKey;
|
|
206
|
+
const signer = crypto.createSign('sha256');
|
|
207
|
+
signer.update(tbs);
|
|
208
|
+
const sig = signer.sign(signKey);
|
|
209
|
+
// ECDSA 签名(DER 编码)→ SEQUENCE(INTEGER r, INTEGER s)
|
|
210
|
+
const rLen = sig.readUInt8(3);
|
|
211
|
+
const rStart = 4;
|
|
212
|
+
const r = sig.subarray(rStart, rStart + rLen);
|
|
213
|
+
const sLen = sig.readUInt8(rStart + rLen + 1);
|
|
214
|
+
const s = sig.subarray(rStart + rLen + 2, rStart + rLen + 2 + sLen);
|
|
215
|
+
const sigDer = derSeq(
|
|
216
|
+
derInt(BigInt('0x' + r.toString('hex'))),
|
|
217
|
+
derInt(BigInt('0x' + s.toString('hex'))),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const certDer = derSeq(tbs, alg, derBitString(sigDer));
|
|
221
|
+
const certPem = '-----BEGIN CERTIFICATE-----\n'
|
|
222
|
+
+ certDer.toString('base64').replace(/(.{64})/g, '$1\n').replace(/\n$/, '')
|
|
223
|
+
+ '\n-----END CERTIFICATE-----\n';
|
|
224
|
+
const keyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
225
|
+
return { certPem, keyPem, certDer, notBefore, notAfter };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── 对外 API ──
|
|
229
|
+
|
|
230
|
+
// CA 自签证书(信任锚):CN=caCn,basicConstraints CA:true + keyUsage keyCertSign|cRLSign
|
|
231
|
+
export function generateCaCert({ cn = 'dsh-punky-swarm CA', days = 3650 } = {}) {
|
|
232
|
+
return createX509({ cn, days, isCa: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 用 CA 签发实体证书(mTLS 服务端/客户端):CN=cn,SAN 含 URI:acps://{aic}(CAI 语义对齐)
|
|
236
|
+
// usages: ['serverAuth'] / ['clientAuth'] / 两者
|
|
237
|
+
export function issueCert({ caCertPem, caKeyPem, cn, aic, altNames = [], days = 365, usages = ['clientAuth'] }) {
|
|
238
|
+
if (typeof caCertPem !== 'string' || typeof caKeyPem !== 'string') {
|
|
239
|
+
throw new Error('issueCert: caCertPem and caKeyPem are required');
|
|
240
|
+
}
|
|
241
|
+
// 从 CA 证书解析 issuer CN(X509Certificate.subject → "CN=xxx")
|
|
242
|
+
let issuerCn;
|
|
243
|
+
try {
|
|
244
|
+
const caCert = new crypto.X509Certificate(caCertPem);
|
|
245
|
+
issuerCn = (caCert.subject.match(/CN=([^,]+)/) ?? [])[1] ?? 'dsh-punky-swarm CA';
|
|
246
|
+
} catch {
|
|
247
|
+
issuerCn = 'dsh-punky-swarm CA';
|
|
248
|
+
}
|
|
249
|
+
const caKey = crypto.createPrivateKey(caKeyPem);
|
|
250
|
+
return createX509({
|
|
251
|
+
cn,
|
|
252
|
+
issuerCn,
|
|
253
|
+
signerPrivateKey: caKey,
|
|
254
|
+
altNames: [
|
|
255
|
+
...(aic ? [{ type: 'uri', value: 'acps://' + aic }] : []),
|
|
256
|
+
...altNames,
|
|
257
|
+
],
|
|
258
|
+
days,
|
|
259
|
+
isCa: false,
|
|
260
|
+
usages,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── 文件管理(cert/key/ca 三路径 config 可配,默认 acps 数据目录)──
|
|
265
|
+
// ensureAcpsCerts({ dir, aic }):幂等——目录已有 ca.pem/ca.key/server.pem/server.key 则复用;
|
|
266
|
+
// 缺则生成落盘。返回 { certFile, keyFile, caFile, caCertPem, certPem }(TLS 上下文 + 联测用)。
|
|
267
|
+
// 目录结构:<dir>/ca.pem + ca.key(自签 CA)、<dir>/server.pem + server.key(CA 签发服务端证书)。
|
|
268
|
+
export function ensureAcpsCerts({ dir, aic = '1.2.156.3088.1.0001.00001.000000.000000.ROOT' } = {}) {
|
|
269
|
+
mkdirSync(dir, { recursive: true });
|
|
270
|
+
const caFile = join(dir, 'ca.pem');
|
|
271
|
+
const caKeyFile = join(dir, 'ca.key');
|
|
272
|
+
const certFile = join(dir, 'server.pem');
|
|
273
|
+
const keyFile = join(dir, 'server.key');
|
|
274
|
+
|
|
275
|
+
let caCertPem;
|
|
276
|
+
let caKeyPem;
|
|
277
|
+
if (existsSync(caFile) && existsSync(caKeyFile)) {
|
|
278
|
+
caCertPem = readFileSync(caFile, 'utf8');
|
|
279
|
+
caKeyPem = readFileSync(caKeyFile, 'utf8');
|
|
280
|
+
} else {
|
|
281
|
+
const ca = generateCaCert();
|
|
282
|
+
caCertPem = ca.certPem;
|
|
283
|
+
caKeyPem = ca.keyPem;
|
|
284
|
+
writeFileSync(caFile, caCertPem, { mode: 0o600 });
|
|
285
|
+
writeFileSync(caKeyFile, caKeyPem, { mode: 0o600 });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let certPem;
|
|
289
|
+
let keyPem;
|
|
290
|
+
if (existsSync(certFile) && existsSync(keyFile)) {
|
|
291
|
+
certPem = readFileSync(certFile, 'utf8');
|
|
292
|
+
keyPem = readFileSync(keyFile, 'utf8');
|
|
293
|
+
} else {
|
|
294
|
+
const leaf = issueCert({
|
|
295
|
+
caCertPem,
|
|
296
|
+
caKeyPem,
|
|
297
|
+
cn: aic,
|
|
298
|
+
aic,
|
|
299
|
+
altNames: [{ type: 'dns', value: 'localhost' }, { type: 'ip', value: '127.0.0.1' }],
|
|
300
|
+
days: 365,
|
|
301
|
+
usages: ['serverAuth', 'clientAuth'],
|
|
302
|
+
});
|
|
303
|
+
certPem = leaf.certPem;
|
|
304
|
+
keyPem = leaf.keyPem;
|
|
305
|
+
writeFileSync(certFile, certPem, { mode: 0o600 });
|
|
306
|
+
writeFileSync(keyFile, keyPem, { mode: 0o600 });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { certFile, keyFile, caFile, caCertPem, certPem };
|
|
310
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
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
|
+
// 外部 ADP 发现客户端(Agent Discovery Protocol):插件 Leader 发现外部 partner。
|
|
19
|
+
// POST {adpBaseUrl}/discover 查询外部 discovery-server,返回 DiscoveryResponse
|
|
20
|
+
// (result.acsMap / result.agents[].agentSkills / result.routes),供调用方选择调用目标。
|
|
21
|
+
//
|
|
22
|
+
// 基准(参考实现 ACPs-community v2.1.0,事实源):
|
|
23
|
+
// - 请求构造/错误语义:demo-leader/leader/assistant/services/discovery_client.py:49-118(discover 86-93)、149-161(DiscoveryClientError)
|
|
24
|
+
// - DiscoveryRequest 模型:acps_sdk/acps_sdk/adp/models.py:245-443(type/query/context/limit/filter/forward*,lowerCamelCase)
|
|
25
|
+
// - DiscoveryResponse 模型:acps_sdk/acps_sdk/adp/models.py:664-757(result 与 error 互斥,CommonResponse 规范)
|
|
26
|
+
// - 服务端 /discover 端点:discovery-server/app/discovery/discovery_api.py:29-83;响应构造 service.py:262-283
|
|
27
|
+
//
|
|
28
|
+
// 与既有本地查询关系(保留,零改动):
|
|
29
|
+
// lib/discovery/service.js = 本地进程内查询(插件自身工具/智能体目录,经 /api/dsh-punky-swarm/discover 暴露);
|
|
30
|
+
// 本模块 = 新增外部查询通道,共享同一协议常量与校验(lib/discovery/schema.js
|
|
31
|
+
// QUERY_TYPES/FILTER_OPERATORS/validateDiscoveryRequest/normalizeLimit——与参考实现 acps_sdk/adp 对齐)。
|
|
32
|
+
// 查询范围选项 scope:local(仅本地)/ external(仅外部)/ both(本地+外部合并),装配可配(acps.discovery.scope)。
|
|
33
|
+
//
|
|
34
|
+
// mini-ADSP(可选):对外提供 /discover 的服务端语义——端点未就绪,
|
|
35
|
+
// 仅预留函数签名 createMiniAdsp()(调用抛 NotImplemented,见文末),不实现。
|
|
36
|
+
//
|
|
37
|
+
// 零新依赖:HTTP 用全局 fetch(Node >= 22)+ AbortSignal.timeout,node:https 内建能力之上。
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
QUERY_TYPE_EXPLICIT,
|
|
41
|
+
validateDiscoveryRequest,
|
|
42
|
+
normalizeLimit,
|
|
43
|
+
successResponse,
|
|
44
|
+
} from '../discovery/schema.js';
|
|
45
|
+
|
|
46
|
+
// ── 查询范围选项(装配可配;外部通道与本地目录的组合方式)──
|
|
47
|
+
export const DISCOVERY_SCOPES = ['local', 'external', 'both'];
|
|
48
|
+
export const DISCOVERY_SCOPE_DEFAULT = 'local';
|
|
49
|
+
|
|
50
|
+
// DiscoveryRequest 透传白名单(forward*/context 之外的字段不注入,防任意字段污染——对齐
|
|
51
|
+
// acps_sdk DiscoveryRequest 公开字段集 models.py:245-443;数值字段由服务器默认值兜底)
|
|
52
|
+
const FORWARD_FIELDS = [
|
|
53
|
+
'forwardDepthLimit', 'forwardFanoutLimit', 'forwardFanoutRemaining',
|
|
54
|
+
'forwardChain', 'forwardTrustedServers', 'forwardSignatures',
|
|
55
|
+
'forwardEachTimeoutMs', 'forwardTotalTimeoutMs',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export class DiscoveryClientError extends Error {
|
|
59
|
+
// 语义对齐 demo-leader discovery_client.py:149-161:
|
|
60
|
+
// adpError = { code, message, data }(协议级错误载荷,可判 retryable/redirect/client/forward 类别)
|
|
61
|
+
constructor(message, adpError = null) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = 'DiscoveryClientError';
|
|
64
|
+
this.adpError = adpError;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 将 lib/discovery/schema.js 校验错误({ error: { code, message, data } })转为 DiscoveryClientError
|
|
69
|
+
function toClientError(err, prefix) {
|
|
70
|
+
const e = err && err.error ? err.error : {};
|
|
71
|
+
return new DiscoveryClientError(prefix + ': ' + (e.message ?? JSON.stringify(err)), e.code !== undefined ? e : null);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 构造 DiscoveryRequest 载荷(对齐参考实现 discovery_client.py:72-77 形态:
|
|
75
|
+
// { type, query, limit, filter } + 可选 context/forward*;lowerCamelCase,exclude_none)
|
|
76
|
+
export function buildDiscoveryRequest(query, opts = {}, defaultLimit = 5) {
|
|
77
|
+
const raw = {
|
|
78
|
+
type: opts.type ?? QUERY_TYPE_EXPLICIT,
|
|
79
|
+
query: query ?? null,
|
|
80
|
+
limit: normalizeLimit(opts.limit ?? defaultLimit),
|
|
81
|
+
};
|
|
82
|
+
if (opts.filter !== undefined && opts.filter !== null) raw.filter = opts.filter;
|
|
83
|
+
if (opts.context !== undefined && opts.context !== null) raw.context = opts.context;
|
|
84
|
+
for (const f of FORWARD_FIELDS) {
|
|
85
|
+
if (opts[f] !== undefined && opts[f] !== null) raw[f] = opts[f];
|
|
86
|
+
}
|
|
87
|
+
return raw;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 响应结构校验 + 协议级错误识别(对齐 acps_sdk DiscoveryResponse 语义 models.py:664-757):
|
|
91
|
+
// - HTTP 200 但 body.error 存在 → 协议级错误(DiscoveryClientError + adpError)
|
|
92
|
+
// - result 必须为对象;agents 若存在必须为数组;acsMap 若存在必须为对象
|
|
93
|
+
export function parseDiscoveryResponse(json, throwOnProtocolError = true) {
|
|
94
|
+
if (json === null || typeof json !== 'object' || Array.isArray(json)) {
|
|
95
|
+
throw new DiscoveryClientError('发现服务返回无效响应: 非 JSON 对象');
|
|
96
|
+
}
|
|
97
|
+
if (json.error !== undefined && json.error !== null) {
|
|
98
|
+
const err = (typeof json.error === 'object') ? json.error : { message: String(json.error) };
|
|
99
|
+
const adpError = { code: err.code, message: err.message, data: err.data };
|
|
100
|
+
if (throwOnProtocolError) {
|
|
101
|
+
throw new DiscoveryClientError('发现服务返回协议错误: ' + (err.message ?? ''), adpError);
|
|
102
|
+
}
|
|
103
|
+
return { error: adpError };
|
|
104
|
+
}
|
|
105
|
+
if (json.result === undefined || json.result === null || typeof json.result !== 'object' || Array.isArray(json.result)) {
|
|
106
|
+
throw new DiscoveryClientError('发现服务返回无效响应: 缺少 result 对象');
|
|
107
|
+
}
|
|
108
|
+
const result = json.result;
|
|
109
|
+
if (result.agents !== undefined && !Array.isArray(result.agents)) {
|
|
110
|
+
throw new DiscoveryClientError('发现服务返回无效响应: result.agents 非数组');
|
|
111
|
+
}
|
|
112
|
+
if (result.acsMap !== undefined
|
|
113
|
+
&& (result.acsMap === null || typeof result.acsMap !== 'object' || Array.isArray(result.acsMap))) {
|
|
114
|
+
throw new DiscoveryClientError('发现服务返回无效响应: result.acsMap 非对象');
|
|
115
|
+
}
|
|
116
|
+
if (result.routes !== undefined && !Array.isArray(result.routes)) {
|
|
117
|
+
throw new DiscoveryClientError('发现服务返回无效响应: result.routes 非数组');
|
|
118
|
+
}
|
|
119
|
+
return successResponse(result);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 便利遍历:展平 result.agents[].agentSkills,关联 acsMap 中的 ACS——
|
|
123
|
+
// 对齐 DiscoveryResult.iter_agent_skills(acps_sdk/adp/models.py:609-626):
|
|
124
|
+
// 返回 [{ aic, acs, skillId, ranking, memo, group }],acs 缺失时为 {}
|
|
125
|
+
export function flattenAgentSkills(result) {
|
|
126
|
+
const acsMap = (result && typeof result.acsMap === 'object' && !Array.isArray(result.acsMap)) ? result.acsMap : {};
|
|
127
|
+
const out = [];
|
|
128
|
+
const agents = Array.isArray(result?.agents) ? result.agents : [];
|
|
129
|
+
for (const group of agents) {
|
|
130
|
+
const skills = Array.isArray(group?.agentSkills) ? group.agentSkills : [];
|
|
131
|
+
for (const s of skills) {
|
|
132
|
+
out.push({
|
|
133
|
+
aic: s?.aic,
|
|
134
|
+
acs: acsMap[s?.aic] ?? {},
|
|
135
|
+
skillId: s?.skillId,
|
|
136
|
+
ranking: s?.ranking,
|
|
137
|
+
memo: s?.memo,
|
|
138
|
+
group: group?.group,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 合并本地+外部响应(scope=both):acsMap 键合并(外部优先)、agents 分组拼接、routes 拼接
|
|
146
|
+
function mergeResponses(local, external) {
|
|
147
|
+
const lr = local?.result ?? {};
|
|
148
|
+
const er = external?.result ?? {};
|
|
149
|
+
const acsMap = { ...(lr.acsMap ?? {}), ...(er.acsMap ?? {}) };
|
|
150
|
+
const agents = [...(Array.isArray(lr.agents) ? lr.agents : []), ...(Array.isArray(er.agents) ? er.agents : [])];
|
|
151
|
+
const routes = [...(Array.isArray(lr.routes) ? lr.routes : []), ...(Array.isArray(er.routes) ? er.routes : [])];
|
|
152
|
+
const result = { acsMap, agents };
|
|
153
|
+
if (routes.length > 0) result.routes = routes;
|
|
154
|
+
return successResponse(result);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ADP 客户端工厂。
|
|
158
|
+
// 参数:
|
|
159
|
+
// baseUrl 外部 discovery-server 根地址(如 http://127.0.0.1:9020);空 = 未配置(discover 抛错,mirror is_configured)
|
|
160
|
+
// timeout 单次请求超时 ms(默认 10000)
|
|
161
|
+
// limit 默认返回上限(默认 5,对齐参考实现 discovery_config limit=5)
|
|
162
|
+
// scope 查询范围 local/external/both(默认 local)
|
|
163
|
+
// localService 既有本地查询服务(lib/discovery/service.js 实例;scope=local/both 时必需)
|
|
164
|
+
// request 可注入 HTTP 实现(默认全局 fetch;测试用 stub 替换)
|
|
165
|
+
export function createAcpsDiscoveryClient({
|
|
166
|
+
baseUrl = '',
|
|
167
|
+
timeout = 10_000,
|
|
168
|
+
limit = 5,
|
|
169
|
+
scope = DISCOVERY_SCOPE_DEFAULT,
|
|
170
|
+
localService = null,
|
|
171
|
+
request = null,
|
|
172
|
+
} = {}) {
|
|
173
|
+
if (!DISCOVERY_SCOPES.includes(scope)) {
|
|
174
|
+
throw new TypeError('acps discovery scope 非法: ' + JSON.stringify(scope) + ',允许: ' + DISCOVERY_SCOPES.join('/'));
|
|
175
|
+
}
|
|
176
|
+
const serverBaseUrl = String(baseUrl ?? '').replace(/\/+$/, '');
|
|
177
|
+
const requestImpl = request ?? ((url, options) => fetch(url, options));
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
scope,
|
|
181
|
+
baseUrl: serverBaseUrl,
|
|
182
|
+
// mirror discovery_client.py:44-47 is_configured
|
|
183
|
+
get isConfigured() {
|
|
184
|
+
return serverBaseUrl.length > 0;
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// discover(query, opts):query=自然语言能力查询;opts = { type, limit, filter, context, scope?, ...forward* }
|
|
188
|
+
// scope 缺省用客户端装配 scope;返回 { result: { acsMap, agents, routes? } }(与本地 service.discover 同形)
|
|
189
|
+
async discover(query, opts = {}) {
|
|
190
|
+
const effectiveScope = opts.scope ?? scope;
|
|
191
|
+
if (!DISCOVERY_SCOPES.includes(effectiveScope)) {
|
|
192
|
+
throw new DiscoveryClientError('scope 非法: ' + JSON.stringify(effectiveScope));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 请求参数校验(提前捕获 query 为空/filter 非法等——对齐 discovery_client.py:80-83 validate_discovery_request)
|
|
196
|
+
const raw = buildDiscoveryRequest(query, opts, limit);
|
|
197
|
+
const verr = validateDiscoveryRequest(raw);
|
|
198
|
+
if (verr) throw toClientError(verr, '请求参数校验失败');
|
|
199
|
+
|
|
200
|
+
// local 分支:既有本地查询服务
|
|
201
|
+
const runLocal = async () => {
|
|
202
|
+
if (!localService || typeof localService.discover !== 'function') {
|
|
203
|
+
throw new DiscoveryClientError('scope=' + effectiveScope + ' 需要本地发现服务(capabilities.discovery.enabled),但未装配');
|
|
204
|
+
}
|
|
205
|
+
const resp = localService.discover(raw);
|
|
206
|
+
// 本地服务校验失败返回 { error } 形态(failureResponse)——转为客户端错误保持一致语义
|
|
207
|
+
if (resp && resp.error) throw toClientError({ error: resp.error }, '本地查询返回错误');
|
|
208
|
+
return resp;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// external 分支:POST {baseUrl}/discover(对齐 discovery_client.py:86-93)
|
|
212
|
+
const runExternal = async () => {
|
|
213
|
+
if (!serverBaseUrl) {
|
|
214
|
+
throw new DiscoveryClientError('发现服务未配置 server_base_url');
|
|
215
|
+
}
|
|
216
|
+
const url = serverBaseUrl + '/discover';
|
|
217
|
+
const body = JSON.stringify(raw);
|
|
218
|
+
let res;
|
|
219
|
+
try {
|
|
220
|
+
res = await requestImpl(url, {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { 'content-type': 'application/json' },
|
|
223
|
+
body,
|
|
224
|
+
signal: AbortSignal.timeout(Number.isFinite(Number(timeout)) && Number(timeout) > 0 ? Number(timeout) : 10_000),
|
|
225
|
+
});
|
|
226
|
+
} catch (e) {
|
|
227
|
+
const name = e?.name ?? '';
|
|
228
|
+
if (name === 'TimeoutError' || name === 'AbortError' || e?.code === 'UND_ERR_CONNECT_TIMEOUT') {
|
|
229
|
+
throw new DiscoveryClientError('发现服务请求超时');
|
|
230
|
+
}
|
|
231
|
+
// 连接失败等(fetch failed / ECONNREFUSED 等)——对齐 discovery_client.py:116-118
|
|
232
|
+
throw new DiscoveryClientError('发现服务调用失败: ' + (e?.message ?? String(e)));
|
|
233
|
+
}
|
|
234
|
+
// HTTP 状态错误——对齐 discovery_client.py:110-112 raise_for_status
|
|
235
|
+
if (!res || typeof res.ok !== 'boolean') {
|
|
236
|
+
throw new DiscoveryClientError('发现服务返回无效响应: 缺少 HTTP 响应');
|
|
237
|
+
}
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
throw new DiscoveryClientError('发现服务返回错误 ' + (res.status ?? 'unknown'));
|
|
240
|
+
}
|
|
241
|
+
let json;
|
|
242
|
+
try {
|
|
243
|
+
json = await res.json();
|
|
244
|
+
} catch {
|
|
245
|
+
throw new DiscoveryClientError('发现服务响应不是有效 JSON');
|
|
246
|
+
}
|
|
247
|
+
return parseDiscoveryResponse(json);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// 按 scope 分发
|
|
251
|
+
if (effectiveScope === 'local') return runLocal();
|
|
252
|
+
if (effectiveScope === 'external') return runExternal();
|
|
253
|
+
// both:本地 + 外部合并;通道不可用时如实降级(缺本地 → 仅外部;缺外部 → 仅本地)
|
|
254
|
+
const localResp = localService && typeof localService.discover === 'function' ? await runLocal() : null;
|
|
255
|
+
let externalResp = null;
|
|
256
|
+
if (serverBaseUrl) {
|
|
257
|
+
try {
|
|
258
|
+
externalResp = await runExternal();
|
|
259
|
+
} catch (e) {
|
|
260
|
+
if (!(e instanceof DiscoveryClientError)) throw e;
|
|
261
|
+
if (!localResp) throw e; // 无本地结果可兜底 → 如实上抛
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!localResp && !externalResp) {
|
|
265
|
+
throw new DiscoveryClientError('scope=both 无可用通道: 需本地发现服务或配置 server_base_url');
|
|
266
|
+
}
|
|
267
|
+
if (!externalResp) return localResp;
|
|
268
|
+
if (!localResp) return externalResp;
|
|
269
|
+
return mergeResponses(localResp, externalResp);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── mini-ADSP(可选,预留)──
|
|
275
|
+
// 对外提供 /discover 的服务端语义(本插件作为 ADP 服务端被外部查询)。
|
|
276
|
+
// mini-ADSP 非门禁;对外 /acps/rpc + .well-known/acs.json 端点就绪前
|
|
277
|
+
// 仅预留本签名,不实现。实现路径(就绪后):复用 lib/discovery/service.js 的 discover 语义,
|
|
278
|
+
// 经 acps.endpoint 挂载对外 /discover。
|
|
279
|
+
export function createMiniAdsp() {
|
|
280
|
+
throw new Error(
|
|
281
|
+
'mini-ADSP 预留接口(未实现):对外 /discover 服务端语义待 endpoint 就绪后按需实现'
|
|
282
|
+
);
|
|
283
|
+
}
|