impel-cli 0.20.40 → 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/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.41 — Windows onboarding: converge over an already-installed Store ChatGPT
4
+
5
+ - Accepts winget's UPDATE_NOT_APPLICABLE result during setup as well as
6
+ update, gated by the pin-exact package probe, ending the unconvergeable
7
+ first-run failure loop on machines where the ChatGPT Store app already
8
+ exists (and, downstream, the managed sign-in loop it caused).
9
+ - Registers a machine-wide staged Store package for the current user
10
+ (download-free, no elevation) when winget sees it but the per-user probe
11
+ does not, still failing closed through the pin-exact probe.
12
+ - Resolves the app-open path strictly through the pinned finder so an
13
+ off-pin Store build can never be staged as the managed app.
14
+ - Names installed-vs-pinned versions in vendor-install diagnostics with a
15
+ measured direction, adds the 0x8A15002B winget hint, and makes Windows
16
+ status require the staged managed app rather than config presence alone.
17
+
3
18
  ## 0.20.40 — Field-report fixes: launcher drift, sync ownership, Codex capability, one-run update
4
19
 
5
20
  - Survives Claude auto-update drift: `impel claude` resolves the exact reviewed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.40",
3
+ "version": "0.20.42",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agents.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
22
22
  IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
23
23
  IMPEL_NATIVE_AGENT_MCP_TARGET,
24
+ impelCliInvocation,
24
25
  impelNativeAgentMcpInvocation,
25
26
  } from "./selfInvocation.js";
26
27
  import {
@@ -31,6 +32,11 @@ import {
31
32
  usesDirectAnswer,
32
33
  } from "./directAnswer.js";
33
34
  import { normalizeTenantId } from "./tenants.js";
35
+ import {
36
+ ensureClaudeNativeInterceptHook,
37
+ nativeInterceptEnabled,
38
+ nativeInterceptTimeoutMs,
39
+ } from "./nativeInterception.js";
34
40
  import {
35
41
  adapterCallerSpawnGuidance,
36
42
  adapterFaithfulCompletionGuidance,
@@ -56,7 +62,9 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
56
62
  export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
57
63
  export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
58
64
  export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
59
- export const MANAGED_AGENT_MANIFEST_VERSION = 22;
65
+ export const MANAGED_AGENT_MANIFEST_VERSION = 23;
66
+ export const IMPEL_NATIVE_PARENT_DIRECT_ENV = "IMPEL_NATIVE_PARENT_DIRECT";
67
+ export const IMPEL_NATIVE_SLASH_COMMANDS_ENV = "IMPEL_NATIVE_SLASH_COMMANDS";
60
68
 
61
69
  // The host model only selects the fixed MCP tool and faithfully returns its
62
70
  // result. Spark minimizes those transport-only turns while the selected Eve
@@ -77,6 +85,18 @@ function nativeAgentToolNames(agent) {
77
85
  function eagerNativeAgentTransportEnabled() {
78
86
  return process.env.IMPEL_NATIVE_EAGER_TRANSPORT !== "0";
79
87
  }
88
+
89
+ function enabledProfileFlag(environment, name) {
90
+ return ["1", "true"].includes(String(environment?.[name] || "").toLowerCase());
91
+ }
92
+
93
+ export function nativeParentDirectEnabled(environment = process.env) {
94
+ return enabledProfileFlag(environment, IMPEL_NATIVE_PARENT_DIRECT_ENV);
95
+ }
96
+
97
+ export function nativeSlashCommandsEnabled(environment = process.env) {
98
+ return enabledProfileFlag(environment, IMPEL_NATIVE_SLASH_COMMANDS_ENV);
99
+ }
80
100
  const MAX_RETIRED_AGENT_BINDINGS = 50;
81
101
  const MAX_NATIVE_AGENT_STATE_BYTES = 512 * 1024;
82
102
  const TERMINAL_NATIVE_AGENT_STATUSES = new Set(["succeeded", "failed", "cancelled", "canceled"]);
@@ -780,6 +800,32 @@ function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = f
780
800
  };
781
801
  }
782
802
 
803
+ function claudeParentDirectInstructions(tenantId, agent) {
804
+ const attribution = `Response from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}):`;
805
+ return [
806
+ `The user explicitly selected the managed agent ${JSON.stringify(agent.title)} (${agent.agentId}) for tenant ${JSON.stringify(tenantId)}.`,
807
+ `Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the user's complete request. Do not spawn a relay subagent and do not perform the request yourself.`,
808
+ `If the answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal; after a continuation exists, never call the answer tool again.`,
809
+ `On success, present the attribution line ${JSON.stringify(attribution)}, followed by the returned finalText verbatim with no rewriting, Markdown changes, or independent synthesis. On failure, attribute the failure to the same named managed agent and do not invent a replacement answer.`,
810
+ ].join(" ");
811
+ }
812
+
813
+ function claudeParentDirectDefinition({ tenantId, agent, invocation }) {
814
+ return {
815
+ prompt: claudeParentDirectInstructions(tenantId, agent),
816
+ permissionMode: "bypassPermissions",
817
+ tools: [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL].map((tool) => nativeToolName(tool)),
818
+ mcpServers: [{
819
+ [MANAGED_AGENT_MCP_SERVER]: {
820
+ type: "stdio",
821
+ command: invocation.command,
822
+ args: [...invocation.args],
823
+ env: { ...(invocation.env || {}) },
824
+ },
825
+ }],
826
+ };
827
+ }
828
+
783
829
  function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
784
830
  const entries = Object.entries(invocation.env || {});
785
831
  const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
@@ -867,34 +913,42 @@ function renderCodexProfile(options) {
867
913
  function boundNativeAgentInvocation(tenantId, agent, invocation, {
868
914
  policyFingerprint = nativeAgentPolicyFingerprint(agent),
869
915
  mode = usesDirectAnswer(agent) ? "answer" : "durable",
916
+ answerToolAttribution = false,
870
917
  } = {}) {
871
- if (!invocation) {
872
- return impelNativeAgentMcpInvocation({
918
+ const bound = !invocation
919
+ ? impelNativeAgentMcpInvocation({
873
920
  tenantId,
874
921
  agentId: agent.agentId,
875
922
  scopeParam: agent.scopeParam,
876
923
  policyFingerprint,
877
924
  mode,
878
- });
879
- }
880
- const mcpIndex = invocation.args?.lastIndexOf("mcp") ?? -1;
881
- if (mcpIndex < 0) throw new Error("native-agent MCP invocation has no mcp command");
882
- return {
883
- ...invocation,
884
- args: [
885
- ...invocation.args.slice(0, mcpIndex + 1),
886
- "--target", IMPEL_NATIVE_AGENT_MCP_TARGET,
887
- "--tenant", tenantId,
888
- "--agent-id", agent.agentId,
889
- "--scope-param", agent.scopeParam,
890
- "--policy-fingerprint", policyFingerprint,
891
- ...(mode === "recovery" ? ["--recovery-only"] : []),
892
- ...(mode === "answer" ? ["--answer-only"] : []),
893
- ],
894
- };
925
+ })
926
+ : (() => {
927
+ const mcpIndex = invocation.args?.lastIndexOf("mcp") ?? -1;
928
+ if (mcpIndex < 0) throw new Error("native-agent MCP invocation has no mcp command");
929
+ return {
930
+ ...invocation,
931
+ args: [
932
+ ...invocation.args.slice(0, mcpIndex + 1),
933
+ "--target", IMPEL_NATIVE_AGENT_MCP_TARGET,
934
+ "--tenant", tenantId,
935
+ "--agent-id", agent.agentId,
936
+ "--scope-param", agent.scopeParam,
937
+ "--policy-fingerprint", policyFingerprint,
938
+ ...(mode === "recovery" ? ["--recovery-only"] : []),
939
+ ...(mode === "answer" ? ["--answer-only"] : []),
940
+ ],
941
+ };
942
+ })();
943
+ return answerToolAttribution
944
+ ? { ...bound, args: [...bound.args, "--agent-title", agent.title] }
945
+ : bound;
895
946
  }
896
947
 
897
- export function renderManagedAgents(client, tenantId, agents, invocation = null, { directCodeMode = true } = {}) {
948
+ export function renderManagedAgents(client, tenantId, agents, invocation = null, {
949
+ directCodeMode = true,
950
+ nativeParentDirect = false,
951
+ } = {}) {
898
952
  if (client !== "claude" && client !== "codex") throw new Error(`unknown agent client ${client}`);
899
953
  const normalizedTenant = normalizeTenantId(tenantId);
900
954
  const fileStems = generatedAgentFileStems(normalizedTenant, agents);
@@ -908,12 +962,20 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
908
962
  // builtin and remove it from the @ mention picker. Claude names are already
909
963
  // collision-safe, filesystem-safe identifiers, so use them as filenames.
910
964
  const fileStem = client === "claude" ? name : fileStems[index];
911
- const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation);
912
- const claudeRendered = client === "claude"
965
+ const parentDirect = client === "claude" && nativeParentDirect && usesDirectAnswer(agent);
966
+ const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation, {
967
+ answerToolAttribution: parentDirect,
968
+ });
969
+ const claudeRendered = client === "claude" && !parentDirect
913
970
  ? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
914
971
  : null;
972
+ const parentLaunchDefinition = parentDirect
973
+ ? claudeParentDirectDefinition({ tenantId: normalizedTenant, agent, invocation: boundInvocation })
974
+ : null;
915
975
  const contents = claudeRendered?.contents
916
- ?? renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode });
976
+ ?? (client === "codex"
977
+ ? renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode })
978
+ : null);
917
979
  const profileName = client === "codex" ? fileStem : null;
918
980
  return {
919
981
  agentId: agent.agentId,
@@ -923,9 +985,10 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
923
985
  policyFingerprint: nativeAgentPolicyFingerprint(agent),
924
986
  retired: false,
925
987
  name,
926
- fileName: `${fileStem}${extension}`,
988
+ fileName: parentDirect ? null : `${fileStem}${extension}`,
927
989
  contents,
928
990
  ...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
991
+ ...(parentLaunchDefinition ? { parentLaunchDefinition } : {}),
929
992
  ...(profileName ? {
930
993
  profileName,
931
994
  profileFileName: `${profileName}.config.toml`,
@@ -941,6 +1004,61 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
941
1004
  });
942
1005
  }
943
1006
 
1007
+ function shellQuote(value) {
1008
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
1009
+ }
1010
+
1011
+ function claudeSlashCommandName(agent, generatedAgentName) {
1012
+ const base = `ask-${generatedAgentName}`;
1013
+ if (base.length <= 63) return base;
1014
+ const prefix = generatedAgentName.slice(0, 50).replace(/-+$/u, "") || "agent";
1015
+ return `ask-${prefix}-${agentBindingHash(agent)}`;
1016
+ }
1017
+
1018
+ function renderClaudeSlashCommand(tenantId, agent, commandName, invocation) {
1019
+ const tokens = [invocation.command, ...invocation.args].map(shellQuote).join(" ");
1020
+ const bashCommand = `${tokens} --prompt "$ARGUMENTS" --json`;
1021
+ const attribution = `Response from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}):`;
1022
+ return [
1023
+ "---",
1024
+ `description: ${JSON.stringify(`Ask managed agent ${agent.title} (${agent.agentId}) in Impel tenant ${tenantId}; its rendered answer is explicitly attributed.`)}`,
1025
+ `argument-hint: ${JSON.stringify("<task>")}`,
1026
+ `allowed-tools: ${JSON.stringify(`Bash(${tokens}:*)`)}`,
1027
+ "---",
1028
+ "",
1029
+ `The JSON below came from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}), not from the host model.`,
1030
+ `Run the fixed command exactly once. If ok is true, output the attribution line ${JSON.stringify(attribution)} followed by finalText verbatim. If ok is false, attribute the reported error to the same managed agent. Do not perform, rewrite, or independently answer the task.`,
1031
+ "",
1032
+ `!\`${bashCommand}\``,
1033
+ "",
1034
+ ].join("\n");
1035
+ }
1036
+
1037
+ export function renderManagedClaudeSlashCommands(tenantId, agents, invocation = null) {
1038
+ const normalizedTenant = normalizeTenantId(tenantId);
1039
+ const names = generatedClientAgentNames("claude", agents);
1040
+ return agents.flatMap((agent, index) => {
1041
+ if (!usesDirectAnswer(agent)) return [];
1042
+ const commandName = claudeSlashCommandName(agent, names[index]);
1043
+ const commandInvocation = invocation || impelCliInvocation([
1044
+ "native", "answer",
1045
+ "--tenant", normalizedTenant,
1046
+ "--agent", agent.agentId,
1047
+ ]);
1048
+ return [{
1049
+ agentId: agent.agentId,
1050
+ name: commandName,
1051
+ fileName: `${commandName}.md`,
1052
+ contents: renderClaudeSlashCommand(
1053
+ normalizedTenant,
1054
+ agent,
1055
+ commandName,
1056
+ commandInvocation,
1057
+ ),
1058
+ }];
1059
+ });
1060
+ }
1061
+
944
1062
  function renderRetiredManagedAgents(client, tenantId, bindings, active, invocation = null, { directCodeMode = true } = {}) {
945
1063
  const usedNames = new Set(active.map(({ name }) => name));
946
1064
  const usedFiles = new Set(active.map(({ fileName }) => fileName));
@@ -1154,6 +1272,54 @@ function validateClaudeLaunchDefinition(record, tenantId) {
1154
1272
  return definition;
1155
1273
  }
1156
1274
 
1275
+ function validateClaudeParentLaunchDefinition(record, tenantId) {
1276
+ const definition = record.parentLaunchDefinition;
1277
+ const expectedTools = [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL]
1278
+ .map((name) => nativeToolName(name));
1279
+ if (!exactObjectKeys(definition, ["prompt", "permissionMode", "tools", "mcpServers"])
1280
+ || definition.prompt !== claudeParentDirectInstructions(tenantId, record)
1281
+ || definition.permissionMode !== "bypassPermissions"
1282
+ || JSON.stringify(definition.tools) !== JSON.stringify(expectedTools)
1283
+ || !Array.isArray(definition.mcpServers)
1284
+ || definition.mcpServers.length !== 1
1285
+ || !exactObjectKeys(definition.mcpServers[0], [MANAGED_AGENT_MCP_SERVER])) {
1286
+ throw new Error("the managed Claude parent-direct definition is invalid; run `impel agents sync claude`");
1287
+ }
1288
+ const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
1289
+ const allowedEnvironment = new Set([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
1290
+ if (!exactObjectKeys(server, ["type", "command", "args", "env"])
1291
+ || server.type !== "stdio"
1292
+ || typeof server.command !== "string"
1293
+ || !server.command
1294
+ || !Array.isArray(server.args)
1295
+ || !server.args.every((argument) => typeof argument === "string")
1296
+ || !server.env
1297
+ || typeof server.env !== "object"
1298
+ || Array.isArray(server.env)
1299
+ || server.env[IMPEL_MANAGED_MCP_ENV] !== "1"
1300
+ || !Object.entries(server.env).every(([key, value]) =>
1301
+ allowedEnvironment.has(key)
1302
+ && typeof value === "string"
1303
+ && value.length <= 4096
1304
+ && redactCredentialText(value) === value
1305
+ )) {
1306
+ throw new Error("the managed Claude parent-direct MCP definition is invalid; run `impel agents sync claude`");
1307
+ }
1308
+ const trustedInvocation = impelNativeAgentMcpInvocation({
1309
+ tenantId,
1310
+ agentId: record.agentId,
1311
+ scopeParam: record.scopeParam,
1312
+ policyFingerprint: record.policyFingerprint,
1313
+ mode: "answer",
1314
+ });
1315
+ const trustedArgs = [...trustedInvocation.args, "--agent-title", record.title];
1316
+ if (server.command !== trustedInvocation.command
1317
+ || JSON.stringify(server.args) !== JSON.stringify(trustedArgs)) {
1318
+ throw new Error("the managed Claude parent-direct binding is invalid; run `impel agents sync claude`");
1319
+ }
1320
+ return definition;
1321
+ }
1322
+
1157
1323
  // A selector absent from the Impel manifest remains Claude-native. A matching
1158
1324
  // record is integrity checked, then re-declared under a random per-launch name
1159
1325
  // through Claude's higher-priority --agents source. This prevents project,
@@ -1192,11 +1358,34 @@ export function resolveManagedClaudeAgent(root, tenantId, selector) {
1192
1358
  const [record] = matches;
1193
1359
  if (typeof record.agentId !== "string"
1194
1360
  || !SAFE_AGENT_ID_RE.test(record.agentId)
1361
+ || typeof record.title !== "string"
1362
+ || !record.title.trim()
1363
+ || record.title.length > 160
1195
1364
  || typeof record.scopeParam !== "string"
1196
1365
  || !SAFE_SCOPE_PARAM_RE.test(record.scopeParam)
1197
1366
  || !/^[a-f0-9]{64}$/u.test(record.policyFingerprint || "")) {
1198
1367
  throw new Error("the managed Claude binding metadata is invalid; run `impel agents sync claude`");
1199
1368
  }
1369
+ if (record.launchMode === "parent-direct") {
1370
+ if (!manifest.nativeParentDirect) {
1371
+ throw new Error("the managed Claude parent-direct binding is stale; run `impel agents sync claude`");
1372
+ }
1373
+ const parentLaunchDefinition = validateClaudeParentLaunchDefinition(record, normalizedTenant);
1374
+ const server = parentLaunchDefinition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
1375
+ return {
1376
+ ...record,
1377
+ fileName: null,
1378
+ path: null,
1379
+ parentDirect: true,
1380
+ parentLaunchDefinition,
1381
+ parentMcpConfigJson: JSON.stringify({
1382
+ mcpServers: { [MANAGED_AGENT_MCP_SERVER]: server },
1383
+ }),
1384
+ };
1385
+ }
1386
+ if (record.launchMode !== undefined) {
1387
+ throw new Error("the managed Claude launch mode is invalid; run `impel agents sync claude`");
1388
+ }
1200
1389
  const fileName = `${record.name}.md`;
1201
1390
  if (path.basename(fileName) !== fileName || !manifest.files.includes(fileName)) {
1202
1391
  throw new Error("the managed Claude agent mapping is invalid; run `impel agents sync claude`");
@@ -1229,6 +1418,59 @@ export function resolveManagedClaudeAgent(root, tenantId, selector) {
1229
1418
  };
1230
1419
  }
1231
1420
 
1421
+ /**
1422
+ * Resolve a synchronized, read-only Claude binding that is allowed to use the
1423
+ * bounded direct-answer transport. The public `impel native answer` command
1424
+ * deliberately trusts the integrity-tracked profile rather than accepting
1425
+ * model- or user-supplied scope/fingerprint routing arguments.
1426
+ */
1427
+ export function resolveManagedClaudeAnswerBinding(root, tenantId, selector) {
1428
+ const resolved = resolveManagedClaudeAgent(root, tenantId, selector);
1429
+ if (!resolved) {
1430
+ throw new Error(
1431
+ `managed Claude agent ${JSON.stringify(selector)} was not found for tenant ${JSON.stringify(normalizeTenantId(tenantId))}`,
1432
+ );
1433
+ }
1434
+ const definition = resolved.parentDirect
1435
+ ? resolved.parentLaunchDefinition
1436
+ : resolved.launchDefinition;
1437
+ const server = definition?.mcpServers?.[0]?.[MANAGED_AGENT_MCP_SERVER];
1438
+ if (!Array.isArray(server?.args) || !server.args.includes("--answer-only")) {
1439
+ throw new Error(
1440
+ `managed Claude agent ${JSON.stringify(selector)} is not configured for direct answers`,
1441
+ );
1442
+ }
1443
+ return {
1444
+ tenantId: normalizeTenantId(tenantId),
1445
+ agentId: resolved.agentId,
1446
+ title: resolved.title,
1447
+ name: resolved.name,
1448
+ scopeParam: resolved.scopeParam,
1449
+ policyFingerprint: resolved.policyFingerprint,
1450
+ };
1451
+ }
1452
+
1453
+ export function managedClaudeAnswerBindings(root, tenantId) {
1454
+ const normalizedTenant = normalizeTenantId(tenantId);
1455
+ const manifest = readManagedAgentManifest(root);
1456
+ if (!manifest
1457
+ || manifest.version !== MANAGED_AGENT_MANIFEST_VERSION
1458
+ || manifest.client !== "claude"
1459
+ || manifest.tenantId !== normalizedTenant
1460
+ || !Array.isArray(manifest.agents)) {
1461
+ return [];
1462
+ }
1463
+ return manifest.agents.flatMap((record) => {
1464
+ if (!record || record.retired || typeof record.agentId !== "string") return [];
1465
+ const definition = record.launchMode === "parent-direct"
1466
+ ? record.parentLaunchDefinition
1467
+ : record.launchDefinition;
1468
+ const args = definition?.mcpServers?.[0]?.[MANAGED_AGENT_MCP_SERVER]?.args;
1469
+ if (!Array.isArray(args) || !args.includes("--answer-only")) return [];
1470
+ return [resolveManagedClaudeAnswerBinding(root, normalizedTenant, record.agentId)];
1471
+ });
1472
+ }
1473
+
1232
1474
  export function resolveManagedCodexAgentProfile(root, tenantId, selector) {
1233
1475
  const normalizedTenant = normalizeTenantId(tenantId);
1234
1476
  const normalizedSelector = boundedString(selector, "agent selector", { max: 512 });
@@ -1279,12 +1521,29 @@ function profileIsFresh(profile, tenantId, now, ttlMs) {
1279
1521
  || Array.isArray(manifest.contentDigests)) return false;
1280
1522
  const expectedDirectProfiles = profile.client === "codex" && profile.directProfiles !== false;
1281
1523
  const expectedDirectCodeMode = profile.client === "codex" && profile.directCodeMode !== false;
1524
+ const expectedNativeParentDirect = profile.client === "claude"
1525
+ && (profile.nativeParentDirect ?? nativeParentDirectEnabled());
1526
+ const expectedNativeSlashCommands = profile.client === "claude"
1527
+ && (profile.nativeSlashCommands ?? nativeSlashCommandsEnabled());
1528
+ const expectedNativeIntercept = profile.client === "claude"
1529
+ && (profile.nativeIntercept ?? nativeInterceptEnabled());
1530
+ const expectedNativeInterceptTimeoutMs = expectedNativeIntercept
1531
+ ? (profile.nativeInterceptTimeoutMs ?? nativeInterceptTimeoutMs())
1532
+ : null;
1282
1533
  if (manifest.directProfiles !== expectedDirectProfiles
1283
- || manifest.directCodeMode !== expectedDirectCodeMode) return false;
1534
+ || manifest.directCodeMode !== expectedDirectCodeMode
1535
+ || Boolean(manifest.nativeParentDirect) !== expectedNativeParentDirect
1536
+ || Boolean(manifest.nativeSlashCommands) !== expectedNativeSlashCommands
1537
+ || Boolean(manifest.nativeIntercept) !== expectedNativeIntercept
1538
+ || (expectedNativeIntercept
1539
+ && manifest.nativeInterceptTimeoutMs !== expectedNativeInterceptTimeoutMs)) return false;
1284
1540
  const syncedAt = Date.parse(manifest.syncedAt || "");
1285
1541
  if (!Number.isFinite(syncedAt) || now - syncedAt >= ttlMs) return false;
1286
1542
  const artifacts = [
1287
1543
  ...manifest.files.map((fileName) => `agents/${fileName}`),
1544
+ ...(Array.isArray(manifest.commands)
1545
+ ? manifest.commands.map((fileName) => `commands/${fileName}`)
1546
+ : []),
1288
1547
  ...(Array.isArray(manifest.profiles) ? manifest.profiles : []),
1289
1548
  ];
1290
1549
  // Renderer changes are invalidated by the manifest version/capability fields
@@ -1306,10 +1565,15 @@ export function syncAgentProfile({
1306
1565
  agents,
1307
1566
  directProfiles = client === "codex",
1308
1567
  directCodeMode = client === "codex",
1568
+ nativeParentDirect = client === "claude" && nativeParentDirectEnabled(),
1569
+ nativeSlashCommands = client === "claude" && nativeSlashCommandsEnabled(),
1570
+ nativeIntercept = client === "claude" && nativeInterceptEnabled(),
1571
+ nativeInterceptTimeoutMs: interceptTimeoutMs = nativeIntercept ? nativeInterceptTimeoutMs() : null,
1309
1572
  now = Date.now(),
1310
1573
  nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
1311
1574
  }) {
1312
1575
  const agentsDir = path.join(root, "agents");
1576
+ const commandsDir = path.join(root, "commands");
1313
1577
  for (const candidate of [root, agentsDir]) {
1314
1578
  if (fs.existsSync(candidate) && fs.lstatSync(candidate).isSymbolicLink()) {
1315
1579
  throw new Error(`refusing to use symlinked native-agent path ${candidate}`);
@@ -1319,7 +1583,15 @@ export function syncAgentProfile({
1319
1583
  privateDirectory(managedDir);
1320
1584
  const manifestPath = path.join(managedDir, MANAGED_AGENT_MANIFEST);
1321
1585
  const prior = readManifest(manifestPath);
1322
- const active = renderManagedAgents(client, tenantId, agents, null, { directCodeMode });
1586
+ if ((nativeSlashCommands || prior?.commands?.length > 0)
1587
+ && fs.existsSync(commandsDir)
1588
+ && fs.lstatSync(commandsDir).isSymbolicLink()) {
1589
+ throw new Error(`refusing to use symlinked native-agent path ${commandsDir}`);
1590
+ }
1591
+ const active = renderManagedAgents(client, tenantId, agents, null, {
1592
+ directCodeMode,
1593
+ nativeParentDirect,
1594
+ });
1323
1595
  const activeBindings = new Set(active.map((agent) =>
1324
1596
  `${agent.agentId}\0${agent.scopeParam}\0${agent.policyFingerprint}`
1325
1597
  ));
@@ -1329,14 +1601,22 @@ export function syncAgentProfile({
1329
1601
  ));
1330
1602
  const retired = renderRetiredManagedAgents(client, tenantId, retiredBindings, active, null, { directCodeMode });
1331
1603
  const rendered = [...active, ...retired];
1604
+ const commands = client === "claude" && nativeSlashCommands
1605
+ ? renderManagedClaudeSlashCommands(tenantId, agents)
1606
+ : [];
1607
+ const renderedFiles = rendered.filter(({ fileName }) => typeof fileName === "string");
1332
1608
  if (new Set(rendered.map(({ name }) => name)).size !== rendered.length
1333
- || new Set(rendered.map(({ fileName }) => fileName)).size !== rendered.length
1609
+ || new Set(renderedFiles.map(({ fileName }) => fileName)).size !== renderedFiles.length
1334
1610
  || (client === "codex" && directProfiles
1335
1611
  && new Set(rendered.map(({ profileFileName }) => profileFileName)).size !== rendered.length)) {
1336
1612
  throw new Error("generated native-agent destinations are not unique");
1337
1613
  }
1614
+ if (new Set(commands.map(({ fileName }) => fileName)).size !== commands.length) {
1615
+ throw new Error("generated native-agent slash command destinations are not unique");
1616
+ }
1338
1617
  const priorFiles = new Set(prior?.files || []);
1339
1618
  const priorProfiles = new Set(prior?.profiles || []);
1619
+ const priorCommands = new Set(prior?.commands || []);
1340
1620
  const priorUsesDiscoveryRoot = Number.isInteger(prior?.version)
1341
1621
  && prior.version >= 2
1342
1622
  && prior.version <= MANAGED_AGENT_MANIFEST_VERSION;
@@ -1362,14 +1642,16 @@ export function syncAgentProfile({
1362
1642
  // Preflight every destination before writing so an unmanaged file with the
1363
1643
  // same generated name is never overwritten.
1364
1644
  for (const agent of rendered) {
1365
- const destination = path.join(agentsDir, agent.fileName);
1366
- if (fs.existsSync(destination)) {
1367
- if (fs.lstatSync(destination).isSymbolicLink()) {
1368
- throw new Error(`refusing to overwrite symlinked native-agent file ${destination}`);
1369
- }
1370
- if ((!priorOwnsManagedFiles || !priorFiles.has(agent.fileName))
1371
- && !reclaimable(`agents/${agent.fileName}`, destination)) {
1372
- throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
1645
+ if (agent.fileName) {
1646
+ const destination = path.join(agentsDir, agent.fileName);
1647
+ if (fs.existsSync(destination)) {
1648
+ if (fs.lstatSync(destination).isSymbolicLink()) {
1649
+ throw new Error(`refusing to overwrite symlinked native-agent file ${destination}`);
1650
+ }
1651
+ if ((!priorOwnsManagedFiles || !priorFiles.has(agent.fileName))
1652
+ && !reclaimable(`agents/${agent.fileName}`, destination)) {
1653
+ throw new Error(`refusing to overwrite unmanaged native-agent file ${destination}`);
1654
+ }
1373
1655
  }
1374
1656
  }
1375
1657
  if (client === "codex" && directProfiles) {
@@ -1385,11 +1667,27 @@ export function syncAgentProfile({
1385
1667
  }
1386
1668
  }
1387
1669
  }
1670
+ for (const command of commands) {
1671
+ const destination = path.join(commandsDir, command.fileName);
1672
+ if (fs.existsSync(destination)) {
1673
+ if (fs.lstatSync(destination).isSymbolicLink()) {
1674
+ throw new Error(`refusing to overwrite symlinked native-agent slash command ${destination}`);
1675
+ }
1676
+ if ((!priorOwnsManagedFiles || !priorCommands.has(command.fileName))
1677
+ && !reclaimable(`commands/${command.fileName}`, destination)) {
1678
+ throw new Error(`refusing to overwrite unmanaged native-agent slash command ${destination}`);
1679
+ }
1680
+ }
1681
+ }
1388
1682
  for (const agent of rendered) {
1389
- atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
1683
+ if (agent.fileName) atomicPrivateWrite(path.join(agentsDir, agent.fileName), agent.contents);
1390
1684
  if (client === "codex" && directProfiles) atomicPrivateWrite(path.join(root, agent.profileFileName), agent.profileContents);
1391
1685
  }
1392
- const currentFiles = new Set(rendered.map((agent) => agent.fileName));
1686
+ for (const command of commands) {
1687
+ atomicPrivateWrite(path.join(commandsDir, command.fileName), command.contents);
1688
+ }
1689
+ const currentFiles = new Set(renderedFiles.map((agent) => agent.fileName));
1690
+ const currentCommands = new Set(commands.map((command) => command.fileName));
1393
1691
  const currentProfiles = new Set(client === "codex" && directProfiles
1394
1692
  ? rendered.map((agent) => agent.profileFileName)
1395
1693
  : []);
@@ -1422,6 +1720,15 @@ export function syncAgentProfile({
1422
1720
  fs.rmSync(path.join(root, stale), { force: true });
1423
1721
  }
1424
1722
  }
1723
+ for (const stale of client === "claude" ? prior?.commands || [] : []) {
1724
+ if (
1725
+ staleRemovable(stale, `commands/${stale}`, path.join(commandsDir, stale))
1726
+ && stale.endsWith(".md")
1727
+ && !currentCommands.has(stale)
1728
+ ) {
1729
+ fs.rmSync(path.join(commandsDir, stale), { force: true });
1730
+ }
1731
+ }
1425
1732
  if (!priorUsesDiscoveryRoot) {
1426
1733
  for (const legacy of prior?.files || []) {
1427
1734
  if (
@@ -1433,20 +1740,36 @@ export function syncAgentProfile({
1433
1740
  }
1434
1741
  }
1435
1742
  }
1743
+ if (client === "claude") {
1744
+ ensureClaudeNativeInterceptHook(root, tenantId, {
1745
+ enabled: nativeIntercept,
1746
+ ...(nativeIntercept ? { timeoutMs: interceptTimeoutMs } : {}),
1747
+ });
1748
+ }
1436
1749
  const contentDigests = Object.fromEntries(rendered.flatMap((agent) => [
1437
- [`agents/${agent.fileName}`, contentDigest(agent.contents)],
1750
+ ...(agent.fileName ? [[`agents/${agent.fileName}`, contentDigest(agent.contents)]] : []),
1438
1751
  ...(client === "codex" && directProfiles
1439
1752
  ? [[agent.profileFileName, contentDigest(agent.profileContents)]]
1440
1753
  : []),
1441
- ]).sort(([left], [right]) => left.localeCompare(right)));
1754
+ ]).concat(commands.map((command) => [
1755
+ `commands/${command.fileName}`,
1756
+ contentDigest(command.contents),
1757
+ ])).sort(([left], [right]) => left.localeCompare(right)));
1442
1758
  atomicPrivateWrite(manifestPath, `${JSON.stringify({
1443
1759
  version: MANAGED_AGENT_MANIFEST_VERSION,
1444
1760
  tenantId,
1445
1761
  client,
1446
1762
  directProfiles: client === "codex" && directProfiles,
1447
1763
  directCodeMode: client === "codex" && directCodeMode,
1764
+ ...(client === "claude" && nativeParentDirect ? { nativeParentDirect: true } : {}),
1765
+ ...(client === "claude" && nativeSlashCommands ? { nativeSlashCommands: true } : {}),
1766
+ ...(client === "claude" && nativeIntercept ? {
1767
+ nativeIntercept: true,
1768
+ nativeInterceptTimeoutMs: interceptTimeoutMs,
1769
+ } : {}),
1448
1770
  syncedAt: new Date(now).toISOString(),
1449
1771
  files: [...currentFiles].sort(),
1772
+ ...(client === "claude" && nativeSlashCommands ? { commands: [...currentCommands].sort() } : {}),
1450
1773
  profiles: [...currentProfiles].sort(),
1451
1774
  contentDigests,
1452
1775
  agents: rendered.map((agent) => ({
@@ -1458,7 +1781,9 @@ export function syncAgentProfile({
1458
1781
  retired: agent.retired,
1459
1782
  name: agent.name,
1460
1783
  ...(client === "claude" ? {
1461
- launchDefinition: agent.launchDefinition,
1784
+ ...(agent.parentLaunchDefinition
1785
+ ? { launchMode: "parent-direct", parentLaunchDefinition: agent.parentLaunchDefinition }
1786
+ : { launchDefinition: agent.launchDefinition }),
1462
1787
  } : {}),
1463
1788
  ...(client === "codex" && directProfiles ? {
1464
1789
  profileName: agent.profileName,
@@ -1474,6 +1799,7 @@ export function syncAgentProfile({
1474
1799
  count: active.length,
1475
1800
  recoveryCount: retired.length,
1476
1801
  files: [...currentFiles],
1802
+ ...(client === "claude" && nativeSlashCommands ? { commands: [...currentCommands] } : {}),
1477
1803
  profiles: [...currentProfiles],
1478
1804
  };
1479
1805
  }
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