116 lines
3.8 KiB
JavaScript
116 lines
3.8 KiB
JavaScript
/**
|
|
* Thin adapter from DeepSeek Harness tool calls to the claude-harness bash hooks.
|
|
*
|
|
* The guard logic stays in `hooks/harness_target.py` (the source of truth, also
|
|
* covered by the repository's own tests). This module only:
|
|
* - maps DSH tool names to the Claude hook vocabulary;
|
|
* - synthesizes the `{ cwd, tool_name, tool_input }` payload;
|
|
* - runs `guard-main.sh` / `require-plan.sh`;
|
|
* - turns `exit 2` into a denial reason and every other failure into a pass,
|
|
* preserving the harness invariant "a parsing failure must not block".
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
|
|
/** DSH tool name -> Claude Code tool name used by the hooks. */
|
|
const TOOL_NAMES = {
|
|
write: 'Write',
|
|
edit: 'Edit',
|
|
multi_edit: 'MultiEdit',
|
|
str_replace_editor: 'Edit',
|
|
bash: 'Bash',
|
|
}
|
|
|
|
/** Tools that `require-plan.sh` must inspect in addition to `guard-main.sh`. */
|
|
const PLAN_TOOLS = new Set(['write', 'edit', 'multi_edit', 'str_replace_editor'])
|
|
|
|
const DEFAULT_TIMEOUT_MS = 15_000
|
|
|
|
/** Claude skill namespaces do not exist in DSH; rewrite the hook messages. */
|
|
export function forDshReason(reason) {
|
|
return String(reason)
|
|
.replaceAll('superpowers:using-git-worktrees', 'using-git-worktrees')
|
|
.replaceAll('superpowers:brainstorming', 'brainstorming')
|
|
}
|
|
|
|
/**
|
|
* Run one hook script with the synthesized payload.
|
|
* @returns the denial text for exit 2, otherwise undefined.
|
|
*/
|
|
export function runHook({ script, hooksDir, payload, logger, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
const path = join(hooksDir, script)
|
|
if (!existsSync(path)) {
|
|
logger?.warn?.('[dsh-harness] hook not found: %s', path)
|
|
return undefined
|
|
}
|
|
try {
|
|
execFileSync('bash', [path], {
|
|
input: JSON.stringify(payload),
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
timeout: timeoutMs,
|
|
})
|
|
return undefined
|
|
} catch (error) {
|
|
if (error?.status === 2) {
|
|
const reason = String(error.stderr ?? '').trim()
|
|
return forDshReason(reason || `BLOCKED by ${script}`)
|
|
}
|
|
// Fail open: a broken hook must never stop the agent.
|
|
logger?.warn?.('[dsh-harness] %s failed open: %s', script, error?.message ?? String(error))
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
/** Normalize the DSH arguments into the shape `harness_target.py` reads. */
|
|
export function normalizeToolInput(toolName, args) {
|
|
const input = { ...(args ?? {}) }
|
|
if (toolName !== 'Bash') {
|
|
const path = input.file_path ?? input.path
|
|
if (typeof path === 'string' && path !== '') input.file_path = path
|
|
}
|
|
return input
|
|
}
|
|
|
|
/**
|
|
* Decide one tool call.
|
|
* @param {object} options
|
|
* @param {{name: string, arguments?: object}} options.exec - DSH tool execution.
|
|
* @param {string} options.cwd - Session working directory.
|
|
* @param {string} options.hooksDir - Directory holding guard-main.sh.
|
|
* @param {object} [options.logger] - Cordis logger.
|
|
* @returns {string|undefined} denial reason.
|
|
*/
|
|
export function checkTool({ exec, cwd, hooksDir, logger }) {
|
|
if (!cwd || !exec || typeof exec.name !== 'string') return undefined
|
|
const toolName = TOOL_NAMES[exec.name]
|
|
if (!toolName) return undefined
|
|
|
|
const payload = {
|
|
cwd,
|
|
tool_name: toolName,
|
|
tool_input: normalizeToolInput(toolName, exec.arguments),
|
|
}
|
|
|
|
const guardReason = runHook({ script: 'guard-main.sh', hooksDir, payload, logger })
|
|
if (guardReason !== undefined) return guardReason
|
|
|
|
if (PLAN_TOOLS.has(exec.name)) {
|
|
const planReason = runHook({ script: 'require-plan.sh', hooksDir, payload, logger })
|
|
if (planReason !== undefined) return planReason
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
/** Bind the module to one bundle configuration. */
|
|
export function createGuard({ hooksDir, logger }) {
|
|
return {
|
|
check(exec, cwd) {
|
|
return checkTool({ exec, cwd, hooksDir, logger })
|
|
},
|
|
}
|
|
}
|