impel-cli 0.20.63-canary.7 → 0.20.64

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
@@ -219,7 +219,7 @@ skill-catalog prompt context; the selected Eve agent still performs the
219
219
  substantive work. Model, profile,
220
220
  developer-instruction, MCP, approval, sandbox, and feature overrides are
221
221
  rejected for its managed path. Ordinary `impel claude`, user-authored Claude
222
- agent, and `impel codex` passthrough are unchanged.
222
+ agent, and `impel codex` are unchanged.
223
223
 
224
224
  ## Remote Fargate sessions
225
225
 
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.64 — Windows cmd-compatible Codex hooks; rebuilt managed macOS package
4
+
5
+ - Emits managed Codex hooks with cmd-compatible quoting on Windows. Codex's
6
+ Windows hook runner passes `commandWindows` to cmd.exe as one raw command
7
+ line, so the PowerShell call syntax used before failed every Windows hook
8
+ even though the same command worked on macOS. Bumps the managed config
9
+ contract to 49 so existing Windows profiles are rewritten.
10
+ - New version so the managed macOS `Impel-CLI` package can be reissued and
11
+ reinstalled by Munki. The 0.20.63 package published with release 0.1.134 was
12
+ re-signed without the Node runtime's JIT entitlements and its bundled Node
13
+ aborts at startup; the impel-apps builders now restore those entitlements,
14
+ but Munki only replaces the package when its version increases.
15
+
16
+ ## 0.20.63 — Route isolated Codex through the typed gateway surface
17
+
18
+ - Moves the `impel codex` and `impel use gateway codex` model provider from the
19
+ raw `/chatgpt_passthrough/backend-api/codex` route to `/openai/v1`, the typed
20
+ OpenAI-compatible surface the managed ChatGPT desktop profile already uses.
21
+ The raw route forwards bytes to chatgpt.com and can only serve Codex-native
22
+ models; every OpenAI-compatible subscription model (MiniMax, GLM, Qwen)
23
+ answered `400 model is not available on this client protocol` there.
24
+ Cross-app mode no longer switches routes; it only changes the catalog.
25
+ - Pins `supports_websockets = false` on every generated Codex provider block.
26
+ Without it Codex dials `wss://…/responses` first and retries the handshake
27
+ five times per turn, showing each gateway refusal to the user, before falling
28
+ back to HTTPS. Bumps the managed config version so existing profiles rewrite.
29
+
3
30
  ## 0.20.62 — Silence the Codex apps connector in desktop profiles
4
31
 
5
32
  - Persists `features.apps = false` in the managed ChatGPT desktop `config.toml`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.63-canary.7",
3
+ "version": "0.20.64",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -34,7 +34,6 @@ import {
34
34
  TELEMETRY_FLUSH_COMMAND,
35
35
  } from "./posthog.js";
36
36
  import { maybePrintTelemetryNotice } from "./telemetryNotice.js";
37
- import { versionLine } from "./runtimeBrand.js";
38
37
 
39
38
  const HELP = `impel — isolated Impel workspaces for every tenant
40
39
 
@@ -94,7 +93,7 @@ Config file:
94
93
  function printVersion() {
95
94
  const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
96
95
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
97
- console.log(versionLine(pkg.version));
96
+ console.log(pkg.version);
98
97
  }
99
98
 
100
99
  // Accepts "claude" | "codex" | "all" | undefined; returns a normalized target
@@ -208,13 +208,16 @@ function codexManagedBlock(gatewayUrl, tenantId, { crossAppModels = false } = {}
208
208
  `# Generated for \`${RUNTIME_BRAND.cli.command} codex\`. Other profile settings outside this block are preserved.`,
209
209
  `[model_providers.${providerId}]`,
210
210
  `name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
211
- // The experimental OpenAI-compatible route can dispatch both provider
212
- // families (the desktop ChatGPT profile's cross-app path); the default
213
- // route is the byte-preserving Codex passthrough.
214
- `base_url = ${JSON.stringify(crossAppModels
215
- ? `${gatewayUrl}/experimental/openai/v1`
216
- : impelCodexBaseUrl(gatewayUrl))}`,
211
+ // The typed OpenAI-compatible route serves Codex-native and every
212
+ // OpenAI-compatible subscription model, and is what the managed ChatGPT
213
+ // desktop profile already uses. Cross-app mode only changes which models
214
+ // the catalog lists, not the route. The old byte-preserving Codex
215
+ // passthrough could serve Codex-native models only.
216
+ `base_url = ${JSON.stringify(impelCodexBaseUrl(gatewayUrl))}`,
217
217
  'wire_api = "responses"',
218
+ // HTTPS only: without this Codex retries a wss:// handshake five times
219
+ // per turn before falling back.
220
+ "supports_websockets = false",
218
221
  `env_key = ${JSON.stringify(CODEX_GATEWAY_TOKEN_ENV)}`,
219
222
  ];
220
223
  if (RUNTIME_BRAND.features.mcp) {
package/src/codexSetup.js CHANGED
@@ -39,11 +39,16 @@ export const CODEX_CONFIG_PATH = path.join(CODEX_HOME, "config.toml");
39
39
 
40
40
  export const PROVIDER_ID = RUNTIME_BRAND.cli.providerId;
41
41
 
42
- // Genuine Codex CLI traffic has its own byte-preserving compatibility route.
43
- // With wire_api = "responses", Codex POSTs to `${base_url}/responses`, so this
44
- // base must stop immediately before that suffix. Do not point the CLI at the
45
- // separate `/v1/responses` SDK front door; the gateway may shape that body.
46
- export const CODEX_CLI_BASE_PATH = "/chatgpt_passthrough/backend-api/codex";
42
+ // Codex CLI traffic uses the gateway's typed OpenAI-compatible surface, the
43
+ // same route the managed ChatGPT desktop profile uses. With
44
+ // wire_api = "responses", Codex POSTs to `${base_url}/responses`, so this base
45
+ // must stop immediately before that suffix. The old raw
46
+ // `/chatgpt_passthrough/backend-api/codex` route forwards bytes to chatgpt.com
47
+ // and can only serve Codex-native models: every OpenAI-compatible subscription
48
+ // model (MiniMax, GLM, Qwen) answered "model is not available on this client
49
+ // protocol" there. Do not point the CLI at the separate `/v1/responses` SDK
50
+ // front door either; the gateway may shape that body differently.
51
+ export const CODEX_CLI_BASE_PATH = "/openai/v1";
47
52
  export const impelCodexBaseUrl = (gatewayUrl) => `${gatewayUrl}${CODEX_CLI_BASE_PATH}`;
48
53
 
49
54
  const START_MARK = `# >>> ${RUNTIME_BRAND.cli.managedMarker} managed block (model_providers.${PROVIDER_ID}) >>>`;
@@ -79,6 +84,10 @@ function providerTablesBlock(
79
84
  `name = ${JSON.stringify(`${RUNTIME_BRAND.product.displayName} Gateway`)}`,
80
85
  `base_url = "${baseUrl}"`,
81
86
  `wire_api = "responses"`,
87
+ // The gateway serves Responses over HTTPS only. Without this pin Codex
88
+ // dials wss://.../responses first and retries the handshake five times
89
+ // (surfacing each refusal to the user) before falling back to HTTPS.
90
+ `supports_websockets = false`,
82
91
  "",
83
92
  `[model_providers.${PROVIDER_ID}.auth]`,
84
93
  `command = ${JSON.stringify(auth.command)}`,
@@ -3,7 +3,6 @@ import {
3
3
  loadConfig,
4
4
  CONFIG_PATH,
5
5
  saveConfig,
6
- foreignChannelOrigin,
7
6
  normalizeGatewayUrl,
8
7
  resolveDefaultGateway,
9
8
  resolveDefaultAppUrl,
@@ -13,8 +12,7 @@ import { promptSecret } from "../prompt.js";
13
12
  import { applyCliUser, fetchTenants, normalizeTenantId } from "../tenants.js";
14
13
  import { RUNTIME_BRAND } from "../runtimeBrand.js";
15
14
 
16
- export async function cmdAuth(argv, overrides = {}) {
17
- const io = { fetchTenants, promptSecret, ...overrides };
15
+ export async function cmdAuth(argv) {
18
16
  const { flags } = parseFlags(argv, {
19
17
  pat: { type: "string" },
20
18
  gateway: { type: "string" },
@@ -22,16 +20,11 @@ export async function cmdAuth(argv, overrides = {}) {
22
20
  tenant: { type: "string" },
23
21
  });
24
22
 
25
- // `auth` is the one command that must run against a config whose origins
26
- // belong to another channel: it is how a device that carried a production
27
- // config is moved onto the canary origins (and the message `loadConfig`
28
- // prints elsewhere tells the tester to run it).
29
- const canary = RUNTIME_BRAND.channel === "canary";
30
- const existing = loadConfig({ rejectForeignOrigins: !canary });
23
+ const existing = loadConfig();
31
24
 
32
25
  let pat = flags.pat;
33
26
  if (!pat) {
34
- pat = await io.promptSecret(`${RUNTIME_BRAND.product.displayName} Personal Access Token (${RUNTIME_BRAND.auth.patPrefix}...): `);
27
+ pat = await promptSecret(`${RUNTIME_BRAND.product.displayName} Personal Access Token (${RUNTIME_BRAND.auth.patPrefix}...): `);
35
28
  }
36
29
  if (!pat) {
37
30
  console.error("impel: no PAT provided, aborting.");
@@ -44,21 +37,12 @@ export async function cmdAuth(argv, overrides = {}) {
44
37
  );
45
38
  }
46
39
 
47
- // Under the canary brand the stored origins are ignored in favour of the
48
- // brand defaults; a production origin can only survive here if it is asked
49
- // for explicitly, and then it is refused below rather than written.
50
- const gatewayUrl = normalizeGatewayUrl(flags.gateway || (canary ? null : existing?.gatewayUrl) || resolveDefaultGateway());
51
- const appUrl = normalizeGatewayUrl(flags.app || (canary ? null : existing?.appUrl) || resolveDefaultAppUrl());
52
- for (const [key, value] of [["gatewayUrl", gatewayUrl], ["appUrl", appUrl]]) {
53
- const expected = foreignChannelOrigin(key, value);
54
- if (expected) {
55
- throw new Error(`${key} ${value} is not a ${RUNTIME_BRAND.channel} origin; this ${RUNTIME_BRAND.channel} build talks only to ${expected}.`);
56
- }
57
- }
40
+ const gatewayUrl = normalizeGatewayUrl(flags.gateway || existing?.gatewayUrl || resolveDefaultGateway());
41
+ const appUrl = normalizeGatewayUrl(flags.app || existing?.appUrl || resolveDefaultAppUrl());
58
42
 
59
43
  const config = { ...(existing || {}), pat, gatewayUrl, appUrl, updatedAt: new Date().toISOString() };
60
44
  try {
61
- const listing = await io.fetchTenants(config);
45
+ const listing = await fetchTenants(config);
62
46
  const requestedTenant = flags.tenant ? normalizeTenantId(flags.tenant) : null;
63
47
  const selected = requestedTenant
64
48
  ? listing.tenants.find((tenant) => tenant.id === requestedTenant || tenant.slug === requestedTenant)
@@ -15,7 +15,7 @@ import {
15
15
  convergenceSummaryDiagnostics,
16
16
  readRecentConvergenceSummary,
17
17
  } from "../convergenceSummary.js";
18
- import { brandedEnvironmentName, brandedText, RUNTIME_BRAND, versionLine } from "../runtimeBrand.js";
18
+ import { brandedEnvironmentName, brandedText, RUNTIME_BRAND } from "../runtimeBrand.js";
19
19
  import { IMPEL_CLI_ENTRYPOINT } from "../selfInvocation.js";
20
20
  import {
21
21
  fetchRemoteVersion,
@@ -68,9 +68,7 @@ export function postInstallCliRunnable(expectedVersion, { spawn = spawnSync, exe
68
68
  windowsHide: true,
69
69
  });
70
70
  if (run.status !== 0 || run.error) return false;
71
- // The freshly installed build is the same package on the same channel, so
72
- // its `--version` line carries the same channel suffix this build prints.
73
- return String(run.stdout || "").trim() === versionLine(expectedVersion.trim());
71
+ return String(run.stdout || "").trim() === expectedVersion.trim();
74
72
  } catch {
75
73
  return false;
76
74
  }
package/src/config.js CHANGED
@@ -47,39 +47,8 @@ export function normalizeGatewayUrl(url) {
47
47
  return LEGACY_GATEWAY_URLS.has(normalized) ? DEFAULT_GATEWAY_URL : normalized;
48
48
  }
49
49
 
50
- const CHANNEL_ORIGIN_KEYS = Object.freeze({
51
- gatewayUrl: Object.freeze({ brandDefault: DEFAULT_GATEWAY_URL, environmentSuffix: "GATEWAY_URL" }),
52
- appUrl: Object.freeze({ brandDefault: DEFAULT_APP_URL, environmentSuffix: "APP_URL" }),
53
- });
54
-
55
- /**
56
- * Under the canary brand, an origin the CLI would use must be the canary row
57
- * the brand carries, or the explicit branded env override (how an engineer
58
- * points a build at a local service). Returns the origin `key` is required to
59
- * be when `value` is foreign, else null. The stable brand accepts every
60
- * origin, exactly as before channels existed.
61
- *
62
- * Why this exists: the canary CLI replaces the stable one on a device under
63
- * the same package name, so an engineer's existing production config would
64
- * otherwise keep a canary build talking to production with production
65
- * credentials (R17).
66
- */
67
- export function foreignChannelOrigin(key, value) {
68
- if (RUNTIME_BRAND.channel !== "canary" || !value) return null;
69
- const { brandDefault, environmentSuffix } = CHANNEL_ORIGIN_KEYS[key];
70
- const normalized = normalizeGatewayUrl(value);
71
- const override = process.env[brandedEnvironmentName(environmentSuffix)];
72
- if (normalized === brandDefault || (override && normalized === normalizeGatewayUrl(override))) return null;
73
- return brandDefault;
74
- }
75
-
76
- /**
77
- * Returns the parsed config object, or null if it doesn't exist yet. Throws
78
- * on malformed JSON, and — under the canary brand — on a stored origin outside
79
- * the canary row unless `rejectForeignOrigins` is false (`impel auth` reads
80
- * that way so it can move a device off a production config).
81
- */
82
- export function loadConfig({ rejectForeignOrigins = true } = {}) {
50
+ /** Returns the parsed config object, or null if it doesn't exist yet. Throws on malformed JSON. */
51
+ export function loadConfig() {
83
52
  let raw;
84
53
  try {
85
54
  raw = fs.readFileSync(CONFIG_PATH, "utf8");
@@ -87,27 +56,15 @@ export function loadConfig({ rejectForeignOrigins = true } = {}) {
87
56
  if (err.code === "ENOENT") return null;
88
57
  throw err;
89
58
  }
90
- let config;
91
59
  try {
92
- config = JSON.parse(raw);
60
+ const config = JSON.parse(raw);
61
+ if (config?.gatewayUrl) config.gatewayUrl = normalizeGatewayUrl(config.gatewayUrl);
62
+ return config;
93
63
  } catch {
94
64
  throw new Error(
95
65
  `${CONFIG_PATH} exists but isn't valid JSON. Fix or delete it, then run \`${RUNTIME_BRAND.cli.command} auth\` again.`
96
66
  );
97
67
  }
98
- if (config?.gatewayUrl) config.gatewayUrl = normalizeGatewayUrl(config.gatewayUrl);
99
- if (rejectForeignOrigins) {
100
- for (const key of Object.keys(CHANNEL_ORIGIN_KEYS)) {
101
- const expected = foreignChannelOrigin(key, config?.[key]);
102
- if (expected) {
103
- throw new Error(
104
- `${CONFIG_PATH} stores ${key} ${normalizeGatewayUrl(config[key])}, but this ${RUNTIME_BRAND.channel} build talks only to ${expected}; `
105
- + `run \`${RUNTIME_BRAND.cli.command} auth\` to re-enrol this device against the ${RUNTIME_BRAND.channel} origins.`
106
- );
107
- }
108
- }
109
- }
110
- return config;
111
68
  }
112
69
 
113
70
  /** Writes the config atomically-ish and locks it down to 0600 (owner read/write only). */
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
 
3
3
  import { main as upstreamMain } from "../cli.js";
4
- import { brandedText, RUNTIME_BRAND, validateRuntimeBrand, versionLine } from "../runtimeBrand.js";
4
+ import { brandedText, RUNTIME_BRAND, validateRuntimeBrand } from "../runtimeBrand.js";
5
5
 
6
6
  const ALIASES = Object.freeze({
7
7
  apps: "app",
@@ -95,7 +95,7 @@ export function createImpelCliExtension({ brand, entrypoint, version }) {
95
95
  return;
96
96
  }
97
97
  if (["--version", "-v"].includes(rawCommand)) {
98
- process.stdout.write(`${versionLine(version)}\n`);
98
+ process.stdout.write(`${version}\n`);
99
99
  return;
100
100
  }
101
101
  const command = ALIASES[rawCommand] || rawCommand;
@@ -328,6 +328,7 @@ export function createGatewayCli(options) {
328
328
  `name = ${JSON.stringify(`${brand.product.displayName} Gateway`)}`,
329
329
  `base_url = ${JSON.stringify(gatewayRoutes.codex(gatewayUrl))}`,
330
330
  'wire_api = "responses"',
331
+ "supports_websockets = false",
331
332
  "",
332
333
  `[model_providers.${brand.cli.providerId}.auth]`,
333
334
  `command = ${JSON.stringify(auth.command)}`,
@@ -2,27 +2,14 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
- import { RUNTIME_BRAND } from "./runtimeBrand.js";
6
5
 
7
6
  const TENANT = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
8
7
  const APPLICATIONS_BY_TOOL = Object.freeze({
9
8
  claude: ["claude"],
10
9
  codex: ["codex", "chatgpt"],
11
10
  });
12
- // Managed-app discovery is channel scoped. The canary installer may replace
13
- // the machine-global runner, but its descriptors and credentials must remain
14
- // disjoint from stable state so reinstall/recovery never crosses channels.
15
- const BRAND_DISPLAY_PREFIX = RUNTIME_BRAND.channel === "canary"
16
- && RUNTIME_BRAND.apps.displayPrefix === "Impel"
17
- ? "Impel Canary"
18
- : RUNTIME_BRAND.apps.displayPrefix;
19
- const MAC_RUNNER = `/Applications/${BRAND_DISPLAY_PREFIX} Managed App Runner.app/Contents/Library/LaunchServices/impel-managed-runner`;
20
- const MAC_DESCRIPTOR_ROOT = `/Library/Application Support/${BRAND_DISPLAY_PREFIX}/Managed App Runner/descriptors`;
21
- const CONFIG_ROOT_SEGMENT = RUNTIME_BRAND.cli.configNamespace;
22
-
23
- function credentialPath(home, tenantId) {
24
- return path.join(home, ".config", CONFIG_ROOT_SEGMENT, "apps", "credentials", `${tenantId}.json`);
25
- }
11
+ const MAC_RUNNER = "/Applications/Impel Managed App Runner.app/Contents/Library/LaunchServices/impel-managed-runner";
12
+ const MAC_DESCRIPTOR_ROOT = "/Library/Application Support/Impel/Managed App Runner/descriptors";
26
13
 
27
14
  function regularFile(file) {
28
15
  try {
@@ -57,7 +44,7 @@ function parseCandidate(manifestPath, launcher, applicationId, platform, home) {
57
44
  || String(tenantId).includes("..")
58
45
  || manifest?.logicalApplicationId !== `${tenantId}.${applicationId}`
59
46
  ) return null;
60
- const credential = credentialPath(home, tenantId);
47
+ const credential = path.join(home, ".config", "impel", "apps", "credentials", `${tenantId}.json`);
61
48
  if (!regularFile(credential)) return null;
62
49
  return {
63
50
  tenantId,
@@ -114,7 +101,7 @@ function runnerEnvelopeCandidate(envelopePath, launcher, applicationId, home) {
114
101
  || String(tenantId).includes("..")
115
102
  || payload?.logicalApplicationId !== `${tenantId}.${applicationId}`
116
103
  ) return null;
117
- const credential = credentialPath(home, tenantId);
104
+ const credential = path.join(home, ".config", "impel", "apps", "credentials", `${tenantId}.json`);
118
105
  if (!regularFile(credential)) return null;
119
106
  return {
120
107
  tenantId,
@@ -198,7 +185,7 @@ export function installedManagedAppCandidates(tool, {
198
185
  platform = process.platform,
199
186
  home = os.homedir(),
200
187
  applicationsRoot = platform === "win32"
201
- ? path.join(process.env.ProgramFiles || "C:\\Program Files", BRAND_DISPLAY_PREFIX, "Apps")
188
+ ? path.join(process.env.ProgramFiles || "C:\\Program Files", "Impel", "Apps")
202
189
  : "/Applications",
203
190
  descriptorRoot = MAC_DESCRIPTOR_ROOT,
204
191
  runnerLauncher = MAC_RUNNER,
@@ -17,4 +17,6 @@
17
17
  // v47 persists `features.apps = false` in the managed ChatGPT desktop profile
18
18
  // so the hosted codex_apps connector stops handshaking against the gateway
19
19
  // without a credential (the same gate 0.20.61 passed to CLI sessions).
20
- export const CURRENT_CONFIG_VERSION = 47;
20
+ // v49 rewrites Codex `commandWindows` from PowerShell syntax to the
21
+ // `cmd.exe` syntax required by Codex's hook runner.
22
+ export const CURRENT_CONFIG_VERSION = 49;
package/src/posthog.js CHANGED
@@ -101,7 +101,7 @@ export const TELEMETRY_CONTRACT = Object.freeze({
101
101
  name: "cli_bug_report",
102
102
  origin: "server",
103
103
  properties: [
104
- { key: "channel", type: "string", required: true, values: ["latest", "next", "canary"] },
104
+ { key: "channel", type: "string", required: true, values: ["latest", "next"] },
105
105
  { key: "cliVersion", type: "string", required: true, maxLength: 32 },
106
106
  { key: "hasMessage", type: "boolean", required: true },
107
107
  {
@@ -123,7 +123,7 @@ export const TELEMETRY_CONTRACT = Object.freeze({
123
123
  name: "cli_command_run",
124
124
  origin: "cli",
125
125
  properties: [
126
- { key: "channel", type: "string", required: true, values: ["latest", "next", "canary"] },
126
+ { key: "channel", type: "string", required: true, values: ["latest", "next"] },
127
127
  { key: "cliVersion", type: "string", required: true, maxLength: 32 },
128
128
  { key: "command", type: "string", required: true, maxLength: 64 },
129
129
  {
@@ -5,7 +5,6 @@ import { tenantCliProfilePaths } from "../cliProfiles.js";
5
5
  import { redactSecretText } from "../config.js";
6
6
  import { runBuffer, runCapture, runInteractive, sleep } from "./process.js";
7
7
  import { runPaths } from "./state.js";
8
- import { RUNTIME_BRAND } from "../runtimeBrand.js";
9
8
 
10
9
  // LEGACY ONLY: this module's SSH upload and PAT-bearing remote configuration
11
10
  // are isolated to portable-legacy. Broker-backed modes use immutable workspace
@@ -16,7 +15,6 @@ const REMOTE_REPO = "/workspace/repo";
16
15
  const REMOTE_BUNDLE = "/tmp/impel-remote-repo.bundle";
17
16
  const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
18
17
  const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/u;
19
- export const REMOTE_CONFIG_ROOT = `/home/agent/.config/${RUNTIME_BRAND.cli.configNamespace}`;
20
18
 
21
19
  function git(args, options = {}) {
22
20
  return runCapture(process.env.IMPEL_REMOTE_GIT_BIN || "git", args, options).stdout.trim();
@@ -192,16 +190,16 @@ export function uploadRemoteConfiguration(state, {
192
190
  const environmentFile = Object.entries(selectedEnvironment)
193
191
  .map(([name, value]) => `export ${name}=${shellQuote(value)}`)
194
192
  .join("\n") + "\n";
195
- const profileRoot = `${REMOTE_CONFIG_ROOT}/cli/tenants/${tenant.tenantId}`;
193
+ const profileRoot = `/home/agent/.config/impel/cli/tenants/${tenant.tenantId}`;
196
194
  const claudeSettings = {
197
195
  permissions: { defaultMode: "bypassPermissions" },
198
196
  sandbox: { enabled: false },
199
197
  };
200
198
  const files = [
201
- { path: `${REMOTE_CONFIG_ROOT}/config.json`, contents: `${JSON.stringify(remoteConfig, null, 2)}\n` },
202
- { path: `${REMOTE_CONFIG_ROOT}/remote/env.sh`, contents: environmentFile },
199
+ { path: "/home/agent/.config/impel/config.json", contents: `${JSON.stringify(remoteConfig, null, 2)}\n` },
200
+ { path: "/home/agent/.config/impel/remote/env.sh", contents: environmentFile },
203
201
  {
204
- path: `${REMOTE_CONFIG_ROOT}/remote/run.json`,
202
+ path: "/home/agent/.config/impel/remote/run.json",
205
203
  contents: `${JSON.stringify({
206
204
  runId: state.runId,
207
205
  credentialTokenId: credential.tokenId,
@@ -216,12 +214,12 @@ const path = require("node:path");
216
214
  const payload = JSON.parse(fs.readFileSync(0, "utf8"));
217
215
  for (const file of payload.files) {
218
216
  const target = path.resolve(file.path);
219
- if (!target.startsWith(payload.root + "/")) throw new Error("refusing remote path");
217
+ if (!target.startsWith("/home/agent/.config/impel/")) throw new Error("refusing remote path");
220
218
  fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
221
219
  fs.writeFileSync(target, file.contents, { mode: 0o600 });
222
220
  fs.chmodSync(target, 0o600);
223
221
  }
224
- `, { files, root: REMOTE_CONFIG_ROOT });
222
+ `, { files });
225
223
  }
226
224
 
227
225
  export function uploadRepository(state, repository) {
@@ -296,8 +294,8 @@ export function transferSessionCheckpoint(state, { provider, sessionId, tenantId
296
294
  const profiles = tenantCliProfilePaths(tenantId);
297
295
  const localRoot = provider === "claude" ? profiles.claudeConfigDir : profiles.codexHome;
298
296
  const remoteRoot = provider === "claude"
299
- ? `${REMOTE_CONFIG_ROOT}/cli/tenants/${tenantId}/claude`
300
- : `${REMOTE_CONFIG_ROOT}/cli/tenants/${tenantId}/codex`;
297
+ ? `/home/agent/.config/impel/cli/tenants/${tenantId}/claude`
298
+ : `/home/agent/.config/impel/cli/tenants/${tenantId}/codex`;
301
299
  const files = collectSessionFiles(localRoot, sessionId);
302
300
  if (files.length === 0) {
303
301
  throw new Error(`could not find ${provider} session ${sessionId} in the selected Impel CLI profile`);
@@ -14,48 +14,22 @@ const SUPPORTED_COMMANDS = new Set([
14
14
  "models", "agents", "update", "use", "experimental",
15
15
  ]);
16
16
 
17
- /**
18
- * The closed per-channel origin row (canary release cycle, KTD9).
19
- *
20
- * A brand names its channel and every default origin must be that channel's
21
- * row: a canary build pointed at production, or a stable build pointed at
22
- * dev, is rejected at validation. `channel` is absent from the shipped
23
- * source's third-party brands and defaults to `stable`, which constrains
24
- * nothing beyond "not the canary row" so embedders' own origins stay valid.
25
- * `scripts/bake-canary-brand.mjs` rewrites DEFAULT below to the canary row at
26
- * publish time; the literals there are what it substitutes.
27
- */
28
- export const CHANNEL_ORIGINS = Object.freeze({
29
- stable: Object.freeze({
30
- gateway: "https://gateway.useimpel.com",
31
- sessions: "https://sessions.useimpel.com",
32
- controlPlane: "https://www.useimpel.com",
33
- }),
34
- canary: Object.freeze({
35
- gateway: "https://gateway.dev.useimpel.com",
36
- sessions: "https://sessions.dev.useimpel.com",
37
- controlPlane: "https://next.dev.useimpel.com",
38
- }),
39
- });
40
- const CANARY_ORIGIN_SET = new Set(Object.values(CHANNEL_ORIGINS.canary));
41
-
42
17
  const DEFAULT = Object.freeze({
43
18
  schemaVersion: 1,
44
- channel: "canary",
45
19
  product: Object.freeze({ id: "impel", displayName: "Impel" }),
46
20
  cli: Object.freeze({
47
21
  command: "impel",
48
22
  packageName: "impel-cli",
49
- configNamespace: "impel-canary",
23
+ configNamespace: "impel",
50
24
  providerId: "impel",
51
25
  managedMarker: "impel-cli",
52
26
  environmentPrefix: "IMPEL",
53
27
  }),
54
28
  auth: Object.freeze({ patPrefix: "impel_pat_", tenantPrefix: "impel_tenant_" }),
55
29
  tenant: Object.freeze({ defaultId: null, displayName: null }),
56
- gateway: Object.freeze({ defaultOrigin: "https://gateway.dev.useimpel.com" }),
57
- sessions: Object.freeze({ defaultOrigin: "https://sessions.dev.useimpel.com" }),
58
- controlPlane: Object.freeze({ defaultOrigin: "https://next.dev.useimpel.com" }),
30
+ gateway: Object.freeze({ defaultOrigin: "https://gateway.useimpel.com" }),
31
+ sessions: Object.freeze({ defaultOrigin: "https://sessions.useimpel.com" }),
32
+ controlPlane: Object.freeze({ defaultOrigin: "https://www.useimpel.com" }),
59
33
  updates: Object.freeze({ registry: null }),
60
34
  apps: Object.freeze({
61
35
  displayPrefix: "Impel",
@@ -100,32 +74,10 @@ function pathSegment(name, value) {
100
74
  return result;
101
75
  }
102
76
 
103
- function channel(value) {
104
- if (value == null) return "stable";
105
- if (value === "stable" || value === "canary") return value;
106
- throw new Error("impel-cli runtime brand channel must be stable or canary");
107
- }
108
-
109
- /** Validate one default origin against the brand's channel row (R10). */
110
- function channelOrigin(name, value, brandChannel) {
111
- const normalized = origin(name, value);
112
- const column = name.split(".")[0];
113
- if (brandChannel === "canary") {
114
- const expected = CHANNEL_ORIGINS.canary[column];
115
- if (normalized !== expected) {
116
- throw new Error(`impel-cli runtime brand ${name} must be ${expected} on the canary channel`);
117
- }
118
- } else if (CANARY_ORIGIN_SET.has(normalized)) {
119
- throw new Error(`impel-cli runtime brand ${name} is a canary origin; the stable channel must not carry it`);
120
- }
121
- return normalized;
122
- }
123
-
124
77
  export function validateRuntimeBrand(input) {
125
78
  if (!input || Array.isArray(input) || typeof input !== "object" || input.schemaVersion !== 1) {
126
79
  throw new Error("impel-cli runtime brand schemaVersion must be 1");
127
80
  }
128
- const brandChannel = channel(input.channel);
129
81
  const command = text("cli.command", input.cli?.command, SAFE_ID);
130
82
  const packageName = text("cli.packageName", input.cli?.packageName || command, SAFE_PACKAGE);
131
83
  if (path.isAbsolute(packageName)) throw new Error("impel-cli runtime brand cli.packageName is invalid");
@@ -140,7 +92,6 @@ export function validateRuntimeBrand(input) {
140
92
  }
141
93
  return Object.freeze({
142
94
  schemaVersion: 1,
143
- channel: brandChannel,
144
95
  product: Object.freeze({
145
96
  id: text("product.id", input.product?.id, SAFE_ID),
146
97
  displayName: text("product.displayName", input.product?.displayName),
@@ -161,15 +112,14 @@ export function validateRuntimeBrand(input) {
161
112
  defaultId: defaultTenant,
162
113
  displayName: input.tenant?.displayName == null ? defaultTenant : text("tenant.displayName", input.tenant.displayName),
163
114
  }),
164
- gateway: Object.freeze({ defaultOrigin: channelOrigin("gateway.defaultOrigin", input.gateway?.defaultOrigin, brandChannel) }),
115
+ gateway: Object.freeze({ defaultOrigin: origin("gateway.defaultOrigin", input.gateway?.defaultOrigin) }),
165
116
  sessions: Object.freeze({
166
- defaultOrigin: channelOrigin(
117
+ defaultOrigin: origin(
167
118
  "sessions.defaultOrigin",
168
119
  input.sessions?.defaultOrigin || input.gateway?.defaultOrigin,
169
- brandChannel,
170
120
  ),
171
121
  }),
172
- controlPlane: Object.freeze({ defaultOrigin: channelOrigin("controlPlane.defaultOrigin", input.controlPlane?.defaultOrigin, brandChannel) }),
122
+ controlPlane: Object.freeze({ defaultOrigin: origin("controlPlane.defaultOrigin", input.controlPlane?.defaultOrigin) }),
173
123
  updates: Object.freeze({
174
124
  registry: input.updates?.registry == null
175
125
  ? null
@@ -208,15 +158,6 @@ export function brandedEnvironmentName(suffix) {
208
158
  return `${RUNTIME_BRAND.cli.environmentPrefix}_${suffix}`;
209
159
  }
210
160
 
211
- /**
212
- * The line `--version` prints. Off the stable channel it names the channel so
213
- * a tester can tell a canary process from a stable one at a glance (R8); the
214
- * stable output is the bare version, byte-identical to before channels existed.
215
- */
216
- export function versionLine(version) {
217
- return RUNTIME_BRAND.channel === "stable" ? String(version) : `${version} (${RUNTIME_BRAND.channel})`;
218
- }
219
-
220
161
  export function brandedText(value) {
221
162
  return String(value)
222
163
  .replaceAll("impel-cli", `${RUNTIME_BRAND.cli.command}-cli`)
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import crypto from "node:crypto";
3
3
  import path from "node:path";
4
4
 
5
+ import { escapeWindowsBareArgument } from "./nativeProcess.js";
5
6
  import { impelCliInvocation } from "./selfInvocation.js";
6
7
  import { RUNTIME_BRAND } from "./runtimeBrand.js";
7
8
 
@@ -146,12 +147,6 @@ function shellQuote(value) {
146
147
  return `'${String(value).replaceAll("'", `'"'"'`)}'`;
147
148
  }
148
149
 
149
- function powershellQuote(value) {
150
- const text = String(value);
151
- if (/[\r\n\0]/u.test(text)) throw new Error("cannot safely write this Windows hook command");
152
- return `'${text.replaceAll("'", "''")}'`;
153
- }
154
-
155
150
  function canonicalize(value) {
156
151
  if (Array.isArray(value)) return value.map(canonicalize);
157
152
  if (!value || typeof value !== "object") return value;
@@ -214,11 +209,9 @@ export function ensureCodexSessionHooks(codexHome, tenantId, surface = "codex_cl
214
209
  const document = readJsonObject(hooksPath);
215
210
  const invocation = managedInvocation("codex", surface, tenantId);
216
211
  const unixCommand = [invocation.command, ...invocation.args].map(shellQuote).join(" ");
217
- const windowsTokens = [invocation.command, ...invocation.args].map(powershellQuote).join(" ");
218
- // Codex runs commandWindows through the session's normal Windows shell,
219
- // which is PowerShell. A quoted executable is only invoked when prefixed by
220
- // the call operator; cmd.exe's doubled outer-quote form is invalid here.
221
- const windowsCommand = `& ${windowsTokens}`;
212
+ const windowsCommand = [invocation.command, ...invocation.args]
213
+ .map(escapeWindowsBareArgument)
214
+ .join(" ");
222
215
  const handler = {
223
216
  type: "command",
224
217
  command: unixCommand,