@numa-tech/numa 1.12.6 → 1.12.8
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 +141 -43
- package/dist/ai-registry/bridge.d.ts +8 -0
- package/dist/ai-registry/bridge.js +63 -0
- package/dist/ai-registry/bridge.js.map +1 -0
- package/dist/ai-registry/client.d.ts +29 -3
- package/dist/ai-registry/client.js +112 -8
- package/dist/ai-registry/client.js.map +1 -1
- package/dist/ai-registry/commands.js +367 -112
- package/dist/ai-registry/commands.js.map +1 -1
- package/dist/ai-registry/errors.d.ts +2 -1
- package/dist/ai-registry/errors.js +12 -1
- package/dist/ai-registry/errors.js.map +1 -1
- package/dist/ai-registry/mcp-installer.d.ts +1 -0
- package/dist/ai-registry/mcp-installer.js +4 -0
- package/dist/ai-registry/mcp-installer.js.map +1 -1
- package/dist/ai-registry/namespace.d.ts +17 -0
- package/dist/ai-registry/namespace.js +53 -0
- package/dist/ai-registry/namespace.js.map +1 -0
- package/dist/ai-registry/schemas.d.ts +127 -2
- package/dist/ai-registry/schemas.js +42 -2
- package/dist/ai-registry/schemas.js.map +1 -1
- package/dist/ai-registry/sync.d.ts +23 -0
- package/dist/ai-registry/sync.js +276 -0
- package/dist/ai-registry/sync.js.map +1 -0
- package/dist/ai-registry/tui.d.ts +2 -0
- package/dist/ai-registry/tui.js +140 -0
- package/dist/ai-registry/tui.js.map +1 -0
- package/dist/ai-registry/workspace.d.ts +191 -0
- package/dist/ai-registry/workspace.js +167 -0
- package/dist/ai-registry/workspace.js.map +1 -0
- package/dist/app-config.d.ts +3 -2
- package/dist/app-config.js +5 -3
- package/dist/app-config.js.map +1 -1
- package/dist/chatgpt-desktop.d.ts +7 -0
- package/dist/chatgpt-desktop.js +95 -0
- package/dist/chatgpt-desktop.js.map +1 -1
- package/dist/cli.js +25 -3
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +1 -0
- package/dist/command-catalog.js +97 -10
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-page.js +2 -0
- package/dist/config-page.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/pipeline/client.d.ts +10 -6
- package/dist/pipeline/client.js +7 -4
- package/dist/pipeline/client.js.map +1 -1
- package/dist/pipeline/commands.d.ts +5 -1
- package/dist/pipeline/commands.js +89 -18
- package/dist/pipeline/commands.js.map +1 -1
- package/dist/pipeline/schemas.d.ts +7 -7
- package/dist/pipeline/schemas.js +2 -2
- package/dist/pipeline/schemas.js.map +1 -1
- package/dist/platform-profile.d.ts +8 -2
- package/dist/platform-profile.js +5 -2
- package/dist/platform-profile.js.map +1 -1
- package/dist/web-console-page.d.ts +3 -3
- package/dist/web-console-page.js +75 -20
- package/dist/web-console-page.js.map +1 -1
- package/dist/web-console.d.ts +1 -1
- package/dist/web-console.js +46 -0
- package/dist/web-console.js.map +1 -1
- package/package.json +8 -5
- package/skills/numa-ai-registry/SKILL.md +25 -19
- package/skills/numa-ai-registry/evals/evals.json +10 -10
- package/skills/numa-ai-registry/references/command-contract.md +41 -6
package/README.md
CHANGED
|
@@ -80,6 +80,8 @@ npm dist-tag ls @numa-tech/numa --registry=https://registry.npmjs.org/
|
|
|
80
80
|
|
|
81
81
|
候选发布只有在三个公共包的 exact-version integrity 和 `candidate` tag 都校验成功后,脚本才会
|
|
82
82
|
创建 `chore(release): vX.Y.Z` commit、附注 tag,并以一次 atomic push 推送分支和 tag。
|
|
83
|
+
每个 `npm publish` 成功后,脚本会在约一分钟的有界窗口内等待 npmjs 的 exact-version 元数据和
|
|
84
|
+
dist-tag 稳定,再继续发布下一个包,避免将正常的 CDN/registry 传播延迟误判为发布失败。
|
|
83
85
|
如果发布中断,运行 `npm run release:resume`;它会复用持久化的原始 tarball 和 integrity,跳过
|
|
84
86
|
已经成功的包。不要重新执行普通 patch 发布,否则可能开始下一个版本。晋级命令会验证干净且
|
|
85
87
|
同步的 release commit/tag、公共 npm 上的候选版本与 tag,再使用权威 `npm dist-tag ls` 接口
|
|
@@ -185,9 +187,11 @@ numa config \
|
|
|
185
187
|
| `numa admin context` | 需角色 | 要求 `platform-admin` 或 `numa-admin` |
|
|
186
188
|
| `numa registry instances list/create/update/delete` | 需登录 | 管理数据库中的 Nacos 实例和 OIDC 配置;写操作需要 Registry admin |
|
|
187
189
|
| `numa registry namespaces list/resolve/check/create/update/delete` | 需登录 | 管理多实例 namespace 路由与跨集群默认项;写操作需要 Registry admin |
|
|
188
|
-
| `numa registry
|
|
189
|
-
| `numa registry
|
|
190
|
-
| `numa registry
|
|
190
|
+
| `numa registry capabilities/init/lock/sync/upgrade` | 需登录 | 读取授权边界并以可提交的 manifest/lock 确定性安装 |
|
|
191
|
+
| `numa registry skill list/inspect/install/upload/...` | 需登录 | 发现、安装和治理 Nacos Skills;服务端按团队、角色和生命周期授权 |
|
|
192
|
+
| `numa registry mcp list/inspect/install/bridge/...` | 需登录 | 通过企业 Router 安装/桥接 MCP,并维护版本、标签和工具状态 |
|
|
193
|
+
| `numa registry approvals/router/audit` | 需登录 | 两人制发布、Router 健康/对账和追加式审计 |
|
|
194
|
+
| `numa registry tui` | 需登录 | 打开 namespace、Skills、MCP、审批、Router、审计全屏工作台 |
|
|
191
195
|
| `numa media capabilities` | 需登录 | 查询媒体 API、Provider 和版本能力 |
|
|
192
196
|
| `numa media tts submit` | 需登录 | 提交 TTS 异步任务 |
|
|
193
197
|
| `numa media video submit` | 需登录 | 提交视频生成异步任务 |
|
|
@@ -230,6 +234,8 @@ CLI 的本地角色检查用于快速反馈和隐藏无权限入口,不能替
|
|
|
230
234
|
- `profileUrl`:完整的公开 client-configuration URL;设置后各模块只使用该
|
|
231
235
|
profile 声明的 service 与 authority;
|
|
232
236
|
- `profileId`:可选的 profile 标识锁定;远端返回其他标识时拒绝启用;
|
|
237
|
+
- `aiRegistryDefaultNamespaceKey`:用户级 Registry 默认 namespace;优先级低于命令行和
|
|
238
|
+
项目 `.numa/registry.json`,高于 profile 默认值;
|
|
233
239
|
- `registryUrl`:npm registry,默认公共 npm,也可配置公司私有 registry;
|
|
234
240
|
- `checkUpdates`:是否检查 CLI 更新。
|
|
235
241
|
|
|
@@ -266,9 +272,9 @@ numa login --no-console --no-config --json
|
|
|
266
272
|
```
|
|
267
273
|
|
|
268
274
|
控制台展示当前身份、Realm/Client 角色、AI Registry、ChatGPT 账号、基础配置和机器可读命令目录。
|
|
269
|
-
AI Registry
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
AI Registry 页包含目录、发布收件箱、实例/双凭据、Namespace/Profile、Router、权限/审计六个
|
|
276
|
+
工作区,并展示 capability 返回的环境、动作和 Router 状态。按钮是否可用只是界面提示,实际
|
|
277
|
+
权限、两人制和生命周期始终由平台服务端决定。
|
|
272
278
|
ChatGPT 账号页先读取 `codex-multi-auth` 的本地账号与额度缓存并立即展示,再使用当前
|
|
273
279
|
Keycloak 会话连接平台补充当前用户可用账号、剩余 limit 和同步状态。平台不可用时页面
|
|
274
280
|
保留本地结果并明确显示“远程同步失败”,不会把 access token 或 refresh token 返回给页面。
|
|
@@ -285,20 +291,27 @@ CSP。OIDC access/refresh token 始终保留在 CLI 进程及私有缓存中,
|
|
|
285
291
|
|
|
286
292
|
## 私有 Nacos AI Registry
|
|
287
293
|
|
|
288
|
-
`numa registry` 对接 `mci-devops-platform` 的 `/api/v1/ai-registry` 类型化 API
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
294
|
+
`numa registry` 对接 `mci-devops-platform` 的 `/api/v1/ai-registry` 类型化 API。Nacos 是
|
|
295
|
+
Skill/MCP 内容、版本、标签和上下线状态的主仓库;平台只维护组织、团队、权限、审批、profile、
|
|
296
|
+
Router 绑定、对账和审计。Numa 只携带当前用户 Keycloak Bearer Token,且 capability 响应若
|
|
297
|
+
包含 Nacos 地址、token、secret、password 或 credential 字段会被客户端拒绝。
|
|
298
|
+
|
|
299
|
+
`GET /api/v1/ai-registry/capabilities` 返回当前用户可见 namespace、环境、动作、Router
|
|
300
|
+
Streamable HTTP/OAuth resource 和客户端适配器。profile v1 可追加
|
|
301
|
+
`services.aiRegistry.capabilityUrl` 与
|
|
302
|
+
`services.aiRegistry.configuration.defaultNamespaceKey`,旧客户端会忽略这些字段。
|
|
292
303
|
|
|
293
304
|
权限契约:
|
|
294
305
|
|
|
295
|
-
- `
|
|
296
|
-
- `
|
|
297
|
-
-
|
|
306
|
+
- 团队 `MEMBER+` 可使用已授权 namespace;
|
|
307
|
+
- 团队 `MAINTAINER/ADMIN/OWNER` 可维护本团队草稿;
|
|
308
|
+
- `ai-registry-reviewer` 可审核发布申请,且不得审核或发布自己的申请;
|
|
309
|
+
- `ai-registry-admin` 管理 Registry,`ops-admin` 才能执行 `*` allowlist、Admin API 和紧急操作。
|
|
298
310
|
|
|
299
|
-
Nacos
|
|
300
|
-
|
|
301
|
-
|
|
311
|
+
每个 Nacos 实例分别配置 `READER` 和 `MAINTAINER` 凭据,可使用 OIDC client credentials 或
|
|
312
|
+
Nacos username/password,并为每套凭据设置精确 namespace allowlist。凭据使用 AES-256-GCM
|
|
313
|
+
密文存储,读取接口只返回 configured 状态,不返回明文或密文。旧单凭据作为 `LEGACY_BOTH`
|
|
314
|
+
继续工作,管理员验证两套新凭据后再显式停用。平台部署从 Secret/KMS 注入数据库密文的
|
|
302
315
|
主密钥环 `AI_REGISTRY_SECRET_ENCRYPTION_KEYS` 与当前 key ID
|
|
303
316
|
`AI_REGISTRY_SECRET_CURRENT_KEY_ID`;这两项是密钥管理材料,不是某个 Nacos 实例的配置。
|
|
304
317
|
密钥环格式为逗号分隔的 `keyId=Base64(32-byte-key)`;轮换时先加入新 key 并切换当前 key ID,
|
|
@@ -307,25 +320,35 @@ SSRF 安全策略仍由 `AI_REGISTRY_ALLOWED_UPSTREAM_HOSTS`、
|
|
|
307
320
|
`AI_REGISTRY_ALLOWED_TOKEN_HOSTS` 控制,allow-list 为空时 fail closed;HTTP 只可通过
|
|
308
321
|
`AI_REGISTRY_ALLOW_HTTP=true` 用于开发环境。
|
|
309
322
|
|
|
310
|
-
Nacos
|
|
323
|
+
Nacos 实例文件示例(凭据字段仅写入,实际字段以平台 capability/API revision 为准):
|
|
311
324
|
|
|
312
325
|
```json
|
|
313
326
|
{
|
|
314
327
|
"code": "cluster-a",
|
|
315
328
|
"displayName": "Nacos Cluster A",
|
|
316
329
|
"baseUrl": "https://nacos.internal.example:8848",
|
|
317
|
-
"
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
330
|
+
"credentials": {
|
|
331
|
+
"reader": {
|
|
332
|
+
"authenticationType": "OIDC_CLIENT_CREDENTIALS",
|
|
333
|
+
"tokenUri": "https://sso.internal.example/realms/mci/protocol/openid-connect/token",
|
|
334
|
+
"clientId": "nacos-registry-reader",
|
|
335
|
+
"clientSecret": "write-only-secret",
|
|
336
|
+
"namespaceAllowlist": ["team-ai"]
|
|
337
|
+
},
|
|
338
|
+
"maintainer": {
|
|
339
|
+
"authenticationType": "NACOS_USERNAME_PASSWORD",
|
|
340
|
+
"username": "registry-maintainer",
|
|
341
|
+
"password": "write-only-password",
|
|
342
|
+
"namespaceAllowlist": ["team-ai"]
|
|
343
|
+
}
|
|
344
|
+
},
|
|
322
345
|
"description": "A 集群",
|
|
323
346
|
"status": "ACTIVE"
|
|
324
347
|
}
|
|
325
348
|
```
|
|
326
349
|
|
|
327
|
-
`instances update` 必须带列表结果中的当前 `version
|
|
328
|
-
|
|
350
|
+
`instances update` 必须带列表结果中的当前 `version`,用于乐观锁保护;省略某套凭据表示保留
|
|
351
|
+
已有密文。建议让包含明文 secret 的输入文件仅对当前用户可读,并在写入后安全删除。
|
|
329
352
|
|
|
330
353
|
namespace 文件示例:
|
|
331
354
|
|
|
@@ -341,13 +364,13 @@ namespace 文件示例:
|
|
|
341
364
|
```
|
|
342
365
|
|
|
343
366
|
`namespaceKey` 是客户端使用的稳定名称,`namespaceId` 是该 Nacos 实例中的真实 ID。同一个
|
|
344
|
-
`namespaceKey`
|
|
345
|
-
|
|
346
|
-
|
|
367
|
+
`namespaceKey` 可以存在于多个实例。Numa 的选择顺序固定为 `--namespace` → 最近的
|
|
368
|
+
`.numa/registry.json` → 用户 `aiRegistryDefaultNamespaceKey` → profile 默认值;选择结果必须
|
|
369
|
+
存在于 capability 授权列表。同名绑定必须由 manifest/`--instance` 消歧,默认值无效时明确失败,
|
|
370
|
+
不会切换到其他可用 namespace。旧 `isDefault` 仅供迁移期兼容客户端使用。
|
|
347
371
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
跨集群重复的旧 namespace 不会自动猜测默认项。
|
|
372
|
+
profile 管理员发布 `defaultNamespaceKey` 且满足最低客户端版本后,才可逐步移除旧 `isDefault`
|
|
373
|
+
fallback。服务器端数据库迁移必须从当前版本新增 Flyway migration,不修改历史迁移。
|
|
351
374
|
|
|
352
375
|
先发现实例和 namespace,再通过 namespace 操作资源:
|
|
353
376
|
|
|
@@ -357,6 +380,13 @@ numa registry namespaces list --namespace team-ai --json
|
|
|
357
380
|
numa registry namespaces resolve team-ai --json
|
|
358
381
|
numa registry namespaces check team-ai --json
|
|
359
382
|
|
|
383
|
+
numa registry capabilities --json
|
|
384
|
+
numa registry init --namespace team-ai --instance cluster-b --json
|
|
385
|
+
# 编辑 .numa/registry.json 后:
|
|
386
|
+
numa registry lock --json
|
|
387
|
+
numa registry sync --json
|
|
388
|
+
numa registry upgrade release-helper --json
|
|
389
|
+
|
|
360
390
|
numa registry skill list --namespace team-ai --search deploy --json
|
|
361
391
|
numa registry skill inspect release-helper --namespace team-ai --json
|
|
362
392
|
numa registry skill install release-helper \
|
|
@@ -365,50 +395,118 @@ numa registry skill install release-helper \
|
|
|
365
395
|
numa registry mcp list --namespace team-ai --json
|
|
366
396
|
numa registry mcp inspect internal-docs --namespace team-ai --json
|
|
367
397
|
numa registry mcp install internal-docs \
|
|
368
|
-
--namespace team-ai --instance cluster-b --agent claude --scope project --json
|
|
398
|
+
--namespace team-ai --instance cluster-b --agent claude --scope project --transport remote --json
|
|
399
|
+
|
|
400
|
+
# 仅供 stdio Agent;复用 Numa 登录/刷新连接 Router
|
|
401
|
+
numa registry mcp bridge --namespace team-ai
|
|
369
402
|
|
|
370
403
|
# 也可使用终端交互界面
|
|
371
404
|
numa registry tui
|
|
372
405
|
```
|
|
373
406
|
|
|
374
|
-
|
|
407
|
+
两人制发布生命周期:
|
|
375
408
|
|
|
376
409
|
```bash
|
|
377
410
|
numa registry skill upload ./release-helper.zip \
|
|
378
411
|
--namespace team-ai --target-version 1.0.0 --json
|
|
379
|
-
numa registry
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
numa registry
|
|
383
|
-
|
|
384
|
-
numa registry
|
|
412
|
+
numa registry approvals submit skill release-helper \
|
|
413
|
+
--namespace team-ai --resource-version 1.0.0 --reason "scanner passed" --json
|
|
414
|
+
# 必须由另一位 reviewer 执行
|
|
415
|
+
numa registry approvals review rr_123 \
|
|
416
|
+
--decision APPROVED --reason "reviewed" --version 1 --json
|
|
417
|
+
numa registry approvals publish rr_123 --version 2 --json
|
|
418
|
+
numa registry audit --namespace team-ai --json
|
|
385
419
|
```
|
|
386
420
|
|
|
421
|
+
生命周期由服务端统一为
|
|
422
|
+
`DRAFT → SUBMITTED → APPROVED/REJECTED → PUBLISHING → PUBLISHED/FAILED → OFFLINE`。
|
|
423
|
+
生产及企业默认 namespace 的 Skill 不允许绕过 scanner;非生产豁免也必须带原因并由 Reviewer
|
|
424
|
+
审批。MCP 发布由服务端执行协议、工具 schema、禁用工具和 Endpoint 健康预检。
|
|
425
|
+
|
|
387
426
|
远端删除必须带 `--yes`。本地同名 Skill/MCP 默认拒绝覆盖;用户明确同意后才使用
|
|
388
427
|
`--force`,支持原子替换的适配器会先保留时间戳备份。Skill 下载必须通过平台返回的
|
|
389
428
|
SHA-256 校验,ZIP 在解压前会检查根 `SKILL.md`、路径穿越、符号链接、重复路径、文件数和
|
|
390
|
-
|
|
429
|
+
膨胀大小。`.numa/registry.lock.json` 保存解析后的精确版本、Nacos 来源、SHA-256 和安装状态;
|
|
430
|
+
普通 `sync` 不解析标签,只有首次 `lock` 或显式 `upgrade` 会变更版本。MCP 客户端只安装
|
|
431
|
+
capability 发布且健康的企业 Router,不安装 Nacos 中的下游 endpoint。
|
|
391
432
|
|
|
392
433
|
### 客户端安装适配
|
|
393
434
|
|
|
394
435
|
| 客户端 | Skill | MCP Server |
|
|
395
436
|
| --- | --- | --- |
|
|
396
|
-
| ChatGPT / Codex coding workspace | `.codex/skills` | ChatGPT
|
|
437
|
+
| ChatGPT / Codex coding workspace | `.codex/skills` | ChatGPT 连接企业 Router 的远程 HTTPS OAuth resource;Numa 返回 App 创建指引 |
|
|
397
438
|
| Codex | `.codex/skills` | 调用官方 `codex mcp add`,仅 user scope |
|
|
398
439
|
| Claude | `.claude/skills` | 项目 `.mcp.json`;user scope 使用 `~/.claude.json` |
|
|
399
440
|
| Cursor | `.cursor/skills` | `.cursor/mcp.json` |
|
|
400
441
|
| WorkBuddy | `.workbuddy/skills` | `.workbuddy/mcp.json`;目录可通过 custom adapter 调整 |
|
|
401
442
|
| OpenClaw | `.openclaw/skills` | 调用官方 `openclaw mcp set`,仅 user scope |
|
|
402
443
|
|
|
403
|
-
ChatGPT
|
|
404
|
-
|
|
405
|
-
|
|
444
|
+
ChatGPT 不能启动本地 stdio bridge;它直接连接企业 Router 的远程 HTTPS OAuth resource。
|
|
445
|
+
其他只支持 stdio 的 Agent 可运行 `numa registry mcp bridge`,由 Numa 获取/刷新用户 Token
|
|
446
|
+
并只发送给 Router,不向 Nacos 或下游 MCP 透传。ChatGPT workspace 仍需管理员按计划和
|
|
447
|
+
RBAC 开启 Developer mode。
|
|
406
448
|
|
|
407
449
|
仓库内置的自用 Skill 位于 `skills/numa-ai-registry/SKILL.md`,并随 npm 包发布。它指导 AI
|
|
408
450
|
按“认证 → 服务解析 → inspect → 安装/治理 → 复查”的顺序使用 Registry,同时明确禁止索取
|
|
409
451
|
Nacos token、绕过 403 或未经确认覆盖本地配置。对应场景评测在
|
|
410
452
|
`skills/numa-ai-registry/evals/evals.json`。
|
|
411
453
|
|
|
454
|
+
## Jenkins 流水线与生产操作
|
|
455
|
+
|
|
456
|
+
`numa pipeline` 通过平台授权代理查询和操作 Jenkins。CLI 不接收 Jenkins 用户名、Token 或
|
|
457
|
+
URL;生产 build、retry 和 stop 由服务端再次校验 `ops-admin` 与审批依据。
|
|
458
|
+
|
|
459
|
+
三个容易混淆的标识含义不同:
|
|
460
|
+
|
|
461
|
+
| 标识 | 生成方 | 用途 | 重试规则 |
|
|
462
|
+
| --- | --- | --- | --- |
|
|
463
|
+
| `requestId` | 平台 | 标识已创建的构建任务,用于 status、log、stop、retry | 新任务产生新值 |
|
|
464
|
+
| `idempotency-key` | CLI 或调用者 | 判断重复提交是否属于同一次部署 | 同一次提交重试必须复用;新部署使用新值 |
|
|
465
|
+
| `approve-id` | 审批流程或管理员 | 记录生产操作依据 | 按审批范围使用,不是任务标识 |
|
|
466
|
+
|
|
467
|
+
`idempotency-key` 是可选参数。不传时 CLI 会自动生成 UUID,并在成功结果中以
|
|
468
|
+
`idempotency_key` 返回。生产部署和自动化脚本建议显式指定:如果网络中断导致结果不明,
|
|
469
|
+
使用原值重新提交,平台会返回原任务或拒绝内容不一致的冲突,不会无意创建第二个构建。
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
numa pipeline inspect mci-devops-platform --json
|
|
473
|
+
|
|
474
|
+
numa pipeline build mci-devops-platform \
|
|
475
|
+
--tier production \
|
|
476
|
+
--branch main \
|
|
477
|
+
--approve-id OPS-APPROVAL-20260818-001 \
|
|
478
|
+
--idempotency-key mci-devops-platform-prd-20260818-001 \
|
|
479
|
+
--wait \
|
|
480
|
+
--json
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
生产操作必须提供 `--approve-id` 或至少 10 个字符的 `--production-reason`。CLI 将二者作为
|
|
484
|
+
独立治理字段提交;不能使用 `--param APPROVE_ID=...` 绕过治理。平台保存两者及审计记录,
|
|
485
|
+
仅在生产 trigger/retry 时把经过验证的 `approveId` 映射为 Jenkins `APPROVE_ID`;
|
|
486
|
+
`productionReason` 不会转发给 Jenkins。
|
|
487
|
+
|
|
488
|
+
`--wait` 和 `status --watch` 约每 2 秒轮询一次,当前没有默认超时,会持续到 `SUCCESS`、
|
|
489
|
+
`FAILED`、`ABORTED`、`CANCELLED` 或 `NOT_BUILT`。按 `Ctrl+C` 或遇到网络错误只会结束本地
|
|
490
|
+
等待,不会停止 Jenkins 构建;真正停止任务需要显式执行 `pipeline stop ... --yes`。
|
|
491
|
+
`build --wait` 仅在 `SUCCESS` 时返回退出码 0,其他终态返回非零。
|
|
492
|
+
|
|
493
|
+
保存 build 返回的 `requestId`,可以随时恢复查询:
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
numa pipeline status req_01ABC --watch
|
|
497
|
+
numa pipeline log req_01ABC --follow
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
如果没有保存 `requestId`,先按应用和环境检索构建记录,再使用返回的 `requestId`:
|
|
501
|
+
|
|
502
|
+
```bash
|
|
503
|
+
numa pipeline builds --app mci-devops-platform --tier production --limit 20 --json
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`pipeline builds` 使用游标分页:`--limit` 是每页条数,不是页码;下一页应把上一页响应中的
|
|
507
|
+
`next_cursor` 传给 `--cursor`。面向 AI 和自动化的同一套操作语义也包含在 `numa commands --json` 的
|
|
508
|
+
`operationalNotes` 字段中,因此基本流水线操作不依赖额外 Skill。
|
|
509
|
+
|
|
412
510
|
## 平台媒体服务
|
|
413
511
|
|
|
414
512
|
自 1.8.0 起,`numa media` 通过当前 Keycloak 会话调用 `/api/v1/media`。第三方
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
import type { AiRegistryClient } from "./client.js";
|
|
3
|
+
export declare function runMcpBridge(client: AiRegistryClient, options: {
|
|
4
|
+
namespace?: string;
|
|
5
|
+
instance?: string;
|
|
6
|
+
workspace?: string;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export declare function bridgeMcpTransports(local: Transport, remote: Transport): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { fetchWithKeycloakIdentity } from "../oauth.js";
|
|
5
|
+
import { AiRegistryError, redactRegistryText } from "./errors.js";
|
|
6
|
+
import { resolveRegistryNamespace } from "./namespace.js";
|
|
7
|
+
export async function runMcpBridge(client, options) {
|
|
8
|
+
const context = await resolveRegistryNamespace(client, options);
|
|
9
|
+
const router = context.capability.router;
|
|
10
|
+
if (!router?.endpoint || router.status !== "READY") {
|
|
11
|
+
throw new AiRegistryError(`Namespace ${context.namespace.namespaceKey} has no ready enterprise Router.`, "ai_registry_router_unavailable");
|
|
12
|
+
}
|
|
13
|
+
const endpoint = new URL(router.endpoint);
|
|
14
|
+
const local = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(endpoint.hostname);
|
|
15
|
+
if (endpoint.protocol !== "https:" && !(endpoint.protocol === "http:" && local)) {
|
|
16
|
+
throw new AiRegistryError("Enterprise Router endpoint must use HTTPS.", "ai_registry_router_invalid");
|
|
17
|
+
}
|
|
18
|
+
await client.checkRouter(context.namespace.id);
|
|
19
|
+
const authConfig = loadConfig("aiRegistry");
|
|
20
|
+
const remote = new StreamableHTTPClientTransport(endpoint, {
|
|
21
|
+
fetch: async (input, init) => fetchWithKeycloakIdentity(authConfig, String(input), () => ({ ...init, headers: new Headers(init?.headers) }))
|
|
22
|
+
});
|
|
23
|
+
await bridgeMcpTransports(new StdioServerTransport(), remote);
|
|
24
|
+
}
|
|
25
|
+
export async function bridgeMcpTransports(local, remote) {
|
|
26
|
+
let closed = false;
|
|
27
|
+
let resolveClosed;
|
|
28
|
+
let rejectClosed;
|
|
29
|
+
const completed = new Promise((resolvePromise, rejectPromise) => {
|
|
30
|
+
resolveClosed = resolvePromise;
|
|
31
|
+
rejectClosed = rejectPromise;
|
|
32
|
+
});
|
|
33
|
+
const fail = (error) => {
|
|
34
|
+
if (closed)
|
|
35
|
+
return;
|
|
36
|
+
closed = true;
|
|
37
|
+
rejectClosed?.(new AiRegistryError(`MCP bridge failed: ${redactRegistryText(error.message)}`, "ai_registry_bridge_failed"));
|
|
38
|
+
void Promise.allSettled([local.close(), remote.close()]);
|
|
39
|
+
};
|
|
40
|
+
const finish = () => {
|
|
41
|
+
if (closed)
|
|
42
|
+
return;
|
|
43
|
+
closed = true;
|
|
44
|
+
resolveClosed?.();
|
|
45
|
+
void Promise.allSettled([local.close(), remote.close()]);
|
|
46
|
+
};
|
|
47
|
+
local.onmessage = (message) => { void remote.send(message).catch(fail); };
|
|
48
|
+
remote.onmessage = (message) => { void local.send(message).catch(fail); };
|
|
49
|
+
local.onerror = fail;
|
|
50
|
+
remote.onerror = fail;
|
|
51
|
+
local.onclose = finish;
|
|
52
|
+
remote.onclose = finish;
|
|
53
|
+
try {
|
|
54
|
+
await remote.start();
|
|
55
|
+
await local.start();
|
|
56
|
+
await completed;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
60
|
+
await completed;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../../src/ai-registry/bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AuthConfig } from "../config.js";
|
|
2
2
|
import { type KeycloakTokenProvider } from "../oauth.js";
|
|
3
|
-
import { type JsonObject, type RegistryInstance, type RegistryNamespace } from "./schemas.js";
|
|
3
|
+
import { type JsonObject, type RegistryCapabilities, type RegistryInstance, type RegistryNamespace, type RegistryRouter } from "./schemas.js";
|
|
4
4
|
export interface AiRegistryClientDependencies {
|
|
5
5
|
fetch?: typeof fetch;
|
|
6
6
|
tokenProvider?: KeycloakTokenProvider;
|
|
@@ -9,6 +9,7 @@ export interface SkillDownload {
|
|
|
9
9
|
bytes: Buffer;
|
|
10
10
|
fileName: string;
|
|
11
11
|
sha256: string;
|
|
12
|
+
version?: string;
|
|
12
13
|
}
|
|
13
14
|
export declare class AiRegistryClient {
|
|
14
15
|
private readonly config;
|
|
@@ -18,6 +19,7 @@ export declare class AiRegistryClient {
|
|
|
18
19
|
private authenticatedFetch;
|
|
19
20
|
private json;
|
|
20
21
|
listInstances(): Promise<RegistryInstance[]>;
|
|
22
|
+
getCapabilities(): Promise<RegistryCapabilities>;
|
|
21
23
|
createInstance(input: JsonObject): Promise<RegistryInstance>;
|
|
22
24
|
updateInstance(instanceId: number, input: JsonObject): Promise<RegistryInstance>;
|
|
23
25
|
deleteInstance(instanceId: number): Promise<void>;
|
|
@@ -48,17 +50,41 @@ export declare class AiRegistryClient {
|
|
|
48
50
|
commitMsg?: string;
|
|
49
51
|
uploadAction?: string;
|
|
50
52
|
}): Promise<unknown>;
|
|
51
|
-
skillAction(namespaceId: number, skillName: string, action: "submit" | "publish" | "online" | "offline" | "scope" | "biz-tags" | "delete", body?: Record<string, unknown>): Promise<unknown>;
|
|
53
|
+
skillAction(namespaceId: number, skillName: string, action: "submit" | "publish" | "online" | "offline" | "scope" | "biz-tags" | "labels" | "delete", body?: Record<string, unknown>): Promise<unknown>;
|
|
52
54
|
listMcpServers(namespaceId: number, options?: {
|
|
53
55
|
search?: string;
|
|
54
56
|
mcpName?: string;
|
|
55
57
|
pageNo?: number;
|
|
56
58
|
pageSize?: number;
|
|
57
59
|
}): Promise<JsonObject>;
|
|
58
|
-
getMcpServer(namespaceId: number, mcpName: string,
|
|
60
|
+
getMcpServer(namespaceId: number, mcpName: string, versionOrOptions?: string | {
|
|
61
|
+
version?: string;
|
|
62
|
+
label?: string;
|
|
63
|
+
}): Promise<JsonObject>;
|
|
59
64
|
createMcpServer(namespaceId: number, specification: JsonObject): Promise<unknown>;
|
|
60
65
|
updateMcpServer(namespaceId: number, mcpName: string, specification: JsonObject): Promise<unknown>;
|
|
61
66
|
deleteMcpServer(namespaceId: number, mcpName: string, version?: string): Promise<unknown>;
|
|
67
|
+
mcpAction(namespaceId: number, mcpName: string, action: "online" | "offline" | "biz-tags", body?: Record<string, unknown>): Promise<unknown>;
|
|
68
|
+
setMcpToolEnabled(namespaceId: number, mcpName: string, toolName: string, enabled: boolean, version?: number): Promise<unknown>;
|
|
69
|
+
createSkillDraft(namespaceId: number, skillName: string, body?: Record<string, unknown>): Promise<unknown>;
|
|
70
|
+
listReleaseRequests(options?: {
|
|
71
|
+
namespaceId?: number;
|
|
72
|
+
state?: string;
|
|
73
|
+
cursor?: string;
|
|
74
|
+
limit?: number;
|
|
75
|
+
}): Promise<JsonObject>;
|
|
76
|
+
createReleaseRequest(body: JsonObject): Promise<unknown>;
|
|
77
|
+
reviewReleaseRequest(requestId: string, body: JsonObject): Promise<unknown>;
|
|
78
|
+
publishReleaseRequest(requestId: string, body?: JsonObject): Promise<unknown>;
|
|
79
|
+
getRouter(namespaceId: number): Promise<RegistryRouter>;
|
|
80
|
+
connectRouter(namespaceId: number, body?: JsonObject): Promise<unknown>;
|
|
81
|
+
checkRouter(namespaceId: number): Promise<JsonObject>;
|
|
82
|
+
reconcileNamespace(namespaceId: number): Promise<JsonObject>;
|
|
83
|
+
listAuditEvents(options?: {
|
|
84
|
+
namespaceId?: number;
|
|
85
|
+
cursor?: string;
|
|
86
|
+
limit?: number;
|
|
87
|
+
}): Promise<JsonObject>;
|
|
62
88
|
private object;
|
|
63
89
|
private instanceResponse;
|
|
64
90
|
private namespaceResponse;
|
|
@@ -4,7 +4,7 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
4
4
|
import { resolveBackendUrl } from "../backend.js";
|
|
5
5
|
import { ensureAccessToken, fetchWithKeycloakIdentity } from "../oauth.js";
|
|
6
6
|
import { AiRegistryError, redactRegistryText } from "./errors.js";
|
|
7
|
-
import { ApiErrorSchema, JsonObjectSchema, RegistryInstancesSchema, RegistryNamespacesSchema } from "./schemas.js";
|
|
7
|
+
import { ApiErrorSchema, JsonObjectSchema, RegistryCapabilitiesSchema, RegistryRouterSchema, RegistryInstancesSchema, RegistryNamespacesSchema } from "./schemas.js";
|
|
8
8
|
const API_ROOT = "/api/v1/ai-registry";
|
|
9
9
|
const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
10
10
|
const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
@@ -50,7 +50,9 @@ function apiError(response, body) {
|
|
|
50
50
|
: typeof body === "string"
|
|
51
51
|
? body
|
|
52
52
|
: response.statusText;
|
|
53
|
-
return new AiRegistryError(`AI Registry HTTP ${response.status}: ${redactRegistryText(message || "request failed")}`, parsed.success ? parsed.data.code ?? "ai_registry_api_error" : "ai_registry_api_error", response.status, parsed.success
|
|
53
|
+
return new AiRegistryError(`AI Registry HTTP ${response.status}: ${redactRegistryText(message || "request failed")}`, parsed.success ? parsed.data.code ?? "ai_registry_api_error" : "ai_registry_api_error", response.status, parsed.success
|
|
54
|
+
? parsed.data.requestId ?? response.headers.get("x-request-id") ?? undefined
|
|
55
|
+
: response.headers.get("x-request-id") ?? undefined, parsed.success ? parsed.data.details : undefined);
|
|
54
56
|
}
|
|
55
57
|
function safeIdentifier(value, label) {
|
|
56
58
|
if (!value || value.length > 160 || /[\\/?#%\u0000-\u001f\u007f]/u.test(value)) {
|
|
@@ -68,7 +70,9 @@ export class AiRegistryClient {
|
|
|
68
70
|
this.tokenProvider = dependencies.tokenProvider ?? ensureAccessToken;
|
|
69
71
|
}
|
|
70
72
|
async authenticatedFetch(path, init = {}) {
|
|
71
|
-
const url =
|
|
73
|
+
const url = /^https?:\/\//iu.test(path)
|
|
74
|
+
? resolveBackendUrl({ ...this.config, allowAbsoluteBackendUrls: true }, { method: "GET", url: path })
|
|
75
|
+
: resolveBackendUrl(this.config, { method: "GET", path });
|
|
72
76
|
const headers = new Headers(init.headers);
|
|
73
77
|
if (!headers.has("Accept"))
|
|
74
78
|
headers.set("Accept", "application/json");
|
|
@@ -91,6 +95,15 @@ export class AiRegistryClient {
|
|
|
91
95
|
}
|
|
92
96
|
return parsed.data;
|
|
93
97
|
}
|
|
98
|
+
async getCapabilities() {
|
|
99
|
+
const raw = await this.json(this.config.capabilityUrl ?? `${API_ROOT}/capabilities`);
|
|
100
|
+
assertCapabilitySafe(raw);
|
|
101
|
+
const parsed = RegistryCapabilitiesSchema.safeParse(raw);
|
|
102
|
+
if (!parsed.success) {
|
|
103
|
+
throw new AiRegistryError(`Capability response is invalid: ${parsed.error.message}`, "ai_registry_invalid_response");
|
|
104
|
+
}
|
|
105
|
+
return parsed.data;
|
|
106
|
+
}
|
|
94
107
|
async createInstance(input) {
|
|
95
108
|
return this.instanceResponse(await this.json(`${API_ROOT}/instances`, { method: "POST", body: JSON.stringify(input) }));
|
|
96
109
|
}
|
|
@@ -179,7 +192,8 @@ export class AiRegistryClient {
|
|
|
179
192
|
const disposition = response.headers.get("content-disposition") ?? "";
|
|
180
193
|
const matched = /filename="?([^";]+)"?/iu.exec(disposition)?.[1];
|
|
181
194
|
const fileName = matched && matched === basename(matched) ? matched : `${safeIdentifier(skillName, "skillName")}.zip`;
|
|
182
|
-
|
|
195
|
+
const version = response.headers.get("x-resource-version")?.trim() || undefined;
|
|
196
|
+
return { bytes, fileName, sha256, ...(version ? { version } : {}) };
|
|
183
197
|
}
|
|
184
198
|
async uploadSkill(namespaceId, filePath, options = {}) {
|
|
185
199
|
const info = await stat(filePath);
|
|
@@ -202,7 +216,7 @@ export class AiRegistryClient {
|
|
|
202
216
|
const path = `${API_ROOT}/namespaces/${namespaceId}/skills/${encodeURIComponent(safeIdentifier(skillName, "skillName"))}`;
|
|
203
217
|
if (action === "delete")
|
|
204
218
|
return this.json(path, { method: "DELETE" });
|
|
205
|
-
const method = action === "scope" || action === "biz-tags" ? "PUT" : "POST";
|
|
219
|
+
const method = action === "scope" || action === "biz-tags" || action === "labels" ? "PUT" : "POST";
|
|
206
220
|
return this.json(`${path}/${action}`, { method, body: JSON.stringify(body) });
|
|
207
221
|
}
|
|
208
222
|
async listMcpServers(namespaceId, options = {}) {
|
|
@@ -215,10 +229,15 @@ export class AiRegistryClient {
|
|
|
215
229
|
query.set("pageSize", String(options.pageSize ?? 20));
|
|
216
230
|
return this.object(`${API_ROOT}/namespaces/${namespaceId}/mcp-servers?${query}`);
|
|
217
231
|
}
|
|
218
|
-
async getMcpServer(namespaceId, mcpName,
|
|
232
|
+
async getMcpServer(namespaceId, mcpName, versionOrOptions) {
|
|
233
|
+
const options = typeof versionOrOptions === "string" ? { version: versionOrOptions } : versionOrOptions ?? {};
|
|
219
234
|
const query = new URLSearchParams();
|
|
220
|
-
if (version)
|
|
221
|
-
|
|
235
|
+
if (options.version && options.label)
|
|
236
|
+
throw new AiRegistryError("Specify version or label, not both.", "ai_registry_input_invalid");
|
|
237
|
+
if (options.version)
|
|
238
|
+
query.set("version", options.version);
|
|
239
|
+
if (options.label)
|
|
240
|
+
query.set("label", options.label);
|
|
222
241
|
const suffix = query.size ? `?${query}` : "";
|
|
223
242
|
return this.object(`${API_ROOT}/namespaces/${namespaceId}/mcp-servers/${encodeURIComponent(safeIdentifier(mcpName, "mcpName"))}${suffix}`);
|
|
224
243
|
}
|
|
@@ -235,6 +254,78 @@ export class AiRegistryClient {
|
|
|
235
254
|
const suffix = query.size ? `?${query}` : "";
|
|
236
255
|
return this.json(`${API_ROOT}/namespaces/${namespaceId}/mcp-servers/${encodeURIComponent(safeIdentifier(mcpName, "mcpName"))}${suffix}`, { method: "DELETE" });
|
|
237
256
|
}
|
|
257
|
+
async mcpAction(namespaceId, mcpName, action, body = {}) {
|
|
258
|
+
const path = `${API_ROOT}/namespaces/${namespaceId}/mcp-servers/${encodeURIComponent(safeIdentifier(mcpName, "mcpName"))}`;
|
|
259
|
+
return this.json(`${path}/${action}`, {
|
|
260
|
+
method: action === "biz-tags" ? "PUT" : "POST",
|
|
261
|
+
body: JSON.stringify(body)
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async setMcpToolEnabled(namespaceId, mcpName, toolName, enabled, version) {
|
|
265
|
+
const path = `${API_ROOT}/namespaces/${namespaceId}/mcp-servers/${encodeURIComponent(safeIdentifier(mcpName, "mcpName"))}/tools/${encodeURIComponent(safeIdentifier(toolName, "toolName"))}`;
|
|
266
|
+
return this.json(path, {
|
|
267
|
+
method: "PUT",
|
|
268
|
+
body: JSON.stringify({ enabled, ...(version == null ? {} : { version }) })
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async createSkillDraft(namespaceId, skillName, body = {}) {
|
|
272
|
+
return this.json(`${API_ROOT}/namespaces/${namespaceId}/skills/${encodeURIComponent(safeIdentifier(skillName, "skillName"))}/drafts`, { method: "POST", body: JSON.stringify(body) });
|
|
273
|
+
}
|
|
274
|
+
async listReleaseRequests(options = {}) {
|
|
275
|
+
const query = new URLSearchParams();
|
|
276
|
+
if (options.namespaceId)
|
|
277
|
+
query.set("namespaceId", String(options.namespaceId));
|
|
278
|
+
if (options.state)
|
|
279
|
+
query.set("state", safeIdentifier(options.state, "state"));
|
|
280
|
+
if (options.cursor)
|
|
281
|
+
query.set("cursor", options.cursor);
|
|
282
|
+
if (options.limit)
|
|
283
|
+
query.set("limit", String(options.limit));
|
|
284
|
+
return this.object(`${API_ROOT}/release-requests${query.size ? `?${query}` : ""}`);
|
|
285
|
+
}
|
|
286
|
+
async createReleaseRequest(body) {
|
|
287
|
+
return this.json(`${API_ROOT}/release-requests`, { method: "POST", body: JSON.stringify(body) });
|
|
288
|
+
}
|
|
289
|
+
async reviewReleaseRequest(requestId, body) {
|
|
290
|
+
return this.json(`${API_ROOT}/release-requests/${encodeURIComponent(safeIdentifier(requestId, "requestId"))}/review`, {
|
|
291
|
+
method: "POST",
|
|
292
|
+
body: JSON.stringify(body)
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async publishReleaseRequest(requestId, body = {}) {
|
|
296
|
+
return this.json(`${API_ROOT}/release-requests/${encodeURIComponent(safeIdentifier(requestId, "requestId"))}/publish`, {
|
|
297
|
+
method: "POST",
|
|
298
|
+
body: JSON.stringify(body)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
async getRouter(namespaceId) {
|
|
302
|
+
const parsed = RegistryRouterSchema.safeParse(await this.json(`${API_ROOT}/namespaces/${namespaceId}/router`));
|
|
303
|
+
if (!parsed.success)
|
|
304
|
+
throw new AiRegistryError(`Router response is invalid: ${parsed.error.message}`, "ai_registry_invalid_response");
|
|
305
|
+
return parsed.data;
|
|
306
|
+
}
|
|
307
|
+
async connectRouter(namespaceId, body = {}) {
|
|
308
|
+
return this.json(`${API_ROOT}/namespaces/${namespaceId}/router/connection`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
body: JSON.stringify(body)
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
async checkRouter(namespaceId) {
|
|
314
|
+
return this.object(`${API_ROOT}/namespaces/${namespaceId}/router/health`);
|
|
315
|
+
}
|
|
316
|
+
async reconcileNamespace(namespaceId) {
|
|
317
|
+
return this.object(`${API_ROOT}/namespaces/${namespaceId}/reconciliation`);
|
|
318
|
+
}
|
|
319
|
+
async listAuditEvents(options = {}) {
|
|
320
|
+
const query = new URLSearchParams();
|
|
321
|
+
if (options.namespaceId)
|
|
322
|
+
query.set("namespaceId", String(options.namespaceId));
|
|
323
|
+
if (options.cursor)
|
|
324
|
+
query.set("cursor", options.cursor);
|
|
325
|
+
if (options.limit)
|
|
326
|
+
query.set("limit", String(options.limit));
|
|
327
|
+
return this.object(`${API_ROOT}/audit-events${query.size ? `?${query}` : ""}`);
|
|
328
|
+
}
|
|
238
329
|
async object(path) {
|
|
239
330
|
const parsed = JsonObjectSchema.safeParse(await this.json(path));
|
|
240
331
|
if (!parsed.success) {
|
|
@@ -257,4 +348,17 @@ export class AiRegistryClient {
|
|
|
257
348
|
return parsed.data;
|
|
258
349
|
}
|
|
259
350
|
}
|
|
351
|
+
function assertCapabilitySafe(value, key = "") {
|
|
352
|
+
if (/(?:credential|password|secret|token|authorization|nacos(?:Base)?Url|baseUrl)/iu.test(key)) {
|
|
353
|
+
throw new AiRegistryError("Capability response contains a forbidden address or credential field.", "ai_registry_capability_unsafe");
|
|
354
|
+
}
|
|
355
|
+
if (Array.isArray(value)) {
|
|
356
|
+
for (const item of value)
|
|
357
|
+
assertCapabilitySafe(item, key);
|
|
358
|
+
}
|
|
359
|
+
else if (value && typeof value === "object") {
|
|
360
|
+
for (const [name, item] of Object.entries(value))
|
|
361
|
+
assertCapabilitySafe(item, name);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
260
364
|
//# sourceMappingURL=client.js.map
|