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 +1 -1
- package/RELEASE_NOTES.md +27 -0
- package/package.json +1 -1
- package/src/cli.js +1 -2
- package/src/cliProfiles.js +9 -6
- package/src/codexSetup.js +14 -5
- package/src/commands/auth.js +6 -22
- package/src/commands/update.js +2 -4
- package/src/config.js +5 -48
- package/src/extension/index.js +2 -2
- package/src/gateway/index.js +1 -0
- package/src/managedApps.js +5 -18
- package/src/managedProfileVersion.js +3 -1
- package/src/posthog.js +2 -2
- package/src/remote/transfer.js +8 -10
- package/src/runtimeBrand.js +7 -66
- package/src/sessionHooks.js +4 -11
- package/src/updates.js +3 -12
- package/src/windowsGit.js +1 -2
- package/scripts/bake-canary-brand.mjs +0 -92
- package/scripts/canary-version.mjs +0 -173
- package/scripts/npm-dist-tags.mjs +0 -134
- package/scripts/pack-canary.mjs +0 -32
- package/scripts/pack-release.mjs +0 -27
- package/scripts/prepare-canary.mjs +0 -28
- package/scripts/release-gate.mjs +0 -133
- package/scripts/release-metadata.mjs +0 -18
- package/scripts/required-checks.mjs +0 -126
- package/scripts/verify-canary-install.mjs +0 -62
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`
|
|
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
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(
|
|
96
|
+
console.log(pkg.version);
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
// Accepts "claude" | "codex" | "all" | undefined; returns a normalized target
|
package/src/cliProfiles.js
CHANGED
|
@@ -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
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
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)}`,
|
package/src/commands/auth.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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)
|
package/src/commands/update.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
convergenceSummaryDiagnostics,
|
|
16
16
|
readRecentConvergenceSummary,
|
|
17
17
|
} from "../convergenceSummary.js";
|
|
18
|
-
import { brandedEnvironmentName, brandedText, RUNTIME_BRAND
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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). */
|
package/src/extension/index.js
CHANGED
|
@@ -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
|
|
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(`${
|
|
98
|
+
process.stdout.write(`${version}\n`);
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
const command = ALIASES[rawCommand] || rawCommand;
|
package/src/gateway/index.js
CHANGED
|
@@ -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)}`,
|
package/src/managedApps.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
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 =
|
|
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 =
|
|
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",
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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
|
{
|
package/src/remote/transfer.js
CHANGED
|
@@ -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 =
|
|
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:
|
|
202
|
-
{ path:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
-
?
|
|
300
|
-
:
|
|
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`);
|
package/src/runtimeBrand.js
CHANGED
|
@@ -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
|
|
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.
|
|
57
|
-
sessions: Object.freeze({ defaultOrigin: "https://sessions.
|
|
58
|
-
controlPlane: Object.freeze({ defaultOrigin: "https://
|
|
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:
|
|
115
|
+
gateway: Object.freeze({ defaultOrigin: origin("gateway.defaultOrigin", input.gateway?.defaultOrigin) }),
|
|
165
116
|
sessions: Object.freeze({
|
|
166
|
-
defaultOrigin:
|
|
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:
|
|
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`)
|
package/src/sessionHooks.js
CHANGED
|
@@ -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
|
|
218
|
-
|
|
219
|
-
|
|
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,
|