impel-cli 0.20.41 → 0.20.42
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/src/agents.js +367 -41
- package/src/apps.js +2 -1
- package/src/cli.js +6 -0
- package/src/commands/launch.js +26 -0
- 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 +138 -15
- package/src/nativeInterception.js +133 -0
package/src/commands/launch.js
CHANGED
|
@@ -99,6 +99,32 @@ export function impelLaunchArguments(tool, argv, {
|
|
|
99
99
|
if (!RUNTIME_BRAND.features.agents) return [...argv];
|
|
100
100
|
if (tool === "claude") {
|
|
101
101
|
if (claudeManagedAgent) {
|
|
102
|
+
if (claudeManagedAgent.parentDirect === true) {
|
|
103
|
+
const { parentLaunchDefinition, parentMcpConfigJson } = claudeManagedAgent;
|
|
104
|
+
if (!parentLaunchDefinition
|
|
105
|
+
|| typeof parentLaunchDefinition.prompt !== "string"
|
|
106
|
+
|| !parentLaunchDefinition.prompt
|
|
107
|
+
|| !Array.isArray(parentLaunchDefinition.tools)
|
|
108
|
+
|| parentLaunchDefinition.tools.length !== 2
|
|
109
|
+
|| typeof parentMcpConfigJson !== "string"
|
|
110
|
+
|| !parentMcpConfigJson) {
|
|
111
|
+
throw new Error("managed Claude parent-direct launch configuration is invalid");
|
|
112
|
+
}
|
|
113
|
+
const tools = parentLaunchDefinition.tools.join(",");
|
|
114
|
+
return [
|
|
115
|
+
...IMPEL_CLAUDE_MANAGED_AGENT_ARGUMENTS,
|
|
116
|
+
"--mcp-config",
|
|
117
|
+
parentMcpConfigJson,
|
|
118
|
+
"--strict-mcp-config",
|
|
119
|
+
"--tools",
|
|
120
|
+
tools,
|
|
121
|
+
"--allowedTools",
|
|
122
|
+
tools,
|
|
123
|
+
"--append-system-prompt",
|
|
124
|
+
parentLaunchDefinition.prompt,
|
|
125
|
+
...argv,
|
|
126
|
+
];
|
|
127
|
+
}
|
|
102
128
|
const { launchName, launchAgentsJson } = claudeManagedAgent;
|
|
103
129
|
if (typeof launchName !== "string"
|
|
104
130
|
|| !launchName
|
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 {
|