cli-confirm-protocol 7.0.19 → 7.0.28
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 +18 -2
- package/broker.mjs +301 -0
- package/installer.mjs +57 -154
- package/package.json +2 -1
- package/skill/SKILL.md +29 -16
- package/skill/skill.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
# Confirm Protocol
|
|
1
|
+
# 确认协议 / Confirm Protocol / Протокол подтверждения
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI.Tax 结构化确认协议官方安装包:统一确认、单选、多选与输入交互,返回经过校验的答案、聊天兼容文本和审计记录。
|
|
4
|
+
|
|
5
|
+
Official CLI.Tax installer for structured confirmation, single-choice, multi-choice, and input interactions with validated answers, chat-compatible rendering, and audit records.
|
|
6
|
+
|
|
7
|
+
Официальный установщик CLI.Tax для структурированных подтверждений, одиночного и множественного выбора и ввода с проверенными ответами, текстом для чата и аудитом.
|
|
4
8
|
|
|
5
9
|
```bash
|
|
6
10
|
npx cli-confirm-protocol@latest install
|
|
7
11
|
```
|
|
8
12
|
|
|
9
13
|
Source: https://github.com/88208555/confirm-protocol-clitax.git
|
|
14
|
+
|
|
15
|
+
## 受限调用与自动评价 / Restricted invocation and automatic evaluation / Ограниченный вызов и автооценка
|
|
16
|
+
|
|
17
|
+
IDE 通过 `invoke` 或 JSON-stdin `broker` 调用。broker 本身只需要 Brain Client HTTPS、受限身份文件和显式传入路径,不需要完整磁盘访问。要保证 IDE 看不到令牌,必须把 broker 作为独立低权限账户或沙箱服务运行并只暴露受限 IPC;同一账户下的 `0600` 不能隔离 IDE 与 broker。服务端在同一次 runtime 请求中事务提交权威评价并返回回执,broker 只验证回执,不发起第二次评价写入。
|
|
18
|
+
|
|
19
|
+
Use `npx cli-confirm-protocol@latest invoke <operation> '<JSON object>'`, or send JSON stdin to `npx cli-confirm-protocol@latest broker`. The broker itself needs only Brain Client HTTPS, its restricted identity file, and explicitly supplied paths; it does not need full-disk access. To keep the token inaccessible to the IDE, run the broker under a separate least-privilege account or sandbox service and expose only restricted IPC. Mode `0600` does not isolate two processes running as the same account.
|
|
20
|
+
|
|
21
|
+
IDE вызывает пакет через `invoke` или JSON-stdin `broker`. Самому broker нужны только HTTPS Brain Client, ограниченный файл идентификации и явно переданные пути; полный доступ к диску не нужен. Чтобы IDE не мог прочитать токен, broker должен работать под отдельной малопривилегированной учётной записью или в sandbox-сервисе с ограниченным IPC. Режим `0600` не изолирует процессы одной учётной записи.
|
|
22
|
+
|
|
23
|
+
The Brain Client server binds the real response and atomically persists the authoritative score and comment within the same runtime request, then returns a committed receipt. The broker verifies `feedbackReceiptId`, `feedbackInvocationId`, and the authoritative digest; it makes no second evaluation write and never creates a score or comment. Not-reported or incomplete validation, P0/P1 findings, blocked, and failed results cannot be positive. Missing credentials or receipts, digest mismatches, invalid responses, and HTTP failures fail explicitly.
|
|
24
|
+
|
|
25
|
+
The local CLI has no command for manually submitting a score or evaluation comment. Humans cannot choose a skill score or write skill evaluation content. Daily chat is outside the evaluation protocol.
|
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 {
|
|
6
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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 === '
|
|
344
|
-
|
|
345
|
-
|
|
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/confirm-protocol-clitax.git"
|
|
18
19
|
},
|
|
19
20
|
"type": "module",
|
|
20
|
-
"version": "7.0.
|
|
21
|
+
"version": "7.0.28"
|
|
21
22
|
}
|
package/skill/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: confirm-protocol
|
|
3
|
-
description: '把技能需要的用户确认转换为统一 interaction
|
|
3
|
+
description: '把技能需要的用户确认转换为统一 interaction 协议,并返回结构化答案、聊天兼容文本、低风险记忆状态、批次和审计记录;用于确认、单选、多选或输入交互,不用于普通聊天,也不代替客户端 UI。Convert skill-required confirmation into one interaction protocol with structured answers, chat-compatible text, low-risk memory state, batches, and audit records; use for confirm, choice, multi-choice, or input interactions, not ordinary chat or as a replacement for client UI. Преобразует требуемое навыком подтверждение в единый interaction-протокол со структурированными ответами, текстом для чата, низкорисковой памятью, пакетами и аудитом; применяется к подтверждению, выбору, множественному выбору и вводу, но не к обычному чату и не вместо UI клиента.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Confirm Protocol
|
|
6
|
+
# 确认协议 / Confirm Protocol / Протокол подтверждения
|
|
7
7
|
|
|
8
|
-
Package version: v7.0.
|
|
8
|
+
Package version: v7.0.28
|
|
9
9
|
|
|
10
10
|
Endpoint: https://cli.tax/Cf8Pr7Tm2Q
|
|
11
11
|
|
|
@@ -13,7 +13,13 @@ Request schema: `confirm-protocol.skill.request/1.0`
|
|
|
13
13
|
|
|
14
14
|
Confirm Protocol 是技能链的统一确认交互层。它只定义协议并验证答案,不替代业务技能,也不把“等待用户”伪装成成功。
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
中文:只有业务技能确实需要确认、单选、多选或文本输入时才调用。高风险请求必须逐项等待真人决定,永远不能记忆、批量或默认放行;没有原生界面时返回编号聊天内容,不伪称已经弹窗。
|
|
17
|
+
|
|
18
|
+
English: Call this skill only when another skill genuinely needs confirmation, one choice, multiple choices, or typed input. High-risk requests always wait for an explicit human decision and can never be remembered, batched, or default-approved. Without a native UI, return the numbered chat rendering and never claim a dialog appeared.
|
|
19
|
+
|
|
20
|
+
Русский: Навык вызывается только когда другому навыку действительно нужны подтверждение, одиночный или множественный выбор либо текстовый ввод. Запрос высокого риска всегда ждёт явного решения человека и никогда не запоминается, не объединяется в пакет и не одобряется по умолчанию. Без нативного UI возвращается нумерованный текст для чата; нельзя утверждать, что окно уже показано.
|
|
21
|
+
|
|
22
|
+
## 强制流程 / Required sequence / Обязательная последовательность
|
|
17
23
|
|
|
18
24
|
1. 调用 `capabilities`,读取全部 `operationSchemas` 与真实能力状态。
|
|
19
25
|
2. 业务技能构造 `confirm.interaction/1.0`,调用 `interaction-request`。
|
|
@@ -21,7 +27,7 @@ Confirm Protocol 是技能链的统一确认交互层。它只定义协议并验
|
|
|
21
27
|
4. 用户作答后调用 `interaction-answer`,得到不可歧义的 `callbackRequest` 和审计记录。
|
|
22
28
|
5. 只有 `risk=low + rememberable=true` 才能调用 `memory-set`。高风险永远不可记忆、不可批量、不可默认超时放行。
|
|
23
29
|
|
|
24
|
-
## 操作
|
|
30
|
+
## 操作 / Operations / Операции
|
|
25
31
|
|
|
26
32
|
- `capabilities` / `help`:能力、JSON Schema 与实现边界。
|
|
27
33
|
- `interaction-request`:验证并返回 interaction 与聊天降级文本。
|
|
@@ -31,27 +37,34 @@ Confirm Protocol 是技能链的统一确认交互层。它只定义协议并验
|
|
|
31
37
|
- `batch-request`:每批最多三个低风险确认;高风险始终独立。
|
|
32
38
|
- `audit-query`:查询调用方提供的审计记录。
|
|
33
39
|
|
|
34
|
-
## 风险规则
|
|
40
|
+
## 风险规则 / Risk rules / Правила риска
|
|
35
41
|
|
|
36
|
-
- `risk=high` 必须带非空风险说明、`rememberable=false`、`timeoutAction=wait`。
|
|
42
|
+
- `risk=high` 必须带非空风险说明、`default=null`、`rememberable=false`、`timeoutAction=wait`。
|
|
37
43
|
- `confirm` / `choice` 只能返回一个合法 option id;`multi` 返回去重后的 id 数组;`input` 返回非空文本。
|
|
44
|
+
- interaction、option、callback、memory 与 audit 对象严格拒绝未声明字段和类型错配;超时必须为 `null` 或大于等于 1 的整数,默认值必须匹配交互类型与已有选项。
|
|
38
45
|
- callback 的 operation 和原 payload 由请求方声明;答案只能追加到副本,不能篡改原 interaction。
|
|
39
|
-
-
|
|
46
|
+
- 记忆与审计状态由已认证客户端或平台持久化。调用方传回的每条状态都必须重新验证;纯运行时是无状态协议层,不宣称已经写入数据库。
|
|
40
47
|
|
|
41
|
-
## 实现状态
|
|
48
|
+
## 实现状态 / Implementation status / Состояние реализации
|
|
42
49
|
|
|
43
50
|
| 能力 | 状态 | 边界 |
|
|
44
51
|
|---|---|---|
|
|
45
52
|
| interaction 请求/回答闭环 | 已实现 | 返回统一 callbackRequest 与 auditEntry |
|
|
46
53
|
| C 档聊天渲染 | 已实现 | 同一协议生成编号文本 |
|
|
47
54
|
| 低风险记忆/批量/审计 | 已实现(调用方持有) | 运行时返回更新后的状态,不伪造平台持久化 |
|
|
48
|
-
| IDE 原生适配器 | local notifier required | 由具体 IDE 插件渲染 |
|
|
49
|
-
| OS 原生 Confirm Notifier | local notifier required | 当前不宣称已弹出系统窗口 |
|
|
55
|
+
| IDE 原生适配器 | 需本地通知器 / local notifier required / нужен локальный уведомитель | 由具体 IDE 插件渲染 |
|
|
56
|
+
| OS 原生 Confirm Notifier | 需本地通知器 / local notifier required / нужен локальный уведомитель | 当前不宣称已弹出系统窗口 |
|
|
50
57
|
| 疲劳检测/移动端转发 | 规划中 | 不进入首版通过证据 |
|
|
51
58
|
|
|
52
|
-
##
|
|
59
|
+
## 受限调用与自动评价闭环
|
|
60
|
+
|
|
61
|
+
- IDE / 智能体必须通过本包 `invoke` 或 JSON-stdin `broker` 调用,不得直接拼装技能 HTTP 请求,也不得读取 BrainClient token。
|
|
62
|
+
- broker 从 `CLITAX_BRAIN_CLIENT_TOKEN_FILE` 读取身份;macOS/Linux 文件必须为当前 broker 账户所有且权限 `0600`,Windows 文件必须位于受限 `%LOCALAPPDATA%\CLI.Tax\broker` 目录。
|
|
63
|
+
- broker 只需要 Brain Client HTTPS、受限身份文件和调用方显式传入的路径,本身不需要完整磁盘访问。若要保证 IDE 无法读取身份文件,必须把 broker 放进独立低权限系统账户或沙箱服务,并只暴露受限 IPC;broker 与 IDE 同账户运行时,`0600` 不能隔离二者,禁止声称令牌已隔离。
|
|
64
|
+
- broker 只用 `Authorization: BrainClient …` 发起一次 runtime 请求。HTTP 成功后必须保留响应顶层原始 `feedbackReceiptId`、`feedbackInvocationId` 和 `feedbackEvaluation.digest`,不得生成、猜测、复用或跨调用转移。
|
|
65
|
+
- Brain Client 服务端必须严格绑定请求/响应的 `requestId` 和 `schemaVersion`,再根据真实状态、验证结果、服务端耗时与 findings 生成并持久化权威评分、评语和摘要。broker 不得生成分数或评语。
|
|
66
|
+
- 同一次 runtime 请求在服务端事务内生成并持久化评价,再返回 `feedbackReceiptId`、`feedbackInvocationId` 和权威摘要;broker 只验证已提交回执,不发起第二次评价写入。`not-reported`、验证不完整、P0/P1 findings、`blocked` 或 `failed` 都不得生成好评。
|
|
67
|
+
- 缺少凭证或 ID、身份不匹配、摘要不匹配、响应非法以及任何 HTTP 失败都必须显式失败,不得静默、不重试成重复评价。
|
|
68
|
+
- 本地 CLI 不提供手工评分或评语提交命令,人类不得选择技能分数或填写技能评价;日常聊天不属于评价协议。
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
真实成功响应顶层包含 `feedbackInvocationId`;自动评价必须携带该标识,禁止自行生成调用记录或复用其他调用的标识。
|
|
56
|
-
自动回传命令为 `npx cli-confirm-protocol@latest feedback <feedbackInvocationId> <0-100整数分> <真实评语>`;不得向人类展示评分控件。
|
|
57
|
-
身份凭证只从 `CLITAX_BRAIN_CLIENT_TOKEN_FILE` 指向的本地受限文件读取,评价按会员账号实名入库。禁止伪造、猜测、复用任何调用标识,禁止让人类选择分数;任何 HTTP 失败都必须视为回传失败。
|
|
70
|
+
调用示例:`npx cli-confirm-protocol@latest invoke <operation> '<JSON对象>'`。IDE 集成可向 `npx cli-confirm-protocol@latest broker` 的 stdin 发送 `{"operation":"capabilities","input":{}}`。
|
package/skill/skill.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confirm-protocol",
|
|
3
3
|
"displayName": "Confirm Protocol",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "统一结构化确认协议:确认、单选、多选与输入交互,返回校验答案、聊天兼容文本、低风险记忆状态、批次与审计。Unified structured confirmation protocol for confirm, choice, multi-choice, and input interactions with validated answers, chat rendering, low-risk memory state, batches, and audit. Единый протокол структурированного подтверждения для подтверждения, выбора, множественного выбора и ввода с проверенными ответами, текстом для чата, низкорисковой памятью, пакетами и аудитом.",
|
|
5
5
|
"schemaVersion": "confirm-protocol.skill.request/1.0",
|
|
6
6
|
"endpoint": "https://cli.tax/Cf8Pr7Tm2Q",
|
|
7
7
|
"method": "POST",
|
|
8
|
-
"version": "v7.0.
|
|
8
|
+
"version": "v7.0.28",
|
|
9
9
|
"type": "Skill"
|
|
10
10
|
}
|