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
package/src/apps.js
CHANGED
|
@@ -25,6 +25,7 @@ import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHook
|
|
|
25
25
|
import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
|
|
26
26
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
27
27
|
import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
28
|
+
import { CURRENT_CONFIG_VERSION } from "./managedProfileVersion.js";
|
|
28
29
|
import { IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS } from "./commands/launch.js";
|
|
29
30
|
import {
|
|
30
31
|
desktopTasksAssetPaths,
|
|
@@ -301,7 +302,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
301
302
|
// 35: enable the Code Mode host in managed desktop profiles (pinned Codex
|
|
302
303
|
// fails closed on code_mode_only models without it) and serve CLI model
|
|
303
304
|
// catalogs from the shared registry so efforts and tiers cannot drift.
|
|
304
|
-
export
|
|
305
|
+
export { CURRENT_CONFIG_VERSION };
|
|
305
306
|
|
|
306
307
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
307
308
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
package/src/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { cmdNuke } from "./commands/nuke.js";
|
|
|
21
21
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
22
22
|
import { cmdConverge } from "./commands/converge.js";
|
|
23
23
|
import { cmdRemote } from "./commands/remote.js";
|
|
24
|
+
import { cmdNative } from "./commands/native.js";
|
|
24
25
|
import { ensureMacDeveloperTools } from "./macDeveloperTools.js";
|
|
25
26
|
import { refuseElevatedMacExecution } from "./privileges.js";
|
|
26
27
|
|
|
@@ -37,11 +38,14 @@ Work:
|
|
|
37
38
|
impel claude --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
|
|
38
39
|
impel codex [args...] Launch Codex with an isolated Impel profile
|
|
39
40
|
impel codex --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
|
|
41
|
+
impel codex --benchmark ... Tag native-agent MCP calls as benchmark traffic
|
|
40
42
|
impel remote handoff|dispatch|handback Move or control provider-native sessions remotely
|
|
41
43
|
impel remote status|viewer|proxy Inspect, control, or connect to a remote run
|
|
42
44
|
impel tenant list List accessible organizations
|
|
43
45
|
impel tenant current Show the CLI's current organization
|
|
44
46
|
impel tenant use <org> Select the organization for CLI launches
|
|
47
|
+
impel native answer --tenant <t> --agent <id> --prompt <task> [--json]
|
|
48
|
+
Ask the named managed agent with attribution
|
|
45
49
|
|
|
46
50
|
Workspace:
|
|
47
51
|
impel tasks list|get|create|update|delete Work with Impel tickets (aliases: task, ticket[s])
|
|
@@ -128,6 +132,9 @@ export async function main(argv) {
|
|
|
128
132
|
case "remote":
|
|
129
133
|
return cmdRemote(rest);
|
|
130
134
|
|
|
135
|
+
case "native":
|
|
136
|
+
return cmdNative(rest);
|
|
137
|
+
|
|
131
138
|
case "status":
|
|
132
139
|
return cmdStatus();
|
|
133
140
|
|
package/src/commands/agents.js
CHANGED
|
@@ -38,6 +38,7 @@ export function managedAgentProfiles(client, options = {}) {
|
|
|
38
38
|
environment,
|
|
39
39
|
homeDir,
|
|
40
40
|
}),
|
|
41
|
+
environment,
|
|
41
42
|
...(client === "codex" ? {
|
|
42
43
|
directProfiles: profile.label !== "Codex CLI (native profile)",
|
|
43
44
|
directCodeMode: profile.label !== "Codex CLI (native profile)",
|
package/src/commands/launch.js
CHANGED
|
@@ -24,9 +24,8 @@ import { parentVerbatimRelayAppendix } from "../verbatimRelay.js";
|
|
|
24
24
|
import { withGitEnvironment } from "../skills.js";
|
|
25
25
|
import { assertProviderScopes, ensureTenantSelection, tenantCredential } from "../tenants.js";
|
|
26
26
|
import { maybePrintUpdateNotice } from "../updates.js";
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
} from "../nativeProcess.js";
|
|
27
|
+
import { nativeSpawnInvocation } from "../nativeProcess.js";
|
|
28
|
+
import { IMPEL_NATIVE_BENCHMARK_ENV } from "../selfInvocation.js";
|
|
30
29
|
import { resolveReviewedVendorCliBinary } from "../vendorCliBinaries.js";
|
|
31
30
|
import { PINNED_VENDOR_CLI_VERSIONS } from "../vendorCliVersions.js";
|
|
32
31
|
import { RUNTIME_BRAND } from "../runtimeBrand.js";
|
|
@@ -99,6 +98,32 @@ export function impelLaunchArguments(tool, argv, {
|
|
|
99
98
|
if (!RUNTIME_BRAND.features.agents) return [...argv];
|
|
100
99
|
if (tool === "claude") {
|
|
101
100
|
if (claudeManagedAgent) {
|
|
101
|
+
if (claudeManagedAgent.parentDirect === true) {
|
|
102
|
+
const { parentLaunchDefinition, parentMcpConfigJson } = claudeManagedAgent;
|
|
103
|
+
if (!parentLaunchDefinition
|
|
104
|
+
|| typeof parentLaunchDefinition.prompt !== "string"
|
|
105
|
+
|| !parentLaunchDefinition.prompt
|
|
106
|
+
|| !Array.isArray(parentLaunchDefinition.tools)
|
|
107
|
+
|| parentLaunchDefinition.tools.length !== 2
|
|
108
|
+
|| typeof parentMcpConfigJson !== "string"
|
|
109
|
+
|| !parentMcpConfigJson) {
|
|
110
|
+
throw new Error("managed Claude parent-direct launch configuration is invalid");
|
|
111
|
+
}
|
|
112
|
+
const tools = parentLaunchDefinition.tools.join(",");
|
|
113
|
+
return [
|
|
114
|
+
...IMPEL_CLAUDE_MANAGED_AGENT_ARGUMENTS,
|
|
115
|
+
"--mcp-config",
|
|
116
|
+
parentMcpConfigJson,
|
|
117
|
+
"--strict-mcp-config",
|
|
118
|
+
"--tools",
|
|
119
|
+
tools,
|
|
120
|
+
"--allowedTools",
|
|
121
|
+
tools,
|
|
122
|
+
"--append-system-prompt",
|
|
123
|
+
parentLaunchDefinition.prompt,
|
|
124
|
+
...argv,
|
|
125
|
+
];
|
|
126
|
+
}
|
|
102
127
|
const { launchName, launchAgentsJson } = claudeManagedAgent;
|
|
103
128
|
if (typeof launchName !== "string"
|
|
104
129
|
|| !launchName
|
|
@@ -279,6 +304,7 @@ function codexAgentLockedOption(argument) {
|
|
|
279
304
|
export function parseCodexAgentLaunchArguments(argv) {
|
|
280
305
|
const passthrough = [];
|
|
281
306
|
let selector = null;
|
|
307
|
+
let benchmark = false;
|
|
282
308
|
let literal = false;
|
|
283
309
|
for (let index = 0; index < argv.length; index += 1) {
|
|
284
310
|
const argument = argv[index];
|
|
@@ -291,6 +317,14 @@ export function parseCodexAgentLaunchArguments(argv) {
|
|
|
291
317
|
passthrough.push(argument);
|
|
292
318
|
continue;
|
|
293
319
|
}
|
|
320
|
+
if (argument === "--benchmark") {
|
|
321
|
+
if (benchmark) throw new Error("`--benchmark` may be specified only once");
|
|
322
|
+
benchmark = true;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (argument.startsWith("--benchmark=")) {
|
|
326
|
+
throw new Error("`--benchmark` does not accept a value");
|
|
327
|
+
}
|
|
294
328
|
if (argument === "--agent" || argument.startsWith("--agent=")) {
|
|
295
329
|
if (selector !== null) throw new Error("`--agent` may be specified only once");
|
|
296
330
|
const value = argument === "--agent" ? argv[index += 1] : argument.slice("--agent=".length);
|
|
@@ -313,7 +347,7 @@ export function parseCodexAgentLaunchArguments(argv) {
|
|
|
313
347
|
}
|
|
314
348
|
}
|
|
315
349
|
}
|
|
316
|
-
return { selector, argv: passthrough };
|
|
350
|
+
return { selector, benchmark, argv: passthrough };
|
|
317
351
|
}
|
|
318
352
|
|
|
319
353
|
function codexJsonOutput(argv) {
|
|
@@ -410,11 +444,16 @@ export async function cmdLaunch(tool, argv) {
|
|
|
410
444
|
let nativeArgv = [...argv];
|
|
411
445
|
let claudeAgentSelector = null;
|
|
412
446
|
let codexAgentSelector = null;
|
|
447
|
+
let codexBenchmark = false;
|
|
413
448
|
if (tool === "claude" && RUNTIME_BRAND.features.agents) {
|
|
414
449
|
({ selector: claudeAgentSelector, argv: nativeArgv } = parseClaudeAgentLaunchArguments(argv));
|
|
415
450
|
} else if (tool === "codex") {
|
|
416
451
|
try {
|
|
417
|
-
({
|
|
452
|
+
({
|
|
453
|
+
selector: codexAgentSelector,
|
|
454
|
+
benchmark: codexBenchmark,
|
|
455
|
+
argv: nativeArgv,
|
|
456
|
+
} = parseCodexAgentLaunchArguments(argv));
|
|
418
457
|
} catch (error) {
|
|
419
458
|
console.error(`impel codex: ${error.message}`);
|
|
420
459
|
process.exitCode = 1;
|
|
@@ -463,12 +502,18 @@ export async function cmdLaunch(tool, argv) {
|
|
|
463
502
|
}
|
|
464
503
|
}
|
|
465
504
|
const environment = { ...process.env };
|
|
505
|
+
if (codexBenchmark) environment[IMPEL_NATIVE_BENCHMARK_ENV] = "1";
|
|
466
506
|
environment.IMPEL_TENANT_ID = tenantId;
|
|
467
507
|
let agentProfile;
|
|
468
508
|
|
|
469
509
|
if (tool === "claude") {
|
|
470
510
|
const profile = ensureImpelClaudeProfile(gatewayUrl, tenantId, { crossAppModels });
|
|
471
|
-
agentProfile = {
|
|
511
|
+
agentProfile = {
|
|
512
|
+
client: "claude",
|
|
513
|
+
root: profile.configDir,
|
|
514
|
+
label: "Impel isolated Claude (impel claude)",
|
|
515
|
+
environment,
|
|
516
|
+
};
|
|
472
517
|
deleteEnvironmentKeys(environment, CLAUDE_DIRECT_AUTH_ENV);
|
|
473
518
|
delete environment.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY;
|
|
474
519
|
environment.CLAUDE_CONFIG_DIR = profile.configDir;
|
|
@@ -482,7 +527,12 @@ export async function cmdLaunch(tool, argv) {
|
|
|
482
527
|
environment.ANTHROPIC_AUTH_TOKEN = gatewayCredential;
|
|
483
528
|
} else if (tool === "codex") {
|
|
484
529
|
const profile = ensureImpelCodexProfile(gatewayUrl, tenantId);
|
|
485
|
-
agentProfile = {
|
|
530
|
+
agentProfile = {
|
|
531
|
+
client: "codex",
|
|
532
|
+
root: profile.codexHome,
|
|
533
|
+
label: "Impel isolated Codex (impel codex)",
|
|
534
|
+
environment,
|
|
535
|
+
};
|
|
486
536
|
deleteEnvironmentKeys(environment, CODEX_DIRECT_AUTH_ENV);
|
|
487
537
|
environment.CODEX_HOME = profile.codexHome;
|
|
488
538
|
environment[CODEX_GATEWAY_TOKEN_ENV] = gatewayCredential;
|
package/src/commands/mcp.js
CHANGED
|
@@ -198,14 +198,73 @@ export function runNativeAgentMcpServer({
|
|
|
198
198
|
output = process.stdout,
|
|
199
199
|
mode = "durable",
|
|
200
200
|
telemetry = () => {},
|
|
201
|
+
environment = process.env,
|
|
202
|
+
random = Math.random,
|
|
203
|
+
answerToolDescription = null,
|
|
201
204
|
}) {
|
|
202
205
|
if (!["durable", "recovery", "answer"].includes(mode)) throw new Error("invalid native-agent MCP mode");
|
|
203
206
|
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
204
207
|
const active = new Map();
|
|
205
208
|
const pending = new Set();
|
|
209
|
+
const needsUpstream = mode === "durable" || mode === "answer";
|
|
210
|
+
const prewarmController = new AbortController();
|
|
211
|
+
let prewarmTimer = null;
|
|
212
|
+
let prewarmStarted = false;
|
|
213
|
+
let refreshTimer = null;
|
|
214
|
+
let refreshInFlight = false;
|
|
206
215
|
let progress = 0;
|
|
207
216
|
const write = (value) => output.write(`${value}\n`);
|
|
208
217
|
|
|
218
|
+
function startPrewarm() {
|
|
219
|
+
if (!needsUpstream || prewarmStarted) return;
|
|
220
|
+
prewarmStarted = true;
|
|
221
|
+
if (prewarmTimer) {
|
|
222
|
+
clearTimeout(prewarmTimer);
|
|
223
|
+
prewarmTimer = null;
|
|
224
|
+
}
|
|
225
|
+
Promise.resolve()
|
|
226
|
+
.then(() => transport.prepareNewSession(prewarmController.signal, { prewarmed: true }))
|
|
227
|
+
.catch(() => {});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function schedulePrewarm() {
|
|
231
|
+
if (!needsUpstream || prewarmStarted || prewarmTimer) return;
|
|
232
|
+
const jitterMs = 50 + Math.min(
|
|
233
|
+
200,
|
|
234
|
+
Math.floor(Math.max(0, Math.min(1, random())) * 201),
|
|
235
|
+
);
|
|
236
|
+
prewarmTimer = setTimeout(() => {
|
|
237
|
+
prewarmTimer = null;
|
|
238
|
+
startPrewarm();
|
|
239
|
+
}, jitterMs);
|
|
240
|
+
prewarmTimer.unref?.();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function refreshIntervalMs() {
|
|
244
|
+
const configured = environment?.IMPEL_NATIVE_PREWARM_REFRESH_MS;
|
|
245
|
+
if (configured === undefined || configured === "0") return 0;
|
|
246
|
+
if (!/^\d{1,9}$/u.test(configured)) return 0;
|
|
247
|
+
const parsed = Number(configured);
|
|
248
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
telemetry("local_server_started");
|
|
252
|
+
schedulePrewarm();
|
|
253
|
+
const refreshMs = needsUpstream ? refreshIntervalMs() : 0;
|
|
254
|
+
if (refreshMs > 0) {
|
|
255
|
+
refreshTimer = setInterval(() => {
|
|
256
|
+
if (refreshInFlight || prewarmController.signal.aborted) return;
|
|
257
|
+
refreshInFlight = true;
|
|
258
|
+
Promise.resolve()
|
|
259
|
+
.then(() => typeof transport.refreshPreparedSession === "function"
|
|
260
|
+
? transport.refreshPreparedSession(prewarmController.signal, { prewarmed: true })
|
|
261
|
+
: transport.prepareNewSession(prewarmController.signal, { prewarmed: true }))
|
|
262
|
+
.catch(() => {})
|
|
263
|
+
.finally(() => { refreshInFlight = false; });
|
|
264
|
+
}, refreshMs);
|
|
265
|
+
refreshTimer.unref?.();
|
|
266
|
+
}
|
|
267
|
+
|
|
209
268
|
function schedule(message) {
|
|
210
269
|
const request = tasksJsonRpcRequestId(message);
|
|
211
270
|
if (!request.valid) {
|
|
@@ -227,6 +286,7 @@ export function runNativeAgentMcpServer({
|
|
|
227
286
|
let telemetryTool;
|
|
228
287
|
try {
|
|
229
288
|
if (message.method === "initialize") {
|
|
289
|
+
startPrewarm();
|
|
230
290
|
write(nativeRpcResult(message.id, {
|
|
231
291
|
protocolVersion: "2025-06-18",
|
|
232
292
|
capabilities: { tools: {} },
|
|
@@ -239,8 +299,13 @@ export function runNativeAgentMcpServer({
|
|
|
239
299
|
return;
|
|
240
300
|
}
|
|
241
301
|
if (message.method === "tools/list") {
|
|
302
|
+
const tools = nativeAgentCompositeTools({ mode });
|
|
303
|
+
if (mode === "answer" && typeof answerToolDescription === "string") {
|
|
304
|
+
const answerTool = tools.find(({ name }) => name === NATIVE_AGENT_ANSWER_TOOL);
|
|
305
|
+
if (answerTool) answerTool.description = answerToolDescription;
|
|
306
|
+
}
|
|
242
307
|
write(nativeRpcResult(message.id, {
|
|
243
|
-
tools
|
|
308
|
+
tools,
|
|
244
309
|
}));
|
|
245
310
|
return;
|
|
246
311
|
}
|
|
@@ -326,6 +391,9 @@ export function runNativeAgentMcpServer({
|
|
|
326
391
|
});
|
|
327
392
|
return new Promise((resolve) => {
|
|
328
393
|
lines.on("close", async () => {
|
|
394
|
+
if (prewarmTimer) clearTimeout(prewarmTimer);
|
|
395
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
396
|
+
prewarmController.abort();
|
|
329
397
|
for (const controller of active.values()) controller.abort();
|
|
330
398
|
await Promise.allSettled([...pending]);
|
|
331
399
|
resolve();
|
|
@@ -340,6 +408,7 @@ export async function cmdMcp(argv = []) {
|
|
|
340
408
|
"agent-id": { type: "string" },
|
|
341
409
|
"scope-param": { type: "string" },
|
|
342
410
|
"policy-fingerprint": { type: "string" },
|
|
411
|
+
"agent-title": { type: "string" },
|
|
343
412
|
"recovery-only": { type: "boolean" },
|
|
344
413
|
"answer-only": { type: "boolean" },
|
|
345
414
|
});
|
|
@@ -361,7 +430,7 @@ export async function cmdMcp(argv = []) {
|
|
|
361
430
|
if (nativeAgentTarget) {
|
|
362
431
|
const expectedFlags = [
|
|
363
432
|
"target", "tenant", "agent-id", "scope-param", "policy-fingerprint",
|
|
364
|
-
"recovery-only", "answer-only",
|
|
433
|
+
"agent-title", "recovery-only", "answer-only",
|
|
365
434
|
];
|
|
366
435
|
const unsupportedFlags = Object.keys(flags).filter((name) => !expectedFlags.includes(name));
|
|
367
436
|
if (unsupportedFlags.length > 0 || positionals.length > 0) {
|
|
@@ -375,9 +444,18 @@ export async function cmdMcp(argv = []) {
|
|
|
375
444
|
if (flags["recovery-only"] === true && flags["answer-only"] === true) {
|
|
376
445
|
throw new Error("native-agent MCP cannot be both recovery-only and answer-only");
|
|
377
446
|
}
|
|
447
|
+
if (Object.hasOwn(flags, "agent-title") && (
|
|
448
|
+
flags["answer-only"] !== true
|
|
449
|
+
|| typeof flags["agent-title"] !== "string"
|
|
450
|
+
|| !flags["agent-title"].trim()
|
|
451
|
+
|| flags["agent-title"].length > 160
|
|
452
|
+
)) {
|
|
453
|
+
throw new Error("native-agent MCP --agent-title requires an answer-only binding");
|
|
454
|
+
}
|
|
378
455
|
} else if (!tasksTarget && (Object.hasOwn(flags, "agent-id")
|
|
379
456
|
|| Object.hasOwn(flags, "scope-param")
|
|
380
457
|
|| Object.hasOwn(flags, "policy-fingerprint")
|
|
458
|
+
|| Object.hasOwn(flags, "agent-title")
|
|
381
459
|
|| Object.hasOwn(flags, "recovery-only")
|
|
382
460
|
|| Object.hasOwn(flags, "answer-only"))) {
|
|
383
461
|
throw new Error("native-agent binding flags require `--target native-agent`");
|
|
@@ -400,7 +478,6 @@ export async function cmdMcp(argv = []) {
|
|
|
400
478
|
? "recovery"
|
|
401
479
|
: (flags["answer-only"] === true ? "answer" : "durable"),
|
|
402
480
|
});
|
|
403
|
-
telemetry("local_server_started");
|
|
404
481
|
return runNativeAgentMcpServer({
|
|
405
482
|
transport: new NativeAgentCompositeTransport({
|
|
406
483
|
tenantId,
|
|
@@ -415,6 +492,12 @@ export async function cmdMcp(argv = []) {
|
|
|
415
492
|
? "recovery"
|
|
416
493
|
: (flags["answer-only"] === true ? "answer" : "durable"),
|
|
417
494
|
telemetry,
|
|
495
|
+
...(flags["agent-title"] ? {
|
|
496
|
+
answerToolDescription: [
|
|
497
|
+
`Ask the managed agent ${JSON.stringify(flags["agent-title"])} (${flags["agent-id"]}) exactly once through this fixed binding.`,
|
|
498
|
+
`On success, present the returned finalText verbatim and explicitly attribute it to managed agent ${JSON.stringify(flags["agent-title"])}; do not spawn a relay subagent.`,
|
|
499
|
+
].join(" "),
|
|
500
|
+
} : {}),
|
|
418
501
|
});
|
|
419
502
|
}
|
|
420
503
|
const endpoint = tasksTarget ? null : `${gatewayUrl}/mcp`;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { parseFlags } from "../args.js";
|
|
4
|
+
import {
|
|
5
|
+
agentProfileRoot,
|
|
6
|
+
managedClaudeAnswerBindings,
|
|
7
|
+
resolveManagedClaudeAnswerBinding,
|
|
8
|
+
} from "../agents.js";
|
|
9
|
+
import {
|
|
10
|
+
loadConfig,
|
|
11
|
+
redactSecretText,
|
|
12
|
+
resolveDefaultGateway,
|
|
13
|
+
} from "../config.js";
|
|
14
|
+
import { extractAnswerFinalText } from "../directAnswer.js";
|
|
15
|
+
import { createNativeAgentTelemetry } from "../nativeAgentTelemetry.js";
|
|
16
|
+
import {
|
|
17
|
+
MANAGED_NATIVE_INTERCEPT_FLAG,
|
|
18
|
+
nativeInterceptTimeoutMs,
|
|
19
|
+
} from "../nativeInterception.js";
|
|
20
|
+
import {
|
|
21
|
+
NATIVE_AGENT_CONTINUATION_SCHEMA,
|
|
22
|
+
NATIVE_AGENT_HANDLE_SCHEMA,
|
|
23
|
+
NATIVE_AGENT_RESULT_SCHEMA,
|
|
24
|
+
NativeAgentCompositeTransport,
|
|
25
|
+
} from "../nativeAgentTransport.js";
|
|
26
|
+
import { normalizeTenantId, tenantCredential } from "../tenants.js";
|
|
27
|
+
import { readHookInput } from "../sessionCollector.js";
|
|
28
|
+
|
|
29
|
+
const ANSWER_SPEC = {
|
|
30
|
+
tenant: { type: "string" },
|
|
31
|
+
agent: { type: "string" },
|
|
32
|
+
prompt: { type: "string" },
|
|
33
|
+
json: { type: "boolean" },
|
|
34
|
+
};
|
|
35
|
+
const INTERCEPT_SPEC = {
|
|
36
|
+
tenant: { type: "string" },
|
|
37
|
+
[MANAGED_NATIVE_INTERCEPT_FLAG]: { type: "boolean" },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function abortError() {
|
|
41
|
+
const error = new Error("native-agent answer timed out or was cancelled");
|
|
42
|
+
error.name = "AbortError";
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function throwIfAborted(signal) {
|
|
47
|
+
if (signal?.aborted) throw abortError();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function terminalAnswer(value) {
|
|
51
|
+
const direct = extractAnswerFinalText(value);
|
|
52
|
+
if (direct !== null) {
|
|
53
|
+
return { ok: true, finalText: direct, ...(value.runId ? { runId: value.runId } : {}) };
|
|
54
|
+
}
|
|
55
|
+
if (value?.schema === NATIVE_AGENT_RESULT_SCHEMA) {
|
|
56
|
+
if (value.status === "succeeded" && typeof value.finalText === "string") {
|
|
57
|
+
return { ok: true, finalText: value.finalText, ...(value.runId ? { runId: value.runId } : {}) };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
finalText: typeof value.output === "string" ? value.output : "",
|
|
62
|
+
...(value.runId ? { runId: value.runId } : {}),
|
|
63
|
+
error: redactSecretText(
|
|
64
|
+
typeof value.error === "string"
|
|
65
|
+
? value.error
|
|
66
|
+
: value.error?.message || "managed agent answer failed",
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Execute the same fixed-binding, idempotent transport used by the managed
|
|
75
|
+
* MCP adapter, including continuation/resume handling and telemetry.
|
|
76
|
+
*/
|
|
77
|
+
export async function answerManagedNativeAgent({
|
|
78
|
+
tenantId,
|
|
79
|
+
agent,
|
|
80
|
+
prompt,
|
|
81
|
+
signal,
|
|
82
|
+
}, {
|
|
83
|
+
config = loadConfig(),
|
|
84
|
+
gatewayUrl = null,
|
|
85
|
+
createTransport = (options) => new NativeAgentCompositeTransport(options),
|
|
86
|
+
createTelemetry = createNativeAgentTelemetry,
|
|
87
|
+
} = {}) {
|
|
88
|
+
if (!config?.pat) {
|
|
89
|
+
throw new Error("not authenticated; run `impel setup` (or `impel auth`) first");
|
|
90
|
+
}
|
|
91
|
+
if (!agent || typeof agent !== "object") throw new Error("managed agent binding is invalid");
|
|
92
|
+
if (typeof prompt !== "string" || !prompt.trim() || prompt.length > 40_000) {
|
|
93
|
+
throw new Error("--prompt must contain between 1 and 40000 characters");
|
|
94
|
+
}
|
|
95
|
+
const normalizedTenant = normalizeTenantId(tenantId);
|
|
96
|
+
const telemetry = createTelemetry({
|
|
97
|
+
tenantId: normalizedTenant,
|
|
98
|
+
agentId: agent.agentId,
|
|
99
|
+
scopeParam: agent.scopeParam,
|
|
100
|
+
mode: "answer",
|
|
101
|
+
});
|
|
102
|
+
const transport = createTransport({
|
|
103
|
+
tenantId: normalizedTenant,
|
|
104
|
+
agentId: agent.agentId,
|
|
105
|
+
scopeParam: agent.scopeParam,
|
|
106
|
+
policyFingerprint: agent.policyFingerprint,
|
|
107
|
+
gatewayUrl: gatewayUrl || config.gatewayUrl || resolveDefaultGateway(),
|
|
108
|
+
credential: tenantCredential(config.pat, normalizedTenant),
|
|
109
|
+
telemetry,
|
|
110
|
+
});
|
|
111
|
+
const correlationId = crypto.randomUUID();
|
|
112
|
+
const startedAt = Date.now();
|
|
113
|
+
telemetry("local_tool_received", { correlationId, tool: "answer_native_agent" });
|
|
114
|
+
try {
|
|
115
|
+
throwIfAborted(signal);
|
|
116
|
+
let value = await transport.answer({ question: prompt, contextKeys: [] }, { signal });
|
|
117
|
+
for (;;) {
|
|
118
|
+
throwIfAborted(signal);
|
|
119
|
+
const terminal = terminalAnswer(value);
|
|
120
|
+
if (terminal) {
|
|
121
|
+
telemetry("local_tool_completed", {
|
|
122
|
+
correlationId,
|
|
123
|
+
tool: "answer_native_agent",
|
|
124
|
+
outcome: terminal.ok ? "succeeded" : "failed",
|
|
125
|
+
durationMs: Date.now() - startedAt,
|
|
126
|
+
durableRunCreated: Boolean(terminal.runId),
|
|
127
|
+
});
|
|
128
|
+
return terminal;
|
|
129
|
+
}
|
|
130
|
+
if (![NATIVE_AGENT_CONTINUATION_SCHEMA, NATIVE_AGENT_HANDLE_SCHEMA].includes(value?.schema)) {
|
|
131
|
+
throw new Error("managed agent answer returned an invalid transport result");
|
|
132
|
+
}
|
|
133
|
+
value = await transport.resume({ handle: value }, { signal });
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
telemetry("local_tool_completed", {
|
|
137
|
+
correlationId,
|
|
138
|
+
tool: "answer_native_agent",
|
|
139
|
+
outcome: error?.name === "AbortError" ? "cancelled" : "failed",
|
|
140
|
+
durationMs: Date.now() - startedAt,
|
|
141
|
+
});
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function answerJson(result) {
|
|
147
|
+
return JSON.stringify({
|
|
148
|
+
ok: result.ok === true,
|
|
149
|
+
finalText: typeof result.finalText === "string" ? result.finalText : "",
|
|
150
|
+
...(result.runId ? { runId: result.runId } : {}),
|
|
151
|
+
...(result.error ? { error: redactSecretText(result.error) } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function escapeRegex(value) {
|
|
156
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function matchManagedNativeMention(prompt, bindings) {
|
|
160
|
+
if (typeof prompt !== "string" || !Array.isArray(bindings)) return null;
|
|
161
|
+
// Reject every prompt containing a second @mention, managed or otherwise.
|
|
162
|
+
// This keeps implicit/mixed delegation on the model-mediated path.
|
|
163
|
+
const mentions = prompt.match(/@[a-z0-9][a-z0-9_.:-]*/giu) || [];
|
|
164
|
+
if (mentions.length !== 1) return null;
|
|
165
|
+
for (const binding of bindings) {
|
|
166
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/u.test(binding?.name || "")) continue;
|
|
167
|
+
const match = new RegExp(`^@${escapeRegex(binding.name)}\\s+([\\s\\S]+)$`, "u").exec(prompt);
|
|
168
|
+
if (!match || !match[1].trim()) continue;
|
|
169
|
+
return { binding, task: match[1] };
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function cmdNativeIntercept(argv, {
|
|
175
|
+
environment = process.env,
|
|
176
|
+
profileRoot = null,
|
|
177
|
+
readInput = readHookInput,
|
|
178
|
+
resolveBindings = managedClaudeAnswerBindings,
|
|
179
|
+
answer = answerManagedNativeAgent,
|
|
180
|
+
timeoutMs = null,
|
|
181
|
+
stdout = (line) => process.stdout.write(`${line}\n`),
|
|
182
|
+
} = {}) {
|
|
183
|
+
try {
|
|
184
|
+
const { flags, positionals } = parseFlags(argv, INTERCEPT_SPEC);
|
|
185
|
+
if (positionals.length > 0
|
|
186
|
+
|| Object.keys(flags).some((name) => !Object.hasOwn(INTERCEPT_SPEC, name))
|
|
187
|
+
|| flags[MANAGED_NATIVE_INTERCEPT_FLAG] !== true
|
|
188
|
+
|| typeof flags.tenant !== "string"
|
|
189
|
+
|| !flags.tenant.trim()) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const tenantId = normalizeTenantId(flags.tenant);
|
|
193
|
+
const input = await readInput();
|
|
194
|
+
if (input.hook_event_name !== "UserPromptSubmit") return null;
|
|
195
|
+
const root = profileRoot || agentProfileRoot("claude", { environment });
|
|
196
|
+
const matched = matchManagedNativeMention(
|
|
197
|
+
input.prompt,
|
|
198
|
+
resolveBindings(root, tenantId),
|
|
199
|
+
);
|
|
200
|
+
if (!matched) return null;
|
|
201
|
+
|
|
202
|
+
const controller = new AbortController();
|
|
203
|
+
const hardTimeoutMs = timeoutMs ?? nativeInterceptTimeoutMs(environment);
|
|
204
|
+
let timeout;
|
|
205
|
+
const deadline = new Promise((resolve) => {
|
|
206
|
+
timeout = setTimeout(() => {
|
|
207
|
+
controller.abort();
|
|
208
|
+
resolve(null);
|
|
209
|
+
}, hardTimeoutMs);
|
|
210
|
+
});
|
|
211
|
+
let result;
|
|
212
|
+
try {
|
|
213
|
+
const answered = Promise.resolve(answer({
|
|
214
|
+
tenantId,
|
|
215
|
+
agent: matched.binding,
|
|
216
|
+
prompt: matched.task,
|
|
217
|
+
signal: controller.signal,
|
|
218
|
+
})).catch(() => null);
|
|
219
|
+
result = await Promise.race([answered, deadline]);
|
|
220
|
+
} finally {
|
|
221
|
+
clearTimeout(timeout);
|
|
222
|
+
}
|
|
223
|
+
if (!result?.ok || typeof result.finalText !== "string") return null;
|
|
224
|
+
stdout(JSON.stringify({
|
|
225
|
+
decision: "block",
|
|
226
|
+
reason: `Response from managed agent ${JSON.stringify(matched.binding.title)} (${matched.binding.agentId}):\n\n${result.finalText}`,
|
|
227
|
+
suppressOriginalPrompt: true,
|
|
228
|
+
}));
|
|
229
|
+
return result;
|
|
230
|
+
} catch {
|
|
231
|
+
// Interception is an optimization only. Invalid input, stale profiles,
|
|
232
|
+
// transport failures, and timeouts all fall through to Claude unchanged.
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function cmdNative(argv, {
|
|
238
|
+
environment = process.env,
|
|
239
|
+
profileRoot = null,
|
|
240
|
+
resolveBinding = resolveManagedClaudeAnswerBinding,
|
|
241
|
+
answer = answerManagedNativeAgent,
|
|
242
|
+
stdout = (line) => console.log(line),
|
|
243
|
+
stderr = (line) => console.error(line),
|
|
244
|
+
} = {}) {
|
|
245
|
+
const [action, ...rest] = argv;
|
|
246
|
+
if (action === "intercept") {
|
|
247
|
+
return cmdNativeIntercept(rest, {
|
|
248
|
+
environment,
|
|
249
|
+
profileRoot,
|
|
250
|
+
stdout,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (action !== "answer") {
|
|
254
|
+
throw new Error("native command expects `answer --tenant <tenant> --agent <id> --prompt <task> [--json]`");
|
|
255
|
+
}
|
|
256
|
+
const { flags, positionals } = parseFlags(rest, ANSWER_SPEC);
|
|
257
|
+
if (positionals.length > 0) throw new Error("native answer does not accept positional arguments");
|
|
258
|
+
for (const flag of ["tenant", "agent", "prompt"]) {
|
|
259
|
+
if (typeof flags[flag] !== "string" || !flags[flag].trim()) {
|
|
260
|
+
throw new Error(`native answer requires --${flag}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const tenantId = normalizeTenantId(flags.tenant);
|
|
264
|
+
const root = profileRoot || agentProfileRoot("claude", { environment });
|
|
265
|
+
let binding;
|
|
266
|
+
try {
|
|
267
|
+
binding = resolveBinding(root, tenantId, flags.agent);
|
|
268
|
+
const result = await answer({ tenantId, agent: binding, prompt: flags.prompt });
|
|
269
|
+
if (flags.json === true) stdout(answerJson(result));
|
|
270
|
+
else if (result.ok) {
|
|
271
|
+
stdout(`Response from managed agent ${JSON.stringify(binding.title)} (${binding.agentId}):\n\n${result.finalText}`);
|
|
272
|
+
} else {
|
|
273
|
+
stderr(`Managed agent ${JSON.stringify(binding.title)} (${binding.agentId}) failed: ${result.error}`);
|
|
274
|
+
}
|
|
275
|
+
if (!result.ok) process.exitCode = 1;
|
|
276
|
+
return result;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
const message = redactSecretText(error?.message || error);
|
|
279
|
+
const result = { ok: false, finalText: "", error: message };
|
|
280
|
+
if (flags.json === true) stdout(answerJson(result));
|
|
281
|
+
else stderr(`Managed agent ${JSON.stringify(binding?.title || flags.agent)} failed: ${message}`);
|
|
282
|
+
process.exitCode = 1;
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Fleet generation shared by managed-profile writers and upstream clients.
|
|
2
|
+
// Keep this isolated from apps.js so latency-sensitive transports do not load
|
|
3
|
+
// desktop bundle machinery just to identify their managed config contract.
|
|
4
|
+
export const CURRENT_CONFIG_VERSION = 35;
|
|
@@ -12,6 +12,7 @@ const SAFE_EVENTS = new Set([
|
|
|
12
12
|
"local_tool_completed",
|
|
13
13
|
"session_prepared",
|
|
14
14
|
"upstream_request_completed",
|
|
15
|
+
"hedge_fired",
|
|
15
16
|
"binding_completed",
|
|
16
17
|
]);
|
|
17
18
|
const SAFE_TOOLS = new Set([
|
|
@@ -94,9 +95,12 @@ export function createNativeAgentTelemetry({
|
|
|
94
95
|
outcome: SAFE_OUTCOMES.has(fields.outcome) ? fields.outcome : undefined,
|
|
95
96
|
status: safeId(fields.status),
|
|
96
97
|
durationMs: safeInteger(fields.durationMs),
|
|
98
|
+
elapsedMs: safeInteger(fields.elapsedMs),
|
|
97
99
|
pollCount: safeInteger(fields.pollCount),
|
|
98
100
|
handshakeReused: typeof fields.handshakeReused === "boolean" ? fields.handshakeReused : undefined,
|
|
99
101
|
bindingReused: typeof fields.bindingReused === "boolean" ? fields.bindingReused : undefined,
|
|
102
|
+
prewarmed: typeof fields.prewarmed === "boolean" ? fields.prewarmed : undefined,
|
|
103
|
+
replayTerminal: typeof fields.replayTerminal === "boolean" ? fields.replayTerminal : undefined,
|
|
100
104
|
durableRunCreated: typeof fields.durableRunCreated === "boolean" ? fields.durableRunCreated : undefined,
|
|
101
105
|
};
|
|
102
106
|
try {
|