impel-cli 0.18.15-beta.9 → 0.18.15

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
@@ -100,50 +100,6 @@ app opens. Tenant variants can remain installed side by side.
100
100
 
101
101
  Linux does not have managed desktop apps. Use the isolated CLI commands there.
102
102
 
103
- ## Experimental managed Cursor
104
-
105
- On macOS, a hidden experiment can prepare an isolated Cursor runtime for the
106
- selected Impel tenant:
107
-
108
- ```sh
109
- impel experimental cursor prepare
110
- impel experimental cursor open [workspace]
111
- impel experimental cursor status
112
- ```
113
-
114
- The experiment never modifies `/Applications/Cursor.app` or the native Cursor
115
- profile. It verifies the installed stable app's Developer ID signature, mirrors
116
- it into the selected tenant's Impel-managed directory, enables Cursor's bundled
117
- local-agent runtime with fail-closed fixed-width patches, verifies extension
118
- integrity and signing, and prepares a tenant-only home, user-data directory,
119
- and extension directory. Cursor's local-mode profile-import service is disabled
120
- so it cannot inspect or copy the native Cursor profile. The managed bundle
121
- recomputes Cursor's signed-source checksum table after its reviewed patches, and
122
- the launcher uses Cursor's process-lifetime secret store because no persistent
123
- Cursor account or provider secret belongs in this tenant runtime.
124
-
125
- The native Cursor installation continues to update through Cursor. Every
126
- `prepare` inspects it again and atomically rebuilds the managed copy
127
- when a compatible version or commit changes. If Cursor changes any reviewed
128
- local-agent, provider-priority, or model-transport source contract, Impel keeps
129
- the previous managed generation intact and refuses the update until the patch is
130
- reviewed. `experimental cursor open` performs this prepare step before every
131
- launch. A real packaged GUI battle test proves that Cursor 3.13.25 can run
132
- Agent through the selected tenant's gateway with Cursor's own non-secret smoke
133
- identity. The logical `default` model is resolved by the gateway to its
134
- registered Cursor default; explicit gateway models continue to use Responses.
135
-
136
- This is not complete hosted-Cursor parity or an offline Cursor.
137
- The managed local transport supports Agent chat, edits, terminal tools, MCP,
138
- vision, reasoning, and gateway model discovery, and projects the live tenant
139
- catalog into Cursor's model picker, but the current vendor build explicitly
140
- disables hosted-only features such as Tab completion, cloud Background Agents,
141
- shared chats/canvases, Bugbot, and server-side repository indexing in local
142
- mode. Enabling those paths would either send work to Cursor-hosted models or
143
- require additional unpublished Cursor protocols, so the experiment does not
144
- claim or silently emulate them. See
145
- [`docs/experimental-managed-cursor.md`](docs/experimental-managed-cursor.md).
146
-
147
103
  ## Use the CLI
148
104
 
149
105
  Select the tenant used by CLI launches:
@@ -223,11 +179,6 @@ impel update --no-recovery # disable recovery for this run
223
179
  schema verified by that CLI. It does not mean cloning an untested moving vendor
224
180
  release.
225
181
 
226
- Stable installations follow npm's `latest` tag. Prerelease installations made
227
- from `impel-cli@next` remain on the `next` channel during `impel update`. If npm
228
- metadata is temporarily unavailable, update preserves the installed CLI build
229
- and continues tenant reconciliation without attempting a downgrade.
230
-
231
182
  ## Status and diagnosis
232
183
 
233
184
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.18.15-beta.9",
3
+ "version": "0.18.15",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,6 @@
12
12
  },
13
13
  "files": [
14
14
  "bin",
15
- "docs/experimental-managed-cursor.md",
16
15
  "src",
17
16
  "README.md"
18
17
  ],
package/src/agents.js CHANGED
@@ -21,6 +21,7 @@ export const NATIVE_AGENT_LIST_TOOL = "impel_specialists-list_native_agents";
21
21
  export const NATIVE_AGENT_START_TOOL = "impel_specialists-start_native_agent_run";
22
22
  export const NATIVE_AGENT_READ_TOOL = "impel_specialists-read_native_agent_run";
23
23
  export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
24
+ export const MANAGED_AGENT_MANIFEST_VERSION = 4;
24
25
 
25
26
  const NATIVE_AGENT_TOOL_NAMES = [
26
27
  NATIVE_AGENT_LIST_TOOL,
@@ -320,7 +321,7 @@ function generatedClientAgentNames(client, agents) {
320
321
  });
321
322
  }
322
323
 
323
- function adapterInstructions(tenantId, agent) {
324
+ function claudeAdapterInstructions(tenantId, agent) {
324
325
  const toolName = nativeToolName;
325
326
  const contextRequirement = agent.requiredContext.length
326
327
  ? ` Required context keys are ${JSON.stringify(agent.requiredContext)}; if any are absent, ask for them before starting the run.`
@@ -333,7 +334,7 @@ function adapterInstructions(tenantId, agent) {
333
334
  `Do not perform the assigned task yourself and do not delegate to any other agent.`,
334
335
  `First call ${toolName(NATIVE_AGENT_LIST_TOOL)} and verify that the exact agentId is still available with sideEffects ${JSON.stringify(agent.sideEffects)}. If it is unavailable or its policy excludes the request, stop with that explicit error.`,
335
336
  sideEffectInstruction,
336
- `Call ${toolName(NATIVE_AGENT_START_TOOL)} exactly once with agentId ${JSON.stringify(agent.agentId)}, scopeParam ${JSON.stringify(agent.scopeParam)}, task set to the complete assigned task, context and contextKeys containing all supplied context, confirmedSideEffects as directed above, and one stable idempotencyKey that you reuse for this logical task.${contextRequirement}`,
337
+ `Call ${toolName(NATIVE_AGENT_START_TOOL)} exactly once with agentId ${JSON.stringify(agent.agentId)}, scopeParam ${JSON.stringify(agent.scopeParam)}, task set to the complete assigned task, optional context set to one string containing all supplied context (omit it when no context was supplied), contextKeys naming the context fields present in that string, confirmedSideEffects as directed above, and one stable idempotencyKey that you reuse for this logical task.${contextRequirement}`,
337
338
  `Then call ${toolName(NATIVE_AGENT_READ_TOOL)} with the returned runId and waitSeconds 20 until the run reaches a terminal state.`,
338
339
  `When it succeeds, return result.finalText faithfully as the answer. When it fails, return the durable runId, preserved output, and error. Never invent or independently synthesize a replacement result.`,
339
340
  ].join(" ");
@@ -343,6 +344,170 @@ function nativeToolName(toolName) {
343
344
  return `mcp__${MANAGED_AGENT_MCP_SERVER}__${toolName}`;
344
345
  }
345
346
 
347
+ const CODEX_TASK_PLACEHOLDER = "__IMPEL_COMPLETE_ASSIGNED_TASK_JSON__";
348
+ const CODEX_CONTEXT_PLACEHOLDER = "__IMPEL_OPTIONAL_CONTEXT_STRING_OR_NULL_JSON__";
349
+ const CODEX_CONTEXT_KEYS_PLACEHOLDER = "__IMPEL_SUPPLIED_CONTEXT_KEYS_JSON__";
350
+ const CODEX_IDEMPOTENCY_KEY_PLACEHOLDER = "__IMPEL_LOGICAL_INVOCATION_IDEMPOTENCY_KEY_JSON__";
351
+
352
+ export function renderCodexAdapterOrchestration(tenantId, agent) {
353
+ const expectedTenantId = normalizeTenantId(tenantId);
354
+ const nestedToolName = (toolName) => nativeToolName(toolName).replaceAll("-", "_");
355
+ const listTool = nestedToolName(NATIVE_AGENT_LIST_TOOL);
356
+ const startTool = nestedToolName(NATIVE_AGENT_START_TOOL);
357
+ const readTool = nestedToolName(NATIVE_AGENT_READ_TOOL);
358
+ return [
359
+ '// @exec: {"yield_time_ms": 30000, "max_output_tokens": 30000}',
360
+ `const assignedTask = ${CODEX_TASK_PLACEHOLDER};`,
361
+ `const suppliedContext = ${CODEX_CONTEXT_PLACEHOLDER};`,
362
+ `const suppliedContextKeys = ${CODEX_CONTEXT_KEYS_PLACEHOLDER};`,
363
+ `const idempotencyKey = ${CODEX_IDEMPOTENCY_KEY_PLACEHOLDER};`,
364
+ `const expectedTenantId = ${JSON.stringify(expectedTenantId)};`,
365
+ `const expectedAgent = ${JSON.stringify(agent)};`,
366
+ "const waitSeconds = 20; // Compatible fallback until the bound server advertises a larger ceiling.",
367
+ `const listToolName = ${JSON.stringify(listTool)};`,
368
+ `const startToolName = ${JSON.stringify(startTool)};`,
369
+ `const readToolName = ${JSON.stringify(readTool)};`,
370
+ "",
371
+ "function stableValue(value) {",
372
+ " if (Array.isArray(value)) return value.map(stableValue);",
373
+ ' if (!value || typeof value !== "object") return value;',
374
+ " const result = {};",
375
+ " for (const key of Object.keys(value).sort()) result[key] = stableValue(value[key]);",
376
+ " return result;",
377
+ "}",
378
+ "",
379
+ "function contentText(response) {",
380
+ ' return response?.content?.find((item) => item?.type === "text" && typeof item.text === "string")?.text;',
381
+ "}",
382
+ "",
383
+ "function toolPayload(response, label) {",
384
+ ' if (!response || typeof response !== "object") throw new Error(label + " returned no result");',
385
+ ' if (response.isError) throw new Error(contentText(response) || label + " failed");',
386
+ ' if (response.structuredContent && typeof response.structuredContent === "object") return response.structuredContent;',
387
+ " const serialized = contentText(response);",
388
+ " if (serialized !== undefined) {",
389
+ " try {",
390
+ " return JSON.parse(serialized);",
391
+ " } catch {",
392
+ ' throw new Error(label + " returned invalid JSON");',
393
+ " }",
394
+ " }",
395
+ " return response;",
396
+ "}",
397
+ "",
398
+ "function preservedOutput(run) {",
399
+ ' if (run && Object.prototype.hasOwnProperty.call(run, "output")) return run.output;',
400
+ ' if (run && Object.prototype.hasOwnProperty.call(run, "result")) return run.result;',
401
+ " return null;",
402
+ "}",
403
+ "",
404
+ "function errorValue(error, fallback) {",
405
+ " if (error === undefined || error === null) return fallback;",
406
+ ' if (error instanceof Error) return error.message;',
407
+ " return error;",
408
+ "}",
409
+ "",
410
+ "async function orchestrate() {",
411
+ " let runId = null;",
412
+ " let latestRun = null;",
413
+ " try {",
414
+ ' if (typeof assignedTask !== "string" || !assignedTask.trim()) throw new Error("the complete assigned task is required");',
415
+ ' if (suppliedContext !== null && typeof suppliedContext !== "string") throw new Error("supplied context must be a string or null");',
416
+ ' if (expectedAgent.requiredContext.length && (typeof suppliedContext !== "string" || !suppliedContext.trim())) {',
417
+ ' throw new Error("nonblank supplied context is required for required context keys");',
418
+ " }",
419
+ ' if (!Array.isArray(suppliedContextKeys)) throw new Error("supplied context keys must be an array");',
420
+ ' if (typeof idempotencyKey !== "string" || !/^[A-Za-z0-9._:-]{16,160}$/u.test(idempotencyKey)) {',
421
+ ' throw new Error("a unique stable logical-invocation idempotencyKey is required");',
422
+ " }",
423
+ " const missingContext = expectedAgent.requiredContext.filter((key) =>",
424
+ " !suppliedContextKeys.includes(key)",
425
+ " );",
426
+ ' if (missingContext.length) throw new Error("missing required context: " + missingContext.join(", "));',
427
+ "",
428
+ " const catalog = toolPayload(await tools[listToolName]({}), \"native-agent catalog\");",
429
+ ' if (catalog.orgId !== expectedTenantId) throw new Error("native-agent catalog tenant mismatch");',
430
+ ' if (!Array.isArray(catalog.agents)) throw new Error("native-agent catalog returned no agents");',
431
+ " const matches = catalog.agents.filter((candidate) =>",
432
+ " candidate?.agentId === expectedAgent.agentId && candidate?.scopeParam === expectedAgent.scopeParam",
433
+ " );",
434
+ ' if (matches.length !== 1) throw new Error("exact native-agent binding is unavailable");',
435
+ " const actualAgent = {};",
436
+ " for (const key of Object.keys(expectedAgent)) actualAgent[key] = matches[0][key];",
437
+ " if (JSON.stringify(stableValue(actualAgent)) !== JSON.stringify(stableValue(expectedAgent))) {",
438
+ ' throw new Error("native-agent catalog policy mismatch");',
439
+ " }",
440
+ "",
441
+ " const startArguments = {",
442
+ " agentId: expectedAgent.agentId,",
443
+ " scopeParam: expectedAgent.scopeParam,",
444
+ " task: assignedTask,",
445
+ " contextKeys: suppliedContextKeys,",
446
+ " idempotencyKey,",
447
+ " };",
448
+ " if (suppliedContext !== null) startArguments.context = suppliedContext;",
449
+ ' if (expectedAgent.sideEffects === "writes") startArguments.confirmedSideEffects = true;',
450
+ " latestRun = toolPayload(await tools[startToolName](startArguments), \"native-agent start\");",
451
+ ' if (typeof latestRun.runId !== "string" || !latestRun.runId) throw new Error("native-agent start returned no runId");',
452
+ " runId = latestRun.runId;",
453
+ "",
454
+ " for (;;) {",
455
+ " const observed = toolPayload(",
456
+ " await tools[readToolName]({ runId, waitSeconds }),",
457
+ ' "native-agent read",',
458
+ " );",
459
+ ' if (observed.runId !== undefined && observed.runId !== runId) throw new Error("native-agent read returned a different runId");',
460
+ " latestRun = observed;",
461
+ ' if (observed.status === "succeeded") {',
462
+ ' const finalText = observed.result?.finalText;',
463
+ ' if (typeof finalText !== "string") throw new Error("succeeded native-agent run returned no finalText");',
464
+ " text(finalText);",
465
+ " return;",
466
+ " }",
467
+ ' if (observed.status === "failed") {',
468
+ " text(JSON.stringify({",
469
+ " runId,",
470
+ " output: preservedOutput(observed),",
471
+ ' error: errorValue(observed.error, "native-agent run failed"),',
472
+ " }));",
473
+ " return;",
474
+ " }",
475
+ " }",
476
+ " } catch (error) {",
477
+ " text(JSON.stringify({",
478
+ " runId,",
479
+ " output: preservedOutput(latestRun),",
480
+ ' error: errorValue(error, "native-agent adapter failed"),',
481
+ " }));",
482
+ " }",
483
+ "}",
484
+ "",
485
+ "await orchestrate();",
486
+ ].join("\n");
487
+ }
488
+
489
+ function codexAdapterInstructions(tenantId, agent) {
490
+ const contextRequirement = agent.requiredContext.length
491
+ ? ` Required context keys are ${JSON.stringify(agent.requiredContext)}; if any are absent, ask for them before starting the run.`
492
+ : "";
493
+ const sideEffectInstruction = agent.sideEffects === "writes"
494
+ ? `The catalog declares that this agent writes user or workspace data. Use this adapter only when the user explicitly selected this exact agent for the current request; that explicit selection is the required side-effect confirmation. If the agent was chosen automatically or the selection is ambiguous, do not run the orchestration and ask the user to select this exact agent explicitly.`
495
+ : `The catalog declares that this agent is read-only; the orchestration omits confirmedSideEffects.`;
496
+ const source = renderCodexAdapterOrchestration(tenantId, agent);
497
+ return [
498
+ `You are a thin transport adapter for the exact Impel native agent ${JSON.stringify(agent.agentId)} in tenant ${JSON.stringify(tenantId)}.`,
499
+ `Callers must spawn this explicit custom Codex agent with fork_turns="none" and must relay your result verbatim; this is caller guidance and cannot enforce host spawn behavior.`,
500
+ `Do not perform the assigned task yourself, do not delegate to any other agent, and do not independently synthesize or rewrite the result.`,
501
+ `Confirm that the assigned request fits the cataloged capabilities ${JSON.stringify(agent.capabilities)} and none of the exclusions ${JSON.stringify(agent.exclusions)} before starting.${contextRequirement}`,
502
+ sideEffectInstruction,
503
+ `Invoke functions.exec exactly once for the orchestration below. Do not call the MCP tools directly or select a separate MCP call for any poll. Replace ${CODEX_TASK_PLACEHOLDER} with a JSON string literal for the complete assigned task. Replace ${CODEX_CONTEXT_PLACEHOLDER} with one JSON string literal containing all supplied caller context, or with null when no context was supplied; never use an object or array. Replace ${CODEX_CONTEXT_KEYS_PLACEHOLDER} with a JSON array naming the context fields present in that string, or [] when context is absent. Replace ${CODEX_IDEMPOTENCY_KEY_PLACEHOLDER} with one new opaque idempotency key for this logical invocation: choose it exactly once, reuse it unchanged for any retry of this invocation, and never reuse it for a separate request even when task and context are identical. Then pass the raw JavaScript without Markdown fences.`,
504
+ `The JavaScript validates the exact tenant, agent binding, and catalog policy; passes the one stable logical-invocation idempotencyKey; starts exactly once; and polls deterministically with the compatible 20-second server wait until status is succeeded or failed. If functions.exec yields a running cell, use functions.wait with max_tokens 30000 only to resume that same orchestration; never start another orchestration or poll the MCP tool yourself.`,
505
+ `After the orchestration completes, return its single text output verbatim with no preface, rewriting, Markdown changes, or independent synthesis. A successful output is result.finalText exactly. A failure output preserves the durable runId, output, and error.`,
506
+ "",
507
+ source,
508
+ ].join("\n\n");
509
+ }
510
+
346
511
  function renderClaudeAgent({ tenantId, agent, name, invocation }) {
347
512
  const description = `Explicitly runs ${agent.title} for Impel tenant ${tenantId}${agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)"}: ${agent.description}`.slice(0, 900);
348
513
  const lines = [
@@ -362,14 +527,14 @@ function renderClaudeAgent({ tenantId, agent, name, invocation }) {
362
527
  ...Object.entries(invocation.env || {}).map(([key, value]) => ` ${key}: ${JSON.stringify(value)}`),
363
528
  "---",
364
529
  "",
365
- adapterInstructions(tenantId, agent),
530
+ claudeAdapterInstructions(tenantId, agent),
366
531
  "",
367
532
  ];
368
533
  return lines.join("\n");
369
534
  }
370
535
 
371
536
  function renderCodexAgent({ tenantId, agent, name, invocation }) {
372
- const description = `Explicitly runs ${agent.title} for Impel tenant ${tenantId}${agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)"}: ${agent.description}`.slice(0, 900);
537
+ const description = `Explicit custom agent: callers must use fork_turns="none" and relay its result verbatim. Runs ${agent.title} for Impel tenant ${tenantId}${agent.sideEffects === "writes" ? " (may write user or workspace data)" : " (read-only)"}: ${agent.description}`.slice(0, 900);
373
538
  const envEntries = Object.entries(invocation.env || {})
374
539
  .map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`)
375
540
  .join(", ");
@@ -377,7 +542,7 @@ function renderCodexAgent({ tenantId, agent, name, invocation }) {
377
542
  `name = ${JSON.stringify(name)}`,
378
543
  `description = ${JSON.stringify(description)}`,
379
544
  'sandbox_mode = "read-only"',
380
- `developer_instructions = ${JSON.stringify(adapterInstructions(tenantId, agent))}`,
545
+ `developer_instructions = ${JSON.stringify(codexAdapterInstructions(tenantId, agent))}`,
381
546
  "",
382
547
  `[mcp_servers.${MANAGED_AGENT_MCP_SERVER}]`,
383
548
  `command = ${JSON.stringify(invocation.command)}`,
@@ -440,7 +605,7 @@ function readManifest(manifestPath) {
440
605
 
441
606
  function profileIsFresh(profile, tenantId, now, ttlMs) {
442
607
  const manifest = readManifest(path.join(profile.root, "agents", MANAGED_AGENT_DIRECTORY, MANAGED_AGENT_MANIFEST));
443
- if (!manifest || manifest.version !== 3 || manifest.tenantId !== tenantId) return false;
608
+ if (!manifest || manifest.version !== MANAGED_AGENT_MANIFEST_VERSION || manifest.tenantId !== tenantId) return false;
444
609
  const syncedAt = Date.parse(manifest.syncedAt || "");
445
610
  if (!Number.isFinite(syncedAt) || now - syncedAt >= ttlMs) return false;
446
611
  return manifest.files.every((fileName) =>
@@ -463,7 +628,7 @@ export function syncAgentProfile({ client, root, label, tenantId, agents, now =
463
628
  const prior = readManifest(manifestPath);
464
629
  const rendered = renderManagedAgents(client, tenantId, agents);
465
630
  const priorFiles = new Set(prior?.files || []);
466
- const priorUsesDiscoveryRoot = prior?.version === 2 || prior?.version === 3;
631
+ const priorUsesDiscoveryRoot = [2, 3, MANAGED_AGENT_MANIFEST_VERSION].includes(prior?.version);
467
632
 
468
633
  // Native clients discover standalone definitions directly under `agents/`.
469
634
  // Preflight every destination before writing so an unmanaged file with the
@@ -505,7 +670,7 @@ export function syncAgentProfile({ client, root, label, tenantId, agents, now =
505
670
  }
506
671
  }
507
672
  atomicPrivateWrite(manifestPath, `${JSON.stringify({
508
- version: 3,
673
+ version: MANAGED_AGENT_MANIFEST_VERSION,
509
674
  tenantId,
510
675
  client,
511
676
  syncedAt: new Date(now).toISOString(),