cli-aimlock 7.0.36 → 7.0.37

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
@@ -65,3 +65,9 @@ Source: https://github.com/88208555/aimlock-clitax.git
65
65
  Brain Client 服务端在同一次 runtime 请求的事务中绑定真实响应、生成并持久化权威评分与评语,再返回已提交回执。broker 只验证 `feedbackReceiptId`、`feedbackInvocationId` 和权威摘要,不发起第二次评价写入,也不生成分数或评语。`not-reported`、验证不完整、P0/P1 findings、`blocked` 或 `failed` 都不得生成好评;缺凭证、缺回执、摘要不匹配、响应非法或 HTTP 失败都会显式失败。
66
66
 
67
67
  本地 CLI 不提供手工评分或评语提交命令,人类不能选择技能分数或填写技能评价。日常聊天不属于评价协议。
68
+
69
+ ## 网络中断与原回执恢复
70
+
71
+ 仅在 TLS 握手前确定尚未发送 HTTP 请求时,broker 才允许最多 3 次连接尝试,并受总超时约束。请求发出后发生断线或响应中断,只用 GET 查询原 requestId 的服务端回执,禁止重发 POST;未取得有效回执时保留不确定状态,不得假定成功或继续依赖步骤。
72
+
73
+ `npx cli-aimlock@latest recover <operation> <requestId>` 可重新查询原调用,不会重做操作或重复计费。链恢复不会跳过人工确认,也不会自动重跑结果不确定的本地命令。代理连接需 Node.js 22.21+ 或 24.5+;不支持的运行时会明确报错。
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto'
2
2
  import { resolve } from 'node:path'
3
3
  import { executeCoordinatorOperation } from 'cli-swarm/coordinator'
4
4
  import { loadOfficialSkillContext } from './installer.mjs'
5
- import { invokeOfficialSkill } from './broker.mjs'
5
+ import { invokeOfficialSkill, recoverOfficialSkill } from './broker.mjs'
6
6
  import { fail, sha256 } from './aimlock-local-fs.mjs'
7
7
  import { errorRecord } from './aimlock-chain-model.mjs'
8
8
  import { saveExecution } from './aimlock-chain-store.mjs'
@@ -52,10 +52,12 @@ export async function callSkill(session, skillId, operation, input) {
52
52
  const invocation = await invokeOfficialSkill(context, operation, input, {
53
53
  environment: dependencies.environment, credentialAccess: dependencies.credentialAccess,
54
54
  request: async (url, options) => {
55
- const request = JSON.parse(options.body).input
56
- call.requestId = request.requestId
57
- call.status = 'dispatched'
58
- await saveExecution(session.file, session.state)
55
+ if (options.method === 'POST') {
56
+ const request = JSON.parse(options.body).input
57
+ call.requestId = request.requestId
58
+ call.status = 'dispatched'
59
+ await saveExecution(session.file, session.state)
60
+ }
59
61
  return dependencies.request(url, options)
60
62
  },
61
63
  })
@@ -103,3 +105,20 @@ export async function callCommand(session, input) {
103
105
  throw error
104
106
  }
105
107
  }
108
+
109
+ export async function recoverSkillCall(session, skillId) {
110
+ const call = session.record.calls.at(-1)
111
+ if (!call || call.kind !== 'skill' || !call.requestId || !['uncertain', 'dispatched', 'recorded'].includes(call.status)) {
112
+ fail('AIMLOCK_CHAIN_RECOVERY_INVALID', 'Only an uncertain skill request can be recovered by receipt query')
113
+ }
114
+ call.status = 'uncertain'
115
+ await saveExecution(session.file, session.state)
116
+ const invocation = await recoverOfficialSkill(skillContext(session.state, skillId),
117
+ call.operation, call.requestId, session.dependencies)
118
+ call.receipt = invocation
119
+ call.status = 'recorded'
120
+ call.error = null
121
+ call.completedAt = new Date().toISOString()
122
+ await saveExecution(session.file, session.state)
123
+ return invocation.response.output
124
+ }
@@ -1,6 +1,7 @@
1
1
  import { stdin, stdout } from 'node:process'
2
2
  import { answerExecution, executionStatus, initializeExecution, resumeExecution } from './aimlock-chain-executor.mjs'
3
3
  import { errorRecord } from './aimlock-chain-model.mjs'
4
+ import { createBrokerTransport } from './broker-transport.mjs'
4
5
  import { fail } from './aimlock-local-fs.mjs'
5
6
 
6
7
  const PLAN_MAX_BYTES = 1_048_576
@@ -29,7 +30,7 @@ export async function dispatchChain(args) {
29
30
  if (!Object.hasOwn(expected, operation) || args.length !== expected[operation]) {
30
31
  fail('AIMLOCK_CHAIN_USAGE_INVALID', CHAIN_USAGE)
31
32
  }
32
- const dependencies = { environment: process.env, request: fetch }
33
+ const dependencies = { environment: process.env, request: createBrokerTransport({ environment: process.env }) }
33
34
  if (operation === 'init') return initializeExecution(repositoryRoot, await readPlan(stdin))
34
35
  if (operation === 'status') return executionStatus(repositoryRoot, chainId)
35
36
  if (operation === 'resume') return resumeExecution(repositoryRoot, chainId, dependencies)
@@ -4,7 +4,7 @@ import { fail } from './aimlock-local-fs.mjs'
4
4
  import { bindInput, errorRecord, validatePlan } from './aimlock-chain-model.mjs'
5
5
  import { assertNewExecution, executionStatus, initialState, loadExecution, recoverInterrupted,
6
6
  saveExecution, withExecutionLock } from './aimlock-chain-store.mjs'
7
- import { callCommand, callCoordinator, callSkill, resolveContexts, verifyContexts } from './aimlock-chain-calls.mjs'
7
+ import { callCommand, callCoordinator, callSkill, recoverSkillCall, resolveContexts, verifyContexts } from './aimlock-chain-calls.mjs'
8
8
  import { answerPending, prepareHuman } from './aimlock-chain-human.mjs'
9
9
  import { skillOutcome } from './aimlock-chain-outcomes.mjs'
10
10
 
@@ -149,6 +149,20 @@ async function coordinatorStep(session, step, input) {
149
149
  }
150
150
  }
151
151
 
152
+ function finishSkillStep(session, step, output) {
153
+ if (step.skillId === 'confirm-protocol' && step.operation === 'interaction-request' && output.status === 'succeeded') {
154
+ session.record.status = 'waiting'
155
+ session.record.output = output
156
+ session.record.pending = { kind: 'human', interaction: output.interaction, waitInput: null,
157
+ presentation: output.chatFallback, response: output,
158
+ continueWhen: Object.hasOwn(step, 'continueWhen') ? step.continueWhen : null }
159
+ } else {
160
+ const outcome = skillOutcome(step, output)
161
+ finish(session.record, output, outcome.status)
162
+ session.record.error = outcome.error
163
+ }
164
+ }
165
+
152
166
  async function executeStep(session, step) {
153
167
  if (!await readyForWork(session, step)) return
154
168
  const input = bindInput(step, session.state)
@@ -164,17 +178,7 @@ async function executeStep(session, step) {
164
178
  } else if (step.kind === 'coordinator') await coordinatorStep(session, step, input)
165
179
  else {
166
180
  const output = await callSkill(session, step.skillId, step.operation, input)
167
- if (step.skillId === 'confirm-protocol' && step.operation === 'interaction-request' && output.status === 'succeeded') {
168
- session.record.status = 'waiting'
169
- session.record.output = output
170
- session.record.pending = { kind: 'human', interaction: output.interaction, waitInput: null,
171
- presentation: output.chatFallback, response: output,
172
- continueWhen: Object.hasOwn(step, 'continueWhen') ? step.continueWhen : null }
173
- } else {
174
- const outcome = skillOutcome(step, output)
175
- finish(session.record, output, outcome.status)
176
- session.record.error = outcome.error
177
- }
181
+ finishSkillStep(session, step, output)
178
182
  }
179
183
  await saveExecution(session.file, session.state)
180
184
  }
@@ -186,6 +190,18 @@ async function advance(root, file, state, dependencies) {
186
190
  if (record.status === 'blocked' && step.kind === 'coordinator' && step.operation === 'lock-acquire'
187
191
  && record.output?.status === 'queued' && record.input) saveQueuePending(record, record.output, record.input)
188
192
  const session = { root, file, state, record, dependencies }
193
+ if (record.status === 'uncertain' && step.kind === 'skill'
194
+ && record.calls.at(-1)?.operation === step.operation) {
195
+ try {
196
+ finishSkillStep(session, step, await recoverSkillCall(session, step.skillId))
197
+ await saveExecution(file, state)
198
+ } catch (error) {
199
+ await failedStep(session, error)
200
+ return state
201
+ }
202
+ if (record.status === 'succeeded') continue
203
+ return state
204
+ }
189
205
  if (['failed', 'uncertain'].includes(record.status)) return state
190
206
  if (record.status === 'waiting' && record.pending?.kind === 'human') return state
191
207
  if (record.status === 'blocked' && record.pending?.kind !== 'coordination-guard') return state
@@ -179,7 +179,7 @@ export function chainStatus(state) {
179
179
  export function errorRecord(error) {
180
180
  if (!(error instanceof Error)) return { name: 'ThrownValue', message: String(error) }
181
181
  const value = { name: error.name, message: error.message }
182
- for (const key of ['code', 'transportCode', 'operation', 'retryable']) {
182
+ for (const key of ['code', 'transportCode', 'operation', 'requestId', 'stage', 'transport', 'recovery', 'receiptStatus', 'retryable']) {
183
183
  if (Object.hasOwn(error, key)) value[key] = error[key]
184
184
  }
185
185
  if (error.cause instanceof Error) value.cause = errorRecord(error.cause)
@@ -209,7 +209,7 @@ const RESPONSE_SCHEMA = "aimlock.skill.response/1.1";
209
209
  const ERROR_SCHEMA = "aimlock.skill.error/1.0";
210
210
  const CONTRACT_SCHEMA = "aimlock.scope-contract/1.0";
211
211
  const COMPILER_NAME = "aimlock";
212
- const COMPILER_VERSION = "v7.0.36";
212
+ const COMPILER_VERSION = "v7.0.37";
213
213
  const KEEP_ALIVE_SECONDS = 90;
214
214
  const KEEP_ALIVE_MESSAGE = "智能目标持续执行中,请勿关闭!";
215
215
  const BYPASS_LINE_BUDGET = 500;
package/brain-client.mjs CHANGED
@@ -4,6 +4,7 @@ import { inspectBrainTarget, brainStateDirectory, saveBrainRequest } from './bra
4
4
  export { inspectBrainTarget } from './brain-client-files.mjs'
5
5
  import { resolve } from 'node:path'
6
6
  import { brainClientAuthorization, transportFailureCode } from './broker.mjs'
7
+ import { createBrokerTransport } from './broker-transport.mjs'
7
8
  import { executeCommand } from './aimlock-chain-process.mjs'
8
9
 
9
10
  export const BRAIN_ENDPOINT = 'https://cli.tax/api/v1/brain'
@@ -45,7 +46,7 @@ export async function invokeBrain(operation, input, dependencies = {}) {
45
46
  if (Buffer.byteLength(body) > MAX_INPUT_BYTES) throw new Error('Brain request exceeds the size limit')
46
47
  let response
47
48
  try {
48
- response = await (dependencies.request ?? fetch)(endpoint, {
49
+ response = await (dependencies.request ?? createBrokerTransport({ environment }))(endpoint, {
49
50
  method: 'POST', redirect: 'error', headers: { Authorization: authorization, 'Content-Type': 'application/json' },
50
51
  body, signal: AbortSignal.timeout(TIMEOUT_MS),
51
52
  })
@@ -0,0 +1,68 @@
1
+ const TRANSPORT_ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]{1,63}$/
2
+ const NETWORK_TRANSPORT_ERROR = 'NETWORK_TRANSPORT'
3
+ const SKILL_INVOCATION_ERROR = 'SKILL_INVOCATION_FAILED'
4
+ const TRANSPORT_STAGES = new Set(['configuration', 'connecting', 'tls', 'request-sent', 'response-body', 'complete'])
5
+ const MAX_TRANSPORT_ATTEMPTS = 3
6
+
7
+ export function transportDiagnostics(value) {
8
+ if (!value || typeof value !== 'object' || !Number.isSafeInteger(value.attempts)
9
+ || value.attempts < 0 || value.attempts > MAX_TRANSPORT_ATTEMPTS
10
+ || !TRANSPORT_STAGES.has(value.stage) || typeof value.submitted !== 'boolean') return null
11
+ return { attempts: value.attempts, stage: value.stage, submitted: value.submitted }
12
+ }
13
+
14
+ export class OfficialSkillInvocationError extends Error {
15
+ constructor(context, request, transportCode, stage, transport) {
16
+ super(`${context.displayName} ${request.operation} invocation failed: network transport ${transportCode}`)
17
+ this.name = 'OfficialSkillInvocationError'
18
+ this.code = NETWORK_TRANSPORT_ERROR
19
+ this.requestId = request.requestId
20
+ this.operation = request.operation
21
+ const diagnostic = transportDiagnostics(transport)
22
+ this.stage = diagnostic ? diagnostic.stage : stage
23
+ if (diagnostic) this.transport = diagnostic
24
+ this.retryable = false
25
+ this.transportCode = transportCode
26
+ }
27
+ }
28
+
29
+ export class OfficialSkillResponseError extends Error {
30
+ constructor(request, stage, message) {
31
+ super(message)
32
+ this.name = 'OfficialSkillResponseError'
33
+ this.code = SKILL_INVOCATION_ERROR
34
+ this.requestId = request.requestId
35
+ this.operation = request.operation
36
+ this.stage = stage
37
+ this.retryable = false
38
+ }
39
+ }
40
+
41
+ export function transportFailureCode(error) {
42
+ const inspected = new Set()
43
+ let candidate = error
44
+ while (candidate && typeof candidate === 'object' && !inspected.has(candidate)) {
45
+ inspected.add(candidate)
46
+ const code = typeof candidate.code === 'string' ? candidate.code.trim() : ''
47
+ if (TRANSPORT_ERROR_CODE_PATTERN.test(code)) return code
48
+ const name = typeof candidate.name === 'string' ? candidate.name.trim() : ''
49
+ if (name === 'AbortError' || name === 'TimeoutError') return name
50
+ candidate = candidate.cause
51
+ }
52
+ return 'UNKNOWN_TRANSPORT_ERROR'
53
+ }
54
+
55
+ export function officialSkillFailureResponse(error) {
56
+ if (error instanceof OfficialSkillInvocationError || error instanceof OfficialSkillResponseError) {
57
+ const failure = { code: error.code, message: error.message, requestId: error.requestId,
58
+ operation: error.operation, stage: error.stage, retryable: error.retryable }
59
+ if (typeof error.transportCode === 'string') failure.transportCode = error.transportCode
60
+ if (typeof error.receiptStatus === 'string') failure.receiptStatus = error.receiptStatus
61
+ if (error.recovery) failure.recovery = error.recovery
62
+ const diagnostic = transportDiagnostics(error.transport)
63
+ if (diagnostic) failure.transport = diagnostic
64
+ return { ok: false, error: failure }
65
+ }
66
+ return { ok: false, error: { code: SKILL_INVOCATION_ERROR,
67
+ message: error instanceof Error ? error.message : 'Skill invocation failed', retryable: false } }
68
+ }
@@ -0,0 +1,110 @@
1
+ import { OfficialSkillResponseError, transportFailureCode, transportDiagnostics } from './broker-failures.mjs'
2
+
3
+ export const SKILL_RECEIPT_SCHEMA = 'skill-runtime-receipt/1.0'
4
+ export const SKILL_RECEIPT_HEADER = 'X-Skill-Receipt'
5
+ const RECEIPT_API_PATH = '/api/v1/skill-invocations'
6
+ const QUERY_TIMEOUT_MS = 5_000
7
+ const QUERY_ATTEMPTS = 3
8
+ const QUERY_DELAY_MS = 250
9
+ const TERMINAL_STATUSES = new Set(['completed', 'failed'])
10
+ const RECEIPT_STATUSES = new Set([...TERMINAL_STATUSES, 'pending', 'expired', 'not-found'])
11
+
12
+ class ReceiptRecoveryError extends OfficialSkillResponseError {
13
+ constructor(request, status, message, transportCode) {
14
+ super(request, 'receipt-query', message)
15
+ this.name = 'ReceiptRecoveryError'
16
+ this.code = 'SKILL_INVOCATION_UNCERTAIN'
17
+ this.receiptStatus = status
18
+ if (transportCode) this.transportCode = transportCode
19
+ }
20
+ }
21
+
22
+ function validateReceipt(response, payload, context, request) {
23
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)
24
+ || payload.schemaVersion !== SKILL_RECEIPT_SCHEMA || payload.runtimeCode !== context.runtimeCode
25
+ || payload.requestId !== request.requestId || !RECEIPT_STATUSES.has(payload.status)) {
26
+ throw new ReceiptRecoveryError(request, 'invalid', 'Receipt query returned invalid or mismatched execution identity')
27
+ }
28
+ const expectedHttpStatus = payload.status === 'pending' ? 202
29
+ : payload.status === 'expired' ? 410 : payload.status === 'not-found' ? 404 : 200
30
+ if (response.status !== expectedHttpStatus || (payload.status !== 'not-found'
31
+ && !Number.isFinite(Date.parse(payload.expiresAt)))) {
32
+ throw new ReceiptRecoveryError(request, 'invalid', 'Receipt status or expiration metadata is invalid')
33
+ }
34
+ if (TERMINAL_STATUSES.has(payload.status) && (!Number.isInteger(payload.httpStatus)
35
+ || payload.httpStatus < 100 || payload.httpStatus > 599 || !payload.response
36
+ || typeof payload.response !== 'object' || Array.isArray(payload.response))) {
37
+ throw new ReceiptRecoveryError(request, 'invalid', 'Receipt has no valid recorded HTTP result')
38
+ }
39
+ if (payload.status === 'failed' && (payload.httpStatus < 400
40
+ || typeof payload.response.error !== 'string' || typeof payload.response.code !== 'string')) {
41
+ throw new ReceiptRecoveryError(request, 'invalid', 'Failed receipt has no valid recorded error')
42
+ }
43
+ return payload
44
+ }
45
+
46
+ async function fetchReceipt(context, request, dependencies, authorization) {
47
+ const endpoint = new URL(context.endpoint)
48
+ const path = `${RECEIPT_API_PATH}/${encodeURIComponent(context.runtimeCode)}/${encodeURIComponent(request.requestId)}`
49
+ let response
50
+ let payload
51
+ try {
52
+ response = await dependencies.request(new URL(path, endpoint.origin).href, {
53
+ method: 'GET', redirect: 'error', headers: { Authorization: authorization },
54
+ signal: AbortSignal.timeout(QUERY_TIMEOUT_MS),
55
+ })
56
+ } catch (error) {
57
+ throw new ReceiptRecoveryError(request, 'query-failed', 'Receipt query failed; execution outcome remains uncertain',
58
+ transportFailureCode(error))
59
+ }
60
+ if (response.status === 401 || response.status === 403) {
61
+ throw new ReceiptRecoveryError(request, 'unauthorized', `Receipt query authorization failed: HTTP ${response.status}`)
62
+ }
63
+ try {
64
+ payload = await response.json()
65
+ } catch (error) {
66
+ if (error instanceof SyntaxError) throw new ReceiptRecoveryError(request, 'invalid', 'Receipt query returned invalid JSON')
67
+ throw new ReceiptRecoveryError(request, 'query-failed', 'Receipt body was interrupted; execution outcome remains uncertain',
68
+ transportFailureCode(error))
69
+ }
70
+ return { receipt: validateReceipt(response, payload, context, request), transport: transportDiagnostics(response.transport) }
71
+ }
72
+
73
+ function receiptOutcome(receipt, context, request, validateInvocation, transport) {
74
+ if (receipt.status === 'failed') {
75
+ throw new OfficialSkillResponseError(request, 'http-response',
76
+ `${context.displayName} ${request.operation} failed: HTTP ${receipt.httpStatus} (recorded receipt)`)
77
+ }
78
+ if (receipt.httpStatus !== 200 || receipt.response.ok !== true) {
79
+ throw new ReceiptRecoveryError(request, 'invalid', 'Completed receipt does not contain a successful HTTP response')
80
+ }
81
+ try {
82
+ const invocation = validateInvocation(receipt.response)
83
+ return { ...invocation, recovery: { status: 'completed', requestId: request.requestId },
84
+ ...(transport ? { transport } : {}) }
85
+ } catch (error) {
86
+ throw new OfficialSkillResponseError(request, 'receipt-validation',
87
+ error instanceof Error ? error.message : 'Recorded skill response validation failed')
88
+ }
89
+ }
90
+
91
+ export async function queryOfficialSkillReceipt(context, request, dependencies, authorization, validateInvocation) {
92
+ let lastFailure
93
+ for (let attempt = 0; attempt < QUERY_ATTEMPTS; attempt += 1) {
94
+ if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, QUERY_DELAY_MS))
95
+ let fetched
96
+ try {
97
+ fetched = await fetchReceipt(context, request, dependencies, authorization)
98
+ } catch (error) {
99
+ if (!(error instanceof ReceiptRecoveryError) || error.receiptStatus !== 'query-failed') throw error
100
+ lastFailure = error
101
+ continue
102
+ }
103
+ const { receipt, transport } = fetched
104
+ if (TERMINAL_STATUSES.has(receipt.status)) return receiptOutcome(receipt, context, request, validateInvocation, transport)
105
+ lastFailure = new ReceiptRecoveryError(request, receipt.status,
106
+ `Invocation outcome is ${receipt.status}; query this requestId again before any new execution`)
107
+ if (receipt.status !== 'pending') throw lastFailure
108
+ }
109
+ throw lastFailure
110
+ }
@@ -0,0 +1,86 @@
1
+ function responseValue(message, chunks, state) {
2
+ const headers = new Headers()
3
+ for (let index = 0; index < message.rawHeaders.length; index += 2) {
4
+ headers.append(message.rawHeaders[index], message.rawHeaders[index + 1])
5
+ }
6
+ const status = message.statusCode
7
+ if (!Number.isInteger(status) || status < 200 || status > 599) throw new Error('BROKER_RESPONSE_STATUS_INVALID')
8
+ const body = Buffer.concat(chunks)
9
+ const emptyBody = status === 204 || status === 205 || status === 304
10
+ const response = new Response(emptyBody ? null : body, { status, headers })
11
+ response.transport = Object.freeze({ ...state, stage: 'complete' })
12
+ return response
13
+ }
14
+
15
+ /** No write/end/flushHeaders is permitted before the destination TLS socket is authenticated. */
16
+ export function attemptBrokerHttps(input, dependencies) {
17
+ return new Promise((resolve, reject) => {
18
+ const { state, signal, failure } = dependencies
19
+ const agent = dependencies.createAgent({ ...input.agentOptions, keepAlive: false, maxSockets: 1, maxCachedSessions: 0 })
20
+ let request
21
+ let settled = false
22
+ let connectTimer
23
+ const cleanup = () => {
24
+ clearTimeout(connectTimer)
25
+ signal.removeEventListener('abort', abort)
26
+ agent.destroy()
27
+ }
28
+ const finishError = code => {
29
+ if (settled) return
30
+ settled = true
31
+ const error = failure(code)
32
+ request?.destroy()
33
+ cleanup()
34
+ reject(error)
35
+ }
36
+ const abort = () => finishError('ABORT_ERR')
37
+ const receive = response => {
38
+ state.stage = 'response-body'
39
+ const chunks = []
40
+ let bytes = 0
41
+ response.on('error', error => finishError(error.code))
42
+ response.on('aborted', () => finishError('BROKER_RESPONSE_TRUNCATED'))
43
+ response.on('close', () => { if (!response.complete) finishError('BROKER_RESPONSE_TRUNCATED') })
44
+ response.on('data', chunk => {
45
+ bytes += chunk.length
46
+ if (bytes > dependencies.maxResponseBytes) finishError('BROKER_RESPONSE_TOO_LARGE')
47
+ else chunks.push(chunk)
48
+ })
49
+ response.on('end', () => {
50
+ if (settled) return
51
+ if (!response.complete) return finishError('BROKER_RESPONSE_TRUNCATED')
52
+ const coding = response.headers['content-encoding']
53
+ if (coding !== undefined && coding !== 'identity') return finishError('BROKER_RESPONSE_ENCODING_UNSUPPORTED')
54
+ let value
55
+ try { value = responseValue(response, chunks, state) }
56
+ catch (error) { return finishError(error.message) }
57
+ settled = true
58
+ cleanup()
59
+ resolve(value)
60
+ })
61
+ }
62
+ try {
63
+ request = dependencies.request(input.target, { method: input.method, headers: input.headers, agent,
64
+ rejectUnauthorized: true }, receive)
65
+ request.on('error', error => finishError(error.code))
66
+ request.once('socket', socket => {
67
+ state.stage = 'tls'
68
+ const send = () => {
69
+ if (settled) return
70
+ if (signal.aborted) return abort()
71
+ if (socket.encrypted !== true || socket.authorized !== true) return finishError('BROKER_TLS_UNAUTHORIZED')
72
+ clearTimeout(connectTimer)
73
+ state.stage = 'request-sent'
74
+ state.submitted = true
75
+ try { request.end(input.body) } catch (error) { finishError(error.code) }
76
+ }
77
+ if (socket.encrypted === true && socket.authorized === true) send()
78
+ else socket.once('secureConnect', send)
79
+ })
80
+ connectTimer = setTimeout(() => finishError('ETIMEDOUT'), dependencies.connectTimeoutMs)
81
+ connectTimer.unref()
82
+ signal.addEventListener('abort', abort, { once: true })
83
+ if (signal.aborted) abort()
84
+ } catch (error) { finishError(error.code) }
85
+ })
86
+ }
@@ -0,0 +1,107 @@
1
+ import { Agent, request as httpsRequest } from 'node:https'
2
+ import { setTimeout as delay } from 'node:timers/promises'
3
+ import { attemptBrokerHttps } from './broker-transport-attempt.mjs'
4
+
5
+ export const BROKER_TRANSPORT_TIMEOUT_MS = 120_000
6
+ export const BROKER_TRANSPORT_CONNECT_TIMEOUT_MS = 8_000
7
+ export const BROKER_TRANSPORT_MAX_ATTEMPTS = 3
8
+ export const BROKER_TRANSPORT_MAX_RESPONSE_BYTES = 16 * 1024 * 1024
9
+ const RETRY_DELAY_MS = 150
10
+ const PROXY_KEYS = ['HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY', 'https_proxy', 'NO_PROXY', 'no_proxy']
11
+ const TRANSIENT_CODES = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENETUNREACH', 'EHOSTUNREACH'])
12
+ const CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,79}$/
13
+
14
+ export class BrokerTransportError extends Error {
15
+ constructor(code, transport) {
16
+ const safeCode = typeof code === 'string' && CODE_PATTERN.test(code) ? code : 'BROKER_TRANSPORT_FAILED'
17
+ super(`HTTPS broker transport failed (${safeCode})`)
18
+ this.name = 'BrokerTransportError'
19
+ this.code = safeCode
20
+ this.transport = Object.freeze({ ...transport })
21
+ }
22
+ }
23
+
24
+ /** Native Agent proxyEnv: https://nodejs.org/api/https.html#new-agentoptions */
25
+ export function brokerProxyEnvironment(environment, nodeVersion = process.versions.node) {
26
+ const proxyEnv = {}
27
+ for (const key of PROXY_KEYS) {
28
+ if (environment[key] !== undefined) {
29
+ if (typeof environment[key] !== 'string') throw new Error('BROKER_PROXY_ENV_INVALID')
30
+ proxyEnv[key] = environment[key]
31
+ }
32
+ }
33
+ const hasProxy = PROXY_KEYS.slice(0, 4).some(key => typeof proxyEnv[key] === 'string' && proxyEnv[key].trim())
34
+ const version = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(nodeVersion)
35
+ if (!version) throw new Error('BROKER_NODE_VERSION_INVALID')
36
+ const major = Number(version[1]), minor = Number(version[2])
37
+ const supportsProxy = major > 24 || major === 24 && minor >= 5 || major === 22 && minor >= 21
38
+ if (hasProxy && !supportsProxy) throw new Error('BROKER_PROXY_UNSUPPORTED_NODE')
39
+ for (const key of PROXY_KEYS.slice(0, 4)) {
40
+ if (!proxyEnv[key]) continue
41
+ let proxy
42
+ try { proxy = new URL(proxyEnv[key]) } catch { throw new Error('BROKER_PROXY_ENV_INVALID') }
43
+ if (!['http:', 'https:'].includes(proxy.protocol)) throw new Error('BROKER_PROXY_ENV_INVALID')
44
+ }
45
+ return supportsProxy ? { proxyEnv } : {}
46
+ }
47
+
48
+ function requestInput(url, options) {
49
+ const target = new URL(url)
50
+ if (target.protocol !== 'https:' || target.username || target.password) throw new Error('BROKER_HTTPS_REQUIRED')
51
+ const method = options.method === undefined ? 'GET' : options.method
52
+ if (typeof method !== 'string' || !/^[A-Z]+$/.test(method)) throw new Error('BROKER_METHOD_INVALID')
53
+ const headers = new Headers(options.headers)
54
+ headers.set('accept-encoding', 'identity')
55
+ const source = options.body
56
+ if (source !== undefined && source !== null && typeof source !== 'string' && !(source instanceof Uint8Array)) {
57
+ throw new Error('BROKER_BODY_INVALID')
58
+ }
59
+ const body = source === undefined || source === null ? undefined : Buffer.from(source)
60
+ if (body !== undefined) headers.set('content-length', String(body.length))
61
+ return { target, method, headers: Object.fromEntries(headers), body }
62
+ }
63
+
64
+ function overallSignal(external) {
65
+ const controller = new AbortController()
66
+ const abort = () => controller.abort(external.reason)
67
+ if (external?.aborted) controller.abort(external.reason)
68
+ else external?.addEventListener('abort', abort, { once: true })
69
+ const timer = setTimeout(() => controller.abort(new Error('ETIMEDOUT')), BROKER_TRANSPORT_TIMEOUT_MS)
70
+ timer.unref()
71
+ return { signal: controller.signal, cleanup() {
72
+ clearTimeout(timer)
73
+ external?.removeEventListener('abort', abort)
74
+ } }
75
+ }
76
+
77
+ /** Dependencies support isolated native TLS tests; production callers supply only environment. */
78
+ export function createBrokerTransport({ environment = process.env, request = httpsRequest,
79
+ createAgent = options => new Agent(options), nodeVersion = process.versions.node } = {}) {
80
+ return async function brokerRequest(url, options = {}) {
81
+ const state = { attempts: 0, stage: 'configuration', submitted: false }
82
+ let configured
83
+ try { configured = { ...requestInput(url, options), agentOptions: brokerProxyEnvironment(environment, nodeVersion) } }
84
+ catch (error) { throw new BrokerTransportError(error.message, state) }
85
+ const overall = overallSignal(options.signal)
86
+ try {
87
+ for (let number = 1; number <= BROKER_TRANSPORT_MAX_ATTEMPTS; number += 1) {
88
+ if (overall.signal.aborted) throw new BrokerTransportError('ABORT_ERR', state)
89
+ state.attempts = number
90
+ state.stage = 'connecting'
91
+ try {
92
+ return await attemptBrokerHttps(configured, { request, createAgent, signal: overall.signal, state,
93
+ connectTimeoutMs: BROKER_TRANSPORT_CONNECT_TIMEOUT_MS, maxResponseBytes: BROKER_TRANSPORT_MAX_RESPONSE_BYTES,
94
+ failure: code => new BrokerTransportError(code, state) })
95
+ } catch (error) {
96
+ const failure = error instanceof BrokerTransportError ? error : new BrokerTransportError(error.code, state)
97
+ const reconnect = !state.submitted && !overall.signal.aborted && TRANSIENT_CODES.has(failure.code)
98
+ && number < BROKER_TRANSPORT_MAX_ATTEMPTS
99
+ if (!reconnect) throw failure
100
+ try { await delay(RETRY_DELAY_MS, undefined, { signal: overall.signal }) }
101
+ catch { throw new BrokerTransportError('ABORT_ERR', state) }
102
+ }
103
+ }
104
+ throw new BrokerTransportError('BROKER_TRANSPORT_ATTEMPTS_EXHAUSTED', state)
105
+ } finally { overall.cleanup() }
106
+ }
107
+ }
package/broker.mjs CHANGED
@@ -2,6 +2,9 @@ import { createHash, randomUUID } from 'node:crypto'
2
2
  import { constants } from 'node:fs'
3
3
  import { lstat, open } from 'node:fs/promises'
4
4
  import { resolve, win32 } from 'node:path'
5
+ import { OfficialSkillInvocationError, OfficialSkillResponseError, transportFailureCode, transportDiagnostics } from './broker-failures.mjs'
6
+ import { queryOfficialSkillReceipt, SKILL_RECEIPT_HEADER, SKILL_RECEIPT_SCHEMA } from './broker-recovery.mjs'
7
+ export { officialSkillFailureResponse, transportFailureCode } from './broker-failures.mjs'
5
8
 
6
9
  export const LOOKUP_TIMEOUT_MS = 8000
7
10
  export const CALL_TIMEOUT_MS = 120_000
@@ -24,20 +27,6 @@ const VALIDATION_STATES = new Set(['passed', 'failed', 'incomplete'])
24
27
  const EVALUATION_SCHEMA = 'skill-automatic-evaluation/1.0'
25
28
  const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
26
29
  const REQUEST_SCHEMA_PATTERN = /^([A-Za-z0-9.-]+\.skill)\.request\/([0-9]+\.[0-9]+)$/
27
- const TRANSPORT_ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]{1,63}$/
28
- const NETWORK_TRANSPORT_ERROR = 'NETWORK_TRANSPORT'
29
- const SKILL_INVOCATION_ERROR = 'SKILL_INVOCATION_FAILED'
30
-
31
- class OfficialSkillInvocationError extends Error {
32
- constructor(context, operation, transportCode) {
33
- super(`${context.displayName} ${operation} invocation failed: network transport ${transportCode}`)
34
- this.name = 'OfficialSkillInvocationError'
35
- this.code = NETWORK_TRANSPORT_ERROR
36
- this.operation = operation
37
- this.retryable = false
38
- this.transportCode = transportCode
39
- }
40
- }
41
30
 
42
31
  function asObject(value, label) {
43
32
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -214,12 +203,21 @@ export function authoritativeEvaluation(value, expected) {
214
203
  return evaluation
215
204
  }
216
205
 
217
- async function responsePayload(response, label) {
206
+ async function responsePayload(response, context, request) {
207
+ const label = `${context.displayName} ${request.operation} response`
208
+ let payload
218
209
  try {
219
- return asObject(await response.json(), label)
220
- } catch {
221
- throw new Error(`${label} is not valid JSON (HTTP ${response.status})`)
210
+ payload = await response.json()
211
+ } catch (error) {
212
+ if (error instanceof SyntaxError) {
213
+ throw new OfficialSkillResponseError(request, 'response-parse', `${label} is not valid JSON (HTTP ${response.status})`)
214
+ }
215
+ throw new OfficialSkillInvocationError(context, request, transportFailureCode(error), 'response-body', error?.transport)
216
+ }
217
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
218
+ throw new OfficialSkillResponseError(request, 'response-validation', `${label} must be an object`)
222
219
  }
220
+ return payload
223
221
  }
224
222
 
225
223
  function invocationRequest(context, operation, input) {
@@ -236,43 +234,6 @@ function invocationRequest(context, operation, input) {
236
234
  }
237
235
  }
238
236
 
239
- export function transportFailureCode(error) {
240
- const inspected = new Set()
241
- let candidate = error
242
- while (candidate && typeof candidate === 'object' && !inspected.has(candidate)) {
243
- inspected.add(candidate)
244
- const code = typeof candidate.code === 'string' ? candidate.code.trim() : ''
245
- if (TRANSPORT_ERROR_CODE_PATTERN.test(code)) return code
246
- const name = typeof candidate.name === 'string' ? candidate.name.trim() : ''
247
- if (name === 'AbortError' || name === 'TimeoutError') return name
248
- candidate = candidate.cause
249
- }
250
- return 'UNKNOWN_TRANSPORT_ERROR'
251
- }
252
-
253
- export function officialSkillFailureResponse(error) {
254
- if (error instanceof OfficialSkillInvocationError) {
255
- return {
256
- ok: false,
257
- error: {
258
- code: error.code,
259
- message: error.message,
260
- operation: error.operation,
261
- retryable: error.retryable,
262
- transportCode: error.transportCode,
263
- },
264
- }
265
- }
266
- return {
267
- ok: false,
268
- error: {
269
- code: SKILL_INVOCATION_ERROR,
270
- message: error instanceof Error ? error.message : 'Skill invocation failed',
271
- retryable: false,
272
- },
273
- }
274
- }
275
-
276
237
  export async function invokeOfficialSkill(context, operation, input, dependencies) {
277
238
  const environment = asObject(dependencies.environment, 'broker environment')
278
239
  if (typeof dependencies.request !== 'function') {
@@ -284,17 +245,64 @@ export async function invokeOfficialSkill(context, operation, input, dependencie
284
245
  try {
285
246
  response = await dependencies.request(context.endpoint, {
286
247
  method: 'POST',
287
- headers: { 'Content-Type': 'application/json', Authorization: authorization },
248
+ headers: { 'Content-Type': 'application/json', Authorization: authorization,
249
+ [SKILL_RECEIPT_HEADER]: SKILL_RECEIPT_SCHEMA },
288
250
  body: JSON.stringify({ input: requestEnvelope }),
289
251
  signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
290
252
  })
291
253
  } catch (error) {
292
- throw new OfficialSkillInvocationError(context, operation, transportFailureCode(error))
254
+ const failure = new OfficialSkillInvocationError(context, requestEnvelope, transportFailureCode(error), 'request', error?.transport)
255
+ return recoverTransportFailure(context, requestEnvelope, dependencies, authorization, failure)
256
+ }
257
+ let payload
258
+ try {
259
+ payload = await responsePayload(response, context, requestEnvelope)
260
+ } catch (error) {
261
+ if (!(error instanceof OfficialSkillInvocationError)) throw error
262
+ return recoverTransportFailure(context, requestEnvelope, dependencies, authorization, error)
293
263
  }
294
- const payload = await responsePayload(response, `${context.displayName} ${operation} response`)
295
264
  if (!response.ok || payload.ok !== true) {
296
- throw new Error(`${context.displayName} ${operation} failed: HTTP ${response.status}`)
265
+ throw new OfficialSkillResponseError(requestEnvelope, 'http-response', `${context.displayName} ${operation} failed: HTTP ${response.status}`)
297
266
  }
267
+ try {
268
+ const invocation = validateInvocationResponse(context, operation, payload, requestEnvelope)
269
+ const transport = transportDiagnostics(response.transport)
270
+ return transport ? { ...invocation, transport } : invocation
271
+ } catch (error) {
272
+ throw new OfficialSkillResponseError(requestEnvelope, 'response-validation',
273
+ error instanceof Error ? error.message : 'Skill response validation failed')
274
+ }
275
+ }
276
+
277
+ async function recoverTransportFailure(context, request, dependencies, authorization, failure) {
278
+ if (failure.transport?.submitted === false) throw failure
279
+ try {
280
+ const invocation = await queryOfficialSkillReceipt(context, request, dependencies, authorization,
281
+ (payload) => validateInvocationResponse(context, request.operation, payload, request))
282
+ return failure.transport ? { ...invocation, transport: failure.transport } : invocation
283
+ } catch (error) {
284
+ if (!(error instanceof OfficialSkillResponseError) || error.code !== 'SKILL_INVOCATION_UNCERTAIN') throw error
285
+ failure.recovery = { status: error.receiptStatus, message: error.message }
286
+ throw failure
287
+ }
288
+ }
289
+
290
+ export async function recoverOfficialSkill(context, operation, requestId, dependencies) {
291
+ const normalizedOperation = requiredString(operation, 'skill operation')
292
+ const normalizedRequestId = requiredString(requestId, 'skill requestId')
293
+ if (!IDENTIFIER_PATTERN.test(normalizedOperation) || !IDENTIFIER_PATTERN.test(normalizedRequestId)) {
294
+ throw new Error('skill recovery identity is invalid')
295
+ }
296
+ expectedResponseSchema(context.schemaVersion)
297
+ const environment = asObject(dependencies.environment, 'broker environment')
298
+ if (typeof dependencies.request !== 'function') throw new Error('broker request dependency is required')
299
+ const authorization = await brainClientAuthorization(context, environment, dependencies.credentialAccess)
300
+ const request = { schemaVersion: context.schemaVersion, requestId: normalizedRequestId, operation: normalizedOperation }
301
+ return queryOfficialSkillReceipt(context, request, dependencies, authorization,
302
+ (payload) => validateInvocationResponse(context, normalizedOperation, payload, request))
303
+ }
304
+
305
+ function validateInvocationResponse(context, operation, payload, requestEnvelope) {
298
306
  const invocationId = payload.feedbackInvocationId
299
307
  if (typeof invocationId !== 'string' || !INVOCATION_PATTERN.test(invocationId)) {
300
308
  throw new Error(`${context.displayName} ${operation} response is missing a valid feedbackInvocationId`)
package/installer.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  brokerCommandInput,
14
14
  invokeCommandInput,
15
15
  invokeOfficialSkill,
16
+ recoverOfficialSkill,
16
17
  officialSkillFailureResponse,
17
18
  } from './broker.mjs'
18
19
 
@@ -26,9 +27,12 @@ export {
26
27
  callOfficialSkill,
27
28
  invokeCommandInput,
28
29
  invokeOfficialSkill,
30
+ recoverOfficialSkill,
29
31
  officialSkillFailureResponse,
30
32
  } from './broker.mjs'
31
33
 
34
+ import { createBrokerTransport } from './broker-transport.mjs'
35
+
32
36
  const INSTALL_META = 'install-meta.json'
33
37
  const BROKER_STDIN_MAX_BYTES = 1_048_576
34
38
 
@@ -91,7 +95,8 @@ export function installTarget(skillName, explicit) {
91
95
  }
92
96
 
93
97
  export async function fetchLatestVersion(context) {
94
- const response = await fetch(context.latestEndpoint, { signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) })
98
+ const request = createBrokerTransport({ environment: process.env })
99
+ const response = await request(context.latestEndpoint, { signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) })
95
100
  if (!response.ok) throw new Error(`cli.tax skill lookup failed: HTTP ${response.status}`)
96
101
  const data = asObject(await response.json(), 'cli.tax skill lookup')
97
102
  return {
@@ -162,6 +167,8 @@ export function defaultUsage(context, extraLines) {
162
167
  ' Invoke through the restricted local broker; a valid real HTTP invocation submits one authority-bound evaluation.',
163
168
  ` npx ${context.npmName}@latest broker`,
164
169
  ' Read one {"operation":"...","input":{...}} request from JSON stdin.',
170
+ ` npx ${context.npmName}@latest recover <operation> <requestId>`,
171
+ ' Query an uncertain invocation without resending or charging again.',
165
172
  'Credential: CLITAX_BRAIN_CLIENT_TOKEN_FILE (the broker reads it; never pass the token).',
166
173
  `Endpoint: ${context.endpoint}`,
167
174
  ]
@@ -170,7 +177,7 @@ export function defaultUsage(context, extraLines) {
170
177
  }
171
178
 
172
179
  function brokerDependencies() {
173
- return { environment: process.env, request: fetch }
180
+ return { environment: process.env, request: createBrokerTransport({ environment: process.env }) }
174
181
  }
175
182
 
176
183
  async function readBrokerSource(input) {
@@ -201,6 +208,19 @@ async function runBrokerInvocation(context, commandInput) {
201
208
  }
202
209
  }
203
210
 
211
+ async function runBrokerRecovery(context, args) {
212
+ if (args.length !== 3) throw new Error('recover requires operation and original requestId')
213
+ try {
214
+ const invocation = await recoverOfficialSkill(context, args[1], args[2], brokerDependencies())
215
+ console.log(JSON.stringify(invocation))
216
+ } catch (error) {
217
+ const response = officialSkillFailureResponse(error)
218
+ console.log(JSON.stringify({ response }))
219
+ console.error(response.error.message)
220
+ process.exitCode = 1
221
+ }
222
+ }
223
+
204
224
  export async function runIntakeHandshake(context, spec) {
205
225
  const invocation = await invokeOfficialSkill(context, 'capabilities', {}, brokerDependencies())
206
226
  const capabilities = invocation.response
@@ -253,6 +273,7 @@ export async function dispatchOfficialSkillCli(options) {
253
273
  if (command === 'install') await installOfficialSkill(context, argument)
254
274
  else if (command === 'check') await checkOfficialSkill(context, argument)
255
275
  else if (command === 'run') await options.runCommand(context)
276
+ else if (command === 'recover') await runBrokerRecovery(context, args)
256
277
  else if (command === 'invoke') await runBrokerInvocation(context, invokeCommandInput(args))
257
278
  else if (command === 'broker') {
258
279
  await runBrokerInvocation(context, brokerCommandInput(await readBrokerSource(stdin)))
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "cli-aimlock": "./cli.mjs"
4
4
  },
5
5
  "dependencies": {
6
- "cli-swarm": "7.0.36"
6
+ "cli-swarm": "7.0.37"
7
7
  },
8
8
  "description": "Aimlock skill installer for CLI.Tax: lock a user request into an executable aim and route Blueprint, Swarm, and Calctool.",
9
9
  "exports": {
@@ -17,6 +17,10 @@
17
17
  "cli.mjs",
18
18
  "installer.mjs",
19
19
  "broker.mjs",
20
+ "broker-failures.mjs",
21
+ "broker-recovery.mjs",
22
+ "broker-transport.mjs",
23
+ "broker-transport-attempt.mjs",
20
24
  "README.md",
21
25
  "skill/SKILL.md",
22
26
  "skill/skill.json",
@@ -49,5 +53,5 @@
49
53
  "url": "https://github.com/88208555/aimlock-clitax.git"
50
54
  },
51
55
  "type": "module",
52
- "version": "7.0.36"
56
+ "version": "7.0.37"
53
57
  }
package/skill/SKILL.md CHANGED
@@ -5,7 +5,7 @@ description: "Aimlock 仅用于大型、深度、跨模块、高风险、需要
5
5
 
6
6
  # Aimlock Skill
7
7
 
8
- Package version: v7.0.36
8
+ Package version: v7.0.37
9
9
 
10
10
  Endpoint: https://cli.tax/R3mQ8kWpXn
11
11
 
@@ -188,3 +188,9 @@ Aimlock returns the protocol; it does not start a timer.
188
188
  4. 运行 `npx cli-aimlock@latest brain check <repositoryRoot> <handoff.json>` 执行批准的检查并回传产物哈希和结果。普通 IDE 回传属于 client-reported,不能据此声称可信验证通过。
189
189
  5. 只有已批准的可信 runner 生成与本次计划和报告绑定的签名收据后,才运行 `npx cli-aimlock@latest brain validate <repositoryRoot> <validation.json>`。没有可信收据时保持已回传状态,不伪造验证。
190
190
  6. 请求发送后结果不确定时,先用 `brain status <repositoryRoot> <status.json>` 按 requestId 或 planId 查询;禁止自动重发规划或重复计费。
191
+
192
+ ## 网络中断与原回执恢复
193
+
194
+ 仅在 TLS 握手前确定尚未发送 HTTP 请求时,broker 才允许最多 3 次连接尝试,并受总超时约束。请求发出后发生断线或响应中断,只用 GET 查询原 requestId 的服务端回执,禁止重发 POST;未取得有效回执时保留不确定状态,不得假定成功或继续依赖步骤。
195
+
196
+ `npx cli-aimlock@latest recover <operation> <requestId>` 可重新查询原调用,不会重做操作或重复计费。链恢复不会跳过人工确认,也不会自动重跑结果不确定的本地命令。代理连接需 Node.js 22.21+ 或 24.5+;不支持的运行时会明确报错。
package/skill/skill.json CHANGED
@@ -6,5 +6,5 @@
6
6
  "name": "aimlock",
7
7
  "schemaVersion": "aimlock.skill.request/1.1",
8
8
  "type": "Skill",
9
- "version": "v7.0.36"
9
+ "version": "v7.0.37"
10
10
  }