@triflux/core 10.87.0 → 10.88.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.
@@ -1,4 +1,381 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ chmodSync,
5
+ copyFileSync,
6
+ existsSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ renameSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+
14
+ const defaultFs = {
15
+ chmodSync,
16
+ copyFileSync,
17
+ existsSync,
18
+ readFileSync,
19
+ realpathSync,
20
+ renameSync,
21
+ unlinkSync,
22
+ writeFileSync,
23
+ };
24
+
25
+ // ARM64 Mach-O agy 바이너리의 time.After 함수와 유휴 대기 호출부 시그니처 (#1022 peer-wake 패치)
26
+ export const AGY_TIME_AFTER_SIG = Buffer.from(
27
+ "900b40f9ff6330eb29010054fe0f1ef8fd831ff8fd2300d16affff97000040f9fd835ff8fe0742f8c0035fd6",
28
+ "hex",
29
+ );
30
+ export const AGY_PEER_WAKE_ORIG_CALL = Buffer.from(
31
+ "e13740f9220041a9423040f9210c40a9e40302aae20303aa80003fd6",
32
+ "hex",
33
+ );
34
+ export const AGY_PEER_WAKE_PATCH_PREFIX = Buffer.from(
35
+ "1f2003d51f2003d51f2003d51f2003d5004099524073a772",
36
+ "hex",
37
+ );
38
+ export const AGY_PEER_WAKE_ORIG_STP = Buffer.from("e08708a9", "hex");
39
+ export const AGY_PEER_WAKE_PATCH_STP = Buffer.from("e0ff08a9", "hex");
40
+ const AGY_PEER_WAKE_STP_DELTA = 0x38;
41
+
42
+ export function encodeArm64Bl(pcOffset, targetOffset) {
43
+ const delta = targetOffset - pcOffset;
44
+ if (delta % 4 !== 0 || delta < -0x8000000 || delta > 0x7fffffc) {
45
+ return null;
46
+ }
47
+ const imm26 = (delta >> 2) & 0x03ffffff;
48
+ const out = Buffer.alloc(4);
49
+ out.writeUInt32LE((0x94000000 | imm26) >>> 0, 0);
50
+ return out;
51
+ }
52
+
53
+ function findUniqueOffset(buf, pattern) {
54
+ const first = buf.indexOf(pattern);
55
+ if (first === -1) return -1;
56
+ if (buf.indexOf(pattern, first + 1) !== -1) return -2;
57
+ return first;
58
+ }
59
+
60
+ export function inspectAgyPeerWakePatch({
61
+ agyPath,
62
+ platform = process.platform,
63
+ arch = process.arch,
64
+ fs = defaultFs,
65
+ } = {}) {
66
+ if (platform !== "darwin" || arch !== "arm64") {
67
+ return { ok: true, status: "skipped", platform, arch };
68
+ }
69
+ if (!agyPath) {
70
+ return { ok: true, status: "skipped", reason: "agy-not-installed" };
71
+ }
72
+ let resolvedPath = agyPath;
73
+ try {
74
+ resolvedPath = fs.realpathSync(agyPath);
75
+ } catch {
76
+ return { ok: true, status: "skipped", reason: "agy-not-found" };
77
+ }
78
+ if (!fs.existsSync(resolvedPath)) {
79
+ return { ok: true, status: "skipped", reason: "agy-not-found" };
80
+ }
81
+
82
+ const patchJsonPath = `${resolvedPath}.patch.json`;
83
+ let buf;
84
+ try {
85
+ buf = fs.readFileSync(resolvedPath);
86
+ } catch (error) {
87
+ return {
88
+ ok: false,
89
+ status: "error",
90
+ patchable: false,
91
+ path: resolvedPath,
92
+ patchJsonPath,
93
+ reason: "read-failed",
94
+ message: error?.message || String(error),
95
+ };
96
+ }
97
+ const sha256 = createHash("sha256").update(buf).digest("hex");
98
+ const timeAfterOffset = findUniqueOffset(buf, AGY_TIME_AFTER_SIG);
99
+ if (timeAfterOffset < 0) {
100
+ return {
101
+ ok: false,
102
+ status: "unsupported",
103
+ patchable: false,
104
+ path: resolvedPath,
105
+ patchJsonPath,
106
+ sha256,
107
+ reason:
108
+ timeAfterOffset === -2
109
+ ? "ambiguous-time-after-signature"
110
+ : "time-after-signature-not-found",
111
+ };
112
+ }
113
+
114
+ const patchedOffset = findUniqueOffset(buf, AGY_PEER_WAKE_PATCH_PREFIX);
115
+ if (
116
+ patchedOffset >= 0 &&
117
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA + 4 <= buf.length
118
+ ) {
119
+ const expectedBl = encodeArm64Bl(
120
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
121
+ timeAfterOffset,
122
+ );
123
+ const actualBl = buf.subarray(
124
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
125
+ patchedOffset + AGY_PEER_WAKE_PATCH_PREFIX.length + 4,
126
+ );
127
+ const actualStp = buf.subarray(
128
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA,
129
+ patchedOffset + AGY_PEER_WAKE_STP_DELTA + 4,
130
+ );
131
+ if (
132
+ expectedBl &&
133
+ actualBl.equals(expectedBl) &&
134
+ actualStp.equals(AGY_PEER_WAKE_PATCH_STP)
135
+ ) {
136
+ let metadataOk = false;
137
+ if (fs.existsSync(patchJsonPath)) {
138
+ try {
139
+ const meta = JSON.parse(fs.readFileSync(patchJsonPath, "utf8"));
140
+ metadataOk =
141
+ meta?.sha256 === sha256 &&
142
+ Array.isArray(meta?.patches) &&
143
+ meta.patches.includes("peer-wake");
144
+ } catch {
145
+ metadataOk = false;
146
+ }
147
+ }
148
+ if (metadataOk) {
149
+ return {
150
+ ok: true,
151
+ status: "patched",
152
+ patchable: false,
153
+ binaryPatched: true,
154
+ path: resolvedPath,
155
+ patchJsonPath,
156
+ sha256,
157
+ callOffset: patchedOffset,
158
+ timeAfterOffset,
159
+ };
160
+ }
161
+ return {
162
+ ok: false,
163
+ status: "metadata-outdated",
164
+ patchable: true,
165
+ binaryPatched: true,
166
+ path: resolvedPath,
167
+ patchJsonPath,
168
+ sha256,
169
+ callOffset: patchedOffset,
170
+ timeAfterOffset,
171
+ fix: "tfx doctor --fix",
172
+ };
173
+ }
174
+ }
175
+
176
+ const origOffset = findUniqueOffset(buf, AGY_PEER_WAKE_ORIG_CALL);
177
+ if (
178
+ origOffset >= 0 &&
179
+ origOffset + AGY_PEER_WAKE_STP_DELTA + 4 <= buf.length
180
+ ) {
181
+ const expectedBl = encodeArm64Bl(
182
+ origOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
183
+ timeAfterOffset,
184
+ );
185
+ const actualStp = buf.subarray(
186
+ origOffset + AGY_PEER_WAKE_STP_DELTA,
187
+ origOffset + AGY_PEER_WAKE_STP_DELTA + 4,
188
+ );
189
+ if (expectedBl && actualStp.equals(AGY_PEER_WAKE_ORIG_STP)) {
190
+ return {
191
+ ok: false,
192
+ status: "unpatched",
193
+ patchable: true,
194
+ binaryPatched: false,
195
+ path: resolvedPath,
196
+ patchJsonPath,
197
+ sha256,
198
+ callOffset: origOffset,
199
+ timeAfterOffset,
200
+ fix: "tfx doctor --fix",
201
+ };
202
+ }
203
+ }
204
+
205
+ return {
206
+ ok: false,
207
+ status: "unsupported",
208
+ patchable: false,
209
+ path: resolvedPath,
210
+ patchJsonPath,
211
+ sha256,
212
+ reason:
213
+ origOffset === -2
214
+ ? "ambiguous-call-site-signature"
215
+ : "call-site-signature-not-found",
216
+ };
217
+ }
218
+
219
+ export function applyAgyPeerWakePatch({
220
+ agyPath,
221
+ version,
222
+ platform = process.platform,
223
+ arch = process.arch,
224
+ fs = defaultFs,
225
+ execFileSyncFn = execFileSync,
226
+ } = {}) {
227
+ const state = inspectAgyPeerWakePatch({ agyPath, platform, arch, fs });
228
+ if (state.status === "patched" || state.status === "skipped") {
229
+ return { ...state, changed: false };
230
+ }
231
+ if (!state.patchable) {
232
+ return { ...state, ok: false, changed: false };
233
+ }
234
+
235
+ let resolvedVersion = version || "";
236
+ if (!resolvedVersion) {
237
+ try {
238
+ const out = String(
239
+ execFileSyncFn(state.path, ["--version"], {
240
+ encoding: "utf8",
241
+ timeout: 5000,
242
+ stdio: ["ignore", "pipe", "ignore"],
243
+ }) || "",
244
+ );
245
+ resolvedVersion = out.match(/\b(\d+\.\d+\.\d+)\b/)?.[1] || "unknown";
246
+ } catch {
247
+ resolvedVersion = "unknown";
248
+ }
249
+ }
250
+
251
+ let finalSha256 = state.sha256;
252
+ let backupEntry = null;
253
+ if (state.status === "unpatched") {
254
+ const rawBuf = fs.readFileSync(state.path);
255
+ const currentSha256 = createHash("sha256").update(rawBuf).digest("hex");
256
+ if (currentSha256 !== state.sha256) {
257
+ throw new Error("agy binary changed during patch preparation");
258
+ }
259
+
260
+ let backupKey = `${resolvedVersion}-orig`;
261
+ let backupPath = `${state.path}.bak-${backupKey}`;
262
+ if (fs.existsSync(backupPath)) {
263
+ const existingBackupSha = createHash("sha256")
264
+ .update(fs.readFileSync(backupPath))
265
+ .digest("hex");
266
+ if (existingBackupSha !== state.sha256) {
267
+ backupKey = `${resolvedVersion}-orig-${state.sha256.slice(0, 8)}`;
268
+ backupPath = `${state.path}.bak-${backupKey}`;
269
+ if (fs.existsSync(backupPath)) {
270
+ const suffixBackupSha = createHash("sha256")
271
+ .update(fs.readFileSync(backupPath))
272
+ .digest("hex");
273
+ if (suffixBackupSha !== state.sha256) {
274
+ throw new Error(`agy backup conflict: ${backupPath}`);
275
+ }
276
+ } else {
277
+ fs.copyFileSync(state.path, backupPath);
278
+ }
279
+ }
280
+ } else {
281
+ fs.copyFileSync(state.path, backupPath);
282
+ }
283
+ backupEntry = { key: backupKey, path: backupPath, sha256: state.sha256 };
284
+
285
+ const buf = Buffer.from(rawBuf);
286
+ const expectedBl = encodeArm64Bl(
287
+ state.callOffset + AGY_PEER_WAKE_PATCH_PREFIX.length,
288
+ state.timeAfterOffset,
289
+ );
290
+ AGY_PEER_WAKE_PATCH_PREFIX.copy(buf, state.callOffset);
291
+ expectedBl.copy(buf, state.callOffset + AGY_PEER_WAKE_PATCH_PREFIX.length);
292
+ AGY_PEER_WAKE_PATCH_STP.copy(
293
+ buf,
294
+ state.callOffset + AGY_PEER_WAKE_STP_DELTA,
295
+ );
296
+
297
+ const tmpPath = `${state.path}.tmp-${process.pid}`;
298
+ fs.writeFileSync(tmpPath, buf, { mode: 0o755 });
299
+ try {
300
+ fs.chmodSync(tmpPath, 0o755);
301
+ execFileSyncFn("codesign", ["-f", "-s", "-", tmpPath], {
302
+ stdio: "ignore",
303
+ timeout: 30000,
304
+ });
305
+ const preRenameSha256 = createHash("sha256")
306
+ .update(fs.readFileSync(state.path))
307
+ .digest("hex");
308
+ if (preRenameSha256 !== state.sha256) {
309
+ throw new Error("agy binary changed before atomic replace");
310
+ }
311
+ fs.renameSync(tmpPath, state.path);
312
+ } catch (error) {
313
+ try {
314
+ fs.unlinkSync(tmpPath);
315
+ } catch {}
316
+ throw error;
317
+ }
318
+ finalSha256 = createHash("sha256")
319
+ .update(fs.readFileSync(state.path))
320
+ .digest("hex");
321
+ }
322
+
323
+ let existingMeta = {};
324
+ if (fs.existsSync(state.patchJsonPath)) {
325
+ try {
326
+ const parsed = JSON.parse(fs.readFileSync(state.patchJsonPath, "utf8"));
327
+ existingMeta =
328
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
329
+ ? parsed
330
+ : {};
331
+ } catch {
332
+ existingMeta = {};
333
+ }
334
+ }
335
+ const patches = Array.isArray(existingMeta.patches)
336
+ ? [...new Set([...existingMeta.patches, "peer-wake"])]
337
+ : ["peer-wake"];
338
+ const backups = {
339
+ ...(backupEntry
340
+ ? {
341
+ [backupEntry.key]: {
342
+ path: backupEntry.path,
343
+ sha256: backupEntry.sha256,
344
+ },
345
+ }
346
+ : {}),
347
+ ...(existingMeta.backups && typeof existingMeta.backups === "object"
348
+ ? existingMeta.backups
349
+ : {}),
350
+ };
351
+ const nextMeta = {
352
+ ...existingMeta,
353
+ version:
354
+ resolvedVersion !== "unknown"
355
+ ? resolvedVersion
356
+ : existingMeta.version || "unknown",
357
+ sha256: finalSha256,
358
+ patches,
359
+ appliedAt: new Date().toISOString(),
360
+ backups,
361
+ };
362
+ fs.writeFileSync(
363
+ state.patchJsonPath,
364
+ `${JSON.stringify(nextMeta, null, 2)}\n`,
365
+ "utf8",
366
+ );
367
+
368
+ return {
369
+ ok: true,
370
+ status: "patched",
371
+ changed: true,
372
+ binaryPatched: true,
373
+ previousStatus: state.status,
374
+ path: state.path,
375
+ patchJsonPath: state.patchJsonPath,
376
+ sha256: finalSha256,
377
+ };
378
+ }
2
379
 
3
380
  export function commandExists(name) {
4
381
  try {
@@ -5,6 +5,7 @@
5
5
  import { readFileSync } from "node:fs";
6
6
  import process from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { isJevEnabled, judgeMcpSelection } from "../../hub/lib/jev-addon.mjs";
8
9
  import { CORE_SERVERS, readManifest } from "./mcp-manifest.mjs";
9
10
  import {
10
11
  DOMAIN_TAG_KEYWORDS,
@@ -639,7 +640,11 @@ export function getCodexMcpConfig(options = {}) {
639
640
  if (registeredServers.length === 0) {
640
641
  return { mcp_servers: {} };
641
642
  }
642
- const allowedServers = new Set(resolveAllowedServers(options));
643
+ const allowedServers = new Set(
644
+ Array.isArray(options.allowedServers)
645
+ ? parseAvailableServers(options.allowedServers)
646
+ : resolveAllowedServers(options),
647
+ );
643
648
  const resolvedProfile = resolveMcpProfile(
644
649
  options.agentType,
645
650
  options.requestedProfile,
@@ -731,9 +736,17 @@ export function buildMcpPolicy(options = {}) {
731
736
  const hint = buildPromptHint(resolvedOptions);
732
737
 
733
738
  // 플러그인 서버는 transport 설정이 없어 CLI override를 받으면 부팅이 실패한다.
739
+ const configServers = codexServersFromConfig(options.codexConfig);
740
+ const registeredServers =
741
+ configServers.length > 0
742
+ ? configServers
743
+ : options.registeredServers !== undefined
744
+ ? parseAvailableServers(options.registeredServers)
745
+ : [];
734
746
  const codexOptions = {
735
747
  ...resolvedOptions,
736
- availableServers: codexServersFromConfig(options.codexConfig),
748
+ availableServers: registeredServers,
749
+ allowedServers,
737
750
  };
738
751
 
739
752
  return {
@@ -749,6 +762,74 @@ export function buildMcpPolicy(options = {}) {
749
762
  };
750
763
  }
751
764
 
765
+ export async function buildMcpPolicyWithJev(options = {}, jevOptions = {}) {
766
+ const basePolicy = buildMcpPolicy(options);
767
+ if (!isJevEnabled(jevOptions)) return basePolicy;
768
+ const taskText = String(options.taskText || "").trim();
769
+ if (!taskText) return basePolicy;
770
+
771
+ const isAutoProfile =
772
+ !options.requestedProfile || options.requestedProfile === "auto";
773
+ if (!isAutoProfile && options.requestedProfile === "none") {
774
+ return basePolicy;
775
+ }
776
+
777
+ const candidateServers = basePolicy.allowedServers;
778
+ if (candidateServers.length === 0) return basePolicy;
779
+
780
+ const selection = await judgeMcpSelection(
781
+ {
782
+ taskText,
783
+ agentType: options.agentType || basePolicy.resolvedProfile,
784
+ availableServers: candidateServers,
785
+ },
786
+ jevOptions,
787
+ );
788
+ if (!selection || !Array.isArray(selection.neededMcpServers))
789
+ return basePolicy;
790
+
791
+ const narrowedAvailable = selection.neededMcpServers.filter((server) =>
792
+ basePolicy.allowedServers.includes(server),
793
+ );
794
+ if (narrowedAvailable.length === 0) {
795
+ return {
796
+ ...buildMcpPolicy({
797
+ ...options,
798
+ requestedProfile: "none",
799
+ registeredServers:
800
+ options.registeredServers ??
801
+ (options.codexConfig === undefined
802
+ ? options.availableServers
803
+ : undefined),
804
+ }),
805
+ requestedProfile: options.requestedProfile || "auto",
806
+ };
807
+ }
808
+
809
+ const nextProfile =
810
+ isAutoProfile &&
811
+ selection.mcpProfile &&
812
+ selection.mcpProfile in PROFILE_DEFINITIONS
813
+ ? selection.mcpProfile
814
+ : options.requestedProfile;
815
+
816
+ const resolved = buildMcpPolicy({
817
+ ...options,
818
+ requestedProfile: nextProfile,
819
+ availableServers: narrowedAvailable,
820
+ registeredServers:
821
+ options.registeredServers ??
822
+ (options.codexConfig === undefined
823
+ ? options.availableServers
824
+ : undefined),
825
+ });
826
+ return {
827
+ ...resolved,
828
+ enabledServers: resolved.allowedServers,
829
+ requestedProfile: options.requestedProfile || "auto",
830
+ };
831
+ }
832
+
752
833
  function shellEscape(value) {
753
834
  return `'${String(value).replace(/'/g, `'"'"'`)}'`;
754
835
  }
@@ -863,7 +944,9 @@ async function runCli(argv = process.argv.slice(2)) {
863
944
 
864
945
  let policy;
865
946
  try {
866
- policy = buildMcpPolicy(args);
947
+ policy = isJevEnabled()
948
+ ? await buildMcpPolicyWithJev(args)
949
+ : buildMcpPolicy(args);
867
950
  } catch (error) {
868
951
  console.error(
869
952
  `[mcp-filter] ${error instanceof Error ? error.message : String(error)}`,