aura-basetools-dsh 0.7.0 → 0.13.1
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 +74 -5
- package/aura-edit/index.js +309 -32
- package/aura-read/index.js +78 -27
- package/cordis.patch.yml +1 -0
- package/dsh/argv-shell.mjs +290 -0
- package/dsh/hook-inproc.mjs +407 -0
- package/package.json +23 -9
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook-inproc —— 把 AuraPolaris 的 PreToolUse 钩子【在宿主进程内】跑完。
|
|
3
|
+
*
|
|
4
|
+
* 【这是什么】宿主 `ctx.shell` 的执行器(`./argv-shell.mjs`)在 `run()` 里对已知形状的钩子调用
|
|
5
|
+
* 走一条短路支路:不 spawn、不过 windows-job runner、不重启 Node,直接调【钩子自己的入口】
|
|
6
|
+
* `hook.mjs` 导出的 `main()`。本文件就是那条支路的全部实现(执行器只负责"认出来 + 调用 + 兜底")。
|
|
7
|
+
*
|
|
8
|
+
* 【为什么不重写判定】判定口径的唯一来源是 AuraPolaris 的 `guard-core.mjs` / `adapters/codex-hook.mjs`,
|
|
9
|
+
* 本文件**一行判定都不写**:它调的就是 `main(argv, { defaultMode: 'shadow' })` —— 与子进程里
|
|
10
|
+
* `if (invokedDirectly) process.exit(await main(process.argv.slice(2), { defaultMode: 'shadow' }))`
|
|
11
|
+
* 是【同一个函数、同一份 argv、同一份 stdin】。少一个转述层就少一处会漂移的第二口径。
|
|
12
|
+
*
|
|
13
|
+
* 【为什么能等价】子进程与本进程的差别只有四处,全部按子进程的取值补齐:
|
|
14
|
+
* ① cwd —— 子进程跑在 spec.workdir 下,本进程 chdir 到那里(钩子用 `process.cwd()` 推 `<cwd>/.polaris` 落点,
|
|
15
|
+
* 不补这一处记录就会落到宿主自己的目录里;实测过,不是理论担忧)。
|
|
16
|
+
* ② argv —— 子进程的 `process.argv.slice(2)` 是钩子的旗标,本进程临时换上同一串(`stateInfo()` 只读一次并缓存)。
|
|
17
|
+
* ③ stdin —— 子进程的 stdin 是桥写进去的载荷,本进程换上一个同步吐完 data+end 的假 stdin
|
|
18
|
+
* (宿主自己的 stdin 可能是 TTY,`readStdin()` 一见 isTTY 就直接收空串)。
|
|
19
|
+
* ④ stdout/stderr —— 子进程的两个流是被抓取的管道,本进程临时接管 `process.stdout/stderr.write`。
|
|
20
|
+
* 环境变量不在其列:子进程环境 = 父进程环境【脱敏后】+ {NO_COLOR,PAGER,GIT_PAGER}
|
|
21
|
+
* (dsh-subprocess 的 scrubbedParentEnv:只删 `/KEY|PASSWORD|SECRET|TOKEN/i` 与 `DSH_*`,再把显式项合并上去),
|
|
22
|
+
* 而钩子读的全部环境名(POLARIS_* / USERPROFILE / HOME / TEMP / TMP / TMPDIR)都不在那两类里。
|
|
23
|
+
*
|
|
24
|
+
* 【模块级缓存:唯一需要设防的地方】钩子模块里有若干"一个进程只算一次"的缓存(`stateInfoCache`、
|
|
25
|
+
* `registryCache`、规则表、可写性探针)。子进程每次都是全新的,本进程不是 —— 所以本文件按
|
|
26
|
+
* 【签名】(旗标 + workdir + 全量环境) 各持一份模块实例:签名一变就用带缓存键的 URL 重新 import
|
|
27
|
+
* 一份干净实例(见下方 hookCache)。这样既不会让第二次调用吃到第一次的落点,也不需要退回 spawn。
|
|
28
|
+
*
|
|
29
|
+
* 【失败方向】本文件任何异常都只回答 `{ ok:false, reason }`,由调用方走原来的 spawn 路径;
|
|
30
|
+
* 它【永远不会】合成判定、永远不返回"放行"。唯一返回 `ok:true` 的情况 = 钩子入口真的跑完并给了退出码。
|
|
31
|
+
*
|
|
32
|
+
* 【开关】`AURA_HOOK_INPROC=0` → 执行器直接走 spawn(本文件连 import 都不做判定)。
|
|
33
|
+
* `AURA_HOOK_INPROC_MARKER=<路径>` → 覆盖计数文件(自证用);两者都不进签名(它们是本支路的控制量,
|
|
34
|
+
* 不是钩子的输入)。
|
|
35
|
+
*
|
|
36
|
+
* @module dsh/hook-inproc
|
|
37
|
+
*/
|
|
38
|
+
import fs from 'node:fs'
|
|
39
|
+
import os from 'node:os'
|
|
40
|
+
import path from 'node:path'
|
|
41
|
+
import { createHash } from 'node:crypto'
|
|
42
|
+
import { pathToFileURL } from 'node:url'
|
|
43
|
+
|
|
44
|
+
/** 计数文件("这次走了哪条路"的可观测标记):小、可追加语义、写失败绝不影响调用。 */
|
|
45
|
+
export const MARKER_DIR = path.join(os.homedir(), '.aura', 'pkg', '.cache')
|
|
46
|
+
export const MARKER_FILE = path.join(MARKER_DIR, 'aura-hook-inproc.json')
|
|
47
|
+
/** 本支路自己的控制量:它们变化不改变钩子行为,故不进签名。 */
|
|
48
|
+
const CONTROL_ENV = new Set(['AURA_HOOK_INPROC', 'AURA_HOOK_INPROC_MARKER'])
|
|
49
|
+
/** 钩子脚本的机名面:`hook.mjs`。同目录必须齐这两个兄弟(是"这就是 AuraPolaris 那个钩子"的最低凭证)。 */
|
|
50
|
+
const HOOK_SCRIPT_RE = /(?:^|[\\/])hook\.mjs$/i
|
|
51
|
+
const SIBLINGS = ['guard-core.mjs', path.join('adapters', 'codex-hook.mjs')]
|
|
52
|
+
const NODE_BASENAMES = new Set(['node', 'node.exe', 'nodejs', 'nodejs.exe'])
|
|
53
|
+
/** 与钩子入口同款的模式默认:直接跑 hook.mjs 时用 shadow(enforce 由档位文件/env 决定)。 */
|
|
54
|
+
const MAIN_OPTS = { defaultMode: 'shadow' }
|
|
55
|
+
const PRE_TOOL_USE = 'pretooluse'
|
|
56
|
+
|
|
57
|
+
const NOOP = () => {}
|
|
58
|
+
|
|
59
|
+
/* ------------------------------------------------------------------ 识别 */
|
|
60
|
+
|
|
61
|
+
/** argv[0] 是不是 Node 解释器(容忍 `node` / `node.exe` / 绝对路径 / 正反斜杠)。 */
|
|
62
|
+
export function isNodeInterpreter (token) {
|
|
63
|
+
if (typeof token !== 'string' || token === '') return false
|
|
64
|
+
return NODE_BASENAMES.has(path.basename(token).toLowerCase())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 取 `--event=` / `--event <值>` 的事件名(两种写法都认;没有则 null)。 */
|
|
68
|
+
export function eventFlagValue (argv) {
|
|
69
|
+
for (let i = 0; i < argv.length; i++) {
|
|
70
|
+
const token = argv[i]
|
|
71
|
+
if (typeof token !== 'string') continue
|
|
72
|
+
if (token.startsWith('--event=')) return token.slice('--event='.length)
|
|
73
|
+
if (token === '--event') return typeof argv[i + 1] === 'string' ? argv[i + 1] : null
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 权威识别:这串 argv 是不是「node <…>/hook.mjs … --event=PreToolUse …」。
|
|
80
|
+
* 容错的是写法(引号已被执行器的词法器剥掉、脚本路径可正可反斜杠、旗标可 `=` 可空格),
|
|
81
|
+
* 严格的是事件:事件名必须就是 PreToolUse(其它事件不归本支路)。
|
|
82
|
+
* @param argv - 已切好的 argv。
|
|
83
|
+
* @returns `{ hookPath, hookArgs }`;不是本支路的目标时返回 null(调用方走 spawn)。
|
|
84
|
+
*/
|
|
85
|
+
export function recognizePreToolUseHook (argv) {
|
|
86
|
+
if (!Array.isArray(argv) || argv.length < 2) return null
|
|
87
|
+
if (!isNodeInterpreter(argv[0])) return null
|
|
88
|
+
let scriptIndex = -1
|
|
89
|
+
for (let i = 1; i < argv.length; i++) {
|
|
90
|
+
const token = argv[i]
|
|
91
|
+
if (typeof token !== 'string' || token.startsWith('-')) continue
|
|
92
|
+
if (HOOK_SCRIPT_RE.test(token)) { scriptIndex = i; break }
|
|
93
|
+
}
|
|
94
|
+
if (scriptIndex < 0) return null
|
|
95
|
+
const event = eventFlagValue(argv)
|
|
96
|
+
if (typeof event !== 'string' || event.trim().toLowerCase() !== PRE_TOOL_USE) return null
|
|
97
|
+
return { hookPath: path.resolve(argv[scriptIndex]), hookArgs: argv.slice(scriptIndex + 1) }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 同目录兄弟文件齐不齐 —— 「别把别人家的 hook.mjs import 进宿主进程」的机械凭证。 */
|
|
101
|
+
export function hookLayoutError (hookPath) {
|
|
102
|
+
const dir = path.dirname(hookPath)
|
|
103
|
+
for (const rel of SIBLINGS) {
|
|
104
|
+
try {
|
|
105
|
+
if (!fs.statSync(path.join(dir, rel)).isFile()) return `not_aurapolaris_hook(${rel})`
|
|
106
|
+
} catch { return `not_aurapolaris_hook(${rel})` }
|
|
107
|
+
}
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* -------------------------------------------------------------- 计数标记 */
|
|
112
|
+
|
|
113
|
+
function markerPath () {
|
|
114
|
+
const override = String(process.env.AURA_HOOK_INPROC_MARKER ?? '').trim()
|
|
115
|
+
return override === '' ? MARKER_FILE : path.resolve(override)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readMarker (file) {
|
|
119
|
+
try {
|
|
120
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
121
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) return data
|
|
122
|
+
} catch { /* 首次 / 坏文件:都按空账本处理 */ }
|
|
123
|
+
return {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 记一次分派:`inproc` = 走了本进程内,`spawn` = 退回子进程(带原因)。
|
|
128
|
+
* 契约:**绝不抛**(写不进去只是少一个读数,不许把宿主调用打挂)。
|
|
129
|
+
* @param kind - `'inproc'` | `'spawn'`。
|
|
130
|
+
* @param reason - spawn 档的原因标签(`disabled` / `threw` / `state_signature_changed` / …)。
|
|
131
|
+
* @returns 落盘后的计数对象,或 null(没写成功)。
|
|
132
|
+
*/
|
|
133
|
+
export function noteHookDispatch (kind, reason = '') {
|
|
134
|
+
try {
|
|
135
|
+
const file = markerPath()
|
|
136
|
+
const prev = readMarker(file)
|
|
137
|
+
const next = {
|
|
138
|
+
inproc: Number(prev.inproc) || 0,
|
|
139
|
+
spawn: Number(prev.spawn) || 0,
|
|
140
|
+
reasons: (prev.reasons && typeof prev.reasons === 'object' && !Array.isArray(prev.reasons)) ? { ...prev.reasons } : {},
|
|
141
|
+
pid: process.pid,
|
|
142
|
+
at: new Date().toISOString(),
|
|
143
|
+
}
|
|
144
|
+
if (kind === 'inproc') next.inproc += 1
|
|
145
|
+
else {
|
|
146
|
+
next.spawn += 1
|
|
147
|
+
const key = String(reason || 'unknown').slice(0, 60)
|
|
148
|
+
next.reasons[key] = (Number(next.reasons[key]) || 0) + 1
|
|
149
|
+
}
|
|
150
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
151
|
+
fs.writeFileSync(file, JSON.stringify(next) + '\n')
|
|
152
|
+
return next
|
|
153
|
+
} catch { return null }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ------------------------------------------------------- 加载 + 关键区 */
|
|
157
|
+
|
|
158
|
+
/** 关键区串行闸:全局补丁(argv/cwd/流)必须独占,并发分派不许交叉。 */
|
|
159
|
+
let criticalChain = Promise.resolve()
|
|
160
|
+
/**
|
|
161
|
+
* 【模块级缓存的正确解法】钩子入口按【签名】各持一份模块实例。
|
|
162
|
+
*
|
|
163
|
+
* 为什么不能只持一份:钩子里有“一个进程只算一次”的缓存(`stateInfoCache` / `registryCache` / 规则表 / 可写性探针),
|
|
164
|
+
* 而它的输入不止 argv —— 还有 workdir 与环境。子进程每次都是全新的;本进程只持一份就会【第二次调用吃到
|
|
165
|
+
* 第一次的落点】。
|
|
166
|
+
*
|
|
167
|
+
* 为什么不能“签名一变就退回 spawn”(本文件 0.6.11–0.6.14 的做法):那是用【每次调用都 spawn】去换正确性。
|
|
168
|
+
* 实测(2026-10-16,宿主 pid 24224):`inproc=46 / spawn=103`,其中 `state_signature_changed=68` ——
|
|
169
|
+
* 即签名在真实宿主里每调用一变,于是“进程内跑”实际上一次都没发生,整条支路的收益归零(固定税回到 ~250ms)。
|
|
170
|
+
*
|
|
171
|
+
* 现在的做法:签名一变就【用带缓存键的 URL 重新 import 一份干净实例】(模块级缓存随之全新),
|
|
172
|
+
* 仍然在进程内跑完 —— 语义与“每次 spawn 一个全新进程”等价,代价只有模块图求值(毫秒量级)。
|
|
173
|
+
* 实测:模块实例与 spawn 两条路的 stdout/退出码/落账逐字相同(历史签名差分记录)。
|
|
174
|
+
*/
|
|
175
|
+
const hookCache = new Map()
|
|
176
|
+
/** 实例上限:签名抖动时不能无界增长。到顶就整批丢掉(旧实例会被 GC,功能不受影响)。 */
|
|
177
|
+
const HOOK_CACHE_MAX = 8
|
|
178
|
+
|
|
179
|
+
/** 同一份签名 → 同一个 URL(键);不同签名 → 不同 URL ⇒ Node 视为不同模块、重新求值。 */
|
|
180
|
+
const hookUrlOf = (hookPath, signature) =>
|
|
181
|
+
`${pathToFileURL(hookPath).href}?aura-inproc=${createHash('sha256').update(signature).digest('hex').slice(0, 16)}`
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 上一次分派的签名(只用来区分“签名换过没有”并记一行读数)。
|
|
185
|
+
* 【不再】用它把调用赶回 spawn —— 换签名现在只换实例(见 hookCache)。
|
|
186
|
+
*/
|
|
187
|
+
let seenSignature = null
|
|
188
|
+
|
|
189
|
+
async function loadHookMain (hookPath, signature) {
|
|
190
|
+
const url = hookUrlOf(hookPath, signature)
|
|
191
|
+
const hit = hookCache.get(url)
|
|
192
|
+
if (hit !== undefined) return hit
|
|
193
|
+
const mod = await import(url)
|
|
194
|
+
if (typeof mod.main !== 'function') return null
|
|
195
|
+
// 预热适配器:钩子入口里那一次 `await import('./adapters/codex-hook.mjs')` 若在关键区里发生,
|
|
196
|
+
// 就会在补丁生效期间插进一次真实文件 I/O(那会让宿主别的事件有机会在这扇窗里说话)——
|
|
197
|
+
// 先在干净的全局状态下把它读进模块缓存,关键区里那次 await 就只剩微任务。
|
|
198
|
+
//
|
|
199
|
+
// 【适配器本身【不】按签名复制】——它必须与钩子入口共用同一份模块实例:钩子靠它的导出说话,
|
|
200
|
+
// 复制一份等于在这里造第二个口径。适配器里的状态是每次调用现算的,与 workdir/环境无关。
|
|
201
|
+
await import(pathToFileURL(path.join(path.dirname(hookPath), 'adapters', 'codex-hook.mjs')).href)
|
|
202
|
+
const hook = { path: hookPath, main: mod.main }
|
|
203
|
+
if (hookCache.size >= HOOK_CACHE_MAX) hookCache.clear()
|
|
204
|
+
hookCache.set(url, hook)
|
|
205
|
+
return hook
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 签名的四个组成部分,分开保存 —— 比对时才知道【是哪一段变了】(只影响读数,不影响判定)。
|
|
210
|
+
* 顺序:路径 / workdir / 旗标 / 环境。
|
|
211
|
+
*/
|
|
212
|
+
function signatureParts (hookPath, hookArgs, workdir) {
|
|
213
|
+
const env = Object.keys(process.env)
|
|
214
|
+
.filter((key) => !CONTROL_ENV.has(key.toUpperCase()))
|
|
215
|
+
.sort()
|
|
216
|
+
.map((key) => `${key}=${process.env[key]}`)
|
|
217
|
+
.join('\u0000')
|
|
218
|
+
return [hookPath, workdir, hookArgs.join('\u0000'), env]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 签名 = 旗标 + workdir + 全量环境(控制量除外)。签名一变就退回 spawn。 */
|
|
222
|
+
function signatureOf (hookPath, hookArgs, workdir) {
|
|
223
|
+
return signatureParts(hookPath, hookArgs, workdir).join('\u0001')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const PART_NAMES = ['hookPath', 'workdir', 'hookArgs', 'env']
|
|
227
|
+
|
|
228
|
+
/** 两个字符串第一个不同的下标(没有则 -1)。 */
|
|
229
|
+
function firstDiffIndex (a, b) {
|
|
230
|
+
const n = Math.min(a.length, b.length)
|
|
231
|
+
for (let i = 0; i < n; i++) if (a[i] !== b[i]) return i
|
|
232
|
+
return a.length === b.length ? -1 : n
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 【读数,不是判据】签名变了就写一行「是哪一段变的」到 `<cache>/aura-hook-inproc-signature.log`。
|
|
237
|
+
* 为什么要它:光看 `inproc` / `spawn` 两个计数,分不出“谁在漂”——是 workdir 换了、旗标变了,
|
|
238
|
+
* 还是宿主往环境里塞了东西。这是唯一能把「模块实例按签名分家」这条修法证伪的现场读数。
|
|
239
|
+
* 契约与 noteHookDispatch 同款:**绝不抛**(写不进去只是少一个读数),绝不改变判定。
|
|
240
|
+
*/
|
|
241
|
+
function noteSignatureDrift (prevSig, hookPath, hookArgs, workdir) {
|
|
242
|
+
try {
|
|
243
|
+
const prev = String(prevSig)
|
|
244
|
+
if (prev.indexOf('\u0001') < 0) return
|
|
245
|
+
const prevParts = prev.split('\u0001')
|
|
246
|
+
const nextParts = signatureParts(hookPath, hookArgs, workdir)
|
|
247
|
+
let which = -1
|
|
248
|
+
let detail = ''
|
|
249
|
+
for (let i = 0; i < nextParts.length; i++) {
|
|
250
|
+
if (prevParts[i] === nextParts[i]) continue
|
|
251
|
+
which = i
|
|
252
|
+
const a = String(prevParts[i] ?? '')
|
|
253
|
+
const b = String(nextParts[i] ?? '')
|
|
254
|
+
const at = firstDiffIndex(a, b)
|
|
255
|
+
detail = `@${at} prev[${a.length}]=${JSON.stringify(a.slice(Math.max(0, at - 40), at + 60))} next[${b.length}]=${JSON.stringify(b.slice(Math.max(0, at - 40), at + 60))}`
|
|
256
|
+
break
|
|
257
|
+
}
|
|
258
|
+
if (which < 0) return
|
|
259
|
+
const file = path.join(MARKER_DIR, 'aura-hook-inproc-signature.log')
|
|
260
|
+
const line = `${new Date().toISOString()}\tpart=${PART_NAMES[which] ?? which}\t${detail}\n`
|
|
261
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
262
|
+
fs.appendFileSync(file, line)
|
|
263
|
+
} catch { /* 读数失败不许影响调用 */ }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function enterCritical (fn) {
|
|
267
|
+
const next = criticalChain.then(fn)
|
|
268
|
+
criticalChain = next.then(NOOP, NOOP)
|
|
269
|
+
return next
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 同步吐完 data+end 的假 stdin:`hook.mjs#readStdin()` 只认这三个行为。 */
|
|
273
|
+
function makeStdin (text) {
|
|
274
|
+
const data = typeof text === 'string' ? text : ''
|
|
275
|
+
let onData = null
|
|
276
|
+
return {
|
|
277
|
+
isTTY: false,
|
|
278
|
+
readable: true,
|
|
279
|
+
setEncoding () { return this },
|
|
280
|
+
on (event, cb) {
|
|
281
|
+
if (event === 'data') onData = cb
|
|
282
|
+
else if (event === 'end') {
|
|
283
|
+
if (typeof onData === 'function' && data.length > 0) onData(data)
|
|
284
|
+
if (typeof cb === 'function') cb()
|
|
285
|
+
}
|
|
286
|
+
return this
|
|
287
|
+
},
|
|
288
|
+
addListener (event, cb) { return this.on(event, cb) },
|
|
289
|
+
once (event, cb) { return this.on(event, cb) },
|
|
290
|
+
removeListener () { return this },
|
|
291
|
+
off () { return this },
|
|
292
|
+
resume () { return this },
|
|
293
|
+
pause () { return this },
|
|
294
|
+
destroy () { return this },
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function makeSink (sink) {
|
|
299
|
+
return (chunk, encoding, callback) => {
|
|
300
|
+
if (typeof chunk === 'string') sink.push(chunk)
|
|
301
|
+
else if (chunk !== undefined && chunk !== null) sink.push(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk))
|
|
302
|
+
const cb = typeof callback === 'function' ? callback : (typeof encoding === 'function' ? encoding : null)
|
|
303
|
+
if (cb !== null) queueMicrotask(() => cb())
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function restoreWrite (stream, own) {
|
|
309
|
+
if (own !== undefined) { Object.defineProperty(stream, 'write', own); return }
|
|
310
|
+
// 原本没有自有属性(方法在原型上):删掉我们的遮蔽即可,原型方法原样可见。
|
|
311
|
+
try { delete stream.write } catch { /* 删不掉就留着(下面兜底赋回原值) */ }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 按子进程的取值把四个全局面换掉(见文件头 ①②③④)。
|
|
316
|
+
* @returns 还原所需的一切;调用方必须在 finally 里交给 restoreGlobals。
|
|
317
|
+
*/
|
|
318
|
+
function patchGlobals (hookPath, hookArgs, stdin, workdir, outSink, errSink) {
|
|
319
|
+
const saved = {
|
|
320
|
+
argv: process.argv,
|
|
321
|
+
cwd: process.cwd(),
|
|
322
|
+
stdin: Object.getOwnPropertyDescriptor(process, 'stdin'),
|
|
323
|
+
stdoutWrite: Object.getOwnPropertyDescriptor(process.stdout, 'write'),
|
|
324
|
+
stderrWrite: Object.getOwnPropertyDescriptor(process.stderr, 'write'),
|
|
325
|
+
}
|
|
326
|
+
process.argv = [process.execPath, hookPath, ...hookArgs]
|
|
327
|
+
process.stdout.write = makeSink(outSink)
|
|
328
|
+
process.stderr.write = makeSink(errSink)
|
|
329
|
+
Object.defineProperty(process, 'stdin', { configurable: true, enumerable: true, value: makeStdin(stdin) })
|
|
330
|
+
if (path.resolve(saved.cwd) !== workdir) process.chdir(workdir)
|
|
331
|
+
return saved
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function restoreGlobals (saved) {
|
|
335
|
+
process.argv = saved.argv
|
|
336
|
+
try { if (path.resolve(process.cwd()) !== path.resolve(saved.cwd)) process.chdir(saved.cwd) } catch { /* 原 cwd 没了:留着现在的,别把调用打挂 */ }
|
|
337
|
+
restoreWrite(process.stdout, saved.stdoutWrite)
|
|
338
|
+
restoreWrite(process.stderr, saved.stderrWrite)
|
|
339
|
+
if (saved.stdin !== undefined) Object.defineProperty(process, 'stdin', saved.stdin)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function runCritical (hook, hookArgs, request, workdir) {
|
|
343
|
+
const outSink = []
|
|
344
|
+
const errSink = []
|
|
345
|
+
const saved = patchGlobals(hook.path, hookArgs, request.stdin, workdir, outSink, errSink)
|
|
346
|
+
let code
|
|
347
|
+
try {
|
|
348
|
+
code = await hook.main([...hookArgs], MAIN_OPTS)
|
|
349
|
+
} finally {
|
|
350
|
+
restoreGlobals(saved)
|
|
351
|
+
}
|
|
352
|
+
if (typeof code !== 'number' || !Number.isFinite(code)) return { ok: false, reason: 'no_exit_code' }
|
|
353
|
+
const stdout = outSink.join('')
|
|
354
|
+
const stderr = errSink.join('')
|
|
355
|
+
// 超帽:spawn 那条路会截断 + 溢写 spill,这里【不近似】—— 退回 spawn 让真身去做(守卫判定是确定性的,
|
|
356
|
+
// 重跑一次结论相同;代价只是多一条记录,收益是不会出现"差一字节的判定")。
|
|
357
|
+
if (Buffer.byteLength(stdout, 'utf8') > request.stdoutMaxBytes) return { ok: false, reason: 'stdout_over_cap' }
|
|
358
|
+
if (Buffer.byteLength(stderr, 'utf8') > request.stderrMaxBytes) return { ok: false, reason: 'stderr_over_cap' }
|
|
359
|
+
return { ok: true, exitCode: code, stdout, stderr }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* ---------------------------------------------------------------- 入口 */
|
|
363
|
+
|
|
364
|
+
async function attempt (request) {
|
|
365
|
+
const argv = Array.isArray(request?.argv) ? request.argv : null
|
|
366
|
+
if (argv === null) return { ok: false, reason: 'no_argv' }
|
|
367
|
+
const rec = recognizePreToolUseHook(argv)
|
|
368
|
+
if (rec === null) return { ok: false, reason: 'unrecognized' }
|
|
369
|
+
const layout = hookLayoutError(rec.hookPath)
|
|
370
|
+
if (layout !== null) return { ok: false, reason: layout }
|
|
371
|
+
if (request.signalAborted === true) return { ok: false, reason: 'caller_aborted' }
|
|
372
|
+
const workdir = path.resolve(String(request.workdir ?? '') || process.cwd())
|
|
373
|
+
const signature = signatureOf(rec.hookPath, rec.hookArgs, workdir)
|
|
374
|
+
// 签名换了 ⇒ 拿一份【干净实例】(不是退回 spawn,见文件头“模块级缓存的正确解法”)。
|
|
375
|
+
// 只记读数:上一次是哪一段变的 —— 它能直接回答“这条支路到底有没有在跑”。
|
|
376
|
+
if (seenSignature !== null && seenSignature !== signature) noteSignatureDrift(seenSignature, rec.hookPath, rec.hookArgs, workdir)
|
|
377
|
+
seenSignature = signature
|
|
378
|
+
const hook = await loadHookMain(rec.hookPath, signature)
|
|
379
|
+
if (hook === null) return { ok: false, reason: 'module_unavailable' }
|
|
380
|
+
const stdoutMaxBytes = positiveInt(request.stdoutMaxBytes)
|
|
381
|
+
const stderrMaxBytes = positiveInt(request.stderrMaxBytes)
|
|
382
|
+
return await enterCritical(() => runCritical(hook, rec.hookArgs, {
|
|
383
|
+
stdin: typeof request.stdin === 'string' ? request.stdin : '',
|
|
384
|
+
stdoutMaxBytes,
|
|
385
|
+
stderrMaxBytes,
|
|
386
|
+
}, workdir))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function positiveInt (value) {
|
|
390
|
+
return Number.isInteger(value) && value > 0 ? value : 64 * 1024
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 试着在本进程内跑完这次 PreToolUse 钩子调用。
|
|
395
|
+
* 契约:**绝不抛**(任何异常都降级成 `{ ok:false, reason }`),**绝不合成判定**。
|
|
396
|
+
* @param request - `{ argv, stdin, workdir, stdoutMaxBytes, stderrMaxBytes, signalAborted }`。
|
|
397
|
+
* @returns `{ ok:true, exitCode, stdout, stderr }` 或 `{ ok:false, reason }`;后者一律由调用方走 spawn。
|
|
398
|
+
*/
|
|
399
|
+
export async function runPreToolUseHookInProcess (request) {
|
|
400
|
+
try {
|
|
401
|
+
return await attempt(request)
|
|
402
|
+
} catch (error) {
|
|
403
|
+
return { ok: false, reason: `dispatcher_error:${String(error?.message ?? error).slice(0, 80)}` }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export default runPreToolUseHookInProcess
|
package/package.json
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aura-basetools-dsh",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness adapter for aura-basetools
|
|
3
|
+
"version": "0.13.1",
|
|
4
|
+
"description": "DeepSeek Harness adapter for aura-basetools: aura_basetools_* tools and hook executor. Depends on the DSH-free core package. / aura-basetools 的 DSH 适配器。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"aura-basetools": "0.
|
|
8
|
+
"aura-basetools": "0.13.1"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
11
|
"@deepseek-ai/dsh-tools": "*",
|
|
12
|
+
"@deepseek-ai/dsh-shell": "*",
|
|
12
13
|
"@deepseek-ai/schemastery": "*"
|
|
13
14
|
},
|
|
15
|
+
"peerDependenciesMeta": {
|
|
16
|
+
"@deepseek-ai/dsh-tools": {
|
|
17
|
+
"optional": true
|
|
18
|
+
},
|
|
19
|
+
"@deepseek-ai/dsh-shell": {
|
|
20
|
+
"optional": true
|
|
21
|
+
},
|
|
22
|
+
"@deepseek-ai/schemastery": {
|
|
23
|
+
"optional": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
14
26
|
"dsh": {
|
|
15
27
|
"bundle": {
|
|
16
28
|
"patch": "./cordis.patch.yml"
|
|
@@ -20,27 +32,29 @@
|
|
|
20
32
|
"./package.json": "./package.json",
|
|
21
33
|
"./aura-read/index.js": "./aura-read/index.js",
|
|
22
34
|
"./aura-edit/index.js": "./aura-edit/index.js",
|
|
35
|
+
"./dsh/*": "./dsh/*",
|
|
23
36
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
24
37
|
},
|
|
25
38
|
"files": [
|
|
26
39
|
"aura-read/",
|
|
27
40
|
"aura-edit/",
|
|
41
|
+
"dsh/",
|
|
28
42
|
"cordis.patch.yml",
|
|
29
43
|
"README.md",
|
|
30
44
|
"LICENSE"
|
|
31
45
|
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"registry": "https://registry.npmjs.org"
|
|
49
|
+
},
|
|
32
50
|
"repository": {
|
|
33
51
|
"type": "git",
|
|
34
52
|
"url": "https://github.com/Ck-epsilon/Aura",
|
|
35
53
|
"directory": "aura-basetools"
|
|
36
54
|
},
|
|
37
|
-
"homepage": "https://github.com/Ck-epsilon/Aura/tree/main/aura-basetools
|
|
55
|
+
"homepage": "https://github.com/Ck-epsilon/Aura/tree/main/aura-basetools",
|
|
38
56
|
"bugs": {
|
|
39
57
|
"url": "https://github.com/Ck-epsilon/Aura/issues"
|
|
40
58
|
},
|
|
41
|
-
"author": "Ck-epsilon"
|
|
42
|
-
"publishConfig": {
|
|
43
|
-
"access": "public",
|
|
44
|
-
"registry": "https://registry.npmjs.org"
|
|
45
|
-
}
|
|
59
|
+
"author": "Ck-epsilon"
|
|
46
60
|
}
|