triflux 10.7.0 → 10.7.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/hooks/safety-guard.mjs
CHANGED
|
@@ -69,6 +69,7 @@ const WT_DIRECT_BLOCK_MESSAGE =
|
|
|
69
69
|
|
|
70
70
|
// ── SSH+PowerShell bash 문법 차단 ────────────────────────────
|
|
71
71
|
// 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
|
|
72
|
+
// macOS/Linux 대상 SSH에는 bash 문법이 정상이므로 hosts.json OS를 확인한다.
|
|
72
73
|
const BASH_SYNTAX_IN_SSH = [
|
|
73
74
|
/2>\/dev\/null/, // 2>/dev/null → PowerShell에서 Out-File C:\dev\null
|
|
74
75
|
/>\s*\/dev\/null/, // >/dev/null
|
|
@@ -82,6 +83,48 @@ const SSH_POWERSHELL_HINT =
|
|
|
82
83
|
"원격 셸이 PowerShell입니다. bash 문법 직접 전달 금지. scp + pwsh -File 패턴 사용. " +
|
|
83
84
|
"2>/dev/null → 2>$null, $() → $(), export → $env:, source → . (dot-source)";
|
|
84
85
|
|
|
86
|
+
/** hosts.json에서 Windows 호스트 식별자 집합을 구축한다. */
|
|
87
|
+
function getWindowsHostIds() {
|
|
88
|
+
const ids = new Set();
|
|
89
|
+
try {
|
|
90
|
+
const paths = [
|
|
91
|
+
join(process.cwd(), "references", "hosts.json"),
|
|
92
|
+
join(process.cwd(), "packages", "triflux", "references", "hosts.json"),
|
|
93
|
+
];
|
|
94
|
+
let hostsConfig = null;
|
|
95
|
+
for (const p of paths) {
|
|
96
|
+
if (existsSync(p)) {
|
|
97
|
+
hostsConfig = JSON.parse(readFileSync(p, "utf8"));
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!hostsConfig?.hosts) return ids;
|
|
102
|
+
for (const [name, cfg] of Object.entries(hostsConfig.hosts)) {
|
|
103
|
+
if (cfg.os !== "windows") continue;
|
|
104
|
+
ids.add(name);
|
|
105
|
+
if (cfg.tailscale?.ip) ids.add(cfg.tailscale.ip);
|
|
106
|
+
if (cfg.tailscale?.dns) ids.add(cfg.tailscale.dns);
|
|
107
|
+
if (cfg.ssh_user) {
|
|
108
|
+
ids.add(`${cfg.ssh_user}@${name}`);
|
|
109
|
+
if (cfg.tailscale?.ip) ids.add(`${cfg.ssh_user}@${cfg.tailscale.ip}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// hosts.json 로드 실패 시 빈 집합 → 차단 안 함 (POSIX 기본)
|
|
114
|
+
}
|
|
115
|
+
return ids;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** SSH 명령의 대상이 Windows 호스트인지 판별한다. */
|
|
119
|
+
function isSshTargetWindows(command) {
|
|
120
|
+
const winIds = getWindowsHostIds();
|
|
121
|
+
if (winIds.size === 0) return false; // Windows 호스트 없으면 POSIX 가정
|
|
122
|
+
for (const id of winIds) {
|
|
123
|
+
if (command.includes(id)) return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
85
128
|
// ── 경고 규칙 ──────────────────────────────────────────────
|
|
86
129
|
const WARN_RULES = [
|
|
87
130
|
{
|
|
@@ -248,10 +291,9 @@ function main() {
|
|
|
248
291
|
}
|
|
249
292
|
}
|
|
250
293
|
|
|
251
|
-
// 0.5. SSH → PowerShell
|
|
252
|
-
//
|
|
253
|
-
if (hasSegmentInvocation(command, [/^\s*ssh\s+/i])) {
|
|
254
|
-
// 세그먼트 분리 후 ssh로 시작하는 세그먼트의 payload만 검사
|
|
294
|
+
// 0.5. SSH → Windows(PowerShell) 호스트에만 bash 문법 전달 차단
|
|
295
|
+
// macOS/Linux 대상은 bash/zsh이므로 허용. hosts.json OS로 판별.
|
|
296
|
+
if (hasSegmentInvocation(command, [/^\s*ssh\s+/i]) && isSshTargetWindows(command)) {
|
|
255
297
|
const segments = command.split(/\s*(?:&&|;|\|\||\|)\s*/);
|
|
256
298
|
for (const seg of segments) {
|
|
257
299
|
const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
|
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
// external source 훅 (session-vault 등)은 여전히 execFile로 실행된다.
|
|
12
12
|
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
15
|
import { createModuleLogger } from "../scripts/lib/logger.mjs";
|
|
16
16
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const SCRIPTS = join(__dirname, "..", "scripts");
|
|
19
|
+
const importMod = (p) => import(pathToFileURL(p).href);
|
|
19
20
|
|
|
20
21
|
const log = createModuleLogger("session-start-fast");
|
|
21
22
|
|
|
@@ -31,7 +32,7 @@ async function runBlocking(stdinData) {
|
|
|
31
32
|
// 1. setup.runCritical — 환경 초기화 필수
|
|
32
33
|
try {
|
|
33
34
|
const t0 = performance.now();
|
|
34
|
-
const setup = await
|
|
35
|
+
const setup = await importMod(join(SCRIPTS, "setup.mjs"));
|
|
35
36
|
const result = await setup.runCritical(stdinData);
|
|
36
37
|
const dur = performance.now() - t0;
|
|
37
38
|
timings.push({ hook: "setup.critical", dur_ms: Math.round(dur) });
|
|
@@ -45,7 +46,7 @@ async function runBlocking(stdinData) {
|
|
|
45
46
|
// 2. mcp-safety-guard.run — EPERM 방지
|
|
46
47
|
try {
|
|
47
48
|
const t0 = performance.now();
|
|
48
|
-
const guard = await
|
|
49
|
+
const guard = await importMod(join(SCRIPTS, "mcp-safety-guard.mjs"));
|
|
49
50
|
guard.run();
|
|
50
51
|
const dur = performance.now() - t0;
|
|
51
52
|
timings.push({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) });
|
|
@@ -67,21 +68,21 @@ function runDeferred(stdinData) {
|
|
|
67
68
|
{
|
|
68
69
|
name: "hub-ensure",
|
|
69
70
|
fn: async () => {
|
|
70
|
-
const mod = await
|
|
71
|
+
const mod = await importMod(join(SCRIPTS, "hub-ensure.mjs"));
|
|
71
72
|
return mod.run(stdinData);
|
|
72
73
|
},
|
|
73
74
|
},
|
|
74
75
|
{
|
|
75
76
|
name: "mcp-gateway-ensure",
|
|
76
77
|
fn: async () => {
|
|
77
|
-
const mod = await
|
|
78
|
+
const mod = await importMod(join(SCRIPTS, "mcp-gateway-ensure.mjs"));
|
|
78
79
|
return mod.run(stdinData);
|
|
79
80
|
},
|
|
80
81
|
},
|
|
81
82
|
{
|
|
82
83
|
name: "setup.deferred",
|
|
83
84
|
fn: async () => {
|
|
84
|
-
const mod = await
|
|
85
|
+
const mod = await importMod(join(SCRIPTS, "setup.mjs"));
|
|
85
86
|
return mod.runDeferred(stdinData);
|
|
86
87
|
},
|
|
87
88
|
},
|
|
@@ -107,7 +108,7 @@ function runDeferred(stdinData) {
|
|
|
107
108
|
*/
|
|
108
109
|
function runBackground(stdinData) {
|
|
109
110
|
// preflight-cache
|
|
110
|
-
|
|
111
|
+
importMod(join(SCRIPTS, "preflight-cache.mjs"))
|
|
111
112
|
.then((mod) => mod.run(stdinData))
|
|
112
113
|
.catch(() => {}); // 완전 무시
|
|
113
114
|
|
|
@@ -78,9 +78,16 @@ function normalizePosixProbeEnv(parsed) {
|
|
|
78
78
|
return Object.freeze({
|
|
79
79
|
claudePath:
|
|
80
80
|
!parsed.claude || parsed.claude === "notfound" ? null : parsed.claude,
|
|
81
|
+
codexPath:
|
|
82
|
+
!parsed.codex || parsed.codex === "notfound" ? null : parsed.codex,
|
|
83
|
+
geminiPath:
|
|
84
|
+
!parsed.gemini || parsed.gemini === "notfound" ? null : parsed.gemini,
|
|
81
85
|
home: parsed.home,
|
|
82
86
|
os,
|
|
83
87
|
shell: parsed.shell === "zsh" ? "zsh" : "bash",
|
|
88
|
+
node: parsed.node || null,
|
|
89
|
+
cores: parsed.cores ? Number(parsed.cores) : null,
|
|
90
|
+
ramGb: parsed.ram_gb ? Number(parsed.ram_gb) : null,
|
|
84
91
|
});
|
|
85
92
|
}
|
|
86
93
|
|
|
@@ -111,10 +118,16 @@ function probeRemoteEnvViaPwsh(host) {
|
|
|
111
118
|
|
|
112
119
|
function probeRemoteEnvViaPosix(host) {
|
|
113
120
|
const script = [
|
|
121
|
+
"export PATH=/opt/homebrew/bin:/opt/homebrew/sbin:$HOME/.local/bin:$PATH",
|
|
114
122
|
"echo shell=$(basename $SHELL)",
|
|
115
123
|
"echo home=$HOME",
|
|
116
124
|
"command -v claude >/dev/null 2>&1 && echo claude=$(command -v claude) || echo claude=notfound",
|
|
125
|
+
"command -v codex >/dev/null 2>&1 && echo codex=$(command -v codex) || echo codex=notfound",
|
|
126
|
+
"command -v gemini >/dev/null 2>&1 && echo gemini=$(command -v gemini) || echo gemini=notfound",
|
|
117
127
|
"echo os=$(uname -s | tr A-Z a-z)",
|
|
128
|
+
"node --version 2>/dev/null && echo node=$(node --version) || echo node=notfound",
|
|
129
|
+
// darwin: sysctl, linux: nproc + /proc/meminfo
|
|
130
|
+
"if [ $(uname -s) = Darwin ]; then echo cores=$(sysctl -n hw.ncpu); echo ram_gb=$(($(sysctl -n hw.memsize) / 1073741824)); else echo cores=$(nproc 2>/dev/null || echo 0); echo ram_gb=$(($(grep MemTotal /proc/meminfo 2>/dev/null | awk '{print $2}') / 1048576)); fi",
|
|
118
131
|
].join("\n");
|
|
119
132
|
|
|
120
133
|
try {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// multi-line prompt text
|
|
16
16
|
|
|
17
17
|
import { readFileSync } from "node:fs";
|
|
18
|
+
import { selectHostForCapability } from "../lib/ssh-command.mjs";
|
|
18
19
|
|
|
19
20
|
/** Shard schema defaults */
|
|
20
21
|
const SHARD_DEFAULTS = Object.freeze({
|
|
@@ -260,6 +261,10 @@ export function planSwarm(prdPath, opts = {}) {
|
|
|
260
261
|
throw new Error(`Dependency cycle detected: ${cycles[0].join(" → ")}`);
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
// Auto-remote suggestion: PRD에 host 미지정 shard가 있고,
|
|
265
|
+
// hosts.json에 가용 원격 호스트가 있으면 제안 데이터를 생성한다.
|
|
266
|
+
const remoteSuggestion = buildRemoteSuggestion(shards, opts.repoRoot);
|
|
267
|
+
|
|
263
268
|
return Object.freeze({
|
|
264
269
|
shards: Object.freeze(shards.map((s) => Object.freeze({ ...s }))),
|
|
265
270
|
leaseMap,
|
|
@@ -267,5 +272,74 @@ export function planSwarm(prdPath, opts = {}) {
|
|
|
267
272
|
mergeOrder,
|
|
268
273
|
conflicts,
|
|
269
274
|
criticalShards: shards.filter((s) => s.critical).map((s) => s.name),
|
|
275
|
+
remoteSuggestion,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* PRD shard에 host가 없고 원격 호스트가 가용하면 분배 제안을 생성한다.
|
|
281
|
+
* 실제 AskUserQuestion 호출은 스킬(tfx-swarm)에서 수행. 여기는 데이터만.
|
|
282
|
+
* @param {Shard[]} shards
|
|
283
|
+
* @param {string} [repoRoot]
|
|
284
|
+
* @returns {object|null} 제안 데이터 또는 null (제안 없음)
|
|
285
|
+
*/
|
|
286
|
+
function buildRemoteSuggestion(shards, repoRoot) {
|
|
287
|
+
const localShards = shards.filter((s) => !s.host);
|
|
288
|
+
if (localShards.length === 0) return null; // 모든 shard에 host 지정됨
|
|
289
|
+
|
|
290
|
+
// 각 shard의 agent에 대해 가용 원격 호스트 조회
|
|
291
|
+
const agentTypes = [...new Set(localShards.map((s) => s.agent))];
|
|
292
|
+
const availableHosts = [];
|
|
293
|
+
|
|
294
|
+
for (const agent of agentTypes) {
|
|
295
|
+
try {
|
|
296
|
+
const hosts = selectHostForCapability(agent, repoRoot);
|
|
297
|
+
for (const h of hosts) {
|
|
298
|
+
if (!availableHosts.find((ah) => ah.name === h.name)) {
|
|
299
|
+
availableHosts.push(h);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
// hosts.json 없거나 파싱 실패 → 무시
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (availableHosts.length === 0) return null;
|
|
308
|
+
|
|
309
|
+
// 분배 제안: 로컬 shard 중 절반(내림)을 원격에 배치
|
|
310
|
+
const remoteCount = Math.min(
|
|
311
|
+
Math.floor(localShards.length / 2),
|
|
312
|
+
availableHosts.length,
|
|
313
|
+
);
|
|
314
|
+
if (remoteCount === 0) return null;
|
|
315
|
+
|
|
316
|
+
// 의존성 없는 shard를 우선 원격 후보로 선택
|
|
317
|
+
const candidates = localShards
|
|
318
|
+
.filter((s) => s.depends.length === 0)
|
|
319
|
+
.concat(localShards.filter((s) => s.depends.length > 0));
|
|
320
|
+
|
|
321
|
+
const suggested = [];
|
|
322
|
+
for (let i = 0; i < remoteCount && i < candidates.length; i++) {
|
|
323
|
+
const shard = candidates[i];
|
|
324
|
+
const host = availableHosts[i % availableHosts.length];
|
|
325
|
+
suggested.push({
|
|
326
|
+
shardName: shard.name,
|
|
327
|
+
host: host.name,
|
|
328
|
+
hostDescription: host.config.description,
|
|
329
|
+
specs: host.specs,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return Object.freeze({
|
|
334
|
+
localCount: localShards.length - remoteCount,
|
|
335
|
+
remoteCount,
|
|
336
|
+
totalShards: shards.length,
|
|
337
|
+
availableHosts: availableHosts.map((h) => ({
|
|
338
|
+
name: h.name,
|
|
339
|
+
description: h.config.description,
|
|
340
|
+
specs: h.specs,
|
|
341
|
+
capabilities: h.config.capabilities,
|
|
342
|
+
})),
|
|
343
|
+
suggested: Object.freeze(suggested),
|
|
270
344
|
});
|
|
271
345
|
}
|