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/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 const CURRENT_CONFIG_VERSION = 35;
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
 
@@ -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)",
@@ -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
- nativeSpawnInvocation,
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
- ({ selector: codexAgentSelector, argv: nativeArgv } = parseCodexAgentLaunchArguments(argv));
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 = { client: "claude", root: profile.configDir, label: "Impel isolated Claude (impel claude)" };
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 = { client: "codex", root: profile.codexHome, label: "Impel isolated Codex (impel codex)" };
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;
@@ -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: nativeAgentCompositeTools({ mode }),
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 {