impel-cli 0.20.35 → 0.20.37

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,24 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.37 — Resume ambiguous native answers
4
+
5
+ - Retries Next's typed ambiguous direct-answer response with the exact persisted
6
+ idempotency key, resuming the existing Eve answer instead of duplicating it.
7
+ - Keeps retries bounded and cancellation-safe while rejecting incomplete or
8
+ untyped retry claims as definitive failures.
9
+ - Preserves strict continuation handles for answer-only adapters and full run
10
+ handles for recovery-only adapters across every nonterminal resume.
11
+
12
+ ## 0.20.36 — Restore Codex execution
13
+
14
+ - Keeps Codex 0.147's stable code-mode host enabled instead of overriding it
15
+ off, restoring filesystem, shell, and repository tools in `impel codex`.
16
+ - Launches ordinary tenant Codex sessions with unrestricted filesystem access
17
+ and no approval prompts by default while preserving read-only fixed-agent
18
+ profiles.
19
+ - Requires the reviewed macOS Codex install to include its separately signed
20
+ `codex-code-mode-host` companion before the CLI can launch it.
21
+
3
22
  ## 0.20.35 — Normalize the Claude Tasks destination
4
23
 
5
24
  - Waits for Claude's native New row before cloning the Tasks destination,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.35",
3
+ "version": "0.20.37",
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
@@ -696,7 +696,7 @@ function retiredAdapterInstructions(tenantId, agent) {
696
696
  `You are a recovery-only transport adapter for retired Impel native-agent binding ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
697
697
  `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
698
  `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.`,
699
+ `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
700
  `Return successful finalText byte-for-byte. For failure, return the preserved runId, output, and error without inventing a replacement result.`,
701
701
  ].join(" ");
702
702
  }
@@ -70,13 +70,14 @@ export const IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS =
70
70
  `${IMPEL_CODEX_SPECIALIST_DELEGATION_INSTRUCTIONS} ${IMPEL_CUSTOM_AGENT_VERBATIM_RELAY_APPENDIX}`;
71
71
 
72
72
  const IMPEL_CODEX_RUNTIME_OVERRIDES = [
73
- // Codex models may select code mode even when the standalone
74
- // `codex-code-mode-host` companion is not present in the vendor install.
75
- // The embedded runtime is the vendor-supported equivalent and is scoped to
76
- // this Impel invocation, so the user's native Codex profile is unaffected.
77
- "features.code_mode_host=false",
73
+ // Codex 0.147 fails closed when a model selects code mode while this stable
74
+ // feature is disabled. The reviewed standalone install includes the signed
75
+ // companion host, so keep the execution path enabled for Impel sessions.
76
+ "features.code_mode_host=true",
78
77
  ];
79
78
 
79
+ const IMPEL_CODEX_UNRESTRICTED_ARGUMENT = "--dangerously-bypass-approvals-and-sandbox";
80
+
80
81
  export function impelLaunchArguments(tool, argv, { codexAgentProfile = null } = {}) {
81
82
  if (!RUNTIME_BRAND.features.agents) return [...argv];
82
83
  if (tool === "claude") {
@@ -89,6 +90,7 @@ export function impelLaunchArguments(tool, argv, { codexAgentProfile = null } =
89
90
  ...(codexAgentProfile ? ["--profile", codexAgentProfile] : [
90
91
  "-c",
91
92
  `developer_instructions=${JSON.stringify(IMPEL_CODEX_PARENT_DELEGATION_INSTRUCTIONS)}`,
93
+ IMPEL_CODEX_UNRESTRICTED_ARGUMENT,
92
94
  ]),
93
95
  ...IMPEL_CODEX_RUNTIME_OVERRIDES.flatMap((override) => ["-c", override]),
94
96
  ...argv,
@@ -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
  }
@@ -38,6 +38,7 @@ export function verifyReviewedMacVendorCli(
38
38
  {
39
39
  run = spawnSync,
40
40
  realpath = fs.realpathSync,
41
+ stat = fs.statSync,
41
42
  versions = PINNED_VENDOR_CLI_VERSIONS,
42
43
  signingTeams = MAC_VENDOR_SIGNING_TEAMS,
43
44
  environmentPrefix = "IMPEL",
@@ -61,6 +62,25 @@ export function verifyReviewedMacVendorCli(
61
62
  if (version !== versions[tool]) return false;
62
63
 
63
64
  const resolved = realpath(binary);
65
+ if (tool === "codex") {
66
+ const codeModeHost = path.join(path.dirname(resolved), "codex-code-mode-host");
67
+ if (!stat(codeModeHost).isFile()) return false;
68
+ const helperVerified = run("/usr/bin/codesign", ["--verify", "--strict", codeModeHost], {
69
+ encoding: "utf8",
70
+ stdio: ["ignore", "pipe", "pipe"],
71
+ timeout: 15_000,
72
+ });
73
+ if (helperVerified?.status !== 0 || helperVerified?.error) return false;
74
+ const helperDetails = run("/usr/bin/codesign", ["-dv", "--verbose=4", codeModeHost], {
75
+ encoding: "utf8",
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ timeout: 15_000,
78
+ });
79
+ if (helperDetails?.status !== 0
80
+ || !String(helperDetails.stderr || helperDetails.stdout || "").includes(`TeamIdentifier=${signingTeams[tool]}`)) {
81
+ return false;
82
+ }
83
+ }
64
84
  const verified = run("/usr/bin/codesign", ["--verify", "--strict", resolved], {
65
85
  encoding: "utf8",
66
86
  stdio: ["ignore", "pipe", "pipe"],