@numa-tech/numa 1.24.0 → 1.25.0

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/docs/kb-access.md CHANGED
@@ -6,9 +6,7 @@
6
6
 
7
7
  平台 Profile 提供 `services.kb` 时,CLI 使用其服务地址、身份源与 audience。没有 Profile 时需显式配置 `NUMA_KB_BASE_URL`,其值是包含网关服务前缀的根地址,例如 `https://gateway.example.com/kb-management`,不追加 `/api/v1`。有 Profile 时不能用环境变量改到其他服务。
8
8
 
9
- KB API Key 使用 `NUMA_KB_API_KEY_FILE` 指向已有的私有凭据文件,或使用 `NUMA_KB_API_KEY` 环境变量,二者不能同时配置。Key 应由知识库授权主体签发,范围覆盖目标知识库和所需操作。不要将 Key 写入命令参数或检查点。
10
-
11
- 默认同时使用 Keycloak 和 API Key。独立 KB 服务使用对应的登录缓存:
9
+ 默认 `NUMA_KB_AUTH_MODE=keycloak`,仅使用所选服务的 Keycloak Bearer JWT,不要求或发送 KB API Key。Skill 可直接执行 `numa kb`,无需注册 MCP;CLI 复用现有服务 Profile、登录缓存及 Token 刷新能力。独立 KB 服务使用对应的登录缓存:
12
10
 
13
11
  ```bash
14
12
  numa login --service kb --no-console --no-config
@@ -16,9 +14,18 @@ numa auth status --service kb --json
16
14
  numa profile --service kb --json
17
15
  ```
18
16
 
19
- 机器身份沿用 Numa 的 `NUMA_KEYCLOAK_AUTH_MODE=client_credentials` 和专用 client 配置,无需浏览器登录。对只要求 KB API Key 的部署,可显式设置 `NUMA_KB_AUTH_MODE=api-key`。CLI 不会在认证失败时自动降级或更换身份。
17
+ 机器身份沿用 Numa 的 `NUMA_KEYCLOAK_AUTH_MODE=client_credentials` 和专用 client 配置,无需浏览器登录。仅对仍支持旧 KB API Key 的部署,可显式设置 `NUMA_KB_AUTH_MODE=api-key`,再用 `NUMA_KB_API_KEY_FILE` 指向私有凭据文件,或设置 `NUMA_KB_API_KEY`(二者互斥)。该模式仅发送 Key,不发送 Bearer。默认 JWT 模式忽略这两个旧 Key 配置,即使环境中仍残留,也不会读取 Key 文件或发送双头。CLI 不会在认证失败时自动降级或更换身份。不要将凭据写入 Skill、命令参数或检查点。
18
+
19
+ Profile 的 `services.kb.authority` 是默认身份源;如平台同时下发了 `services.kb.issuerAuthorities`,可使用已有的 `--issuer` 选择其中的别名。登录、状态、请求和退出必须选择相同别名:
20
+
21
+ ```bash
22
+ numa login --service kb --issuer BUSINESS_ALIAS --no-console --no-config
23
+ numa auth status --service kb --issuer BUSINESS_ALIAS --json
24
+ numa --issuer BUSINESS_ALIAS kb collections list --json
25
+ numa auth logout --service kb --issuer BUSINESS_ALIAS
26
+ ```
20
27
 
21
- 退出 KB 登录使用 `numa auth logout --service kb`。这不会删除其他服务的缓存。
28
+ 不传 `--issuer` 时使用平台默认配置;未知别名和未下发的身份源会被拒绝,不接受任意 Issuer URL。该选项只选择服务允许的身份源,不改变 KB 服务地址。退出只删除对应 Issuer/Client/租户/Audience 的会话缓存。
22
29
 
23
30
  ## 基本操作
24
31
 
@@ -55,7 +62,7 @@ numa kb tasks status TASK --wait --timeout 600 --json
55
62
  numa kb tasks retry TASK --json
56
63
  ```
57
64
 
58
- 首次上传在传输前保存检查点;不指定 `--state-file` 时自动生成,并在回执中返回路径。检查点只保存目标绑定、文件摘要、固定请求 ID、会话和任务 ID,不保存 API Key、Token 或签名 URL。上传中断后保留该文件,使用相同文件源和凭据配置运行 `--resume`;本地文件变化或目标身份变化时拒绝恢复。
65
+ 首次上传在传输前保存检查点;不指定 `--state-file` 时自动生成,并在回执中返回路径。检查点只保存目标绑定、文件摘要、固定请求 ID、会话和任务 ID,不保存 API Key、Token 或签名 URL。上传中断后保留该文件,使用相同文件源和凭据配置运行 `--resume`;本地文件变化或目标绑定变化时拒绝恢复。绑定包含服务地址、认证模式、配置的租户、Issuer、Client 和 Audience;它不是当前登录用户的标识,不能代替 KB 对会话所有者的校验。旧的 JWT + Key 检查点因认证绑定变化会被拒绝复用;先查询原任务/会话结果,再决定是否发起新上传,勿直接重复提交。
59
66
 
60
67
  恢复是文件级:向 KB 查询哪些对象已上传,跳过已上传项,只重新传输缺失项。单次 PUT 中断后从该文件起点重新开始。CLI 不会为已提交的会话重复创建解析任务。Ctrl-C 中止当前工作并保留恢复记录。
61
68
 
@@ -82,4 +89,4 @@ npm run test:kb
82
89
  npm run root:typecheck
83
90
  ```
84
91
 
85
- 去掉 `--check` 可在审查服务端变化后更新快照。导出不连接部署环境,不加载真实业务数据。受控本地 HTTP 测试覆盖协议和文件行为,不能代替目标环境的真实网关、Keycloak、API Key 和 OSS 联调。
92
+ 去掉 `--check` 可在审查服务端变化后更新快照。导出不连接部署环境,不加载真实业务数据。受控本地 HTTP 测试覆盖协议和文件行为,不能代替目标环境的真实网关、Keycloak 和 OSS 联调(旧 Key 模式另行验证)。
@@ -0,0 +1,65 @@
1
+ # Jev 三场景接入样例
2
+
3
+ 本目录和 [numa-jev Skill](../../skills/numa-jev/SKILL.md) 是源码交付;未发布到企业仓库,未部署到目标环境。OpenRouter 仅由 DevOps 平台经指定代理调用。测试完全离线,不使用真实 Key 或推理额度。
4
+
5
+ ## 终端 Agent / WorkBuddy
6
+
7
+ 分别安装经过核验的 Skill 与 MCP 资源。安装步骤沿用 [Numa Skills/MCP 工作流](../../skills/numa-cli/references/skills-workflows.md),不要将资源名、namespace、instance、版本猜成文档中的示例值。
8
+
9
+ 工具发现后调用 `numa_jev_capabilities({})`。将 [合成请求](../../skills/numa-jev/assets/decision.json) 放入 `numa_jev_decide` 参数的 `body` 中。成功取 `structuredContent.data.answers`;HTTP API 不使用这个封装。连接、Jev 权限、上游能力分别核验。
10
+
11
+ ## ChatGPT
12
+
13
+ 目标为正式远程 HTTPS MCP + 用户 OAuth,使用同样的工具合同。现有源码的本地 stdio 服务或安装 Skill 不能证明该链路可用。上线必须补齐远程 OAuth 兼容、实际 Router 导出和客户端连接验证;工具说明承担本地 Skill 中必要的用法指导。
14
+
15
+ 验收时用真实授权测试用户检查:连接 OAuth 完成、工具可见、capabilities 可读、一次合成请求成功;无 Jev 权限的身份不能执行推理。这里只列验收步骤,不执行外部调用或创建连接。
16
+
17
+ ## K8s 自研 Agent
18
+
19
+ [platform-client.ts](platform-client.ts) 展示固定平台地址、服务令牌注入、无自动重放的 HTTP 调用。它是供复制调整的薄示例,不是已发布 SDK;业务应按自己的 schema 检查答案类型/概率并应用经过验证的阈值。网关前缀保留在 base URL 中,例如 `https://platform.example.invalid/devops`。
20
+
21
+ Node 应用可复用已核实版本的 `@numa-tech/cli-auth`,而非自己保存/解析用户 token 缓存:
22
+
23
+ ```ts
24
+ import { ensureAccessToken, type AuthConfig } from "@numa-tech/cli-auth";
25
+ import { createJevClient } from "./platform-client.js";
26
+
27
+ // All values come from the workload's reviewed environment configuration.
28
+ // JEV_OIDC_CLIENT_SECRET is injected by a K8s Secret, never committed.
29
+ const auth: AuthConfig = {
30
+ authenticationMode: "client_credentials",
31
+ discoveryUrl: process.env.JEV_OIDC_DISCOVERY_URL!,
32
+ issuerUrl: process.env.JEV_OIDC_ISSUER!,
33
+ clientId: process.env.JEV_OIDC_CLIENT_ID!,
34
+ clientSecret: process.env.JEV_OIDC_CLIENT_SECRET!,
35
+ scope: process.env.JEV_OIDC_SCOPE ?? "",
36
+ // Required by the shared config type; machine mode never writes user caches.
37
+ tokenFile: "/unused-in-client-credentials-mode",
38
+ redirectHost: "127.0.0.1",
39
+ redirectPort: 8765,
40
+ redirectPath: "/callback",
41
+ openBrowser: false
42
+ };
43
+ const client = createJevClient({
44
+ platformBaseUrl: process.env.JEV_PLATFORM_BASE_URL!,
45
+ getAccessToken: () => ensureAccessToken(auth)
46
+ });
47
+ // Call client.decide(request) inside the application's authorized workflow.
48
+ // Validate required environment variables at service startup. Do not log auth.
49
+ ```
50
+
51
+ 部署配置应包含平台/issuer 地址、独立服务 client、Secret 注入、相应 Jev 功能授权及到这些企业服务的网络权限。`OPENROUTER_API_KEY`、代理配置及代理认证仅归平台。若应用自身使用出网代理访问企业服务,按现有客户端网络配置处理;不能以此替代平台的 OpenRouter 代理绑定。
52
+
53
+ ## 离线验证与待验收项
54
+
55
+ 从 Numa 仓库根目录运行:
56
+
57
+ ```sh
58
+ node --import tsx --test examples/jev/platform-client.test.ts
59
+ ```
60
+
61
+ 测试验证路径前缀、HTTP/MCP 请求分层、每次操作令牌获取、鉴权失败不调用平台、401/403/429/5xx/断连无重放、缺少答案失败及禁止凭据 URL。合成答案只用于 transport 测试,不表示模型质量。
62
+
63
+ 示例仅保留白名单平台错误码和 HTTP 状态,有界读取错误响应;身份 SDK 的原始异常转换为 `IDENTITY_UNAVAILABLE`,且不会派发推理请求。遇到新增未知错误码仍保留状态,按平台维护后的白名单更新示例,不输出上游原文。
64
+
65
+ 目标环境上线后另外验证:用户与服务身份授权;OpenRouter 必须有代理;代理停用后无直连回退;ChatGPT OAuth 的真实往返;用量与审计归属正确。真实端到端验收会调用收费上游,须使用新配置的有效密钥与明确授权的环境。本目录离线验证不宣称这些上线项完成。
@@ -0,0 +1,136 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import test from "node:test";
4
+ import { createJevClient, JevCallError, type JevRequest } from "./platform-client.js";
5
+
6
+ const body: JevRequest = JSON.parse(await readFile(new URL("../../skills/numa-jev/assets/decision.json", import.meta.url), "utf8"));
7
+ const responseBody = {
8
+ model: "synthetic-offline-fixture",
9
+ answers: {
10
+ urgent: { type: "noul", noul: 0.9 },
11
+ department: { type: "choice", choice: "billing", probabilities: { billing: 0.85, technical: 0.05, sales: 0.05, other: 0.05 }, confidence: 0.6 },
12
+ frustration: { type: "score", score: 1.1, legend: { "0": "calm", "1": "frustrated", "2": "angry" }, probabilities: { "0": 0.1, "1": 0.7, "2": 0.2 }, confidence: 0.3 }
13
+ },
14
+ usage: { input_tokens: 0, output_tokens: 0 },
15
+ id: "offline-fixture"
16
+ };
17
+
18
+ test("K8s HTTP transport preserves the gateway prefix and sends only the platform contract", async () => {
19
+ let calls = 0;
20
+ const client = createJevClient({
21
+ platformBaseUrl: "https://platform.example.invalid/devops/",
22
+ getAccessToken: async () => "fake-platform-token",
23
+ fetch: async (input, init) => {
24
+ calls++;
25
+ assert.equal(String(input), "https://platform.example.invalid/devops/api/v1/jev/decisions");
26
+ assert.equal(init?.redirect, "error");
27
+ assert.equal(new Headers(init?.headers).get("authorization"), "Bearer fake-platform-token");
28
+ assert.deepEqual(JSON.parse(String(init?.body)), body);
29
+ assert.equal(init?.method, "POST");
30
+ return Response.json(responseBody);
31
+ }
32
+ });
33
+ assert.deepEqual(await client.decide(body), responseBody);
34
+ assert.equal(calls, 1);
35
+ });
36
+
37
+ for (const status of [401, 403, 429, 502, 503]) {
38
+ test(`HTTP ${status} never triggers a second possibly billable POST`, async () => {
39
+ let calls = 0;
40
+ const client = createJevClient({
41
+ platformBaseUrl: "https://platform.example.invalid",
42
+ getAccessToken: async () => "fake-platform-token",
43
+ fetch: async () => {
44
+ calls++;
45
+ return Response.json({ message: "private-upstream-detail" }, { status });
46
+ }
47
+ });
48
+ await assert.rejects(client.decide(body), (error: unknown) => {
49
+ assert.ok(error instanceof JevCallError);
50
+ assert.equal(error.status, status);
51
+ assert.equal(error.message.includes("private-upstream-detail"), false);
52
+ return true;
53
+ });
54
+ assert.equal(calls, 1);
55
+ });
56
+ }
57
+
58
+ test("network interruption is unknown result, not a new request or direct fallback", async () => {
59
+ let calls = 0;
60
+ const client = createJevClient({
61
+ platformBaseUrl: "https://platform.example.invalid",
62
+ getAccessToken: async () => "fake-platform-token",
63
+ fetch: async () => { calls++; throw new Error("transport detail containing credentials"); }
64
+ });
65
+ await assert.rejects(client.decide(body), { code: "RESULT_UNKNOWN" });
66
+ assert.equal(calls, 1);
67
+ });
68
+
69
+ test("missing answers cannot be consumed as a successful empty decision", async () => {
70
+ const client = createJevClient({
71
+ platformBaseUrl: "https://platform.example.invalid",
72
+ getAccessToken: async () => "fake-platform-token",
73
+ fetch: async () => Response.json({ model: "fixture", answers: {} })
74
+ });
75
+ await assert.rejects(client.decide(body), { code: "MISSING_ANSWER" });
76
+ });
77
+
78
+ test("identity failure prevents dispatch and token is acquired per logical operation", async () => {
79
+ let tokenRequests = 0;
80
+ let calls = 0;
81
+ const client = createJevClient({
82
+ platformBaseUrl: "https://platform.example.invalid",
83
+ getAccessToken: async () => {
84
+ if (++tokenRequests === 2) throw new Error("issuer raw error containing fixture-secret");
85
+ return "fake-platform-token";
86
+ },
87
+ fetch: async () => { calls++; return Response.json(responseBody); }
88
+ });
89
+ await client.decide(body);
90
+ await assert.rejects(client.decide(body), (error: unknown) => {
91
+ assert.ok(error instanceof JevCallError);
92
+ assert.equal(error.code, "IDENTITY_UNAVAILABLE");
93
+ assert.equal(error.message.includes("fixture-secret"), false);
94
+ assert.equal(error.cause, undefined);
95
+ return true;
96
+ });
97
+ assert.equal(tokenRequests, 2);
98
+ assert.equal(calls, 1);
99
+ });
100
+
101
+ test("known platform codes survive while provider details and unknown codes stay private", async () => {
102
+ for (const code of ["OUTBOUND_PROXY_REQUIRED", "JEV_AUDIT_UNAVAILABLE", "fixture-secret"]) {
103
+ const client = createJevClient({
104
+ platformBaseUrl: "https://platform.example.invalid",
105
+ getAccessToken: async () => "fake-platform-token",
106
+ fetch: async () => Response.json({ code, message: "fixture-secret" }, { status: 503 })
107
+ });
108
+ await assert.rejects(client.decide(body), (error: unknown) => {
109
+ assert.ok(error instanceof JevCallError);
110
+ assert.equal(error.code, code === "fixture-secret" ? "PLATFORM_HTTP_ERROR" : code);
111
+ assert.equal(error.status, 503);
112
+ assert.equal(error.message.includes("fixture-secret"), false);
113
+ return true;
114
+ });
115
+ }
116
+ });
117
+
118
+ test("oversized error streams are cancelled before interpreting untrusted error content", async () => {
119
+ let cancelled = false;
120
+ const client = createJevClient({
121
+ platformBaseUrl: "https://platform.example.invalid",
122
+ getAccessToken: async () => "fake-platform-token",
123
+ fetch: async () => new Response(new ReadableStream<Uint8Array>({
124
+ start(controller) { controller.enqueue(new Uint8Array(16 * 1024 + 1)); },
125
+ cancel() { cancelled = true; }
126
+ }), { status: 502 })
127
+ });
128
+ await assert.rejects(client.decide(body), { status: 502, code: "PLATFORM_HTTP_ERROR" });
129
+ assert.equal(cancelled, true);
130
+ });
131
+
132
+ test("caller credentials cannot be placed in a URL and HTTPS is required", () => {
133
+ for (const platformBaseUrl of ["http://platform.example.invalid", "https://user:secret@platform.example.invalid", "https://platform.example.invalid?token=secret"]) {
134
+ assert.throws(() => createJevClient({ platformBaseUrl, getAccessToken: async () => "unused" }), /trusted HTTPS/u);
135
+ }
136
+ });
@@ -0,0 +1,122 @@
1
+ /** Example transport for K8s services. No upstream key, proxy, or retry logic. */
2
+ export interface JevRequest {
3
+ state: string | Record<string, unknown> | unknown[];
4
+ questions: Record<string, unknown>;
5
+ model?: string;
6
+ }
7
+
8
+ export interface JevResult {
9
+ model: string;
10
+ answers: Record<string, unknown>;
11
+ usage?: Record<string, unknown>;
12
+ id?: string;
13
+ provider?: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export class JevCallError extends Error {
18
+ constructor(public readonly status: number | undefined, public readonly code: string) {
19
+ // Do not include request data, bearer tokens, or the upstream error body.
20
+ super(`Jev call failed (${status ?? "transport"}, ${code}); not retried.`);
21
+ this.name = "JevCallError";
22
+ }
23
+ }
24
+
25
+ const SAFE_PLATFORM_CODES = new Set([
26
+ "JEV_ACCESS_DENIED", "JEV_AUTHENTICATION_REQUIRED", "JEV_PERMISSION_UNAVAILABLE",
27
+ "JEV_INVALID_REQUEST", "JEV_REQUEST_TOO_LARGE", "JEV_INVALID_RESPONSE", "JEV_NOT_CONFIGURED",
28
+ "JEV_BUSY", "JEV_AUDIT_UNAVAILABLE", "JEV_BALANCE_INSUFFICIENT", "JEV_RATE_LIMITED",
29
+ "JEV_PROXY_AUTH_REQUIRED", "JEV_UPSTREAM_FORBIDDEN", "JEV_UPSTREAM_REJECTED",
30
+ "JEV_UPSTREAM_TIMEOUT", "JEV_UPSTREAM_UNAUTHORIZED", "JEV_UPSTREAM_UNAVAILABLE",
31
+ "OUTBOUND_PROXY_REQUIRED", "OUTBOUND_PROXY_UNAVAILABLE", "OUTBOUND_PROXY_AUTH_REQUIRED",
32
+ "OUTBOUND_CONNECT_FAILED", "OUTBOUND_CREDENTIAL_UNAVAILABLE", "OUTBOUND_INVALID_CONFIGURATION"
33
+ ]);
34
+
35
+ async function safeErrorCode(response: Response): Promise<string> {
36
+ const fallback = "PLATFORM_HTTP_ERROR";
37
+ const reader = response.body?.getReader();
38
+ if (!reader) return fallback;
39
+ try {
40
+ let bytes = 0;
41
+ let text = "";
42
+ const decoder = new TextDecoder();
43
+ while (true) {
44
+ const chunk = await reader.read();
45
+ if (chunk.done) break;
46
+ bytes += chunk.value.byteLength;
47
+ if (bytes > 16 * 1024) return fallback;
48
+ text += decoder.decode(chunk.value, { stream: true });
49
+ }
50
+ const value: unknown = JSON.parse(text + decoder.decode());
51
+ if (value && typeof value === "object" && "code" in value
52
+ && typeof value.code === "string" && SAFE_PLATFORM_CODES.has(value.code)) return value.code;
53
+ return fallback;
54
+ } catch {
55
+ return fallback;
56
+ } finally {
57
+ await reader.cancel().catch(() => undefined);
58
+ reader.releaseLock();
59
+ }
60
+ }
61
+
62
+ export function createJevClient(options: {
63
+ platformBaseUrl: string;
64
+ getAccessToken: () => Promise<string>;
65
+ fetch?: typeof fetch;
66
+ timeoutMs?: number;
67
+ }) {
68
+ const base = new URL(options.platformBaseUrl);
69
+ if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash) {
70
+ throw new Error("Configure a trusted HTTPS platform base URL without credentials, query, or fragment.");
71
+ }
72
+ base.pathname = `${base.pathname.replace(/\/+$/u, "")}/api/v1/jev/decisions`;
73
+ const timeoutMs = options.timeoutMs ?? 45_000;
74
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("timeoutMs must be positive.");
75
+
76
+ return {
77
+ async decide(body: JevRequest): Promise<JevResult> {
78
+ // Fetch a current service token for each operation; the identity provider
79
+ // wrapper owns expiry-aware in-memory caching, not this transport.
80
+ let token: string;
81
+ try {
82
+ token = await options.getAccessToken();
83
+ if (!token || /\s/u.test(token)) throw new Error();
84
+ } catch {
85
+ // Identity SDKs may include raw issuer responses in exceptions. No
86
+ // inference was sent, and neither the error nor its cause is propagated.
87
+ throw new JevCallError(undefined, "IDENTITY_UNAVAILABLE");
88
+ }
89
+ try {
90
+ const response = await (options.fetch ?? fetch)(base, {
91
+ method: "POST",
92
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
93
+ body: JSON.stringify(body),
94
+ redirect: "error",
95
+ signal: AbortSignal.timeout(timeoutMs)
96
+ });
97
+ if (!response.ok) {
98
+ // Avoid trusting arbitrary upstream messages or echoing request state.
99
+ throw new JevCallError(response.status, await safeErrorCode(response));
100
+ }
101
+ const value: unknown = await response.json();
102
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
103
+ throw new JevCallError(response.status, "INVALID_RESPONSE");
104
+ }
105
+ const result = value as JevResult;
106
+ if (typeof result.model !== "string" || !result.answers || typeof result.answers !== "object" || Array.isArray(result.answers)) {
107
+ throw new JevCallError(response.status, "INVALID_RESPONSE");
108
+ }
109
+ for (const name of Object.keys(body.questions)) {
110
+ if (!Object.hasOwn(result.answers, name)) {
111
+ throw new JevCallError(response.status, "MISSING_ANSWER");
112
+ }
113
+ }
114
+ return result;
115
+ } catch (error) {
116
+ if (error instanceof JevCallError) throw error;
117
+ // A transport/parse failure can happen after inference was billed.
118
+ throw new JevCallError(undefined, "RESULT_UNKNOWN");
119
+ }
120
+ }
121
+ };
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@numa-tech/numa",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Cross-platform CLI for the Numa internal developer platform with Keycloak authentication and MCP support.",
5
5
  "author": "numa-tech",
6
6
  "keywords": [
@@ -75,8 +75,8 @@
75
75
  "dependencies": {
76
76
  "@clack/prompts": "^1.7.0",
77
77
  "@modelcontextprotocol/sdk": "^1.29.0",
78
- "@numa-tech/cli-auth": "1.24.0",
79
- "@tokensrc/codex": "1.24.0",
78
+ "@numa-tech/cli-auth": "1.25.0",
79
+ "@tokensrc/codex": "1.25.0",
80
80
  "ali-oss": "^6.23.0",
81
81
  "commander": "^14.0.3",
82
82
  "ink": "^6.8.0",
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TypeSafe AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: numa-jev
3
+ license: MIT
4
+ metadata:
5
+ version: "0.1.0"
6
+ description: 通过企业 Jev MCP 与 DevOps 平台使用 TypeSafe 的结构化判断,协助终端 Agent、ChatGPT 做分类、路由、评分和验证,或为 K8s 自研 Agent 编写集成代码。按任务构造 state 与 choice、score、noul 问题,处理概率和失败;使用 Numa 用户或服务身份,不管理 OpenRouter 密钥。
7
+ ---
8
+
9
+ # Numa Jev
10
+
11
+ Jev 为应用提供可组合的语义判断,不替代写代码或对话的主模型。代码处理规则、计算、查找、授权和执行,Jev 回答需要语义理解的具体问题。
12
+
13
+ 本包基于官方 `typesafe-ai` Skill 定制,保留问题设计方法,改为企业 MCP / 平台接入。来源与许可见 [来源记录](references/upstream.md) 和 [LICENSE](LICENSE)。这是源码版本,安装、企业发布、MCP 注册和目标环境部署分别核验。
14
+
15
+ ## 选择使用方式
16
+
17
+ - **现在做判断**:读取 [MCP 调用指引](references/mcp-usage.md),使用已连接且授权的 Jev MCP 工具。终端 Agent 加载本 Skill;ChatGPT 依赖实际连接的远程工具及其说明,不能假定会加载本地 Skill 文件。
18
+ - **为应用接入 Jev**:读取 [应用集成指引](references/application-integration.md)。生成调用平台 API 或 MCP 的代码;K8s 运行时使用独立服务身份,不需要安装 Skill。
19
+ - **安装或修复连接**:加载同来源基础 Skill `numa-cli@1.4.1`,核实实际 CLI 帮助、环境和已发布资源,再完成用户授权的 Skill 安装与 MCP 配置。依赖 manifest 只声明 Skill,安装本包不会自动连接 MCP。缺少基础 Skill 时报告缺口并按既有授权恢复;不要猜测远端版本存在。
20
+
21
+ 终端、ChatGPT 和服务端调用都通过平台授权。客户端不索取 OpenRouter Key,不设置上游代理,不因连接失败直连 OpenRouter。**平台到 OpenRouter 必须走已配置的服务端出网代理**;故障交给平台维护者处理,客户端不得解除代理或改换身份。
22
+
23
+ ## 设计判断
24
+
25
+ 先明确最终应用行为,再拆出必要的判断。字符串适合简单文本;多部分上下文使用命名 JSON 字段,放入相关事实、来源、当前状态和必要规则。只提交任务需要的上下文,不加入凭据。
26
+
27
+ | 结果含义 | 问题类型 | 使用要点 |
28
+ | --- | --- | --- |
29
+ | 从候选集中选一个 | `choice` | 给出候选及说明;可能都不匹配时包含 `other` / 无匹配项 |
30
+ | 某命题是否成立 | `noul` | 每个独立标签一个问题,可同时成立;返回成立概率 |
31
+ | 在有顺序的情形间评分 | `score` | 等级描述具体情形,每级能单独理解;多个维度分别评分 |
32
+
33
+ 完整问题写进 `instructions`,候选定义写进 `criteria`。问题 ID 仅用于结果索引,不能替代问题语义。引用嵌套上下文时使用明确路径,例如 `ticket.message`。模型无法选择未提供的候选;选择原文值之前先检查候选覆盖。
34
+
35
+ 每个问题描述一个可独立使用的判断。官方支持结构化描述;企业 MCP 和平台可能有更窄的校验,以当前工具 schema 和 capabilities 为准。暂不支持某种结构时,将必要定义表述清楚,避免丢失含义,也不要绕过平台。
36
+
37
+ **同一 state 上相互独立的问题合并为一次请求**。问题不能看到同批其他答案;预先提出的分支问题须明确假设,由代码只消费适用分支。只有需要前一步答案来查资料、构造新状态或生成下一组选项时,才发下一次请求。遵守平台问题数与大小限制,统计实际用量。
38
+
39
+ ## 组合与解读
40
+
41
+ - `choice` 返回选择及各选项概率;`score` 返回等级位置与分布。Score 对 N 个等级通常在 0 到 N−1 间,可以是小数,不能当成百分比或成立概率。
42
+ - `noul` 是 0–1 的成立概率,无独立 confidence;接近 0.5 表示 yes/no 难分,不表示“中等程度”。
43
+ - Choice/Score 的 confidence 描述分布集中程度,不证明事实正确,也不授予执行权限。阈值须用用户领域样本和误判后果验证;不要把演示阈值当通用政策。
44
+ - 评分可以组合权重、排序或筛选;必须命中的规则单独判断,不能用其他高分抵消。证据与问题未变时,调整业务权重不必重新推理。
45
+ - 已知规则和最终执行留在代码/业务工作流。低置信度时按业务要求澄清或交给人/主 LLM;无需消费的分支不因低置信度影响主路径。
46
+ - 分清观察事实与推断;状态已变化时检查旧答案是否仍适用。报告类型化结果和必要用量,不编造 Jev 的推理过程。解释文字由当前 Agent 根据输入和答案组织。
47
+
48
+ ## 文档与验证
49
+
50
+ 开发集成前从 [官方索引](https://docs.typesafe.ai/llms.txt) 查相关页面,重点读取 [API](https://docs.typesafe.ai/api)、[置信度](https://docs.typesafe.ai/confidence) 和贴近业务的 cookbook;OpenRouter 的传输契约见 [Jev Decisions](https://openrouter.ai/docs/guides/community/jev)。官方直连地址、API Key 和 SDK 默认重试配置不能直接搬到企业调用中。
51
+
52
+ 需要探索设计时,可从候选值选择、证据相关性、组合评分、声明验证和状态变化后的再判断中选择适合目标的形态,不局限于分类。以代表性样本验证最终业务行为,分别定位证据缺失、判断错误、组合代码错误和服务故障。
53
+
54
+ [请求示例](assets/decision.json) 是合成客服状态,含三个独立问题。先离线验证形状和业务分支;实际调用会产生上游用量,执行范围沿用用户任务授权。平台 capability 成功不等于 OpenRouter 推理链路已验证。
@@ -0,0 +1,37 @@
1
+ {
2
+ "state": {
3
+ "ticket": {
4
+ "message": "客户说:付款失败了三天,请尽快帮忙。",
5
+ "source": "synthetic-example"
6
+ }
7
+ },
8
+ "questions": {
9
+ "urgent": {
10
+ "type": "noul",
11
+ "instructions": "ticket.message 是否明确要求紧急处理?",
12
+ "criteria": {
13
+ "true": "明确要求尽快处理或表达时间紧迫",
14
+ "false": "没有表达时间紧迫或要求尽快处理"
15
+ }
16
+ },
17
+ "department": {
18
+ "type": "choice",
19
+ "instructions": "根据 ticket.message 描述的问题,应由哪个团队先受理?",
20
+ "criteria": {
21
+ "billing": "付款、发票或退款问题",
22
+ "technical": "与付款无关的软件缺陷、故障或接口集成问题",
23
+ "sales": "购买、报价或升级问题",
24
+ "other": "以上团队都不适合,或内容不足以确定"
25
+ }
26
+ },
27
+ "frustration": {
28
+ "type": "score",
29
+ "instructions": "ticket.message 中客户表达的挫败程度如何?",
30
+ "criteria": [
31
+ "平静描述情况,没有表达不满",
32
+ "表达不满或焦急,但没有辱骂或强烈愤怒",
33
+ "表达强烈愤怒或辱骂"
34
+ ]
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "numa-jev",
4
+ "version": "0.1.0",
5
+ "dependencies": [
6
+ { "name": "numa-cli", "version": "1.4.1" }
7
+ ]
8
+ }
@@ -0,0 +1,36 @@
1
+ # 为 K8s Agent 开发集成
2
+
3
+ Skill 在开发阶段指导编码 Agent,运行容器不需要读取 Skill。应用可以使用已有 MCP 客户端调用 [同一工具合同](mcp-usage.md),也可以直接调用平台 HTTP;两者必须保留真实服务主体,不能共享某个管理员的个人登录缓存。
4
+
5
+ ## HTTP 合同
6
+
7
+ API base 使用目标环境已确认的配置,必要时包含网关服务前缀,不能默认选生产。路径:
8
+
9
+ - `GET /api/v1/jev/capabilities`:`jev-runtime-access/view`。
10
+ - `POST /api/v1/jev/decisions`:`jev-runtime-access/edit`,JSON 正文为 `{state, questions, model?}`,**HTTP 不带 MCP 的 `body` 外层**。
11
+
12
+ 成功 HTTP body 是原生结果 `{model, answers, usage, id?, provider?}`,没有 MCP 的 `structuredContent` 或 `status/data` 外层。API 失败保留 HTTP 状态与平台安全错误码,不能按成功答案解析。错误语义见 [MCP 指引](mcp-usage.md)。
13
+
14
+ 配置固定平台地址并限制重定向;使用服务端正常校验的 TLS。令牌只发到配置的企业服务端点,不传到 OpenRouter。设置超时和应用并发上限;每副本的并发限制不等于跨集群消费额度。POST 默认不自动重试,SDK/HTTP 框架的隐式重试也要关闭。
15
+
16
+ ## 服务身份
17
+
18
+ 复用团队现有的 OIDC `client_credentials` 机制:独立 confidential client、已登记服务账号、对应 Jev 功能授权,凭据由部署 Secret 注入。令牌在内存缓存并按到期刷新;不通过运行容器登录人类 CLI,不复制个人 token 文件。
19
+
20
+ Node 应用可复用 Numa 的 `@numa-tech/cli-auth` 中 `ensureAccessToken` 和 `AuthConfig`,先核对项目已有版本。配置 `authenticationMode: "client_credentials"`、服务 client ID、目标 issuer/discovery、scope,以及唯一凭据源。`clientSecret` 可由 K8s Secret 环境注入;若选 `clientSecretFile`,当前库要求当前用户拥有的普通私有文件,不接受符号链接,需留意 K8s projected Secret 的挂载形式。不要因文件读取失败改宽权限。
21
+
22
+ 代码组织为令牌提供者 + 固定平台 HTTP 客户端 + 业务结果消费,测试时注入伪令牌提供者和 transport。鉴权库负责令牌生命周期,平台再次校验权限;`client_credentials` 令牌本身不保证业务权限。401 后刷新供下一次独立请求使用,不在未知执行状态下自动重放 POST。
23
+
24
+ ## 出网代理与部署边界
25
+
26
+ K8s Agent 只需到身份服务、企业 Router 或平台 API 的网络连接。**OpenRouter 必须从 DevOps 平台经过已绑定的 `OPENROUTER` 服务代理出网**;客户端不携带代理 URL、代理密码、OpenRouter Key 或任意 provider 路由参数。
27
+
28
+ 平台部署先验证启用/密钥/授权/代理绑定。代理不可用应失败封闭,不回退直连。无密钥代理探测至多证明网络与 TLS 可达;只有一次授权的真实判断才能证明模型、账户、额度和出网链路可用。源码和离线测试不能替代目标环境验收。
29
+
30
+ 当前平台代理管理支持公网 HTTP CONNECT / HTTPS,带认证代理仅支持 HTTPS;SOCKS 和私网/sidecar 代理不在现有支持范围。实际代理地址需满足已配置的平台网络校验,不因接入失败关闭地址限制。
31
+
32
+ ## 业务验证
33
+
34
+ 先用合成/脱敏代表样本离线测试解析、缺失答案、低置信度分支及 401/403/429/超时处理,再在明确授权的环境做少量真实调用。保留每次观察的 state 版本与问题版本,防止将旧结果用于变化后的状态;不要把 state、凭据或原始上游错误写入通用日志。
35
+
36
+ 建立领域样本集测量模型误判和端到端行为:路由是否正确、不确定时是否升级处理、是否有漏掉的候选。固定规则仍由代码执行;低分、错误、超时均不能自动授予业务权限。
@@ -0,0 +1,53 @@
1
+ # 终端 Agent 与 ChatGPT
2
+
3
+ ## 连接与发现
4
+
5
+ 通过 Numa 的实际 Skills/MCP 工作流分别安装本 Skill 和配置已发布的 Jev MCP 资源。先加载基础 `numa-cli` 的启动与 Skills/MCP 参考文档,核实当前命令帮助和 registry 中的 namespace、instance、版本、Router 健康状态。资源名称与工具名称不是同一概念,不能假定 MCP 资源必定叫 `numa-jev`。
6
+
7
+ - **WorkBuddy 等终端 Agent**:由对应适配器配置企业 Router;只支持 stdio 的环境通过受支持的 Numa bridge 连接。客户端只得到 Router 连接资料,不能配置平台下游地址或上游凭据。
8
+ - **ChatGPT**:目标接入是远程 HTTPS MCP + 用户 OAuth。需先完成平台与 Router 的远程 OAuth 兼容及工具导出,再由用户连接并核验工具;本次源码开发不代表该链路已经可用。工具描述应包含 Jev 使用语义,因为 ChatGPT 未必加载本地 Skill。CLI 产生连接信息不等于已经修改 ChatGPT 的连接设置。
9
+
10
+ 使用客户端的工具发现能力,确认实际工具 schema 和授权信息。有些客户端会给工具名添加服务前缀;按发现的工具调用,不猜前缀,不依赖本地 `numa serve` 就证明远程 Router 已导出同样工具。以下名称和参数是本包配套实现的契约,旧环境缺少时报告未部署/未连接。
11
+
12
+ ## 工具契约
13
+
14
+ `numa_jev_capabilities` 无参数 `{}`,查询需要 `jev-runtime-access/view`。检查启用状态、密钥配置状态、模型与限制;能力查询不调用推理,也不能证明出网代理通畅。
15
+
16
+ `numa_jev_decide` 输入是 **`body` 包装对象**:
17
+
18
+ ```json
19
+ {
20
+ "body": {
21
+ "state": "客户说:付款失败了三天,请尽快帮忙。",
22
+ "questions": {
23
+ "urgent": {
24
+ "type": "noul",
25
+ "instructions": "客户是否明确要求尽快处理?",
26
+ "criteria": {"true": "明确要求尽快处理", "false": "没有此类诉求"}
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ 调用需要 `jev-runtime-access/edit`;获得 MCP 连接权限不自动获得 Jev 权限。`model` 可省略,让平台选默认模型。只有用户需要指定且 capability allowlist 支持时才提交;不要将 TypeSafe 直连的模型别名直接替换到 OpenRouter 协议。
34
+
35
+ 当前企业合同:1–16 个问题;Choice 2–32 个候选;Score 2–10 个等级;请求最大 256 KiB。以部署环境返回的能力及 schema 为准。state 可为字符串、对象、数组。核心示例使用字符串说明;若使用结构化 instructions/criteria,先确认目标部署支持该版本。
36
+
37
+ 成功结果读取 `structuredContent.data`,外层 `status` 应为成功状态,里面保留原生 `model`、`answers`、`usage` 及上游存在时的 `id`、`provider`。先检查 MCP `isError`,不要将错误内容当作答案;缺少预期问题答案或类型不符时停止业务分支。
38
+
39
+ ## 失败与结果
40
+
41
+ | 现象 | 下一步 |
42
+ | --- | --- |
43
+ | 工具不可见 | 检查客户端连接、Router 工具导出与 scope;报告当前缺口 |
44
+ | 401 | 检查平台用户 OAuth 或机器令牌;不复制浏览器 Cookie/令牌缓存 |
45
+ | `JEV_ACCESS_DENIED` | 检查已发布 `jev-runtime-access` 权限及正确调用身份 |
46
+ | `JEV_UPSTREAM_FORBIDDEN` | 平台维护者核对供应商账户、地区、模型;不换身份绕过 |
47
+ | 402 / 上游额度错误 | 由平台账户维护者处理;客户端不索取上游 Key |
48
+ | 429 | 区分平台并发和上游限流,记录安全错误码与 request ID |
49
+ | 网络错误、5xx、代理未配置/不可达 | 报告服务故障;平台代理需修复,不能直连回退 |
50
+
51
+ 推理调用可能计费,超时、断连或审计失败时原请求可能已完成;不自动重复 `tools/call` 或 POST。机器令牌过期可以刷新以备后续独立请求,但不据此重放结果未知的判断。需要再次评估时沿用明确任务授权并告知可能重复用量。
52
+
53
+ 输出问题结果、必要分布/用量与本次限制。低置信度和服务错误是不同情况;服务错误不能填成 0 分或“无风险”。本工具只提供判断,后续部署、发送消息或修改数据仍按原业务工作流执行。
@@ -0,0 +1,13 @@
1
+ # 来源与定制范围
2
+
3
+ 基础是 [typesafe-ai/skills](https://github.com/typesafe-ai/skills) 的 `typesafe-ai` Skill,版权为 `Copyright (c) 2026 TypeSafe AI`,MIT 许可原文随本包 [LICENSE](../LICENSE) 保留。
4
+
5
+ 本次读取本地已安装 `typesafe-ai/SKILL.md`,SHA-256:
6
+ `71ea90d7906c6554c4f4c460ef7361b2d26f59116ccdae986dc6d997b9389f52`。
7
+ 未从安装内容确认 Git commit,因此不声称对应某个上游发布版本。维护时比较该文件与新的官方 Skill,保留许可并复核改变的 API/概率语义。
8
+
9
+ 定制内容是企业认证与 MCP 接入、平台代理边界、三种客户端场景和故障处理。保留官方关于状态、问题含义、独立问题并行、概率解释及程序组合的设计方法;不复制全部在线手册。
10
+
11
+ 平台仓库旧版 `skills/numa-jev` 指导 `numa request` / Python 包装脚本调用 HTTP。本包优先指导 Agent 使用已连接的 MCP 工具;HTTP 仍供服务端应用集成。迁移时分别发布新的 Skill 和 MCP 定义,验证工具可见后切换;不覆盖旧版安装目录,也不假设同名 Skill 会自动更新。发布前检查同名资源已有版本,以实际 registry 信息确定版本,`0.1.0` 仅为当前源码版本。
12
+
13
+ 在线资料核验日期:2026-09-28。已读取文档索引、API、Score、Confidence、结构化描述、Intent routing 和 OpenRouter Jev 文档。`.md` 地址失败时使用标准页面;概念以官方资料为准,企业实际支持范围以平台能力和工具 schema 为准。