impel-cli 0.20.41 → 0.20.43
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/package.json +1 -1
- package/scripts/profile-native-codex.mjs +604 -57
- package/src/agents.js +557 -45
- package/src/apps.js +2 -1
- package/src/cli.js +7 -0
- package/src/commands/agents.js +1 -0
- package/src/commands/launch.js +57 -7
- package/src/commands/mcp.js +86 -3
- package/src/commands/native.js +285 -0
- package/src/managedProfileVersion.js +4 -0
- package/src/nativeAgentTelemetry.js +4 -0
- package/src/nativeAgentTransport.js +147 -17
- package/src/nativeInterception.js +133 -0
- package/src/selfInvocation.js +20 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
7
|
CONFIG_DIR,
|
|
@@ -24,9 +25,11 @@ import {
|
|
|
24
25
|
import { extractAnswerFinalText } from "./directAnswer.js";
|
|
25
26
|
import {
|
|
26
27
|
IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
|
|
28
|
+
nativeBenchmarkHeaderValue,
|
|
27
29
|
} from "./selfInvocation.js";
|
|
28
30
|
import { normalizeTenantId } from "./tenants.js";
|
|
29
31
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
32
|
+
import { CURRENT_CONFIG_VERSION } from "./managedProfileVersion.js";
|
|
30
33
|
|
|
31
34
|
export {
|
|
32
35
|
NATIVE_AGENT_ANSWER_TOOL,
|
|
@@ -38,6 +41,10 @@ export {
|
|
|
38
41
|
export const NATIVE_AGENT_HANDLE_SCHEMA = "impel.native-agent-run.v1";
|
|
39
42
|
export const NATIVE_AGENT_RESULT_SCHEMA = "impel.native-agent-result.v1";
|
|
40
43
|
export const NATIVE_AGENT_RECOVERY_SCHEMA = "impel.native-agent-recovery.v1";
|
|
44
|
+
export const NATIVE_AGENT_BENCHMARK_HEADER = "X-Impel-Client-Benchmark";
|
|
45
|
+
export const NATIVE_AGENT_CLIENT_BUILD = `cli/${JSON.parse(
|
|
46
|
+
fs.readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"),
|
|
47
|
+
).version}+manifest.v${CURRENT_CONFIG_VERSION}`;
|
|
41
48
|
|
|
42
49
|
const STATE_SCHEMA = "impel.native-agent-state.v1";
|
|
43
50
|
const DELETION_SCHEMA = "impel.native-agent-deletion.v1";
|
|
@@ -47,8 +54,9 @@ const SAFE_FINGERPRINT_RE = /^[a-f0-9]{64}$/u;
|
|
|
47
54
|
const SAFE_INVOCATION_RE = /^[a-f0-9-]{36}$/u;
|
|
48
55
|
const DEFAULT_WAIT_SECONDS = 20;
|
|
49
56
|
const MAX_WAIT_SECONDS = 35;
|
|
50
|
-
const DEFAULT_ATTACHMENT_WINDOW_MS = 40_000;
|
|
51
|
-
const DEFAULT_UPSTREAM_TIMEOUT_MS = 42_000;
|
|
57
|
+
export const DEFAULT_ATTACHMENT_WINDOW_MS = 40_000;
|
|
58
|
+
export const DEFAULT_UPSTREAM_TIMEOUT_MS = 42_000;
|
|
59
|
+
const DEFAULT_ANSWER_HEDGE_MS = 38_000;
|
|
52
60
|
const DEFAULT_MAX_POLLS = 8;
|
|
53
61
|
const DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
54
62
|
const DEFAULT_MAX_RETAINED_RUNS = 500;
|
|
@@ -68,6 +76,11 @@ const MANAGED_VIRTUAL_KEY_MISS_RETRY_DELAY_MS = 250;
|
|
|
68
76
|
const NEXT_AMBIGUOUS_ANSWER_CODE = "native_agent_answer_ambiguous";
|
|
69
77
|
const MCP_TOOL_RESULT_ERROR = Symbol("native-agent MCP tool result error");
|
|
70
78
|
const TERMINAL_STATUSES = new Set(["succeeded", "failed", "cancelled", "canceled"]);
|
|
79
|
+
const SAFE_CLIENT_BUILD_RE = /^[A-Za-z0-9._+/-]{1,128}$/u;
|
|
80
|
+
|
|
81
|
+
if (!SAFE_CLIENT_BUILD_RE.test(NATIVE_AGENT_CLIENT_BUILD)) {
|
|
82
|
+
throw new Error("native-agent client build header is invalid");
|
|
83
|
+
}
|
|
71
84
|
|
|
72
85
|
export function nativeAgentAttachmentWindowMs(environment = process.env) {
|
|
73
86
|
const configured = environment?.[IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV];
|
|
@@ -82,6 +95,30 @@ export function nativeAgentAttachmentWindowMs(environment = process.env) {
|
|
|
82
95
|
return parsed;
|
|
83
96
|
}
|
|
84
97
|
|
|
98
|
+
export function nativeAgentAnswerHedgeMs(environment = process.env, { warn = console.warn } = {}) {
|
|
99
|
+
const configured = environment?.IMPEL_NATIVE_ANSWER_HEDGE_MS;
|
|
100
|
+
if (configured === undefined) return DEFAULT_ANSWER_HEDGE_MS;
|
|
101
|
+
if (!/^\d{1,9}$/u.test(configured)) {
|
|
102
|
+
warn(`IMPEL_NATIVE_ANSWER_HEDGE_MS must be a non-negative integer; using ${DEFAULT_ANSWER_HEDGE_MS}ms`);
|
|
103
|
+
return DEFAULT_ANSWER_HEDGE_MS;
|
|
104
|
+
}
|
|
105
|
+
const parsed = Number(configured);
|
|
106
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
107
|
+
warn(`IMPEL_NATIVE_ANSWER_HEDGE_MS must be a safe integer; using ${DEFAULT_ANSWER_HEDGE_MS}ms`);
|
|
108
|
+
return DEFAULT_ANSWER_HEDGE_MS;
|
|
109
|
+
}
|
|
110
|
+
if (parsed === 0) return 0;
|
|
111
|
+
const maximum = Math.min(DEFAULT_ATTACHMENT_WINDOW_MS, DEFAULT_UPSTREAM_TIMEOUT_MS) - 1;
|
|
112
|
+
if (parsed >= DEFAULT_ATTACHMENT_WINDOW_MS || parsed >= DEFAULT_UPSTREAM_TIMEOUT_MS) {
|
|
113
|
+
warn(
|
|
114
|
+
`IMPEL_NATIVE_ANSWER_HEDGE_MS must be below ${DEFAULT_ATTACHMENT_WINDOW_MS}ms `
|
|
115
|
+
+ `and ${DEFAULT_UPSTREAM_TIMEOUT_MS}ms; clamping to ${maximum}ms`,
|
|
116
|
+
);
|
|
117
|
+
return maximum;
|
|
118
|
+
}
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
function stableValue(value) {
|
|
86
123
|
if (Array.isArray(value)) return value.map(stableValue);
|
|
87
124
|
if (!value || typeof value !== "object") return value;
|
|
@@ -704,6 +741,10 @@ export class NativeAgentUpstreamSession {
|
|
|
704
741
|
telemetry = () => {},
|
|
705
742
|
now = Date.now,
|
|
706
743
|
requestIdFactory = crypto.randomUUID,
|
|
744
|
+
answerHedgeMs = nativeAgentAnswerHedgeMs(),
|
|
745
|
+
setTimeoutImpl = setTimeout,
|
|
746
|
+
clearTimeoutImpl = clearTimeout,
|
|
747
|
+
environment = process.env,
|
|
707
748
|
}) {
|
|
708
749
|
this.endpoint = `${normalizeGatewayUrl(gatewayUrl)}/mcp`;
|
|
709
750
|
this.credential = credential;
|
|
@@ -714,6 +755,10 @@ export class NativeAgentUpstreamSession {
|
|
|
714
755
|
this.telemetry = telemetry;
|
|
715
756
|
this.now = now;
|
|
716
757
|
this.requestIdFactory = requestIdFactory;
|
|
758
|
+
this.answerHedgeMs = Math.max(0, answerHedgeMs);
|
|
759
|
+
this.setTimeoutImpl = setTimeoutImpl;
|
|
760
|
+
this.clearTimeoutImpl = clearTimeoutImpl;
|
|
761
|
+
this.benchmarkHeaderValue = nativeBenchmarkHeaderValue(environment);
|
|
717
762
|
this.sessionId = null;
|
|
718
763
|
this.nextId = 1;
|
|
719
764
|
}
|
|
@@ -729,7 +774,7 @@ export class NativeAgentUpstreamSession {
|
|
|
729
774
|
let timedOut = false;
|
|
730
775
|
const onAbort = () => controller.abort();
|
|
731
776
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
732
|
-
const timeout =
|
|
777
|
+
const timeout = this.setTimeoutImpl(() => {
|
|
733
778
|
timedOut = true;
|
|
734
779
|
controller.abort();
|
|
735
780
|
}, this.timeoutMs);
|
|
@@ -743,6 +788,10 @@ export class NativeAgentUpstreamSession {
|
|
|
743
788
|
"Content-Type": "application/json",
|
|
744
789
|
Accept: "application/json, text/event-stream",
|
|
745
790
|
"X-Impel-Client-Request-Id": correlationId,
|
|
791
|
+
"X-Impel-Client-Build": NATIVE_AGENT_CLIENT_BUILD,
|
|
792
|
+
...(this.benchmarkHeaderValue
|
|
793
|
+
? { [NATIVE_AGENT_BENCHMARK_HEADER]: this.benchmarkHeaderValue }
|
|
794
|
+
: {}),
|
|
746
795
|
...(this.sessionId ? { "Mcp-Session-Id": this.sessionId } : {}),
|
|
747
796
|
},
|
|
748
797
|
body: JSON.stringify(message),
|
|
@@ -760,7 +809,7 @@ export class NativeAgentUpstreamSession {
|
|
|
760
809
|
const detail = timedOut ? "request timed out" : redactSecretText(error?.message || error);
|
|
761
810
|
throw new NativeAgentUpstreamError(`native-agent MCP request failed: ${detail}`, { ambiguous: true });
|
|
762
811
|
} finally {
|
|
763
|
-
|
|
812
|
+
this.clearTimeoutImpl(timeout);
|
|
764
813
|
signal?.removeEventListener("abort", onAbort);
|
|
765
814
|
}
|
|
766
815
|
this.telemetry("upstream_request_completed", {
|
|
@@ -814,17 +863,78 @@ export class NativeAgentUpstreamSession {
|
|
|
814
863
|
return toolsResponse.result.tools;
|
|
815
864
|
}
|
|
816
865
|
|
|
817
|
-
async
|
|
866
|
+
async ping({ signal } = {}) {
|
|
867
|
+
const id = this.nextId++;
|
|
868
|
+
const messages = await this.post({ jsonrpc: "2.0", id, method: "ping", params: {} }, { signal });
|
|
869
|
+
const response = messages.find((message) => message?.id === id);
|
|
870
|
+
if (response?.error || !response?.result || typeof response.result !== "object") {
|
|
871
|
+
throw new NativeAgentUpstreamError("native-agent MCP ping failed");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async callAnswerWithHedge(message, { signal, onHedge } = {}) {
|
|
876
|
+
throwIfAborted(signal);
|
|
877
|
+
const startedAt = this.now();
|
|
878
|
+
const controller = new AbortController();
|
|
879
|
+
let hedgeFired = false;
|
|
880
|
+
let elapsedMs = 0;
|
|
881
|
+
let replayTerminal = false;
|
|
882
|
+
const onAbort = () => controller.abort();
|
|
883
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
884
|
+
const timer = this.setTimeoutImpl(() => {
|
|
885
|
+
if (controller.signal.aborted) return;
|
|
886
|
+
hedgeFired = true;
|
|
887
|
+
elapsedMs = Math.max(0, this.now() - startedAt);
|
|
888
|
+
onHedge?.();
|
|
889
|
+
controller.abort();
|
|
890
|
+
}, this.answerHedgeMs);
|
|
891
|
+
try {
|
|
892
|
+
try {
|
|
893
|
+
const messages = await this.post(message, { signal: controller.signal });
|
|
894
|
+
return toolPayload(messages.find((candidate) => candidate?.id === message.id));
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (!hedgeFired || signal?.aborted) throw error;
|
|
897
|
+
} finally {
|
|
898
|
+
this.clearTimeoutImpl(timer);
|
|
899
|
+
signal?.removeEventListener("abort", onAbort);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
const messages = await this.post(message, { signal });
|
|
904
|
+
const payload = toolPayload(messages.find((candidate) => candidate?.id === message.id));
|
|
905
|
+
replayTerminal = TERMINAL_STATUSES.has(payload?.status);
|
|
906
|
+
return payload;
|
|
907
|
+
} finally {
|
|
908
|
+
this.telemetry("hedge_fired", { elapsedMs, replayTerminal });
|
|
909
|
+
}
|
|
910
|
+
} finally {
|
|
911
|
+
this.clearTimeoutImpl(timer);
|
|
912
|
+
signal?.removeEventListener("abort", onAbort);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async call(name, args, { signal = this.signal, hedge = true } = {}) {
|
|
917
|
+
let hedgeFiredForCall = false;
|
|
818
918
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
819
919
|
const id = this.nextId++;
|
|
920
|
+
const message = {
|
|
921
|
+
jsonrpc: "2.0",
|
|
922
|
+
id,
|
|
923
|
+
method: "tools/call",
|
|
924
|
+
params: { name, arguments: args },
|
|
925
|
+
};
|
|
820
926
|
try {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
927
|
+
if (name === NATIVE_AGENT_UPSTREAM_ANSWER_TOOL
|
|
928
|
+
&& hedge
|
|
929
|
+
&& !hedgeFiredForCall
|
|
930
|
+
&& this.answerHedgeMs > 0) {
|
|
931
|
+
return await this.callAnswerWithHedge(message, {
|
|
932
|
+
signal,
|
|
933
|
+
onHedge: () => { hedgeFiredForCall = true; },
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
const messages = await this.post(message, { signal });
|
|
937
|
+
return toolPayload(messages.find((candidate) => candidate?.id === id));
|
|
828
938
|
} catch (error) {
|
|
829
939
|
if (attempt === 0 && retryableManagedVirtualKeyMiss(error)) {
|
|
830
940
|
// This gateway-authored miss happens before a native run can exist.
|
|
@@ -1700,7 +1810,7 @@ export class NativeAgentCompositeTransport {
|
|
|
1700
1810
|
}
|
|
1701
1811
|
}
|
|
1702
1812
|
|
|
1703
|
-
async prepareExistingSession(signal) {
|
|
1813
|
+
async prepareExistingSession(signal, { prewarmed = false } = {}) {
|
|
1704
1814
|
const reused = Boolean(this.preparedSession);
|
|
1705
1815
|
const startedAt = this.now();
|
|
1706
1816
|
if (!this.preparedSession) {
|
|
@@ -1717,15 +1827,16 @@ export class NativeAgentCompositeTransport {
|
|
|
1717
1827
|
this.telemetry("session_prepared", {
|
|
1718
1828
|
durationMs: this.now() - startedAt,
|
|
1719
1829
|
handshakeReused: reused,
|
|
1830
|
+
prewarmed,
|
|
1720
1831
|
});
|
|
1721
1832
|
return prepared;
|
|
1722
1833
|
}
|
|
1723
1834
|
|
|
1724
|
-
async prepareNewSession(signal) {
|
|
1725
|
-
const prepared = await this.prepareExistingSession(signal);
|
|
1835
|
+
async prepareNewSession(signal, { prewarmed = false } = {}) {
|
|
1836
|
+
const prepared = await this.prepareExistingSession(signal, { prewarmed });
|
|
1726
1837
|
const startedAt = this.now();
|
|
1727
1838
|
if (this.preparedAgent) {
|
|
1728
|
-
this.telemetry("binding_completed", { durationMs: 0, bindingReused: true });
|
|
1839
|
+
this.telemetry("binding_completed", { durationMs: 0, bindingReused: true, prewarmed });
|
|
1729
1840
|
return { ...prepared, agent: this.preparedAgent };
|
|
1730
1841
|
}
|
|
1731
1842
|
const catalog = normalizeNativeAgentCatalog(
|
|
@@ -1746,10 +1857,29 @@ export class NativeAgentCompositeTransport {
|
|
|
1746
1857
|
this.telemetry("binding_completed", {
|
|
1747
1858
|
durationMs: this.now() - startedAt,
|
|
1748
1859
|
bindingReused: false,
|
|
1860
|
+
prewarmed,
|
|
1749
1861
|
});
|
|
1750
1862
|
return { ...prepared, agent: this.preparedAgent };
|
|
1751
1863
|
}
|
|
1752
1864
|
|
|
1865
|
+
async refreshPreparedSession(signal, { prewarmed = true } = {}) {
|
|
1866
|
+
const prepared = await this.prepareNewSession(signal, { prewarmed });
|
|
1867
|
+
try {
|
|
1868
|
+
await prepared.session.ping({ signal });
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
const cached = this.preparedSession;
|
|
1871
|
+
if (cached) {
|
|
1872
|
+
try {
|
|
1873
|
+
if ((await cached).session === prepared.session) this.preparedSession = null;
|
|
1874
|
+
} catch {
|
|
1875
|
+
this.preparedSession = null;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
throw error;
|
|
1879
|
+
}
|
|
1880
|
+
return prepared;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1753
1883
|
async preparePersistedStartSession(signal, state) {
|
|
1754
1884
|
const prepared = await this.prepareExistingSession(signal);
|
|
1755
1885
|
const catalog = normalizeNativeAgentCatalog(
|
|
@@ -2189,7 +2319,7 @@ export class NativeAgentCompositeTransport {
|
|
|
2189
2319
|
started = await session.call(
|
|
2190
2320
|
answerMode ? NATIVE_AGENT_UPSTREAM_ANSWER_TOOL : NATIVE_AGENT_START_TOOL,
|
|
2191
2321
|
outboundArguments,
|
|
2192
|
-
{ signal },
|
|
2322
|
+
{ signal, hedge: answerMode && attempt === 0 },
|
|
2193
2323
|
);
|
|
2194
2324
|
if (answerMode) {
|
|
2195
2325
|
if (!started?.runId) {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { impelCliInvocation } from "./selfInvocation.js";
|
|
5
|
+
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
6
|
+
|
|
7
|
+
export const IMPEL_NATIVE_INTERCEPT_ENV = "IMPEL_NATIVE_INTERCEPT";
|
|
8
|
+
export const IMPEL_NATIVE_INTERCEPT_TIMEOUT_ENV = "IMPEL_NATIVE_INTERCEPT_TIMEOUT_MS";
|
|
9
|
+
export const DEFAULT_NATIVE_INTERCEPT_TIMEOUT_MS = 100_000;
|
|
10
|
+
export const MANAGED_NATIVE_INTERCEPT_FLAG = "impel-managed-native-intercept-v1";
|
|
11
|
+
|
|
12
|
+
export function nativeInterceptEnabled(environment = process.env) {
|
|
13
|
+
return ["1", "true"].includes(
|
|
14
|
+
String(environment?.[IMPEL_NATIVE_INTERCEPT_ENV] || "").toLowerCase(),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function nativeInterceptTimeoutMs(environment = process.env) {
|
|
19
|
+
const configured = environment?.[IMPEL_NATIVE_INTERCEPT_TIMEOUT_ENV];
|
|
20
|
+
if (configured === undefined || configured === "") return DEFAULT_NATIVE_INTERCEPT_TIMEOUT_MS;
|
|
21
|
+
if (!/^\d{1,6}$/u.test(configured)) {
|
|
22
|
+
throw new Error(`${IMPEL_NATIVE_INTERCEPT_TIMEOUT_ENV} must be an integer from 1000 through 300000`);
|
|
23
|
+
}
|
|
24
|
+
const timeoutMs = Number(configured);
|
|
25
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 300_000) {
|
|
26
|
+
throw new Error(`${IMPEL_NATIVE_INTERCEPT_TIMEOUT_ENV} must be an integer from 1000 through 300000`);
|
|
27
|
+
}
|
|
28
|
+
return timeoutMs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJsonObject(filePath) {
|
|
32
|
+
if (!fs.existsSync(filePath)) return {};
|
|
33
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
34
|
+
if (!raw) return {};
|
|
35
|
+
try {
|
|
36
|
+
const value = JSON.parse(raw);
|
|
37
|
+
if (!value || Array.isArray(value) || typeof value !== "object") throw new Error();
|
|
38
|
+
return value;
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error(`${filePath} exists but isn't a valid JSON object. Fix or remove it, then re-run.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function writePrivateJson(filePath, value) {
|
|
45
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
46
|
+
const temporaryPath = `${filePath}.tmp-${process.pid}`;
|
|
47
|
+
try {
|
|
48
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
49
|
+
renameWithWindowsRetry(temporaryPath, filePath);
|
|
50
|
+
try { fs.chmodSync(filePath, 0o600); } catch { /* Best effort on Windows. */ }
|
|
51
|
+
} finally {
|
|
52
|
+
try { fs.rmSync(temporaryPath, { force: true }); } catch { /* Rename removed it. */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handlerIsManaged(handler) {
|
|
57
|
+
return handler
|
|
58
|
+
&& typeof handler === "object"
|
|
59
|
+
&& Array.isArray(handler.args)
|
|
60
|
+
&& handler.args.includes(`--${MANAGED_NATIVE_INTERCEPT_FLAG}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function removeManagedHandlers(groups) {
|
|
64
|
+
if (!Array.isArray(groups)) return { groups, changed: false };
|
|
65
|
+
let changed = false;
|
|
66
|
+
const preserved = [];
|
|
67
|
+
for (const group of groups) {
|
|
68
|
+
if (!group || typeof group !== "object" || !Array.isArray(group.hooks)) {
|
|
69
|
+
preserved.push(group);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const hooks = group.hooks.filter((handler) => {
|
|
73
|
+
if (!handlerIsManaged(handler)) return true;
|
|
74
|
+
changed = true;
|
|
75
|
+
return false;
|
|
76
|
+
});
|
|
77
|
+
if (hooks.length > 0) preserved.push({ ...group, hooks });
|
|
78
|
+
}
|
|
79
|
+
return { groups: preserved, changed };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function claudeNativeInterceptHandler(tenantId, timeoutMs, invocation = null) {
|
|
83
|
+
const command = invocation || impelCliInvocation([
|
|
84
|
+
"native", "intercept",
|
|
85
|
+
"--tenant", tenantId,
|
|
86
|
+
`--${MANAGED_NATIVE_INTERCEPT_FLAG}`,
|
|
87
|
+
]);
|
|
88
|
+
return {
|
|
89
|
+
type: "command",
|
|
90
|
+
command: command.command,
|
|
91
|
+
args: [...command.args],
|
|
92
|
+
timeout: Math.ceil(timeoutMs / 1000),
|
|
93
|
+
async: false,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Install or remove only Impel's synchronous UserPromptSubmit interception hook. */
|
|
98
|
+
export function ensureClaudeNativeInterceptHook(configDir, tenantId, {
|
|
99
|
+
enabled = false,
|
|
100
|
+
timeoutMs = DEFAULT_NATIVE_INTERCEPT_TIMEOUT_MS,
|
|
101
|
+
invocation = null,
|
|
102
|
+
} = {}) {
|
|
103
|
+
const settingsPath = path.join(configDir, "settings.json");
|
|
104
|
+
if (!enabled) {
|
|
105
|
+
try {
|
|
106
|
+
if (!fs.readFileSync(settingsPath, "utf8").includes(`--${MANAGED_NATIVE_INTERCEPT_FLAG}`)) {
|
|
107
|
+
return { settingsPath, enabled: false, changed: false };
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error?.code === "ENOENT") return { settingsPath, enabled: false, changed: false };
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const settings = readJsonObject(settingsPath);
|
|
115
|
+
const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks)
|
|
116
|
+
? { ...settings.hooks }
|
|
117
|
+
: {};
|
|
118
|
+
const removed = removeManagedHandlers(hooks.UserPromptSubmit);
|
|
119
|
+
if (!enabled && !removed.changed) {
|
|
120
|
+
return { settingsPath, enabled: false, changed: false };
|
|
121
|
+
}
|
|
122
|
+
if (enabled) {
|
|
123
|
+
removed.groups.push({
|
|
124
|
+
hooks: [claudeNativeInterceptHandler(tenantId, timeoutMs, invocation)],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (removed.groups.length > 0) hooks.UserPromptSubmit = removed.groups;
|
|
128
|
+
else delete hooks.UserPromptSubmit;
|
|
129
|
+
if (Object.keys(hooks).length > 0) settings.hooks = hooks;
|
|
130
|
+
else delete settings.hooks;
|
|
131
|
+
writePrivateJson(settingsPath, settings);
|
|
132
|
+
return { settingsPath, enabled, changed: true };
|
|
133
|
+
}
|
package/src/selfInvocation.js
CHANGED
|
@@ -82,9 +82,23 @@ export const IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES = [
|
|
|
82
82
|
"IMPEL_NATIVE_HOST",
|
|
83
83
|
"IMPEL_NATIVE_HOST_BUILD",
|
|
84
84
|
];
|
|
85
|
+
export const IMPEL_NATIVE_BENCHMARK_ENV = "IMPEL_NATIVE_BENCHMARK";
|
|
85
86
|
export const IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV = "IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_MS";
|
|
86
87
|
export const CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS = 100_000;
|
|
87
88
|
|
|
89
|
+
const SAFE_NATIVE_BENCHMARK_HEADER_VALUE = /^[\x21-\x7e]{1,128}$/u;
|
|
90
|
+
|
|
91
|
+
export function nativeBenchmarkHeaderValue(environment = process.env) {
|
|
92
|
+
const value = environment?.[IMPEL_NATIVE_BENCHMARK_ENV];
|
|
93
|
+
if (value === undefined) return null;
|
|
94
|
+
if (typeof value !== "string"
|
|
95
|
+
|| value !== "1"
|
|
96
|
+
|| !SAFE_NATIVE_BENCHMARK_HEADER_VALUE.test(value)) {
|
|
97
|
+
throw new Error(`${IMPEL_NATIVE_BENCHMARK_ENV} must be exactly 1 using visible ASCII`);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
function managedMcpEnvironment(environment = process.env) {
|
|
89
103
|
const telemetry = Object.fromEntries(
|
|
90
104
|
IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES.flatMap((name) => {
|
|
@@ -92,7 +106,12 @@ function managedMcpEnvironment(environment = process.env) {
|
|
|
92
106
|
return typeof value === "string" && value.length > 0 ? [[name, value]] : [];
|
|
93
107
|
}),
|
|
94
108
|
);
|
|
95
|
-
|
|
109
|
+
const benchmark = nativeBenchmarkHeaderValue(environment);
|
|
110
|
+
return {
|
|
111
|
+
[IMPEL_MANAGED_MCP_ENV]: "1",
|
|
112
|
+
...telemetry,
|
|
113
|
+
...(benchmark ? { [IMPEL_NATIVE_BENCHMARK_ENV]: benchmark } : {}),
|
|
114
|
+
};
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
export function impelMcpInvocation(args = [], options = {}) {
|