impel-cli 0.18.15-beta.9 → 0.18.16

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
@@ -98,6 +98,12 @@ Names include the tenant, such as `Impel Claude (Acme)` and
98
98
  `Impel ChatGPT (Acme)`. The selected CLI tenant does not affect which tenant an
99
99
  app opens. Tenant variants can remain installed side by side.
100
100
 
101
+ Impel-managed apps do not run the vendors' in-app auto-updaters. The generated
102
+ Claude policy disables its updater, and the ChatGPT launcher disables Sparkle
103
+ before starting the pinned app. Run `impel update` to move managed apps to the
104
+ next exact vendor builds reviewed and shipped with the CLI. This does not
105
+ change the update settings of native personal Claude or ChatGPT installations.
106
+
101
107
  Linux does not have managed desktop apps. Use the isolated CLI commands there.
102
108
 
103
109
  ## Experimental managed Cursor
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.16",
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 @@ 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(),
package/src/apps.js CHANGED
@@ -98,9 +98,11 @@ export const PINNED_VENDOR_APPS = Object.freeze({
98
98
  windows: Object.freeze({
99
99
  storeProductId: "9PLM9XGG6VKS",
100
100
  packageName: "OpenAI.Codex",
101
- packageVersion: "26.721.11231.0",
102
- advertisedPackageVersion: "26.727.4816.0",
103
- codexVersion: "0.146.0-alpha.3.1",
101
+ // The Microsoft Store manifest identifies this exact reviewed build.
102
+ // Keep one Windows package version so the manifest contract and local
103
+ // AppX validation cannot silently drift apart again.
104
+ packageVersion: "26.727.6591.0",
105
+ codexVersion: "0.146.0-alpha.9.2",
104
106
  publisherId: "2p2nqsd0c76g0",
105
107
  executable: "app\\ChatGPT.exe",
106
108
  updateManifestUrl: "https://persistent.oaistatic.com/codex-app-prod/windows-store-update.json",
@@ -411,8 +411,9 @@ export function ensureWindowsChatGPTApp({ update = false } = {}, dependencies =
411
411
  return { binary: pinnedBefore, action: "existing", result: null };
412
412
  }
413
413
 
414
- // `impel update` also repairs missing surfaces. A missing Store package must
415
- // be installed, not sent to `winget upgrade` (which returns 0x8A150014).
414
+ // `impel update` converges an older official Store package to the current
415
+ // reviewed pin. A missing Store package must be installed, not sent to
416
+ // `winget upgrade` (which returns 0x8A150014).
416
417
  const shouldUpdate = Boolean(update && before);
417
418
  // The msstore source rejects its AppX package version as a winget --version
418
419
  // selector. Install the current Store package, then fail closed below unless