Files
hakorune/tools/codex-tmux-driver/codex-hook-wrapper.js
Moe Charm 4e1b595796 AI協調開発研究ドキュメントの完成と Phase 10.9-β 進捗
【AI協調開発研究】
- AI二重化モデルの学術論文draft完成(workshop_paper_draft.md)
- 「隠れた危機」分析とbirthの原則哲学化
- TyEnv「唯一の真実」協調会話を保存・研究資料に統合
- papers管理構造の整備(wip/under-review/published分離)

【Phase 10.9-β HostCall進捗】
- JitConfigBox: relax_numeric フラグ追加(i64→f64コアーション制御)
- HostcallRegistryBox: 署名検証・白黒リスト・コアーション対応
- JitHostcallRegistryBox: Nyash側レジストリ操作API
- Lower統合: env直読 → jit::config::current() 参照に統一
- 数値緩和設定: NYASH_JIT_HOSTCALL_RELAX_NUMERIC/Config.set_flag

【検証サンプル拡充】
- math.sin/cos/abs/min/max 関数スタイル(examples/jit_math_function_style_*.nyash)
- 境界ケース: 署名不一致・コアーション許可・mutating拒否サンプル
- E2E実証: String.length→allow, Array.push→fallback, math関数の署名一致観測

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 12:09:09 +09:00

292 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// codex-hook-wrapper.js
// Codexバイナリにフックをかけて入出力を横取りするラッパー
// 使い方: このファイルを codex として PATH に配置
const { spawn } = require('child_process');
const path = require('path');
let WebSocket;
try {
WebSocket = require('ws');
} catch (e) {
console.error('FATAL: Cannot find module "ws"');
console.error('Hint: run "npm install" inside tools/codex-tmux-driver, or ensure the wrapper is symlinked to that directory.');
process.exit(1);
}
const fs = require('fs');
// 設定
// 実バイナリは環境変数で上書き可能。未設定かつ存在しない場合はエラーにする。
const REAL_CODEX = process.env.CODEX_REAL_BIN || '/home/tomoaki/.volta/tools/image/packages/@openai/codex/lib/node_modules/@openai/codex/bin/codex-x86_64-unknown-linux-musl';
const HOOK_SERVER = process.env.CODEX_HOOK_SERVER || 'ws://localhost:8770';
const LOG_FILE = process.env.CODEX_LOG_FILE || '/tmp/codex-hook.log';
const ENTER_MODE = (process.env.CODEX_HOOK_ENTER || 'crlf').toLowerCase(); // lf|cr|crlf
const ENABLE_HOOK = process.env.CODEX_HOOK_ENABLE !== 'false';
const USE_SCRIPT_PTY = process.env.CODEX_USE_SCRIPT_PTY === 'true'; // default false
const SHOW_BANNER = process.env.CODEX_HOOK_BANNER !== 'false';
const ECHO_INJECT = process.env.CODEX_HOOK_ECHO_INJECT === 'true';
const PRE_NEWLINE = process.env.CODEX_HOOK_PRENEWLINE === 'true';
const INJECT_PREFIX = process.env.CODEX_HOOK_INJECT_PREFIX || '';
const INJECT_SUFFIX = process.env.CODEX_HOOK_INJECT_SUFFIX || '';
// WebSocket接続オプショナル
let ws = null;
if (ENABLE_HOOK) {
console.error(`[codex-hook] Attempting to connect to ${HOOK_SERVER}...`);
try {
ws = new WebSocket(HOOK_SERVER);
ws.on('open', () => {
// 目印ログ(接続先確認)
log('hook-connect', { url: HOOK_SERVER });
console.error(`[codex-hook] ✅ Successfully connected to ${HOOK_SERVER}`);
});
ws.on('error', (e) => {
// 接続エラーは無視(フォールバック動作)
console.error(`[codex-hook] ❌ Connection error: ${e?.message || e}`);
});
ws.on('close', () => {
console.error(`[codex-hook] 🔌 Connection closed`);
});
} catch (e) {
// WebSocketサーバーが起動していない場合は通常動作
console.error(`[codex-hook] ❌ Failed to create WebSocket: ${e}`);
}
}
// ログ関数
function log(type, data) {
const timestamp = new Date().toISOString();
const logEntry = { timestamp, type, data };
// ファイルログ
fs.appendFileSync(LOG_FILE, JSON.stringify(logEntry) + '\n');
// WebSocket送信
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(logEntry));
}
}
// Codexプロセス起動
if (!fs.existsSync(REAL_CODEX)) {
console.error(`FATAL: REAL_CODEX not found: ${REAL_CODEX}`);
console.error('Set CODEX_REAL_BIN to the real Codex binary path.');
process.exit(1);
}
// 引数決定(無指定時は既定コマンドを許可)
let userArgs = process.argv.slice(2);
const DEFAULT_CMD = process.env.CODEX_WRAPPER_DEFAULT_CMD; // 例: "exec --ask-for-approval never"
if (userArgs.length === 0 && DEFAULT_CMD) {
try {
userArgs = DEFAULT_CMD.split(' ').filter(Boolean);
if (SHOW_BANNER) {
console.error(`[codex-hook] using default cmd: ${DEFAULT_CMD}`);
}
} catch {}
}
// script(1) を使って擬似TTY経由で起動インジェクションを確実に通すため
function shEscape(s) { return `'${String(s).replace(/'/g, `'\''`)}'`; }
let codexProcess;
let usingPty = false;
try {
if (USE_SCRIPT_PTY) {
const cmdStr = [REAL_CODEX, ...userArgs].map(shEscape).join(' ');
// -q: quiet, -f: flush, -e: return child exit code, -c: command
codexProcess = spawn('script', ['-qfec', cmdStr, '/dev/null'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env
});
usingPty = true;
log('start-info', { mode: 'pty(script)', cmd: cmdStr });
}
} catch (e) {
// フォールバック
}
if (!codexProcess) {
codexProcess = spawn(REAL_CODEX, userArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env
});
log('start-info', { mode: 'pipe', bin: REAL_CODEX, args: process.argv.slice(2) });
}
// 入力フック(標準入力 → Codex
let inputBuffer = '';
process.stdin.on('data', (chunk) => {
const data = chunk.toString();
inputBuffer += data;
// 改行で区切って入力を記録
if (data.includes('\n')) {
const lines = inputBuffer.split('\n');
inputBuffer = lines.pop() || '';
lines.forEach(line => {
if (line.trim()) {
log('input', line);
// 入力パターン検出
if (ENABLE_HOOK) {
detectInputPattern(line);
}
}
});
}
// そのままCodexに転送
codexProcess.stdin.write(chunk);
});
// 出力フックCodex → 標準出力)
let outputBuffer = '';
codexProcess.stdout.on('data', (chunk) => {
const data = chunk.toString();
outputBuffer += data;
// バッファリングして意味のある単位で記録
if (data.includes('\n') || data.includes('▌')) {
log('output', outputBuffer);
// 出力パターン検出
if (ENABLE_HOOK) {
detectOutputPattern(outputBuffer);
}
outputBuffer = '';
}
// そのまま標準出力へ
process.stdout.write(chunk);
});
// エラー出力
codexProcess.stderr.on('data', (chunk) => {
log('error', chunk.toString());
process.stderr.write(chunk);
});
// プロセス終了
codexProcess.on('exit', (code, signal) => {
log('exit', { code, signal });
if (ws) {
try {
ws.send(JSON.stringify({
type: 'hook-event',
event: 'codex-exit',
data: { code, signal }
}));
} catch {}
ws.close();
}
process.exit(typeof code === 'number' ? code : 0);
});
// 入力パターン検出
function detectInputPattern(input) {
const patterns = {
question: /\?$|どうしますか|どう思いますか/,
command: /^(status|help|exit|clear)/,
code: /^(function|box|if|for|while|return)/
};
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(input)) {
log('input-pattern', { type, input });
// 特定パターンでの自動介入
if (type === 'question' && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'hook-event',
event: 'question-detected',
data: input
}));
}
}
}
}
// 出力パターン検出
function detectOutputPattern(output) {
const patterns = {
thinking: /考え中|Processing|Thinking|分析中/,
complete: /完了|Complete|Done|終了/,
error: /エラー|Error|失敗|Failed/,
waiting: /waiting|待機中|入力待ち|▌/
};
for (const [type, pattern] of Object.entries(patterns)) {
if (pattern.test(output)) {
log('output-pattern', { type, output: output.substring(0, 100) });
// 待機状態での介入ポイント
if (type === 'waiting' && ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'hook-event',
event: 'waiting-detected',
data: output
}));
}
}
}
}
// シグナルハンドリング
process.on('SIGINT', () => {
codexProcess.kill('SIGINT');
});
process.on('SIGTERM', () => {
codexProcess.kill('SIGTERM');
});
// WebSocketからの介入コマンド受信
if (ws) {
ws.on('message', (data) => {
try {
const cmd = JSON.parse(data.toString());
if (cmd.type === 'inject-input') {
// Codexに入力を注入
log('inject', cmd.data);
// Enterの扱いは環境依存のため、モードで切替デフォルト: crlf
let eol = '\r\n';
if (ENTER_MODE === 'lf') eol = '\n';
else if (ENTER_MODE === 'cr') eol = '\r';
try {
const payload = `${INJECT_PREFIX}${cmd.data}${INJECT_SUFFIX}`;
if (PRE_NEWLINE) {
codexProcess.stdin.write('\n');
}
const written = codexProcess.stdin.write(payload + eol);
if (ECHO_INJECT) {
// メッセージだけをシンプルに表示
process.stdout.write(`\n\n${payload}\n`);
}
} catch (e) {
console.error(`[codex-hook] ❌ Error writing to stdin: ${e}`);
log('inject-error', e?.message || String(e));
}
}
} catch (e) {
console.error(`[codex-hook] ❌ Error parsing message: ${e}`);
}
});
}
// 起動ログ
log('start', {
args: process.argv.slice(2),
pid: process.pid,
hookEnabled: ENABLE_HOOK,
usingPty
});
if (SHOW_BANNER) {
console.error(`[codex-hook] active (pty=${usingPty ? 'on' : 'off'} enter=${ENTER_MODE}) REAL_CODEX=${REAL_CODEX}`);
}