cli-validator 7.0.18 → 7.0.25

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 CHANGED
@@ -17,4 +17,10 @@ Source: https://github.com/88208555/Validator-clitax.git
17
17
 
18
18
  `validator.skill.request/1.0` 协议,端点 `https://cli.tax/Xx9ZkQmW3p`。
19
19
 
20
- 反馈:技能详情页「使用评价」支持 好评 / 差评 / 日常聊天。好评与差评计入市场口碑(跑马灯每日清理),日常消息保留 7 天。
20
+ ## 受限调用与自动评价
21
+
22
+ 使用 `npx cli-validator@latest invoke <operation> '<JSON对象>'`,或让 IDE 以 JSON stdin 调用 `npx cli-validator@latest broker`。broker 本身只需要 Brain Client HTTPS、受限身份文件和显式传入路径,不需要完整磁盘访问。要保证 IDE 看不到 token,必须把 broker 作为独立低权限账户或沙箱服务运行并只暴露受限 IPC;同一系统账户下的 `0600` 不能隔离 IDE 与 broker。
23
+
24
+ Brain Client 服务端在同一次 runtime 请求的事务中绑定真实响应、生成并持久化权威评分与评语,再返回已提交回执。broker 只验证 `feedbackReceiptId`、`feedbackInvocationId` 和权威摘要,不发起第二次评价写入,也不生成分数或评语。`not-reported`、验证不完整、P0/P1 findings、`blocked` 或 `failed` 都不得生成好评;缺凭证、缺回执、摘要不匹配、响应非法或 HTTP 失败都会显式失败。
25
+
26
+ 本地 CLI 不提供手工评分或评语提交命令,人类不能选择技能分数或填写技能评价。日常聊天不属于评价协议。
package/broker.mjs ADDED
@@ -0,0 +1,301 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { constants } from 'node:fs'
3
+ import { lstat, open } from 'node:fs/promises'
4
+ import { resolve, win32 } from 'node:path'
5
+
6
+ export const LOOKUP_TIMEOUT_MS = 8000
7
+ export const CALL_TIMEOUT_MS = 120_000
8
+ const FEEDBACK_API_PATH = '/api/v1/telemetry/skill-usage'
9
+ const TOKEN_FILE_ENV = 'CLITAX_BRAIN_CLIENT_TOKEN_FILE'
10
+ const TOKEN_FILE_VERSION = 'member-brain.client-token-file/1.0'
11
+ const AUTH_SCHEME = 'BrainClient'
12
+ const TOKEN_FILE_MAX_BYTES = 16_384
13
+ const POSIX_TOKEN_FILE_MODE = 0o600
14
+ const WINDOWS_BROKER_DIRECTORY = ['CLI.Tax', 'broker']
15
+ const FEEDBACK_COMMENT_MAX = 500
16
+ const EVALUATION_DURATION_MAX = 86_400_000
17
+ const SCORE_MIN = 0
18
+ const SCORE_MAX = 100
19
+ const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
20
+ const INVOCATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
21
+ const DIGEST_PATTERN = /^[0-9a-f]{64}$/
22
+ const PROTOCOL_STATUSES = new Set(['succeeded', 'blocked', 'failed'])
23
+ const VALIDATION_STATES = new Set(['passed', 'failed', 'incomplete'])
24
+ const EVALUATION_SCHEMA = 'skill-automatic-evaluation/1.0'
25
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
26
+ const REQUEST_SCHEMA_PATTERN = /^([A-Za-z0-9.-]+\.skill)\.request\/([0-9]+\.[0-9]+)$/
27
+
28
+ function asObject(value, label) {
29
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
30
+ throw new Error(`${label} must be an object`)
31
+ }
32
+ return value
33
+ }
34
+
35
+ function requiredString(value, label) {
36
+ const text = typeof value === 'string' ? value.trim() : ''
37
+ if (!text) throw new Error(`${label} is required`)
38
+ return text
39
+ }
40
+
41
+ function boundedInteger(value, label) {
42
+ if (!Number.isFinite(value) || value < 0 || !Number.isSafeInteger(value)) {
43
+ throw new Error(`${label} must be a non-negative safe integer`)
44
+ }
45
+ return value
46
+ }
47
+
48
+ function insideWindowsDirectory(candidate, directory) {
49
+ const relative = win32.relative(directory, candidate)
50
+ return relative === '' || (!relative.startsWith('..\\') && relative !== '..' && !win32.isAbsolute(relative))
51
+ }
52
+
53
+ export function brainClientTokenPath(environment, platform = process.platform) {
54
+ const configured = requiredString(environment[TOKEN_FILE_ENV], TOKEN_FILE_ENV)
55
+ if (platform !== 'win32') return resolve(configured)
56
+ if (!win32.isAbsolute(configured)) {
57
+ throw new Error('Windows Brain Client token file path must be absolute')
58
+ }
59
+ const localAppData = requiredString(environment.LOCALAPPDATA, 'LOCALAPPDATA')
60
+ const brokerDirectory = win32.resolve(localAppData, ...WINDOWS_BROKER_DIRECTORY)
61
+ const candidate = win32.resolve(configured)
62
+ if (!insideWindowsDirectory(candidate, brokerDirectory)) {
63
+ throw new Error(`Windows Brain Client token file must be inside ${brokerDirectory}`)
64
+ }
65
+ return candidate
66
+ }
67
+
68
+ function assertTokenFileStatus(status, platform, currentUserId) {
69
+ if (!status.isFile() || status.size < 1 || status.size > TOKEN_FILE_MAX_BYTES) {
70
+ throw new Error('Brain Client token file must be a non-empty restricted file')
71
+ }
72
+ if (platform === 'win32') return
73
+ if (!Number.isInteger(currentUserId)) {
74
+ throw new Error('Brain Client token file ownership cannot be verified')
75
+ }
76
+ if (status.uid !== currentUserId || (status.mode & 0o777) !== POSIX_TOKEN_FILE_MODE) {
77
+ throw new Error('Brain Client token file must be owned by the current user with mode 0600')
78
+ }
79
+ }
80
+
81
+ function parseTokenFile(source) {
82
+ let tokenFile
83
+ try {
84
+ tokenFile = asObject(JSON.parse(source), 'Brain Client token file')
85
+ } catch {
86
+ throw new Error('Brain Client token file must contain valid JSON')
87
+ }
88
+ const expectedKeys = ['authorizationScheme', 'endpoint', 'schemaVersion', 'token']
89
+ if (Object.keys(tokenFile).sort().join('\n') !== expectedKeys.join('\n')) {
90
+ throw new Error('Brain Client token file contains unknown or missing fields')
91
+ }
92
+ return tokenFile
93
+ }
94
+
95
+ export async function brainClientAuthorization(context, environment, dependencies = {}) {
96
+ const platform = dependencies.platform ?? process.platform
97
+ const tokenFilePath = brainClientTokenPath(environment, platform)
98
+ const inspectPath = dependencies.lstat ?? lstat
99
+ const openPath = dependencies.open ?? open
100
+ const currentUserId = platform === 'win32'
101
+ ? null
102
+ : (dependencies.getuid ?? process.getuid)?.()
103
+ const linkStatus = await inspectPath(tokenFilePath)
104
+ if (linkStatus.isSymbolicLink()) throw new Error('Brain Client token file cannot be a symlink')
105
+ const noFollow = platform === 'win32' ? 0 : (constants.O_NOFOLLOW ?? 0)
106
+ const handle = await openPath(tokenFilePath, constants.O_RDONLY | noFollow)
107
+ try {
108
+ const status = await handle.stat()
109
+ assertTokenFileStatus(status, platform, currentUserId)
110
+ const tokenFile = parseTokenFile(await handle.readFile('utf8'))
111
+ const endpoint = new URL(requiredString(tokenFile.endpoint, 'Brain Client endpoint'))
112
+ if (tokenFile.schemaVersion !== TOKEN_FILE_VERSION
113
+ || tokenFile.authorizationScheme !== AUTH_SCHEME
114
+ || endpoint.origin !== new URL(context.endpoint).origin
115
+ || endpoint.pathname !== FEEDBACK_API_PATH || endpoint.search || endpoint.hash
116
+ || endpoint.username || endpoint.password
117
+ || !TOKEN_PATTERN.test(tokenFile.token)) {
118
+ throw new Error('Brain Client token file authority is invalid')
119
+ }
120
+ return `${AUTH_SCHEME} ${tokenFile.token}`
121
+ } finally {
122
+ await handle.close()
123
+ }
124
+ }
125
+
126
+ function canonicalJson(value) {
127
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
128
+ return JSON.stringify(value)
129
+ }
130
+ if (typeof value === 'number') {
131
+ if (!Number.isFinite(value)) throw new Error('automatic evaluation contains a non-finite number')
132
+ return JSON.stringify(value)
133
+ }
134
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`
135
+ const record = asObject(value, 'automatic evaluation')
136
+ return `{${Object.keys(record).sort().map((key) => (
137
+ `${JSON.stringify(key)}:${canonicalJson(record[key])}`
138
+ )).join(',')}}`
139
+ }
140
+
141
+ function expectedResponseSchema(requestSchema) {
142
+ const matched = requiredString(requestSchema, 'skill request schemaVersion')
143
+ .match(REQUEST_SCHEMA_PATTERN)
144
+ if (!matched) throw new Error('skill request schemaVersion is invalid')
145
+ return `${matched[1]}.response/${matched[2]}`
146
+ }
147
+
148
+ function protocolResponse(protocolValue, requestEnvelope) {
149
+ const protocol = asObject(protocolValue, 'skill protocol response')
150
+ const responseSchema = expectedResponseSchema(requestEnvelope.schemaVersion)
151
+ if (protocol.schemaVersion !== responseSchema) {
152
+ throw new Error('skill protocol response schemaVersion does not match the request')
153
+ }
154
+ if (protocol.requestId !== requestEnvelope.requestId) {
155
+ throw new Error('skill protocol response requestId does not match the request')
156
+ }
157
+ const status = requiredString(protocol.status, 'skill protocol status')
158
+ if (!PROTOCOL_STATUSES.has(status)) {
159
+ throw new Error('skill protocol status must be succeeded, blocked, or failed')
160
+ }
161
+ return { protocol, responseSchema, status }
162
+ }
163
+
164
+ export function authoritativeEvaluation(value, expected) {
165
+ const evaluation = asObject(value, 'server automatic evaluation')
166
+ const expectedKeys = [
167
+ 'digest', 'durationMs', 'findingCount', 'operation', 'p0Count', 'p1Count', 'p2Count',
168
+ 'requestId', 'responseSchemaVersion', 'schemaVersion', 'score', 'status', 'userComment',
169
+ 'validation',
170
+ ]
171
+ if (Object.keys(evaluation).sort().join('\n') !== expectedKeys.join('\n')) {
172
+ throw new Error('server automatic evaluation contains unknown or missing fields')
173
+ }
174
+ if (evaluation.schemaVersion !== EVALUATION_SCHEMA
175
+ || evaluation.operation !== expected.operation
176
+ || evaluation.requestId !== expected.requestId
177
+ || evaluation.responseSchemaVersion !== expected.responseSchema
178
+ || evaluation.status !== expected.status
179
+ || !VALIDATION_STATES.has(evaluation.validation)
180
+ || typeof evaluation.userComment !== 'string' || !evaluation.userComment.trim()
181
+ || Buffer.byteLength(evaluation.userComment, 'utf8') > FEEDBACK_COMMENT_MAX
182
+ || typeof evaluation.digest !== 'string' || !DIGEST_PATTERN.test(evaluation.digest)) {
183
+ throw new Error('server automatic evaluation authority is invalid')
184
+ }
185
+ for (const field of ['durationMs', 'findingCount', 'p0Count', 'p1Count', 'p2Count', 'score']) {
186
+ boundedInteger(evaluation[field], `server automatic evaluation ${field}`)
187
+ }
188
+ if (evaluation.score < SCORE_MIN || evaluation.score > SCORE_MAX
189
+ || evaluation.durationMs > EVALUATION_DURATION_MAX
190
+ || evaluation.findingCount < evaluation.p0Count + evaluation.p1Count + evaluation.p2Count) {
191
+ throw new Error('server automatic evaluation bounds are invalid')
192
+ }
193
+ if ((evaluation.status !== 'succeeded' || evaluation.validation !== 'passed'
194
+ || evaluation.p0Count > 0 || evaluation.p1Count > 0) && evaluation.score >= 60) {
195
+ throw new Error('server automatic evaluation cannot report a positive score')
196
+ }
197
+ const { digest, ...core } = evaluation
198
+ const actualDigest = createHash('sha256').update(canonicalJson(core)).digest('hex')
199
+ if (digest !== actualDigest) throw new Error('server automatic evaluation digest is invalid')
200
+ return evaluation
201
+ }
202
+
203
+ async function responsePayload(response, label) {
204
+ try {
205
+ return asObject(await response.json(), label)
206
+ } catch {
207
+ throw new Error(`${label} is not valid JSON (HTTP ${response.status})`)
208
+ }
209
+ }
210
+
211
+ function invocationRequest(context, operation, input) {
212
+ const normalizedOperation = requiredString(operation, 'skill operation')
213
+ if (!IDENTIFIER_PATTERN.test(normalizedOperation)) {
214
+ throw new Error('skill operation is invalid')
215
+ }
216
+ expectedResponseSchema(context.schemaVersion)
217
+ return {
218
+ schemaVersion: context.schemaVersion,
219
+ requestId: `${context.runtimeCode}-${randomUUID()}`,
220
+ operation: normalizedOperation,
221
+ input: asObject(input, 'skill operation input'),
222
+ }
223
+ }
224
+
225
+ export async function invokeOfficialSkill(context, operation, input, dependencies) {
226
+ const environment = asObject(dependencies.environment, 'broker environment')
227
+ if (typeof dependencies.request !== 'function') {
228
+ throw new Error('broker request dependency is required')
229
+ }
230
+ const authorization = await brainClientAuthorization(context, environment, dependencies.credentialAccess)
231
+ const requestEnvelope = invocationRequest(context, operation, input)
232
+ let response
233
+ try {
234
+ response = await dependencies.request(context.endpoint, {
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json', Authorization: authorization },
237
+ body: JSON.stringify({ input: requestEnvelope }),
238
+ signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
239
+ })
240
+ } catch {
241
+ throw new Error(`${context.displayName} ${operation} invocation failed`)
242
+ }
243
+ const payload = await responsePayload(response, `${context.displayName} ${operation} response`)
244
+ if (!response.ok || payload.ok !== true) {
245
+ throw new Error(`${context.displayName} ${operation} failed: HTTP ${response.status}`)
246
+ }
247
+ const invocationId = payload.feedbackInvocationId
248
+ if (typeof invocationId !== 'string' || !INVOCATION_PATTERN.test(invocationId)) {
249
+ throw new Error(`${context.displayName} ${operation} response is missing a valid feedbackInvocationId`)
250
+ }
251
+ const feedbackReceiptId = payload.feedbackReceiptId
252
+ const feedbackRequestId = payload.feedbackRequestId
253
+ if (typeof feedbackReceiptId !== 'string' || !INVOCATION_PATTERN.test(feedbackReceiptId)
254
+ || feedbackRequestId !== `automatic-${invocationId}`) {
255
+ throw new Error(`${context.displayName} ${operation} response is missing a committed feedback receipt`)
256
+ }
257
+ const protocolAuthority = protocolResponse(payload.output, requestEnvelope)
258
+ const evaluation = authoritativeEvaluation(payload.feedbackEvaluation, {
259
+ operation: requestEnvelope.operation,
260
+ requestId: requestEnvelope.requestId,
261
+ responseSchema: protocolAuthority.responseSchema,
262
+ status: protocolAuthority.status,
263
+ })
264
+ const feedback = {
265
+ id: feedbackReceiptId,
266
+ requestId: feedbackRequestId,
267
+ duplicated: false,
268
+ }
269
+ return { response: payload, invocationId, evaluation, feedback }
270
+ }
271
+
272
+ export async function callOfficialSkill(context, operation, input, dependencies) {
273
+ return (await invokeOfficialSkill(context, operation, input, dependencies)).response
274
+ }
275
+
276
+ export function invokeCommandInput(args) {
277
+ const operation = requiredString(args[1], 'skill operation')
278
+ const source = args.slice(2).join(' ').trim()
279
+ if (!source) return { operation, input: {} }
280
+ try {
281
+ return { operation, input: asObject(JSON.parse(source), 'skill operation input') }
282
+ } catch {
283
+ throw new Error('skill operation input must be a JSON object')
284
+ }
285
+ }
286
+
287
+ export function brokerCommandInput(source) {
288
+ let parsed
289
+ try {
290
+ parsed = asObject(JSON.parse(source), 'broker request')
291
+ } catch {
292
+ throw new Error('broker request must be a JSON object')
293
+ }
294
+ if (Object.keys(parsed).some((key) => !['operation', 'input'].includes(key))) {
295
+ throw new Error('broker request contains unknown fields')
296
+ }
297
+ return {
298
+ operation: requiredString(parsed.operation, 'skill operation'),
299
+ input: asObject(parsed.input, 'skill operation input'),
300
+ }
301
+ }
package/installer.mjs CHANGED
@@ -2,29 +2,33 @@
2
2
  * 八个官方技能共用这一份安装器。packages/*-cli/installer.mjs 必须与本文件字节一致。
3
3
  * 禁止第二套超时、第二套版本来源、第二套 bin 名。
4
4
  */
5
- import { randomUUID } from 'node:crypto'
6
- import { constants, existsSync, readFileSync } from 'node:fs'
7
- import { cp, lstat, mkdir, open, rm, writeFile } from 'node:fs/promises'
5
+ import { existsSync, readFileSync } from 'node:fs'
6
+ import { cp, mkdir, rm, writeFile } from 'node:fs/promises'
8
7
  import { dirname, join, resolve } from 'node:path'
9
8
  import { stdin, stdout } from 'node:process'
10
9
  import { createInterface } from 'node:readline/promises'
11
10
  import { fileURLToPath } from 'node:url'
11
+ import {
12
+ LOOKUP_TIMEOUT_MS,
13
+ brokerCommandInput,
14
+ invokeCommandInput,
15
+ invokeOfficialSkill,
16
+ } from './broker.mjs'
17
+
18
+ export {
19
+ CALL_TIMEOUT_MS,
20
+ LOOKUP_TIMEOUT_MS,
21
+ authoritativeEvaluation,
22
+ brainClientAuthorization,
23
+ brainClientTokenPath,
24
+ brokerCommandInput,
25
+ callOfficialSkill,
26
+ invokeCommandInput,
27
+ invokeOfficialSkill,
28
+ } from './broker.mjs'
12
29
 
13
- export const LOOKUP_TIMEOUT_MS = 8000
14
- export const CALL_TIMEOUT_MS = 120_000
15
30
  const INSTALL_META = 'install-meta.json'
16
- const FEEDBACK_API_PATH = '/api/v1/telemetry/skill-usage'
17
- const BRAIN_CLIENT_TOKEN_FILE_ENV = 'CLITAX_BRAIN_CLIENT_TOKEN_FILE'
18
- const BRAIN_CLIENT_TOKEN_FILE_VERSION = 'member-brain.client-token-file/1.0'
19
- const BRAIN_CLIENT_AUTH_SCHEME = 'BrainClient'
20
- const BRAIN_CLIENT_TOKEN_FILE_MAX_BYTES = 16_384
21
- const BRAIN_CLIENT_TOKEN_FILE_MODE = 0o600
22
- const FEEDBACK_COMMENT_MAX = 500
23
- const FEEDBACK_SCORE_MIN = 0
24
- const FEEDBACK_SCORE_MAX = 100
25
- const BRAIN_CLIENT_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/
26
- const FEEDBACK_INVOCATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
27
- const FEEDBACK_SCORE_PATTERN = /^(?:0|[1-9]\d{0,2})$/
31
+ const BROKER_STDIN_MAX_BYTES = 1_048_576
28
32
 
29
33
  function asObject(value, label) {
30
34
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -94,138 +98,6 @@ export async function fetchLatestVersion(context) {
94
98
  }
95
99
  }
96
100
 
97
- export async function callOfficialSkill(context, operation, input) {
98
- const requestId = `${context.npmName}-${Date.now()}`
99
- const response = await fetch(context.endpoint, {
100
- method: 'POST',
101
- headers: { 'Content-Type': 'application/json' },
102
- body: JSON.stringify({
103
- input: {
104
- schemaVersion: context.schemaVersion,
105
- requestId,
106
- operation,
107
- input,
108
- },
109
- }),
110
- signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
111
- })
112
- let payload
113
- try {
114
- payload = await response.json()
115
- } catch {
116
- throw new Error(`${context.displayName} ${operation} failed: non-JSON response (HTTP ${response.status}). Check ${context.endpoint}.`)
117
- }
118
- if (!response.ok || payload?.ok !== true) {
119
- const message = payload?.error?.message
120
- if (typeof message !== 'string' || !message.trim()) {
121
- throw new Error(`${context.displayName} ${operation} failed: HTTP ${response.status}`)
122
- }
123
- throw new Error(`${context.displayName} ${operation} failed: ${message}`)
124
- }
125
- return payload
126
- }
127
-
128
- export function feedbackCommandInput(args) {
129
- const invocationId = requiredString(args[1], 'feedback invocation id')
130
- if (!FEEDBACK_INVOCATION_PATTERN.test(invocationId)) {
131
- throw new Error('feedback invocation id must be the UUID returned by a real skill response')
132
- }
133
- const scoreText = requiredString(args[2], 'feedback score')
134
- if (!FEEDBACK_SCORE_PATTERN.test(scoreText)) {
135
- throw new Error(`feedback score must be an integer between ${FEEDBACK_SCORE_MIN} and ${FEEDBACK_SCORE_MAX}`)
136
- }
137
- const score = Number(scoreText)
138
- if (!Number.isInteger(score) || score < FEEDBACK_SCORE_MIN || score > FEEDBACK_SCORE_MAX) {
139
- throw new Error(`feedback score must be between ${FEEDBACK_SCORE_MIN} and ${FEEDBACK_SCORE_MAX}`)
140
- }
141
- const userComment = args.slice(3).join(' ').trim()
142
- if (!userComment) throw new Error('feedback comment is required')
143
- if (userComment.length > FEEDBACK_COMMENT_MAX) {
144
- throw new Error(`feedback comment must be at most ${FEEDBACK_COMMENT_MAX} characters`)
145
- }
146
- return { invocationId, score, userComment }
147
- }
148
-
149
- async function brainClientAuthorization(context, environment) {
150
- const configuredPath = typeof environment[BRAIN_CLIENT_TOKEN_FILE_ENV] === 'string'
151
- ? environment[BRAIN_CLIENT_TOKEN_FILE_ENV].trim() : ''
152
- if (!configuredPath) throw new Error(`${BRAIN_CLIENT_TOKEN_FILE_ENV} is required`)
153
- if (process.platform === 'win32' || typeof process.getuid !== 'function') {
154
- throw new Error('Brain Client token file ownership cannot be verified')
155
- }
156
- const tokenFilePath = resolve(configuredPath)
157
- const linkStatus = await lstat(tokenFilePath)
158
- if (linkStatus.isSymbolicLink()) throw new Error('Brain Client token file cannot be a symlink')
159
- const handle = await open(tokenFilePath, constants.O_RDONLY | constants.O_NOFOLLOW)
160
- try {
161
- const status = await handle.stat()
162
- if (!status.isFile() || status.uid !== process.getuid()
163
- || (status.mode & 0o777) !== BRAIN_CLIENT_TOKEN_FILE_MODE
164
- || status.size < 1 || status.size > BRAIN_CLIENT_TOKEN_FILE_MAX_BYTES) {
165
- throw new Error('Brain Client token file must be owned by the current user with mode 0600')
166
- }
167
- const tokenFile = asObject(JSON.parse(await handle.readFile('utf8')), 'Brain Client token file')
168
- const expectedKeys = ['authorizationScheme', 'endpoint', 'schemaVersion', 'token']
169
- if (Object.keys(tokenFile).sort().join('\n') !== expectedKeys.join('\n')) {
170
- throw new Error('Brain Client token file contains unknown or missing fields')
171
- }
172
- const endpoint = new URL(requiredString(tokenFile.endpoint, 'Brain Client endpoint'))
173
- if (tokenFile.schemaVersion !== BRAIN_CLIENT_TOKEN_FILE_VERSION
174
- || tokenFile.authorizationScheme !== BRAIN_CLIENT_AUTH_SCHEME
175
- || endpoint.origin !== new URL(context.endpoint).origin
176
- || endpoint.pathname !== FEEDBACK_API_PATH || endpoint.search || endpoint.hash
177
- || endpoint.username || endpoint.password
178
- || !BRAIN_CLIENT_TOKEN_PATTERN.test(tokenFile.token)) {
179
- throw new Error('Brain Client token file authority is invalid')
180
- }
181
- return `${BRAIN_CLIENT_AUTH_SCHEME} ${tokenFile.token}`
182
- } finally {
183
- await handle.close()
184
- }
185
- }
186
-
187
- export async function submitOfficialSkillFeedback(context, args, environment, request) {
188
- const input = feedbackCommandInput(args)
189
- const authorization = await brainClientAuthorization(context, environment)
190
- const requestId = `${context.runtimeCode}-${randomUUID()}`
191
- let response
192
- try {
193
- response = await request(new URL(FEEDBACK_API_PATH, context.endpoint), {
194
- method: 'POST',
195
- headers: {
196
- 'Content-Type': 'application/json',
197
- Authorization: authorization,
198
- },
199
- body: JSON.stringify({
200
- requestId,
201
- skillId: context.runtimeCode,
202
- invocationId: input.invocationId,
203
- score: input.score,
204
- userComment: input.userComment,
205
- }),
206
- signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS),
207
- })
208
- } catch {
209
- throw new Error('cli.tax feedback request failed')
210
- }
211
- let payload
212
- try {
213
- payload = asObject(await response.json(), 'cli.tax feedback response')
214
- } catch (error) {
215
- if (error instanceof Error && error.message.startsWith('cli.tax feedback response')) throw error
216
- throw new Error(`cli.tax feedback failed: non-JSON response (HTTP ${response.status})`)
217
- }
218
- if (!response.ok || payload.ok !== true) {
219
- throw new Error(`cli.tax feedback failed: HTTP ${response.status}`)
220
- }
221
- if (payload.requestId !== requestId || typeof payload.id !== 'string'
222
- || !FEEDBACK_INVOCATION_PATTERN.test(payload.id)
223
- || typeof payload.duplicated !== 'boolean') {
224
- throw new Error('cli.tax feedback response authority is invalid')
225
- }
226
- return { id: payload.id, requestId, duplicated: payload.duplicated }
227
- }
228
-
229
101
  export async function installOfficialSkill(context, explicit) {
230
102
  const target = installTarget(context.skillName, explicit)
231
103
  await mkdir(target, { recursive: true })
@@ -283,21 +155,52 @@ export function defaultUsage(context, extraLines) {
283
155
  ` npx ${context.npmName}@latest check [directory]`,
284
156
  ' Check whether the installed skill has a newer version.',
285
157
  ` npx ${context.npmName}@latest run`,
286
- ' Run the skill handshake: discover capabilities and collect intake answers.',
158
+ " Run this skill's applicability or onboarding flow; only a real HTTP invocation can trigger automatic evaluation.",
159
+ ` npx ${context.npmName}@latest invoke <operation> <JSON-object>`,
160
+ ' Invoke through the restricted local broker; a valid real HTTP invocation submits one authority-bound evaluation.',
161
+ ` npx ${context.npmName}@latest broker`,
162
+ ' Read one {"operation":"...","input":{...}} request from JSON stdin.',
163
+ 'Credential: CLITAX_BRAIN_CLIENT_TOKEN_FILE (the broker reads it; never pass the token).',
287
164
  `Endpoint: ${context.endpoint}`,
288
165
  ]
289
166
  if (extraLines?.length) lines.push('', ...extraLines)
290
167
  return lines.join('\n')
291
168
  }
292
169
 
170
+ function brokerDependencies() {
171
+ return { environment: process.env, request: fetch }
172
+ }
173
+
174
+ async function readBrokerSource(input) {
175
+ let source = ''
176
+ for await (const chunk of input) {
177
+ source += chunk
178
+ if (Buffer.byteLength(source, 'utf8') > BROKER_STDIN_MAX_BYTES) {
179
+ throw new Error(`broker request must be at most ${BROKER_STDIN_MAX_BYTES} bytes`)
180
+ }
181
+ }
182
+ if (!source.trim()) throw new Error('broker request is required on stdin')
183
+ return source
184
+ }
185
+
186
+ async function runBrokerInvocation(context, commandInput) {
187
+ const invocation = await invokeOfficialSkill(
188
+ context, commandInput.operation, commandInput.input, brokerDependencies(),
189
+ )
190
+ console.log(JSON.stringify(invocation))
191
+ return invocation
192
+ }
193
+
293
194
  export async function runIntakeHandshake(context, spec) {
294
- const capabilities = await callOfficialSkill(context, 'capabilities', {})
195
+ const invocation = await invokeOfficialSkill(context, 'capabilities', {}, brokerDependencies())
196
+ const capabilities = invocation.response
295
197
  const output = capabilities.output && typeof capabilities.output === 'object' ? capabilities.output : {}
296
198
  const skill = output.skill && typeof output.skill === 'object' ? output.skill : {}
297
199
  const version = typeof skill.version === 'string' && skill.version.trim()
298
200
  ? skill.version.trim()
299
201
  : context.skillVersion
300
202
  console.log(`${context.displayName} ${version}`)
203
+ console.log(`Automatic feedback accepted: ${invocation.feedback.id}`)
301
204
  if (typeof spec.afterCapabilities === 'function') spec.afterCapabilities(output)
302
205
  const readline = createInterface({ input: stdin, output: stdout })
303
206
  const answers = []
@@ -340,9 +243,9 @@ export async function dispatchOfficialSkillCli(options) {
340
243
  if (command === 'install') await installOfficialSkill(context, argument)
341
244
  else if (command === 'check') await checkOfficialSkill(context, argument)
342
245
  else if (command === 'run') await options.runCommand(context)
343
- else if (command === 'feedback') {
344
- const receipt = await submitOfficialSkillFeedback(context, args, process.env, fetch)
345
- console.log(`${context.displayName} feedback accepted: ${receipt.id}`)
246
+ else if (command === 'invoke') await runBrokerInvocation(context, invokeCommandInput(args))
247
+ else if (command === 'broker') {
248
+ await runBrokerInvocation(context, brokerCommandInput(await readBrokerSource(stdin)))
346
249
  }
347
250
  else if (command === 'help' || command === '--help' || command === '-h') {
348
251
  console.log(options.usage ? options.usage(context) : defaultUsage(context, options.extraUsageLines))
package/package.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "files": [
7
7
  "cli.mjs",
8
8
  "installer.mjs",
9
+ "broker.mjs",
9
10
  "README.md",
10
11
  "skill/SKILL.md",
11
12
  "skill/skill.json"
@@ -17,5 +18,5 @@
17
18
  "url": "https://github.com/88208555/Validator-clitax.git"
18
19
  },
19
20
  "type": "module",
20
- "version": "7.0.18"
21
+ "version": "7.0.25"
21
22
  }
package/skill/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: '交付前质量门禁:三道防线(静态/动态/对抗)递
5
5
 
6
6
  # Validator
7
7
 
8
- Package version: v7.0.18
8
+ Package version: v7.0.25
9
9
 
10
10
  Validator 是技能链最后一站,只消费冻结目标和真实执行证据;模型解释没有裁判权。
11
11
 
@@ -95,15 +95,15 @@ GoldenBaseline 只有 `frozen: true` 才有效。来源只允许 `repository-com
95
95
  - Swarm 只转运 TestEvidence 和返工任务,不能把 worker 自报提升为终审证据。
96
96
  - Calctool final-gate 是生成方自检,Validator 仍要求独立可信 receipt。
97
97
 
98
- ## 反馈通道
98
+ ## 受限调用与自动评价闭环
99
99
 
100
- 评价只能由 IDE / 智能体自动回传,严禁人类干预:
100
+ - IDE / 智能体必须通过本包 `invoke` 或 JSON-stdin `broker` 调用,不得直接拼装技能 HTTP 请求,也不得读取 BrainClient token。
101
+ - broker 从 `CLITAX_BRAIN_CLIENT_TOKEN_FILE` 读取身份;macOS/Linux 文件必须为当前 broker 账户所有且权限 `0600`,Windows 文件必须位于受限 `%LOCALAPPDATA%\CLI.Tax\broker` 目录。
102
+ - broker 只需要 Brain Client HTTPS、受限身份文件和调用方显式传入的路径,本身不需要完整磁盘访问。若要保证 IDE 无法读取身份文件,必须把 broker 放进独立低权限系统账户或沙箱服务,并只暴露受限 IPC;broker 与 IDE 同账户运行时,`0600` 不能隔离二者,禁止声称令牌已隔离。
103
+ - broker 只用 `Authorization: BrainClient …` 发起一次 runtime 请求。HTTP 成功后必须保留响应顶层原始 `feedbackReceiptId`、`feedbackInvocationId` 和 `feedbackEvaluation.digest`,不得生成、猜测、复用或跨调用转移。
104
+ - Brain Client 服务端必须严格绑定请求/响应的 `requestId` 和 `schemaVersion`,再根据真实状态、验证结果、服务端耗时与 findings 生成并持久化权威评分、评语和摘要。broker 不得生成分数或评语。
105
+ - 同一次 runtime 请求在服务端事务内生成并持久化评价,再返回 `feedbackReceiptId`、`feedbackInvocationId` 和权威摘要;broker 只验证已提交回执,不发起第二次评价写入。`not-reported`、验证不完整、P0/P1 findings、`blocked` 或 `failed` 都不得生成好评。
106
+ - 缺少凭证或 ID、身份不匹配、摘要不匹配、响应非法以及任何 HTTP 失败都必须显式失败,不得静默、不重试成重复评价。
107
+ - 本地 CLI 不提供手工评分或评语提交命令,人类不得选择技能分数或填写技能评价;日常聊天不属于评价协议。
101
108
 
102
- 1. 仅当一次真实技能 HTTP 调用成功,并且响应顶层包含 `feedbackInvocationId` 时,调用方才可进入评价流程。
103
- 2. 调用方必须使用本次响应的原始 ID,按真实执行结果生成整数分数(0–100)和真实评语,再自动执行:
104
- `npx cli-validator@latest feedback <feedbackInvocationId> <score> <comment...>`
105
- 3. 自动回传必须使用会员中心 Brain 客户端生成的同一身份文件,并通过 `CLITAX_BRAIN_CLIENT_TOKEN_FILE` 指向该 `0600` 文件;服务端以会员账号实名入库,不接受匿名评价。
106
- 4. 禁止伪造、猜测、复用或跨调用转移 `feedbackInvocationId`;禁止让人类选择分数、填写评价或代为执行命令。
107
- 5. 缺少会员客户端身份或 ID、分数越界、空评语、响应不合法及任何 HTTP 失败都必须视为回传失败,不得记为成功。
108
-
109
- 日常交流走技能详情页的独立聊天通道,不使用 `feedback` 命令,也不计入评价、评分或首页跑马灯。
109
+ 调用示例:`npx cli-validator@latest invoke <operation> '<JSON对象>'`。IDE 集成可向 `npx cli-validator@latest broker` 的 stdin 发送 `{"operation":"capabilities","input":{}}`。
package/skill/skill.json CHANGED
@@ -5,6 +5,6 @@
5
5
  "method": "POST",
6
6
  "name": "validator",
7
7
  "type": "Skill",
8
- "version": "v7.0.18",
8
+ "version": "v7.0.25",
9
9
  "schemaVersion": "validator.skill.request/1.0"
10
10
  }