impel-cli 0.20.36 → 0.20.38

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/README.md CHANGED
@@ -180,24 +180,35 @@ impel claude --model opus
180
180
  impel codex exec "review this repository"
181
181
  ```
182
182
 
183
- To run one explicitly selected tenant agent in a single Codex thread, select
183
+ To run one explicitly selected tenant agent in a single native thread, select
184
184
  it by exact agent ID (preferred) or unique exact title:
185
185
 
186
186
  ```sh
187
+ impel claude --agent research-agent --print "answer this question"
187
188
  impel codex --agent research-agent exec "answer this question"
188
189
  ```
189
190
 
190
- This path loads a generated, tenant-bound profile with the ultra-fast Codex
191
- Spark model at low reasoning for the thin adapter turns, a read-only local
192
- sandbox, the exact fixed MCP binding, and only that binding's answer/run,
193
- resume, and recovery tools. It omits unrelated local repository, app,
194
- collaboration, permission, environment, and skill-catalog prompt context; the
195
- selected Eve agent still performs the substantive work. The path also omits
196
- the normal parent specialist instructions, so there is no parent spawn, parent
197
- wait, or relay turn. Model, profile, developer-instruction, MCP, approval,
198
- sandbox, and feature overrides are rejected with `--agent`. The selected
199
- tenant and profile integrity must match before Codex starts. Ordinary `impel
200
- codex` passthrough is unchanged.
191
+ Both managed paths verify the selected tenant manifest and generated adapter
192
+ bytes, resolve the canonical native agent name, and omit the normal parent
193
+ specialist instructions. Claude Code receives the explicit 2.1.220 opt-in and
194
+ session permission mode required to run only that adapter's fixed MCP
195
+ allowlist. Impel re-declares the verified adapter under a random per-launch
196
+ name and loads only the isolated user settings source, so project, user, plugin,
197
+ and fixed organization agent names cannot replace it. User-authored Claude
198
+ agents that are absent from the Impel manifest keep Claude's ordinary
199
+ permission behavior. Model, prompt, agent-definition, tool, MCP, settings,
200
+ plugin, worktree, and permission overrides are rejected for a verified managed
201
+ Claude selection.
202
+
203
+ Codex loads a generated, tenant-bound profile with the ultra-fast Codex Spark
204
+ model at low reasoning for the thin adapter turns, a read-only local sandbox,
205
+ the exact fixed MCP binding, and only that binding's answer/run, resume, and
206
+ recovery tools. It omits unrelated local repository, app, collaboration,
207
+ permission, environment, and skill-catalog prompt context; the selected Eve
208
+ agent still performs the substantive work. Model, profile,
209
+ developer-instruction, MCP, approval, sandbox, and feature overrides are
210
+ rejected for its managed path. Ordinary `impel claude`, user-authored Claude
211
+ agent, and `impel codex` passthrough are unchanged.
201
212
 
202
213
  ## Remote Fargate sessions
203
214
 
@@ -518,6 +529,7 @@ must remain explicit and destructive-state aware.
518
529
  impel setup
519
530
  impel update
520
531
  impel claude
532
+ impel claude --agent <id|exact-title> ...
521
533
  impel codex
522
534
  impel codex --agent <id|exact-title> ...
523
535
  impel tenant list|current|use
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.38 — Restore managed Claude agent permissions
4
+
5
+ - Verifies an exact synchronized Claude agent and its generated file digest
6
+ before enabling Claude Code 2.1.220's required bypass-permission opt-in and
7
+ session mode for that fixed MCP-only adapter.
8
+ - Resolves exact agent IDs and titles to a byte-verified definition, declares
9
+ it under a random per-launch name with user-only setting sources, omits the
10
+ parent delegation prompt, and rejects model, prompt, tool, MCP, settings,
11
+ plugin, worktree, and permission overrides on the managed path.
12
+ - Leaves ordinary Claude launches, user-authored Claude agents, and every
13
+ Codex launch path unchanged.
14
+
15
+ ## 0.20.37 — Resume ambiguous native answers
16
+
17
+ - Retries Next's typed ambiguous direct-answer response with the exact persisted
18
+ idempotency key, resuming the existing Eve answer instead of duplicating it.
19
+ - Keeps retries bounded and cancellation-safe while rejecting incomplete or
20
+ untyped retry claims as definitive failures.
21
+ - Preserves strict continuation handles for answer-only adapters and full run
22
+ handles for recovery-only adapters across every nonterminal resume.
23
+
3
24
  ## 0.20.36 — Restore Codex execution
4
25
 
5
26
  - Keeps Codex 0.147's stable code-mode host enabled instead of overriding it
@@ -6,7 +6,7 @@ is treated as an eager host.
6
6
 
7
7
  | Surface | Pinned build | Exact selected tools | Selected path | Host behavior |
8
8
  | --- | --- | --- | --- | --- |
9
- | Claude Code | 2.1.220 | Yes, agent frontmatter `tools` | Eager | The bound run/answer, resume, and recovery tools are direct child tools. No catalog discovery or code-cell polling is required. |
9
+ | Claude Code | 2.1.220 | Yes, agent frontmatter `tools` | Eager; optional single-thread selection | The bound run/answer, resume, and recovery tools are direct child tools. `impel claude --agent` verifies the exact managed adapter before enabling 2.1.220's bypass-permission opt-in and mode. |
10
10
  | Claude Desktop | 1.26832.0 (embedded Code 2.1.222) | Yes, agent frontmatter `tools` | Eager | Same direct-tool contract as Claude Code. |
11
11
  | Codex CLI through `impel codex` | 0.147.0 | Yes, with `features.code_mode.direct_only_tool_namespaces = ["mcp__impel_agent"]` | Eager; optional single-thread profile | Existing `@Agent` children call the fixed MCP namespace directly. `impel codex --agent` also removes the parent spawn/wait/relay thread. |
12
12
  | ChatGPT desktop Codex | 26.803.41515 (embedded Codex 0.147.0-alpha.6.5) | Yes, with the same exact native namespace | Eager child, compatible parent | Keeps the app's supported `@Agent` parent/child route, but the child no longer needs code cells or code-mode waits. |
@@ -16,6 +16,17 @@ is treated as an eager host.
16
16
  - Eager profiles expose only the exact fixed binding's answer/run, resume, and
17
17
  recovery tools. `IMPEL_NATIVE_EAGER_TRANSPORT=0` independently restores the
18
18
  legacy host-visible tool configuration without deleting profiles or runs.
19
+ - A top-level Claude selection receives `--allow-dangerously-skip-permissions`
20
+ plus `--permission-mode bypassPermissions` only after its manifest record,
21
+ canonical definition, fixed MCP binding, and generated file bytes have been
22
+ verified. User-authored agents keep native Claude behavior.
23
+ - The verified definition is passed through `--agents` under a random
24
+ per-launch name while `--setting-sources user` excludes project/local
25
+ definitions. This outranks user and plugin names and makes collisions with a
26
+ fixed organization-managed agent name impractical.
27
+ - Verified Claude selections reject model, prompt, agent-definition, tool,
28
+ MCP, settings, plugin, worktree, and permission-policy overrides before
29
+ native launch.
19
30
  - Every local composite keeps one upstream MCP session for its process lifetime,
20
31
  validates the exact binding once, and persists the authoritative run handle
21
32
  before ambiguous retries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.36",
3
+ "version": "0.20.38",
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
@@ -17,6 +17,7 @@ import {
17
17
  } from "./config.js";
18
18
  import {
19
19
  CODEX_NATIVE_AGENT_ATTACHMENT_WINDOW_MS,
20
+ IMPEL_MANAGED_MCP_ENV,
20
21
  IMPEL_NATIVE_AGENT_ATTACHMENT_WINDOW_ENV,
21
22
  IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES,
22
23
  IMPEL_NATIVE_AGENT_MCP_TARGET,
@@ -55,7 +56,7 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
55
56
  export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
56
57
  export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
57
58
  export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
58
- export const MANAGED_AGENT_MANIFEST_VERSION = 19;
59
+ export const MANAGED_AGENT_MANIFEST_VERSION = 20;
59
60
 
60
61
  // The host model only selects the fixed MCP tool and faithfully returns its
61
62
  // result. Spark minimizes those transport-only turns while the selected Eve
@@ -82,6 +83,7 @@ const TERMINAL_NATIVE_AGENT_STATUSES = new Set(["succeeded", "failed", "cancelle
82
83
  const SAFE_AGENT_ID_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
83
84
  const SAFE_SCOPE_PARAM_RE = /^[A-Za-z0-9_.:-]{1,160}$/u;
84
85
  const MAX_CATALOG_ITEMS = 500;
86
+ const MAX_CLAUDE_INLINE_AGENT_JSON_BYTES = 12 * 1024;
85
87
  const RETRYABLE_AGENT_CATALOG_ERROR = /request timed out|fetch failed|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network error/iu;
86
88
  const RESERVED_AGENT_NAMES = Object.freeze({
87
89
  claude: new Set(["explore", "general-purpose", "plan"]),
@@ -696,7 +698,7 @@ function retiredAdapterInstructions(tenantId, agent) {
696
698
  `You are a recovery-only transport adapter for retired Impel native-agent binding ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
697
699
  `Collaboration tools are unavailable for this adapter. Never call send_message, followup_task, interrupt_agent, spawn_agent, or wait_agent. Return one final response to the caller only after the native-agent transport reaches terminal.`,
698
700
  `You cannot start new work. Never call run_native_agent and never perform or recreate the assigned task yourself.`,
699
- `Call ${nativeToolName(NATIVE_AGENT_RECOVER_TOOL)} with {} to obtain integrity-validated pending handles for this fixed binding, then call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with the intended handle until it returns a terminal result. If multiple handles cannot be safely associated with the request, report them instead of choosing.`,
701
+ `Call ${nativeToolName(NATIVE_AGENT_RECOVER_TOOL)} with {} to obtain integrity-validated ${JSON.stringify("impel.native-agent-run.v1")} pending handles for this fixed binding, then call ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} with the intended complete handle. Every nonterminal resume returns the same handle schema; pass that complete returned handle unchanged to the next resume until terminal. If multiple handles cannot be safely associated with the request, report them instead of choosing.`,
700
702
  `Return successful finalText byte-for-byte. For failure, return the preserved runId, output, and error without inventing a replacement result.`,
701
703
  ].join(" ");
702
704
  }
@@ -711,44 +713,73 @@ function customAgentDescription(tenantId, agent) {
711
713
  ).slice(0, 900);
712
714
  }
713
715
 
714
- function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = false }) {
716
+ function claudeAgentDefinition({ tenantId, agent, invocation, recoveryOnly = false }) {
715
717
  const description = recoveryOnly
716
718
  ? `Recovery-only access to pending runs for retired Impel binding ${agent.agentId} in tenant ${tenantId}.`
717
719
  : customAgentDescription(tenantId, agent);
718
720
  const toolNames = recoveryOnly ? NATIVE_AGENT_RECOVERY_TOOL_NAMES : nativeAgentToolNames(agent);
719
721
  const eager = eagerNativeAgentTransportEnabled();
722
+ return {
723
+ description,
724
+ prompt: recoveryOnly ? retiredAdapterInstructions(tenantId, agent) : claudeAdapterInstructions(tenantId, agent),
725
+ model: "haiku",
726
+ ...(eager ? {
727
+ permissionMode: "bypassPermissions",
728
+ tools: toolNames.map((tool) => nativeToolName(tool)),
729
+ } : {}),
730
+ mcpServers: [{
731
+ [MANAGED_AGENT_MCP_SERVER]: {
732
+ type: "stdio",
733
+ command: invocation.command,
734
+ args: [...invocation.args],
735
+ env: { ...(invocation.env || {}) },
736
+ },
737
+ }],
738
+ };
739
+ }
740
+
741
+ function renderClaudeAgentDefinition(name, definition) {
742
+ const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
720
743
  const lines = [
721
744
  "---",
722
745
  `name: ${JSON.stringify(name)}`,
723
- `description: ${JSON.stringify(description)}`,
724
- "model: haiku",
725
- ...(eager
746
+ `description: ${JSON.stringify(definition.description)}`,
747
+ `model: ${definition.model}`,
748
+ ...(definition.permissionMode
726
749
  ? [
727
750
  // This agent receives only the exact fixed-binding MCP allowlist below,
728
751
  // so bypassing prompts cannot grant filesystem, shell, connector, or
729
752
  // arbitrary MCP access. It lets non-interactive/dontAsk hosts execute
730
753
  // the selected managed adapter instead of auto-denying its sole call.
731
- "permissionMode: bypassPermissions",
754
+ `permissionMode: ${definition.permissionMode}`,
732
755
  "tools:",
733
- ...toolNames.map((tool) => ` - ${JSON.stringify(nativeToolName(tool))}`),
756
+ ...definition.tools.map((tool) => ` - ${JSON.stringify(tool)}`),
734
757
  ]
735
758
  : []),
736
759
  "mcpServers:",
737
760
  ` - ${MANAGED_AGENT_MCP_SERVER}:`,
738
761
  " type: stdio",
739
- ` command: ${JSON.stringify(invocation.command)}`,
762
+ ` command: ${JSON.stringify(server.command)}`,
740
763
  " args:",
741
- ...invocation.args.map((argument) => ` - ${JSON.stringify(argument)}`),
764
+ ...server.args.map((argument) => ` - ${JSON.stringify(argument)}`),
742
765
  " env:",
743
- ...Object.entries(invocation.env || {}).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`),
766
+ ...Object.entries(server.env).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`),
744
767
  "---",
745
768
  "",
746
- recoveryOnly ? retiredAdapterInstructions(tenantId, agent) : claudeAdapterInstructions(tenantId, agent),
769
+ definition.prompt,
747
770
  "",
748
771
  ];
749
772
  return lines.join("\n");
750
773
  }
751
774
 
775
+ function renderClaudeAgent({ tenantId, agent, name, invocation, recoveryOnly = false }) {
776
+ const definition = claudeAgentDefinition({ tenantId, agent, invocation, recoveryOnly });
777
+ return {
778
+ contents: renderClaudeAgentDefinition(name, definition),
779
+ launchDefinition: definition,
780
+ };
781
+ }
782
+
752
783
  function codexInvocationEnvironment(invocation, { durableProfile = false } = {}) {
753
784
  const entries = Object.entries(invocation.env || {});
754
785
  const transient = new Set(IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES);
@@ -871,9 +902,11 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
871
902
  // collision-safe, filesystem-safe identifiers, so use them as filenames.
872
903
  const fileStem = client === "claude" ? name : fileStems[index];
873
904
  const boundInvocation = boundNativeAgentInvocation(normalizedTenant, agent, invocation);
874
- const contents = client === "claude"
905
+ const claudeRendered = client === "claude"
875
906
  ? renderClaudeAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation })
876
- : renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode });
907
+ : null;
908
+ const contents = claudeRendered?.contents
909
+ ?? renderCodexAgent({ tenantId: normalizedTenant, agent, name, invocation: boundInvocation, directCodeMode });
877
910
  const profileName = client === "codex" ? fileStem : null;
878
911
  return {
879
912
  agentId: agent.agentId,
@@ -885,6 +918,7 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
885
918
  name,
886
919
  fileName: `${fileStem}${extension}`,
887
920
  contents,
921
+ ...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
888
922
  ...(profileName ? {
889
923
  profileName,
890
924
  profileFileName: `${profileName}.config.toml`,
@@ -939,9 +973,11 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
939
973
  policyFingerprint: binding.policyFingerprint,
940
974
  mode: "recovery",
941
975
  });
942
- const contents = client === "claude"
976
+ const claudeRendered = client === "claude"
943
977
  ? renderClaudeAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true })
944
- : renderCodexAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true, directCodeMode });
978
+ : null;
979
+ const contents = claudeRendered?.contents
980
+ ?? renderCodexAgent({ tenantId, agent, name, invocation: boundInvocation, recoveryOnly: true, directCodeMode });
945
981
  const profileName = client === "codex" ? path.basename(fileName, extension) : null;
946
982
  return {
947
983
  agentId: binding.agentId,
@@ -953,6 +989,7 @@ function renderRetiredManagedAgents(client, tenantId, bindings, active, invocati
953
989
  name,
954
990
  fileName,
955
991
  contents,
992
+ ...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
956
993
  ...(profileName ? {
957
994
  profileName,
958
995
  profileFileName: `${profileName}.config.toml`,
@@ -999,7 +1036,8 @@ function managedManifestPath(root) {
999
1036
  }
1000
1037
 
1001
1038
  function contentDigest(contents) {
1002
- return crypto.createHash("sha256").update(contents, "utf8").digest("hex");
1039
+ const bytes = Buffer.isBuffer(contents) ? contents : Buffer.from(contents, "utf8");
1040
+ return crypto.createHash("sha256").update(bytes).digest("hex");
1003
1041
  }
1004
1042
 
1005
1043
  function managedArtifactIsCurrent(root, relativePath, expectedDigest) {
@@ -1009,8 +1047,9 @@ function managedArtifactIsCurrent(root, relativePath, expectedDigest) {
1009
1047
  if (!/^[a-f0-9]{64}$/u.test(expectedDigest || "")) return false;
1010
1048
  const artifactPath = path.join(root, ...segments);
1011
1049
  try {
1012
- if (fs.lstatSync(artifactPath).isSymbolicLink()) return false;
1013
- return contentDigest(fs.readFileSync(artifactPath, "utf8")) === expectedDigest;
1050
+ const stat = fs.lstatSync(artifactPath);
1051
+ if (stat.isSymbolicLink() || !stat.isFile()) return false;
1052
+ return contentDigest(fs.readFileSync(artifactPath)) === expectedDigest;
1014
1053
  } catch {
1015
1054
  return false;
1016
1055
  }
@@ -1033,6 +1072,156 @@ export function readManagedAgentManifest(root) {
1033
1072
  return readManifest(managedManifestPath(root));
1034
1073
  }
1035
1074
 
1075
+ function exactObjectKeys(value, keys) {
1076
+ return value
1077
+ && typeof value === "object"
1078
+ && !Array.isArray(value)
1079
+ && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
1080
+ }
1081
+
1082
+ function validateClaudeLaunchDefinition(record, tenantId) {
1083
+ const definition = record.launchDefinition;
1084
+ if (!exactObjectKeys(definition, [
1085
+ "description", "prompt", "model", "permissionMode", "tools", "mcpServers",
1086
+ ])
1087
+ || typeof definition.description !== "string"
1088
+ || !definition.description
1089
+ || definition.description.length > 900
1090
+ || typeof definition.prompt !== "string"
1091
+ || !definition.prompt
1092
+ || Buffer.byteLength(definition.prompt, "utf8") > 256 * 1024
1093
+ || definition.model !== "haiku"
1094
+ || definition.permissionMode !== "bypassPermissions"
1095
+ || !Array.isArray(definition.tools)
1096
+ || definition.tools.length < 2
1097
+ || definition.tools.length > 3
1098
+ || new Set(definition.tools).size !== definition.tools.length
1099
+ || !definition.tools.every((tool) => [
1100
+ NATIVE_AGENT_ANSWER_TOOL,
1101
+ NATIVE_AGENT_RUN_TOOL,
1102
+ NATIVE_AGENT_RESUME_TOOL,
1103
+ NATIVE_AGENT_RECOVER_TOOL,
1104
+ ].map((name) => nativeToolName(name)).includes(tool))
1105
+ || !Array.isArray(definition.mcpServers)
1106
+ || definition.mcpServers.length !== 1
1107
+ || !exactObjectKeys(definition.mcpServers[0], [MANAGED_AGENT_MCP_SERVER])) {
1108
+ throw new Error("the managed Claude launch definition is invalid; run `impel agents sync claude`");
1109
+ }
1110
+ const server = definition.mcpServers[0][MANAGED_AGENT_MCP_SERVER];
1111
+ const allowedEnvironment = new Set([IMPEL_MANAGED_MCP_ENV, ...IMPEL_NATIVE_AGENT_TELEMETRY_ENV_NAMES]);
1112
+ if (!exactObjectKeys(server, ["type", "command", "args", "env"])
1113
+ || server.type !== "stdio"
1114
+ || typeof server.command !== "string"
1115
+ || !server.command
1116
+ || !Array.isArray(server.args)
1117
+ || !server.args.every((argument) => typeof argument === "string")
1118
+ || !server.env
1119
+ || typeof server.env !== "object"
1120
+ || Array.isArray(server.env)
1121
+ || server.env[IMPEL_MANAGED_MCP_ENV] !== "1"
1122
+ || !Object.entries(server.env).every(([key, value]) =>
1123
+ allowedEnvironment.has(key)
1124
+ && typeof value === "string"
1125
+ && value.length <= 4096
1126
+ && redactCredentialText(value) === value
1127
+ )) {
1128
+ throw new Error("the managed Claude MCP definition is invalid; run `impel agents sync claude`");
1129
+ }
1130
+ const answerOnly = server.args.at(-1) === "--answer-only";
1131
+ const trustedInvocation = impelNativeAgentMcpInvocation({
1132
+ tenantId,
1133
+ agentId: record.agentId,
1134
+ scopeParam: record.scopeParam,
1135
+ policyFingerprint: record.policyFingerprint,
1136
+ mode: answerOnly ? "answer" : "durable",
1137
+ });
1138
+ const expectedTools = (answerOnly
1139
+ ? [NATIVE_AGENT_ANSWER_TOOL, NATIVE_AGENT_RESUME_TOOL]
1140
+ : NATIVE_AGENT_TOOL_NAMES
1141
+ ).map((name) => nativeToolName(name));
1142
+ if (server.command !== trustedInvocation.command
1143
+ || JSON.stringify(server.args) !== JSON.stringify(trustedInvocation.args)
1144
+ || JSON.stringify(definition.tools) !== JSON.stringify(expectedTools)) {
1145
+ throw new Error("the managed Claude binding is invalid; run `impel agents sync claude`");
1146
+ }
1147
+ return definition;
1148
+ }
1149
+
1150
+ // A selector absent from the Impel manifest remains Claude-native. A matching
1151
+ // record is integrity checked, then re-declared under a random per-launch name
1152
+ // through Claude's higher-priority --agents source. This prevents project,
1153
+ // user, plugin, and fixed managed-settings names from replacing the adapter.
1154
+ export function resolveManagedClaudeAgent(root, tenantId, selector) {
1155
+ const normalizedTenant = normalizeTenantId(tenantId);
1156
+ if (typeof selector !== "string" || !selector || selector.length > 512) return null;
1157
+ const manifest = readManagedAgentManifest(root);
1158
+ if (!manifest
1159
+ || manifest.version !== MANAGED_AGENT_MANIFEST_VERSION
1160
+ || manifest.client !== "claude"
1161
+ || manifest.tenantId !== normalizedTenant
1162
+ || !Array.isArray(manifest.agents)) {
1163
+ return null;
1164
+ }
1165
+ const records = manifest.agents.filter((record) => record
1166
+ && typeof record === "object"
1167
+ && typeof record.name === "string");
1168
+ const active = records.filter((record) => !record.retired);
1169
+ let matches = active.filter((record) => record.agentId === selector);
1170
+ if (matches.length === 0) matches = active.filter((record) => record.title === selector);
1171
+ if (matches.length === 0) matches = active.filter((record) => record.name === selector);
1172
+ if (matches.length === 0 && records.some((record) => record.retired && [
1173
+ record.agentId,
1174
+ record.title,
1175
+ record.name,
1176
+ ].includes(selector))) {
1177
+ throw new Error(`managed Claude agent ${JSON.stringify(selector)} is retired and cannot start new work`);
1178
+ }
1179
+ if (matches.length === 0) return null;
1180
+ if (matches.length !== 1) {
1181
+ throw new Error(
1182
+ `managed Claude agent ${JSON.stringify(selector)} is ambiguous for tenant ${JSON.stringify(normalizedTenant)}`,
1183
+ );
1184
+ }
1185
+ const [record] = matches;
1186
+ if (typeof record.agentId !== "string"
1187
+ || !SAFE_AGENT_ID_RE.test(record.agentId)
1188
+ || typeof record.scopeParam !== "string"
1189
+ || !SAFE_SCOPE_PARAM_RE.test(record.scopeParam)
1190
+ || !/^[a-f0-9]{64}$/u.test(record.policyFingerprint || "")) {
1191
+ throw new Error("the managed Claude binding metadata is invalid; run `impel agents sync claude`");
1192
+ }
1193
+ const fileName = `${record.name}.md`;
1194
+ if (path.basename(fileName) !== fileName || !manifest.files.includes(fileName)) {
1195
+ throw new Error("the managed Claude agent mapping is invalid; run `impel agents sync claude`");
1196
+ }
1197
+ const relativePath = `agents/${fileName}`;
1198
+ if (!managedArtifactIsCurrent(root, relativePath, manifest.contentDigests?.[relativePath])) {
1199
+ throw new Error("the managed Claude agent failed its integrity check; run `impel agents sync claude`");
1200
+ }
1201
+ const launchDefinition = validateClaudeLaunchDefinition(record, normalizedTenant);
1202
+ if (contentDigest(renderClaudeAgentDefinition(record.name, launchDefinition))
1203
+ !== manifest.contentDigests?.[relativePath]) {
1204
+ throw new Error("the managed Claude launch definition does not match its agent file; run `impel agents sync claude`");
1205
+ }
1206
+ const suffix = crypto.randomBytes(12).toString("hex");
1207
+ const prefix = `impel-${record.name}`.slice(0, 63 - suffix.length - 1).replace(/-+$/u, "");
1208
+ const launchName = `${prefix}-${suffix}`;
1209
+ const launchAgentsJson = JSON.stringify({ [launchName]: launchDefinition });
1210
+ if (Buffer.byteLength(launchAgentsJson, "utf8") > MAX_CLAUDE_INLINE_AGENT_JSON_BYTES) {
1211
+ throw new Error(
1212
+ `managed Claude agent ${JSON.stringify(selector)} is too large for a safe cross-platform direct launch; shorten its synchronized catalog policy`,
1213
+ );
1214
+ }
1215
+ return {
1216
+ ...record,
1217
+ fileName,
1218
+ path: path.join(root, "agents", fileName),
1219
+ launchName,
1220
+ launchDefinition,
1221
+ launchAgentsJson,
1222
+ };
1223
+ }
1224
+
1036
1225
  export function resolveManagedCodexAgentProfile(root, tenantId, selector) {
1037
1226
  const normalizedTenant = normalizeTenantId(tenantId);
1038
1227
  const normalizedSelector = boundedString(selector, "agent selector", { max: 512 });
@@ -1236,6 +1425,9 @@ export function syncAgentProfile({
1236
1425
  policyFingerprint: agent.policyFingerprint,
1237
1426
  retired: agent.retired,
1238
1427
  name: agent.name,
1428
+ ...(client === "claude" ? {
1429
+ launchDefinition: agent.launchDefinition,
1430
+ } : {}),
1239
1431
  ...(client === "codex" && directProfiles ? {
1240
1432
  profileName: agent.profileName,
1241
1433
  profileFileName: agent.profileFileName,
package/src/cli.js CHANGED
@@ -34,6 +34,7 @@ Get started:
34
34
 
35
35
  Work:
36
36
  impel claude [args...] Launch Claude Code with an isolated Impel profile
37
+ impel claude --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
37
38
  impel codex [args...] Launch Codex with an isolated Impel profile
38
39
  impel codex --agent <id|exact-title> ... Run one fixed tenant agent without a parent hop
39
40
  impel remote handoff|dispatch|handback Move or control provider-native sessions remotely
@@ -6,7 +6,11 @@ import {
6
6
  ensureImpelClaudeProfile,
7
7
  ensureImpelCodexProfile,
8
8
  } from "../cliProfiles.js";
9
- import { resolveManagedCodexAgentProfile, syncAgentProfilesSafe } from "../agents.js";
9
+ import {
10
+ resolveManagedClaudeAgent,
11
+ resolveManagedCodexAgentProfile,
12
+ syncAgentProfilesSafe,
13
+ } from "../agents.js";
10
14
  import {
11
15
  crossAppModelsEnabled,
12
16
  loadConfig,
@@ -77,10 +81,43 @@ const IMPEL_CODEX_RUNTIME_OVERRIDES = [
77
81
  ];
78
82
 
79
83
  const IMPEL_CODEX_UNRESTRICTED_ARGUMENT = "--dangerously-bypass-approvals-and-sandbox";
84
+ const IMPEL_CLAUDE_MANAGED_AGENT_ARGUMENTS = [
85
+ "--allow-dangerously-skip-permissions",
86
+ "--permission-mode",
87
+ "bypassPermissions",
88
+ // Pinned Claude 2.1.220 treats agents, skills, commands, and settings as
89
+ // source-scoped. Load only the isolated user profile so project/local agent
90
+ // definitions cannot replace the integrity-verified adapter after launch.
91
+ "--setting-sources",
92
+ "user",
93
+ ];
80
94
 
81
- export function impelLaunchArguments(tool, argv, { codexAgentProfile = null } = {}) {
95
+ export function impelLaunchArguments(tool, argv, {
96
+ claudeManagedAgent = null,
97
+ codexAgentProfile = null,
98
+ } = {}) {
82
99
  if (!RUNTIME_BRAND.features.agents) return [...argv];
83
100
  if (tool === "claude") {
101
+ if (claudeManagedAgent) {
102
+ const { launchName, launchAgentsJson } = claudeManagedAgent;
103
+ if (typeof launchName !== "string"
104
+ || !launchName
105
+ || typeof launchAgentsJson !== "string"
106
+ || !launchAgentsJson) {
107
+ throw new Error("managed Claude launch configuration is invalid");
108
+ }
109
+ if (Buffer.byteLength(launchAgentsJson, "utf8") > 12 * 1024) {
110
+ throw new Error("managed Claude launch configuration is too large");
111
+ }
112
+ return [
113
+ ...IMPEL_CLAUDE_MANAGED_AGENT_ARGUMENTS,
114
+ "--agents",
115
+ launchAgentsJson,
116
+ "--agent",
117
+ launchName,
118
+ ...argv,
119
+ ];
120
+ }
84
121
  return ["--append-system-prompt", IMPEL_CLAUDE_PARENT_DELEGATION_INSTRUCTIONS, ...argv];
85
122
  }
86
123
  if (tool === "codex") {
@@ -99,6 +136,113 @@ export function impelLaunchArguments(tool, argv, { codexAgentProfile = null } =
99
136
  throw new Error(`unsupported CLI launcher: ${tool}`);
100
137
  }
101
138
 
139
+ const CLAUDE_MANAGED_AGENT_LOCKED_OPTIONS = new Set([
140
+ "--add-dir",
141
+ "--agents",
142
+ "--allow-tools",
143
+ "--allow-dangerously-skip-permissions",
144
+ "--allowedTools",
145
+ "--allowed-tools",
146
+ "--append-subagent-system-prompt",
147
+ "--append-system-prompt",
148
+ "--append-system-prompt-file",
149
+ "--bare",
150
+ "--brief",
151
+ "--chrome",
152
+ "--dangerously-skip-permissions",
153
+ "--disable-slash-commands",
154
+ "--disallowedTools",
155
+ "--disallowed-tools",
156
+ "--effort",
157
+ "--exclude-dynamic-system-prompt-sections",
158
+ "--fallback-model",
159
+ "--file",
160
+ "--ide",
161
+ "--managed-settings",
162
+ "--mcp-config",
163
+ "--model",
164
+ "--no-chrome",
165
+ "--permission-mode",
166
+ "--permission-prompt-tool",
167
+ "--plugin-dir",
168
+ "--plugin-dir-no-mcp",
169
+ "--plugin-url",
170
+ "--safe-mode",
171
+ "--setting-sources",
172
+ "--settings",
173
+ "--strict-mcp-config",
174
+ "--system-prompt",
175
+ "--system-prompt-file",
176
+ "--tmux",
177
+ "--tools",
178
+ "--worktree",
179
+ ]);
180
+
181
+ function claudeManagedAgentLockedOption(argument) {
182
+ if (argument === "-w" || (argument.startsWith("-w") && argument.length > 2)) return "-w";
183
+ if (CLAUDE_MANAGED_AGENT_LOCKED_OPTIONS.has(argument)) return argument;
184
+ return [...CLAUDE_MANAGED_AGENT_LOCKED_OPTIONS].find((option) =>
185
+ argument.startsWith(`${option}=`)
186
+ ) || null;
187
+ }
188
+
189
+ export function parseClaudeAgentLaunchArguments(argv) {
190
+ const passthrough = [];
191
+ const selectors = [];
192
+ let literal = false;
193
+ for (let index = 0; index < argv.length; index += 1) {
194
+ const argument = argv[index];
195
+ if (literal) {
196
+ passthrough.push(argument);
197
+ continue;
198
+ }
199
+ if (argument === "--") {
200
+ literal = true;
201
+ passthrough.push(argument);
202
+ continue;
203
+ }
204
+ if (argument === "--agent") {
205
+ const value = argv[index += 1];
206
+ selectors.push(typeof value === "string" && value && !value.startsWith("-") ? value : null);
207
+ continue;
208
+ }
209
+ if (argument.startsWith("--agent=")) {
210
+ const value = argument.slice("--agent=".length);
211
+ selectors.push(value && !value.startsWith("-") ? value : null);
212
+ continue;
213
+ }
214
+ passthrough.push(argument);
215
+ }
216
+ const selector = selectors.length === 1 ? selectors[0] : null;
217
+ return {
218
+ selector,
219
+ argv: selector === null ? [...argv] : passthrough,
220
+ };
221
+ }
222
+
223
+ export function assertManagedClaudeAgentLaunchArguments(argv) {
224
+ let literal = false;
225
+ for (let index = 0; index < argv.length; index += 1) {
226
+ const argument = argv[index];
227
+ if (literal) continue;
228
+ if (argument === "--") {
229
+ literal = true;
230
+ continue;
231
+ }
232
+ if (argument === "--agent") {
233
+ index += 1;
234
+ continue;
235
+ }
236
+ if (argument.startsWith("--agent=")) continue;
237
+ const locked = claudeManagedAgentLockedOption(argument);
238
+ if (locked) {
239
+ throw new Error(
240
+ `${locked} cannot be used with a verified managed Claude agent; Impel locks model, instructions, tools, MCP, settings, and permission policy`,
241
+ );
242
+ }
243
+ }
244
+ }
245
+
102
246
  const CODEX_AGENT_LOCKED_OPTIONS = new Set([
103
247
  "-c",
104
248
  "--config",
@@ -264,8 +408,11 @@ function runNativeCli(tool, argv, environment) {
264
408
 
265
409
  export async function cmdLaunch(tool, argv) {
266
410
  let nativeArgv = [...argv];
411
+ let claudeAgentSelector = null;
267
412
  let codexAgentSelector = null;
268
- if (tool === "codex") {
413
+ if (tool === "claude" && RUNTIME_BRAND.features.agents) {
414
+ ({ selector: claudeAgentSelector, argv: nativeArgv } = parseClaudeAgentLaunchArguments(argv));
415
+ } else if (tool === "codex") {
269
416
  try {
270
417
  ({ selector: codexAgentSelector, argv: nativeArgv } = parseCodexAgentLaunchArguments(argv));
271
418
  } catch (error) {
@@ -356,6 +503,29 @@ export async function cmdLaunch(tool, argv) {
356
503
  });
357
504
  }
358
505
 
506
+ let claudeManagedAgent = null;
507
+ if (RUNTIME_BRAND.features.agents && claudeAgentSelector !== null) {
508
+ try {
509
+ claudeManagedAgent = resolveManagedClaudeAgent(
510
+ agentProfile.root,
511
+ tenantId,
512
+ claudeAgentSelector,
513
+ );
514
+ if (claudeManagedAgent) {
515
+ assertManagedClaudeAgentLaunchArguments(nativeArgv);
516
+ } else {
517
+ // `--agent` is also Claude's native user-agent selector. If the exact
518
+ // value is not in the integrity-tracked Impel manifest, preserve the
519
+ // user's original argv and ordinary permission behavior byte-for-byte.
520
+ nativeArgv = [...argv];
521
+ }
522
+ } catch (error) {
523
+ console.error(`impel claude: ${redactSecretText(error?.message || error)}`);
524
+ process.exitCode = 1;
525
+ return;
526
+ }
527
+ }
528
+
359
529
  let codexAgentProfile = null;
360
530
  if (codexAgentSelector !== null) {
361
531
  try {
@@ -377,7 +547,10 @@ export async function cmdLaunch(tool, argv) {
377
547
  const { env: launchEnvironment } = withGitEnvironment(environment, { baseEnvironment: environment });
378
548
  const exitCode = await runNativeCli(
379
549
  tool,
380
- impelLaunchArguments(tool, nativeArgv, { codexAgentProfile }),
550
+ impelLaunchArguments(tool, nativeArgv, {
551
+ claudeManagedAgent,
552
+ codexAgentProfile,
553
+ }),
381
554
  launchEnvironment,
382
555
  );
383
556
  if (exitCode !== 0) process.exitCode = exitCode;
@@ -65,6 +65,7 @@ const CIBI_START_IN_PROGRESS_MESSAGE = "The durable Eve drain workflow is still
65
65
  const CTOS_START_TIMEOUT_MESSAGE = `MCP tool call failed for ${NATIVE_AGENT_START_TOOL}: The operation was aborted due to timeout: mcp tool call failed`;
66
66
  const MANAGED_VIRTUAL_KEY_MISS_MESSAGE = "virtual key not found. The provided virtual key does not exist or has been revoked.";
67
67
  const MANAGED_VIRTUAL_KEY_MISS_RETRY_DELAY_MS = 250;
68
+ const NEXT_AMBIGUOUS_ANSWER_CODE = "native_agent_answer_ambiguous";
68
69
  const MCP_TOOL_RESULT_ERROR = Symbol("native-agent MCP tool result error");
69
70
  const TERMINAL_STATUSES = new Set(["succeeded", "failed", "cancelled", "canceled"]);
70
71
 
@@ -332,6 +333,7 @@ function readPrivateState(filePath, maxBytes) {
332
333
  "idempotencyKey",
333
334
  "startArguments",
334
335
  "upstreamRunId",
336
+ "answerRunId",
335
337
  "status",
336
338
  "startAttempts",
337
339
  "startMode",
@@ -365,6 +367,13 @@ function readPrivateState(filePath, maxBytes) {
365
367
  || (state.fence !== undefined && (!Number.isSafeInteger(state.fence) || state.fence < 0))
366
368
  || (state.sequence !== undefined && (!Number.isSafeInteger(state.sequence) || state.sequence < 0))
367
369
  || (state.version !== undefined && (!Number.isSafeInteger(state.version) || state.version < 0))
370
+ || (state.answerRunId !== undefined
371
+ && (state.startMode !== "answer"
372
+ || state.startAttempts < 1
373
+ || state.upstreamRunId !== null
374
+ || typeof state.answerRunId !== "string"
375
+ || !state.answerRunId.trim()
376
+ || state.answerRunId.length > 512))
368
377
  || (state.upstreamRunId !== null && (typeof state.upstreamRunId !== "string" || !state.upstreamRunId))) {
369
378
  throw new Error("invalid field");
370
379
  }
@@ -417,6 +426,22 @@ function continuationForState(state) {
417
426
  };
418
427
  }
419
428
 
429
+ function attachmentHandleForState(state) {
430
+ return state.startMode === "answer"
431
+ ? continuationForState(state)
432
+ : handleForState(state);
433
+ }
434
+
435
+ function resumedHandleForState(state, incomingHandle) {
436
+ // The strict MCP surfaces use disjoint handle families: answer-only accepts
437
+ // continuation.v1, while recovery-only accepts run.v1. Preserve the
438
+ // validated incoming family across every nonterminal resume so either
439
+ // surface can continue the same persisted direct-answer state.
440
+ return incomingHandle.schema === NATIVE_AGENT_HANDLE_SCHEMA
441
+ ? handleForState(state)
442
+ : attachmentHandleForState(state);
443
+ }
444
+
420
445
  function normalizeHandle(value) {
421
446
  if (value?.schema === NATIVE_AGENT_CONTINUATION_SCHEMA) {
422
447
  exactObject(value, ["schema", "invocationId"], "native-agent continuation");
@@ -642,6 +667,24 @@ function retryableManagedVirtualKeyMiss(error) {
642
667
  && error.message.includes(MANAGED_VIRTUAL_KEY_MISS_MESSAGE);
643
668
  }
644
669
 
670
+ function retryableNativeAgentAnswerRunId(error) {
671
+ const data = error?.data;
672
+ if (!(error instanceof NativeAgentUpstreamError)
673
+ || error.code !== -32000
674
+ || !data
675
+ || typeof data !== "object"
676
+ || Array.isArray(data)
677
+ || data.code !== NEXT_AMBIGUOUS_ANSWER_CODE
678
+ || data.retryable !== true
679
+ || data.sameIdempotencyKeyRequired !== true
680
+ || typeof data.runId !== "string"
681
+ || !data.runId.trim()
682
+ || data.runId.length > 512) {
683
+ return null;
684
+ }
685
+ return data.runId;
686
+ }
687
+
645
688
  export class NativeAgentUpstreamSession {
646
689
  constructor({
647
690
  gatewayUrl,
@@ -792,6 +835,14 @@ export class NativeAgentUpstreamSession {
792
835
  && error.message === CTOS_START_TIMEOUT_MESSAGE))) {
793
836
  error.ambiguous = true;
794
837
  }
838
+ if (name === NATIVE_AGENT_UPSTREAM_ANSWER_TOOL
839
+ && retryableNativeAgentAnswerRunId(error) !== null) {
840
+ // Next has confirmed that the direct-answer ledger remains live and
841
+ // that only an exact idempotent replay may resume it. Keep generic
842
+ // JSON-RPC errors definitive; this branch requires the complete
843
+ // typed resume contract.
844
+ error.ambiguous = true;
845
+ }
795
846
  throw error;
796
847
  }
797
848
  }
@@ -1826,7 +1877,7 @@ export class NativeAgentCompositeTransport {
1826
1877
  }, prepared.agent);
1827
1878
  state = await this.allocateState(startArguments, { startMode: "answer", signal: attachment.signal });
1828
1879
  lock = this.acquireLock(state.invocationId);
1829
- if (!lock) return handleForState(state);
1880
+ if (!lock) return continuationForState(state);
1830
1881
  const result = await this.attach(state, prepared, {
1831
1882
  signal: attachment.signal,
1832
1883
  lock,
@@ -1936,13 +1987,18 @@ export class NativeAgentCompositeTransport {
1936
1987
  const prepared = state.upstreamRunId
1937
1988
  ? await this.prepareExistingSession(attachment.signal)
1938
1989
  : await this.preparePersistedStartSession(attachment.signal, state);
1939
- return await this.attach(state, prepared, {
1990
+ const resumed = await this.attach(state, prepared, {
1940
1991
  signal: attachment.signal,
1941
1992
  onProgress,
1942
1993
  lock,
1943
1994
  });
1995
+ return resumed?.schema === NATIVE_AGENT_RESULT_SCHEMA
1996
+ ? resumed
1997
+ : resumedHandleForState(state, handle);
1944
1998
  } catch (error) {
1945
- if (error?.name === "AbortError") return handle;
1999
+ if (error?.name === "AbortError") {
2000
+ return validated ? resumedHandleForState(state, handle) : handle;
2001
+ }
1946
2002
  if (validated && !state?.upstreamRunId && error?.code === "LOCAL_POLICY_CHANGED_BEFORE_START") {
1947
2003
  this.compactTerminalFailure(
1948
2004
  state,
@@ -1959,7 +2015,7 @@ export class NativeAgentCompositeTransport {
1959
2015
  } catch {
1960
2016
  // Never write after losing ownership of the bounded lease.
1961
2017
  }
1962
- return handle;
2018
+ return resumedHandleForState(state, handle);
1963
2019
  }
1964
2020
  throw error;
1965
2021
  } finally {
@@ -2078,6 +2134,7 @@ export class NativeAgentCompositeTransport {
2078
2134
  }
2079
2135
  const runId = boundedString(payload.runId, "native-agent runId", 512);
2080
2136
  const next = structuredClone(state);
2137
+ delete next.answerRunId;
2081
2138
  next.upstreamRunId = runId;
2082
2139
  next.status = typeof payload.status === "string"
2083
2140
  && payload.status.length <= 160
@@ -2151,10 +2208,22 @@ export class NativeAgentCompositeTransport {
2151
2208
  break;
2152
2209
  } catch (error) {
2153
2210
  if (error?.name === "AbortError") throw error;
2211
+ const answerRunId = state.startMode === "answer"
2212
+ ? retryableNativeAgentAnswerRunId(error)
2213
+ : null;
2214
+ if (answerRunId !== null) {
2215
+ if (state.answerRunId && state.answerRunId !== answerRunId) {
2216
+ this.persistDiagnostic(state, new Error(
2217
+ "native-agent answer retry contract returned a different runId",
2218
+ ), lock);
2219
+ return attachmentHandleForState(state);
2220
+ }
2221
+ state.answerRunId = answerRunId;
2222
+ }
2154
2223
  if (!error?.ambiguous) {
2155
2224
  if (startOutcomeUncertain) {
2156
2225
  this.persistDiagnostic(state, error, lock);
2157
- return handleForState(state);
2226
+ return attachmentHandleForState(state);
2158
2227
  }
2159
2228
  this.compactTerminalFailure(
2160
2229
  state,
@@ -2169,7 +2238,15 @@ export class NativeAgentCompositeTransport {
2169
2238
  this.persistDiagnostic(state, error, lock);
2170
2239
  }
2171
2240
  }
2172
- if (!started) return handleForState(state);
2241
+ if (!started) return attachmentHandleForState(state);
2242
+ if (state.startMode === "answer"
2243
+ && state.answerRunId
2244
+ && started.runId !== state.answerRunId) {
2245
+ this.persistDiagnostic(state, new Error(
2246
+ "native-agent answer retry returned a different runId",
2247
+ ), lock);
2248
+ return attachmentHandleForState(state);
2249
+ }
2173
2250
 
2174
2251
  // Persist the upstream identity before accepting any optional or large
2175
2252
  // response fields. From this point every attachment failure is safely
@@ -2184,7 +2261,7 @@ export class NativeAgentCompositeTransport {
2184
2261
  for (let poll = 0; poll < this.maxPolls; poll += 1) {
2185
2262
  throwIfAborted(signal);
2186
2263
  const remainingMs = deadline - this.now();
2187
- if (remainingMs <= 1_500) return handleForState(state);
2264
+ if (remainingMs <= 1_500) return attachmentHandleForState(state);
2188
2265
  const boundedWait = Math.max(1, Math.min(waitSeconds, Math.floor((remainingMs - 1_000) / 1000)));
2189
2266
  const observed = await session.call(NATIVE_AGENT_READ_TOOL, {
2190
2267
  runId: state.upstreamRunId,
@@ -2197,7 +2274,7 @@ export class NativeAgentCompositeTransport {
2197
2274
  result = resultForState(state);
2198
2275
  if (result.schema === NATIVE_AGENT_RESULT_SCHEMA) return result;
2199
2276
  }
2200
- return state.startMode === "answer" ? continuationForState(state) : handleForState(state);
2277
+ return attachmentHandleForState(state);
2201
2278
  } catch (error) {
2202
2279
  if (!state.upstreamRunId) throw error;
2203
2280
  try {
@@ -2206,7 +2283,7 @@ export class NativeAgentCompositeTransport {
2206
2283
  // A lost lease forbids stale writes. The already-persisted handle is
2207
2284
  // still the only safe return value.
2208
2285
  }
2209
- return handleForState(state);
2286
+ return attachmentHandleForState(state);
2210
2287
  }
2211
2288
  }
2212
2289
  }
@@ -5,8 +5,18 @@ import { environmentValue } from "./nativeProcess.js";
5
5
  import { brandedEnvironmentName, RUNTIME_BRAND } from "./runtimeBrand.js";
6
6
 
7
7
  /** The running package's own bin script, used directly for live child spawns. */
8
- export const IMPEL_CLI_ENTRYPOINT = process.env.IMPEL_CLI_EXTENSION_ENTRYPOINT
9
- || fileURLToPath(new URL("../bin/impel.js", import.meta.url));
8
+ const PACKAGE_CLI_ENTRYPOINT = fileURLToPath(new URL("../bin/impel.js", import.meta.url));
9
+ const extensionEntrypoint = process.env.IMPEL_CLI_EXTENSION_ENTRYPOINT;
10
+ // The public Impel package never delegates its trusted MCP command prefix to
11
+ // inherited process state. First-party extension wrappers install a validated
12
+ // non-Impel runtime brand before import, must actually be executing the named
13
+ // entrypoint, and then prove that same path in createImpelCliExtension.
14
+ export const IMPEL_CLI_ENTRYPOINT = RUNTIME_BRAND.cli.packageName === "impel-cli"
15
+ || typeof extensionEntrypoint !== "string"
16
+ || !path.isAbsolute(extensionEntrypoint)
17
+ || path.resolve(process.argv[1] || "") !== path.resolve(extensionEntrypoint)
18
+ ? PACKAGE_CLI_ENTRYPOINT
19
+ : extensionEntrypoint;
10
20
 
11
21
  // Matches a real package install (npm/pnpm/volta all place the package under
12
22
  // node_modules/impel-cli; an unsupported project-local install matches too,