@triflux/remote 10.43.0 → 10.45.0
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/cto/hygiene.mjs +1 -1
- package/hub/lib/cto-env.mjs +1 -0
- package/hub/pipe.mjs +10 -9
- package/hub/schema.sql +62 -2
- package/hub/server.mjs +276 -132
- package/hub/store.mjs +699 -41
- package/hub/team/backend.mjs +4 -2
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +24 -3
- package/hub/team/cli/services/runtime-mode.mjs +23 -9
- package/hub/team/conductor.mjs +101 -24
- package/hub/team/cto-auto-collect.mjs +2 -6
- package/hub/team/cto-pull.mjs +4 -0
- package/hub/team/dashboard-open.mjs +1 -0
- package/hub/team/execution-mode.mjs +50 -9
- package/hub/team/headless.mjs +539 -92
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/launcher-template.mjs +17 -1
- package/hub/team/notify.mjs +31 -11
- package/hub/team/orchestrator.mjs +36 -9
- package/hub/team/pane.mjs +176 -14
- package/hub/team/psmux.mjs +79 -70
- package/hub/team/retry-state-machine.mjs +41 -13
- package/hub/team/session.mjs +57 -0
- package/hub/team/swarm-hypervisor.mjs +4 -2
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +22 -1
- package/hub/workers/codex-mcp.mjs +26 -5
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +367 -0
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +48 -4
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +24 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
- package/cto/steward.mjs +0 -362
- package/hub/log-retention.mjs +0 -726
package/hub/team/backend.mjs
CHANGED
|
@@ -21,10 +21,12 @@ export function buildAntigravityCommand(
|
|
|
21
21
|
// and `"` reach the shell verbatim (P1: shell injection).
|
|
22
22
|
const promptFile = `${resultFile}.prompt`;
|
|
23
23
|
writeFileSync(promptFile, prompt);
|
|
24
|
+
// agy 1.1.27 부터 --print 는 값(프롬프트)이 필수이고 stdin 프롬프트는 거부된다.
|
|
25
|
+
// 파일 내용을 셸이 재해석하지 않는 형태(따옴표 안 명령 치환 / Get-Content 식)로 값에 넣는다.
|
|
24
26
|
if (isWindows) {
|
|
25
|
-
return `Get-Content -Raw '${promptFile}'
|
|
27
|
+
return `agy --dangerously-skip-permissions --print (Get-Content -Raw '${promptFile}') > '${resultFile}' 2>'${resultFile}.err'`;
|
|
26
28
|
}
|
|
27
|
-
return `agy --
|
|
29
|
+
return `agy --dangerously-skip-permissions --print "$(cat '${promptFile}')" > '${resultFile}' 2>'${resultFile}.err'`;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const _require = createRequire(import.meta.url);
|
|
@@ -321,10 +321,12 @@ export async function probeClaudeDaemonCandidates({
|
|
|
321
321
|
short,
|
|
322
322
|
sessionId,
|
|
323
323
|
timeoutMs = 6000,
|
|
324
|
+
tmpRoot = "/tmp",
|
|
324
325
|
} = {}) {
|
|
325
326
|
const candidates = await buildClaudeDaemonDiscoveryCandidates({
|
|
326
327
|
configDir,
|
|
327
328
|
env,
|
|
329
|
+
tmpRoot,
|
|
328
330
|
});
|
|
329
331
|
const callerProvenance = detectCallerProvenance(env);
|
|
330
332
|
const targetRequested = Boolean(short || sessionId);
|
|
@@ -88,6 +88,8 @@ export async function startHeadlessTeam({
|
|
|
88
88
|
timeoutSec: timeoutSec || 900,
|
|
89
89
|
layout,
|
|
90
90
|
autoAttach: !!autoAttach,
|
|
91
|
+
leadPane: process.env.TMUX_PANE,
|
|
92
|
+
leadTmux: process.env.TMUX,
|
|
91
93
|
dashboard: !!dashboard,
|
|
92
94
|
dashboardLayout,
|
|
93
95
|
dashboardSize: dashboardSize ?? 0.5,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
attachSession,
|
|
6
6
|
configureTeammateKeybindings,
|
|
7
7
|
createSession,
|
|
8
|
+
splitAttachToClient,
|
|
8
9
|
} from "../../../session.mjs";
|
|
9
10
|
import { BOLD, DIM, GREEN, RESET } from "../../../shared.mjs";
|
|
10
11
|
import { ok, warn } from "../../render.mjs";
|
|
@@ -62,12 +63,23 @@ export async function startMuxTeam({
|
|
|
62
63
|
|
|
63
64
|
ok("CLI 초기화 대기 (3초)...");
|
|
64
65
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
65
|
-
await orchestrate(sessionId, assignments, {
|
|
66
|
+
const promptInjectionFailures = await orchestrate(sessionId, assignments, {
|
|
66
67
|
hubUrl,
|
|
67
68
|
teammateMode,
|
|
68
69
|
lead: { target: leadTarget, cli: lead, task },
|
|
70
|
+
onInjectionFailure(failure) {
|
|
71
|
+
warn(
|
|
72
|
+
`${failure.cli} ${failure.role} prompt 제출 실패 (${failure.target}): ${failure.message}`,
|
|
73
|
+
);
|
|
74
|
+
},
|
|
69
75
|
});
|
|
70
|
-
|
|
76
|
+
if (promptInjectionFailures.length === 0) {
|
|
77
|
+
ok("리드/워커 프롬프트 주입 완료");
|
|
78
|
+
} else {
|
|
79
|
+
warn(
|
|
80
|
+
`prompt 제출 실패 ${promptInjectionFailures.length}건 — 세션 상태에 기록함`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
71
83
|
|
|
72
84
|
return {
|
|
73
85
|
sessionName: sessionId,
|
|
@@ -78,6 +90,7 @@ export async function startMuxTeam({
|
|
|
78
90
|
teammateMode,
|
|
79
91
|
startedAt: Date.now(),
|
|
80
92
|
hubUrl,
|
|
93
|
+
promptInjectionFailures,
|
|
81
94
|
members,
|
|
82
95
|
panes: Object.fromEntries(
|
|
83
96
|
members.map((member) => [
|
|
@@ -113,7 +126,15 @@ export async function startMuxTeam({
|
|
|
113
126
|
);
|
|
114
127
|
console.log(` ${DIM}Ctrl+B → D: 세션 분리 (백그라운드)${RESET}\n`);
|
|
115
128
|
if (process.stdout.isTTY && process.stdin.isTTY) attachSession(sessionId);
|
|
116
|
-
else
|
|
129
|
+
else if (
|
|
130
|
+
splitAttachToClient(sessionId, {
|
|
131
|
+
targetPane: process.env.TMUX_PANE,
|
|
132
|
+
tmuxEnv: process.env.TMUX,
|
|
133
|
+
})
|
|
134
|
+
) {
|
|
135
|
+
ok("non-TTY 환경: 바깥 tmux 클라이언트 창을 분할해 attach함");
|
|
136
|
+
console.log(` ${DIM}분할 pane에서 나가기: Ctrl+B → D${RESET}\n`);
|
|
137
|
+
} else {
|
|
117
138
|
warn("TTY 미지원 환경이라 자동 attach를 생략함");
|
|
118
139
|
console.log(` ${DIM}수동 연결: tfx multi attach${RESET}\n`);
|
|
119
140
|
}
|
|
@@ -5,16 +5,28 @@ import {
|
|
|
5
5
|
sessionExists,
|
|
6
6
|
} from "../../session.mjs";
|
|
7
7
|
|
|
8
|
-
export function normalizeTeammateMode(mode = "auto") {
|
|
8
|
+
export function normalizeTeammateMode(mode = "auto", deps = {}) {
|
|
9
|
+
const platform = deps.platform || process.platform;
|
|
10
|
+
const env = deps.env || process.env;
|
|
11
|
+
const detectMux = deps.detectMultiplexer || detectMultiplexer;
|
|
9
12
|
const raw = String(mode).toLowerCase();
|
|
10
13
|
if (raw === "inline" || raw === "native") return "in-process";
|
|
11
14
|
if (raw === "headless" || raw === "hl") return "headless";
|
|
12
|
-
if (raw === "in-process" || raw === "tmux"
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
+
if (raw === "in-process" || raw === "tmux") return raw;
|
|
16
|
+
if (raw === "psmux") return platform === "win32" ? "psmux" : "in-process";
|
|
17
|
+
if (
|
|
18
|
+
raw === "wt" ||
|
|
19
|
+
raw === "windows-terminal" ||
|
|
20
|
+
raw === "windows_terminal"
|
|
21
|
+
) {
|
|
22
|
+
return platform === "win32" ? "wt" : "in-process";
|
|
23
|
+
}
|
|
15
24
|
if (raw === "auto") {
|
|
16
|
-
if (
|
|
17
|
-
const mux =
|
|
25
|
+
if (env.TMUX) return "tmux";
|
|
26
|
+
const mux = detectMux();
|
|
27
|
+
if (platform !== "win32") {
|
|
28
|
+
return mux === "tmux" ? "tmux" : "in-process";
|
|
29
|
+
}
|
|
18
30
|
if (mux === "psmux") return "psmux";
|
|
19
31
|
if (mux === "tmux") return "tmux";
|
|
20
32
|
return "in-process";
|
|
@@ -57,9 +69,11 @@ export function isTeamAlive(state) {
|
|
|
57
69
|
return sessionExists(state.sessionName);
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
export function ensureTmuxOrExit() {
|
|
61
|
-
const
|
|
62
|
-
|
|
72
|
+
export function ensureTmuxOrExit(deps = {}) {
|
|
73
|
+
const platform = deps.platform || process.platform;
|
|
74
|
+
const detectMux = deps.detectMultiplexer || detectMultiplexer;
|
|
75
|
+
const mux = detectMux();
|
|
76
|
+
if (mux && (platform === "win32" || mux === "tmux")) return mux;
|
|
63
77
|
const error = new Error("tmux 미발견");
|
|
64
78
|
error.code = "TMUX_REQUIRED";
|
|
65
79
|
throw error;
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -19,6 +19,13 @@ import {
|
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { dirname, join } from "node:path";
|
|
21
21
|
import { createRegistry } from "@triflux/core/mesh/mesh-registry.mjs";
|
|
22
|
+
import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
|
|
23
|
+
import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
|
|
24
|
+
import {
|
|
25
|
+
buildDisabledCliError,
|
|
26
|
+
normalizeCliName,
|
|
27
|
+
resolveCliPolicy,
|
|
28
|
+
} from "../../scripts/lib/machine-profile.mjs";
|
|
22
29
|
import { broker } from "@triflux/core/hub/account-broker.mjs";
|
|
23
30
|
import { runPreflight as runCodexPreflight } from "@triflux/core/hub/codex-preflight.mjs";
|
|
24
31
|
import {
|
|
@@ -604,28 +611,53 @@ export function createConductor(opts = {}) {
|
|
|
604
611
|
}
|
|
605
612
|
}
|
|
606
613
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
614
|
+
let spawnSpec;
|
|
615
|
+
try {
|
|
616
|
+
spawnSpec = buildSpawnSpecForMode(MODES.HEADLESS, {
|
|
617
|
+
cli: session.config.agent,
|
|
618
|
+
prompt: session.config.prompt,
|
|
619
|
+
profile: session.config.profile,
|
|
620
|
+
role: session.config.role,
|
|
621
|
+
model: session.config.model,
|
|
622
|
+
env: session.config.env,
|
|
623
|
+
mcpServers: session.config.mcpServers,
|
|
624
|
+
excludeMcpServers,
|
|
625
|
+
resolveCommand: opts.deps?.resolveCliExecutable,
|
|
626
|
+
onWarning: (message, details = {}) => {
|
|
627
|
+
eventLog.append("mcp_server_skipped", {
|
|
628
|
+
session: session.id,
|
|
629
|
+
message,
|
|
630
|
+
...details,
|
|
631
|
+
});
|
|
632
|
+
emitter.emit("warning", {
|
|
633
|
+
type: "mcp_server_skipped",
|
|
634
|
+
session: session.id,
|
|
635
|
+
message,
|
|
636
|
+
...details,
|
|
637
|
+
});
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
} catch (err) {
|
|
641
|
+
// respawnSession 은 `void` 로 호출되므로 여기서 던지면 처리되지 않은 거부가
|
|
642
|
+
// 된다. spec 생성 실패를 명시 실패로 접어 세션이 매달리지 않게 한다.
|
|
643
|
+
// 정책 차단과 그 밖의 spec 생성 실패는 원인이 다르므로 이벤트를 나눈다.
|
|
644
|
+
const message = err?.message || String(err);
|
|
645
|
+
const blockedByPolicy = err?.code === "TFX_CLI_DISABLED";
|
|
646
|
+
const eventName = blockedByPolicy
|
|
647
|
+
? "session_spawn_blocked"
|
|
648
|
+
: "session_spawn_failed";
|
|
649
|
+
const reason = blockedByPolicy ? "spawn_blocked" : "spawn_failed";
|
|
650
|
+
const detail = {
|
|
651
|
+
session: session.id,
|
|
652
|
+
agent: session.config.agent,
|
|
653
|
+
code: err?.code || null,
|
|
654
|
+
error: message,
|
|
655
|
+
};
|
|
656
|
+
eventLog.append(eventName, detail);
|
|
657
|
+
emitter.emit("warning", { type: eventName, ...detail });
|
|
658
|
+
handleFailure(session, `${reason}:${message}`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
629
661
|
|
|
630
662
|
// #90 branch guard: shard spawn cwd가 main 브랜치면 즉시 abort.
|
|
631
663
|
// swarm shard는 반드시 shard 전용 worktree 브랜치에서 실행되어야 한다.
|
|
@@ -902,8 +934,25 @@ export function createConductor(opts = {}) {
|
|
|
902
934
|
} else if (agent === "gemini") {
|
|
903
935
|
remoteBin = "gemini -y";
|
|
904
936
|
} else {
|
|
905
|
-
|
|
906
|
-
|
|
937
|
+
const codexHome = session.config.env?.CODEX_HOME;
|
|
938
|
+
const profile = resolveNestedCodexAgentProfile(
|
|
939
|
+
session.config.role || "executor",
|
|
940
|
+
{
|
|
941
|
+
profileOverride: session.config.profile ?? "auto",
|
|
942
|
+
globalProfile:
|
|
943
|
+
session.config.env?.TFX_CODEX_PROFILE ??
|
|
944
|
+
process.env.TFX_CODEX_PROFILE,
|
|
945
|
+
codexHome,
|
|
946
|
+
},
|
|
947
|
+
);
|
|
948
|
+
const configArgs = codexProfileConfigOverrides(profile, {
|
|
949
|
+
codexHome,
|
|
950
|
+
disallowUltra: true,
|
|
951
|
+
enforceCanonicalProfile: true,
|
|
952
|
+
})
|
|
953
|
+
.map((override) => `-c '${override.replace(/'/g, `'\\''`)}'`)
|
|
954
|
+
.join(" ");
|
|
955
|
+
remoteBin = `codex exec${configArgs ? ` ${configArgs}` : ""} -s danger-full-access --dangerously-bypass-approvals-and-sandbox`;
|
|
907
956
|
}
|
|
908
957
|
|
|
909
958
|
// prompt는 stdin으로 전달 — 셸 이스케이프 문제 완전 회피
|
|
@@ -1155,6 +1204,34 @@ export function createConductor(opts = {}) {
|
|
|
1155
1204
|
}
|
|
1156
1205
|
}
|
|
1157
1206
|
|
|
1207
|
+
// TFX_DISABLE_* SSOT 집행. 계정 임대와 worker sandbox 생성보다 앞에 둔다.
|
|
1208
|
+
// 뒤에 두면 정책으로 거부된 세션이 샌드박스 디렉터리와 auth.json 사본을
|
|
1209
|
+
// 남기고, 임대 반납이 회로 차단기 실패로 집계돼 멀쩡한 계정이 cooldown 에
|
|
1210
|
+
// 들어간다. 다른 CLI 로 자동 전환하지 않고 호출자에게 그대로 던진다.
|
|
1211
|
+
// claude 처럼 정책 대상이 아닌 agent 는 프로파일 파일을 읽지 않는다 (F4 와 동일).
|
|
1212
|
+
if (!config.remote && normalizeCliName(config.agent)) {
|
|
1213
|
+
const blockedCli = buildDisabledCliError(
|
|
1214
|
+
config.agent,
|
|
1215
|
+
resolveCliPolicy(process.env),
|
|
1216
|
+
);
|
|
1217
|
+
if (blockedCli) {
|
|
1218
|
+
eventLog.append("session_spawn_blocked", {
|
|
1219
|
+
session: config.id,
|
|
1220
|
+
agent: config.agent,
|
|
1221
|
+
code: blockedCli.code || null,
|
|
1222
|
+
error: blockedCli.message,
|
|
1223
|
+
});
|
|
1224
|
+
emitter.emit("warning", {
|
|
1225
|
+
type: "session_spawn_blocked",
|
|
1226
|
+
session: config.id,
|
|
1227
|
+
agent: config.agent,
|
|
1228
|
+
code: blockedCli.code || null,
|
|
1229
|
+
error: blockedCli.message,
|
|
1230
|
+
});
|
|
1231
|
+
throw blockedCli;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1158
1235
|
// broker lease (graceful — broker null if accounts.json absent)
|
|
1159
1236
|
let lease = null;
|
|
1160
1237
|
const activeBroker = getActiveBroker();
|
|
@@ -2,16 +2,12 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { resolveLakeRootDir } from "../../cto/lake-root.mjs";
|
|
5
|
+
import { resolveRoleControlSnapshot } from "@triflux/core/hub/lib/cto-env.mjs";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_DEBOUNCE_MS = 120_000;
|
|
7
|
-
const OFF_VALUES = new Set(["0", "false", "off", "no"]);
|
|
8
8
|
|
|
9
9
|
export function isCtoAutoCollectDisabled(env = process.env) {
|
|
10
|
-
return
|
|
11
|
-
String(env?.TFX_CTO_AUTO_COLLECT ?? "")
|
|
12
|
-
.trim()
|
|
13
|
-
.toLowerCase(),
|
|
14
|
-
);
|
|
10
|
+
return !resolveRoleControlSnapshot(env).auto_collect_enabled;
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
function freshCurrentMd(projectRoot, now, debounceMs) {
|
package/hub/team/cto-pull.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { resolveRoleControlSnapshot } from "@triflux/core/hub/lib/cto-env.mjs";
|
|
5
|
+
|
|
4
6
|
function defaultLakeRoot() {
|
|
5
7
|
return join(process.cwd(), ".triflux", "lake");
|
|
6
8
|
}
|
|
@@ -9,9 +11,11 @@ function defaultLakeRoot() {
|
|
|
9
11
|
* Read the CTO north-star lake snapshot without affecting runtime control flow.
|
|
10
12
|
* @param {object} [opts]
|
|
11
13
|
* @param {string} [opts.lakeRoot] Directory containing current.json.
|
|
14
|
+
* @param {object} [opts.env] Environment values used for the CTO kill-switch.
|
|
12
15
|
* @returns {object|null}
|
|
13
16
|
*/
|
|
14
17
|
export function readCtoSnapshot(opts = {}) {
|
|
18
|
+
if (!resolveRoleControlSnapshot(opts.env).north_star_enabled) return null;
|
|
15
19
|
try {
|
|
16
20
|
const lakeRoot = opts.lakeRoot || defaultLakeRoot();
|
|
17
21
|
const parsed = JSON.parse(
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve as pathResolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
// scripts/lib is bundled into @triflux/remote itself (package.json files), so
|
|
7
|
-
// import the local mirror relatively rather than via @triflux/core. The core
|
|
8
|
-
// peer range (^10.0.0-alpha.1) could resolve to a build that predates this
|
|
9
|
-
// export, which would fail at import time; the bundled copy is version-skew
|
|
10
|
-
// safe and always carries the matching codexProfileConfigOverrides.
|
|
5
|
+
import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
|
|
11
6
|
import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
|
|
7
|
+
import {
|
|
8
|
+
resolveGeminiModel,
|
|
9
|
+
resolveGeminiProfileForPurpose,
|
|
10
|
+
} from "../../scripts/lib/gemini-profiles.mjs";
|
|
11
|
+
import {
|
|
12
|
+
buildDisabledCliError,
|
|
13
|
+
normalizeCliName,
|
|
14
|
+
resolveCliPolicy,
|
|
15
|
+
} from "../../scripts/lib/machine-profile.mjs";
|
|
12
16
|
import { whichCommand } from "@triflux/core/hub/platform.mjs";
|
|
13
17
|
|
|
14
18
|
const WIN32_EXT_PRECEDENCE = [".cmd", ".exe", ".bat", ".ps1"];
|
|
@@ -101,6 +105,19 @@ export function resolveCliExecutable(cli, opts = {}) {
|
|
|
101
105
|
|
|
102
106
|
export function buildSpawnSpecForMode(mode, opts = {}) {
|
|
103
107
|
const cli = normalizeCli(opts.cli || "codex");
|
|
108
|
+
|
|
109
|
+
// TFX_DISABLE_* SSOT 집행. route 를 우회하는 spawn 경로도 tfx-route.sh 와 같은
|
|
110
|
+
// 정책을 따른다. 정책 판독은 런타임 process.env 와 machine profile 만 쓰고,
|
|
111
|
+
// 자식에게 넘길 세션 env(opts.env)는 쓰지 않는다. 주입 시임은 opts.cliPolicy
|
|
112
|
+
// (계산된 정책)와 opts.policyEnv(env 객체) 둘뿐이다.
|
|
113
|
+
// claude 처럼 정책 대상이 아닌 CLI 는 프로파일 파일을 읽지 않는다.
|
|
114
|
+
if (normalizeCliName(cli)) {
|
|
115
|
+
const cliPolicy =
|
|
116
|
+
opts.cliPolicy ?? resolveCliPolicy(opts.policyEnv ?? process.env);
|
|
117
|
+
const blockedCli = buildDisabledCliError(cli, cliPolicy);
|
|
118
|
+
if (blockedCli) throw blockedCli;
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
const prompt = asPrompt(opts.prompt);
|
|
105
122
|
const commandName = cli === "antigravity" ? "agy" : cli;
|
|
106
123
|
const resolvedCommand = resolveCliExecutable(commandName, opts);
|
|
@@ -131,12 +148,26 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
|
|
|
131
148
|
};
|
|
132
149
|
|
|
133
150
|
if (cli === "antigravity") {
|
|
134
|
-
|
|
151
|
+
// agy 1.1.27: --print 는 값(프롬프트)을 받는 마지막 인자다. stdin 프롬프트는
|
|
152
|
+
// "empty prompt" 로 거부되므로 argv 로 넘긴다.
|
|
153
|
+
// effort 는 역할별 SSOT(scripts/lib/gemini-profiles.mjs). 명시 model 이 우선한다.
|
|
154
|
+
const model =
|
|
155
|
+
typeof opts.model === "string" && opts.model.trim()
|
|
156
|
+
? resolveGeminiModel(opts.model.trim(), {
|
|
157
|
+
profilesPath: opts.geminiProfilesPath,
|
|
158
|
+
})
|
|
159
|
+
: resolveGeminiModel(
|
|
160
|
+
resolveGeminiProfileForPurpose(opts.role || opts.agent),
|
|
161
|
+
{ profilesPath: opts.geminiProfilesPath },
|
|
162
|
+
);
|
|
163
|
+
const args = ["--dangerously-skip-permissions"];
|
|
164
|
+
if (model) args.push("--model", model);
|
|
165
|
+
args.push("--print", prompt);
|
|
135
166
|
return {
|
|
136
167
|
...wrap(args),
|
|
137
168
|
useExec: true,
|
|
138
169
|
shell: false,
|
|
139
|
-
stdinPrompt:
|
|
170
|
+
stdinPrompt: false,
|
|
140
171
|
prompt,
|
|
141
172
|
};
|
|
142
173
|
}
|
|
@@ -165,7 +196,17 @@ export function buildSpawnSpecForMode(mode, opts = {}) {
|
|
|
165
196
|
// still contains an inline [profiles.X] table (which codex re-injects on
|
|
166
197
|
// config rewrite). These go straight to spawn argv (shell: false), so push
|
|
167
198
|
// the raw TOML-scalar overrides without shell-quoting.
|
|
168
|
-
|
|
199
|
+
const codexHome = opts.env?.CODEX_HOME;
|
|
200
|
+
const codexProfile = resolveNestedCodexAgentProfile(opts.role || "executor", {
|
|
201
|
+
profileOverride: opts.profile ?? "auto",
|
|
202
|
+
globalProfile: opts.env?.TFX_CODEX_PROFILE ?? process.env.TFX_CODEX_PROFILE,
|
|
203
|
+
codexHome,
|
|
204
|
+
});
|
|
205
|
+
for (const override of codexProfileConfigOverrides(codexProfile, {
|
|
206
|
+
codexHome,
|
|
207
|
+
disallowUltra: true,
|
|
208
|
+
enforceCanonicalProfile: true,
|
|
209
|
+
})) {
|
|
169
210
|
args.push("-c", override);
|
|
170
211
|
}
|
|
171
212
|
if (Array.isArray(opts.mcpServers)) {
|