impel-cli 0.16.1 → 0.16.2
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 +32 -7
- package/package.json +1 -1
- package/src/apps.js +20 -1
- package/src/claudeSandbox.js +80 -0
- package/src/claudeSetup.js +13 -0
- package/src/cliProfiles.js +2 -0
- package/src/commands/use.js +7 -0
- package/src/gateway/index.js +2 -0
package/README.md
CHANGED
|
@@ -551,19 +551,37 @@ impel codex exec --json "review the current diff"
|
|
|
551
551
|
switch their native profiles in place; the isolated launchers are the safer
|
|
552
552
|
default when personal and Impel sessions should coexist.
|
|
553
553
|
|
|
554
|
+
Every Impel-managed Claude configuration uses the same strict sandbox defaults:
|
|
555
|
+
the isolated `impel claude` profile, each tenant's Claude Desktop Code profile,
|
|
556
|
+
the native profile while `impel use gateway claude` is active, and branded
|
|
557
|
+
gateway-only CLI profiles. App config version 12 makes existing tenant profiles
|
|
558
|
+
refresh onto this policy after the CLI update.
|
|
559
|
+
|
|
554
560
|
### `impel use gateway [claude|codex|all]` (alias `impel on`)
|
|
555
561
|
|
|
556
562
|
Turns **on** gateway mode for the given tool (default `all`). Requires `impel auth`
|
|
557
563
|
first.
|
|
558
564
|
|
|
559
565
|
**Claude Code** — merges into `~/.claude/settings.json` (creating it if needed,
|
|
560
|
-
leaving
|
|
566
|
+
leaving permissions, hooks, other env vars, and custom sandbox filesystem/network
|
|
567
|
+
rules untouched). Gateway mode enables Claude's strict auto-allow sandbox by
|
|
568
|
+
default: Bash runs inside the OS sandbox without routine prompts, the
|
|
569
|
+
`dangerouslyDisableSandbox` escape hatch is disabled, and supported platforms
|
|
570
|
+
fail closed if isolation cannot initialize. Native Windows keeps startup
|
|
571
|
+
available because Claude's sandbox is supported only on macOS, Linux, and WSL2.
|
|
561
572
|
|
|
562
573
|
```json
|
|
563
574
|
{
|
|
564
575
|
"apiKeyHelper": "impel token",
|
|
565
576
|
"env": {
|
|
566
577
|
"ANTHROPIC_BASE_URL": "https://gateway.useimpel.com/anthropic"
|
|
578
|
+
},
|
|
579
|
+
"sandbox": {
|
|
580
|
+
"enabled": true,
|
|
581
|
+
"failIfUnavailable": true,
|
|
582
|
+
"autoAllowBashIfSandboxed": true,
|
|
583
|
+
"allowUnsandboxedCommands": false,
|
|
584
|
+
"excludedCommands": []
|
|
567
585
|
}
|
|
568
586
|
}
|
|
569
587
|
```
|
|
@@ -597,9 +615,9 @@ refresh_interval_ms = 300000
|
|
|
597
615
|
```
|
|
598
616
|
|
|
599
617
|
Before overwriting anything, `impel` **backs up** what was there (any prior
|
|
600
|
-
`apiKeyHelper
|
|
601
|
-
value) into `~/.config/impel/config.json`, so `impel use account`
|
|
602
|
-
exactly. If `config.toml` already has a `[model_providers.impel]` table that
|
|
618
|
+
`apiKeyHelper`, `ANTHROPIC_BASE_URL`, Claude sandbox policy, and the prior Codex
|
|
619
|
+
`model_provider` value) into `~/.config/impel/config.json`, so `impel use account`
|
|
620
|
+
can restore it exactly. If `config.toml` already has a `[model_providers.impel]` table that
|
|
603
621
|
wasn't written by this tool, `impel` refuses to touch the file and tells you to
|
|
604
622
|
resolve it manually rather than risk emitting invalid/duplicate TOML.
|
|
605
623
|
|
|
@@ -908,7 +926,7 @@ impel status # see where each tool stands right now
|
|
|
908
926
|
```
|
|
909
927
|
|
|
910
928
|
How the clean revert is guaranteed: `impel on` records the pre-existing values
|
|
911
|
-
(your own `apiKeyHelper` / `ANTHROPIC_BASE_URL
|
|
929
|
+
(your own `apiKeyHelper` / `ANTHROPIC_BASE_URL` / Claude sandbox policy, and Codex's prior
|
|
912
930
|
`model_provider`) into `~/.config/impel/config.json` **before** overwriting them,
|
|
913
931
|
and only ever captures a real *account-mode* value (re-running `impel on` never
|
|
914
932
|
clobbers the backup with Impel's own values). `impel off` removes only keys that
|
|
@@ -916,8 +934,9 @@ still point at Impel and restores those backups — so a full `on` → `off`
|
|
|
916
934
|
round-trip leaves `settings.json` and `config.toml` byte-for-byte back to where
|
|
917
935
|
they started, with all your unrelated keys and tables intact.
|
|
918
936
|
|
|
919
|
-
If you'd rather revert by hand: delete `apiKeyHelper
|
|
920
|
-
`ANTHROPIC_BASE_URL
|
|
937
|
+
If you'd rather revert by hand: delete `apiKeyHelper`, the Impel
|
|
938
|
+
`ANTHROPIC_BASE_URL`, and the Impel-managed sandbox keys from
|
|
939
|
+
`~/.claude/settings.json`; delete the block between the
|
|
921
940
|
`# >>> impel-cli managed block ... >>>` / `# <<< impel-cli managed block <<<`
|
|
922
941
|
markers in `~/.codex/config.toml` and reset `model_provider`; and
|
|
923
942
|
`rm -rf ~/.config/impel` to forget the PAT + gateway entirely.
|
|
@@ -969,6 +988,12 @@ OpenAI Responses front door for SDK/Eve traffic and may normalize the body
|
|
|
969
988
|
- https://developers.openai.com/codex/auth
|
|
970
989
|
- https://developers.openai.com/codex/ide
|
|
971
990
|
|
|
991
|
+
## Sources consulted for the Claude sandbox config
|
|
992
|
+
|
|
993
|
+
- https://code.claude.com/docs/en/settings#sandbox-settings
|
|
994
|
+
- https://code.claude.com/docs/en/sandboxing#configure-the-sandbox-for-your-organization
|
|
995
|
+
- https://github.com/anthropics/claude-code/blob/v2.1.205/examples/settings/settings-bash-sandbox.json
|
|
996
|
+
|
|
972
997
|
## Sources consulted for concurrent tenant apps
|
|
973
998
|
|
|
974
999
|
- Electron v43.1.0 release: https://github.com/electron/electron/releases/tag/v43.1.0
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -13,6 +13,7 @@ import { normalizeTenantId } from "./tenants.js";
|
|
|
13
13
|
import { impelCliInvocation } from "./selfInvocation.js";
|
|
14
14
|
import { crossAppModelsEnabled, redactSecretText } from "./config.js";
|
|
15
15
|
import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
|
|
16
|
+
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
16
17
|
|
|
17
18
|
export const CLAUDE_CONFIG_ID = "1ced0000-0000-4000-8000-000000000001";
|
|
18
19
|
const CHATGPT_CONFIG_START = "# >>> impel app managed gateway >>>";
|
|
@@ -162,7 +163,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
162
163
|
|
|
163
164
|
// Bump when the written config/manifest schema changes; a mismatch forces the
|
|
164
165
|
// slow open path (and thus a full config rewrite) after a CLI update.
|
|
165
|
-
export const CURRENT_CONFIG_VERSION =
|
|
166
|
+
export const CURRENT_CONFIG_VERSION = 12;
|
|
166
167
|
|
|
167
168
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
168
169
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -576,6 +577,7 @@ export function installManagedAppFiles({
|
|
|
576
577
|
if (target === "claude") {
|
|
577
578
|
migrateLegacyClaudeAppSessions(paths.claude.userData, homeDir);
|
|
578
579
|
writeClaudeDefaultApp(paths);
|
|
580
|
+
writeClaudeCodeSettings(paths);
|
|
579
581
|
writeClaudeConfig(paths, config, models);
|
|
580
582
|
}
|
|
581
583
|
else writeChatGPTConfig(paths, config, models, vendorCodexModels, chatgptInvocations);
|
|
@@ -859,6 +861,23 @@ function writeClaudeConfig(paths, config, models) {
|
|
|
859
861
|
}, null, 2) + "\n", 0o600);
|
|
860
862
|
}
|
|
861
863
|
|
|
864
|
+
function writeClaudeCodeSettings(paths) {
|
|
865
|
+
const settingsPath = path.join(paths.claude.userData, "settings.json");
|
|
866
|
+
let settings = {};
|
|
867
|
+
if (fs.existsSync(settingsPath)) {
|
|
868
|
+
try {
|
|
869
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
870
|
+
} catch {
|
|
871
|
+
throw new Error(`Claude Code settings are not valid JSON: ${settingsPath}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
875
|
+
throw new Error(`Claude Code settings must contain a JSON object: ${settingsPath}`);
|
|
876
|
+
}
|
|
877
|
+
applyImpelClaudeSandbox(settings);
|
|
878
|
+
writeAtomic(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 0o600);
|
|
879
|
+
}
|
|
880
|
+
|
|
862
881
|
function writeClaudeDefaultApp(paths) {
|
|
863
882
|
const configPath = path.join(paths.claude.userData, "claude_desktop_config.json");
|
|
864
883
|
let current = {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const MANAGED_SANDBOX_KEYS = Object.freeze([
|
|
2
|
+
"enabled",
|
|
3
|
+
"failIfUnavailable",
|
|
4
|
+
"autoAllowBashIfSandboxed",
|
|
5
|
+
"allowUnsandboxedCommands",
|
|
6
|
+
"excludedCommands",
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
function isJsonObject(value) {
|
|
10
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sameJsonValue(left, right) {
|
|
14
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Claude Code's strict, auto-allow sandbox profile.
|
|
19
|
+
*
|
|
20
|
+
* Native Windows does not implement Claude's sandbox. Keep it enabled so the
|
|
21
|
+
* same profile becomes effective in WSL, but do not make native Windows fail
|
|
22
|
+
* at startup solely because the platform cannot initialize it.
|
|
23
|
+
*/
|
|
24
|
+
export function impelClaudeSandboxPolicy(platform = process.platform) {
|
|
25
|
+
return {
|
|
26
|
+
enabled: true,
|
|
27
|
+
failIfUnavailable: platform !== "win32",
|
|
28
|
+
autoAllowBashIfSandboxed: true,
|
|
29
|
+
allowUnsandboxedCommands: false,
|
|
30
|
+
excludedCommands: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Apply the managed sandbox keys while preserving unrelated sandbox config. */
|
|
35
|
+
export function applyImpelClaudeSandbox(settings, platform = process.platform) {
|
|
36
|
+
const current = isJsonObject(settings.sandbox) ? settings.sandbox : {};
|
|
37
|
+
settings.sandbox = {
|
|
38
|
+
...current,
|
|
39
|
+
...impelClaudeSandboxPolicy(platform),
|
|
40
|
+
};
|
|
41
|
+
return settings.sandbox;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Capture the exact pre-Impel value for reversible native-profile switching. */
|
|
45
|
+
export function captureClaudeSandbox(settings) {
|
|
46
|
+
return Object.hasOwn(settings, "sandbox")
|
|
47
|
+
? { present: true, value: settings.sandbox }
|
|
48
|
+
: { present: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Restore only managed keys that still hold Impel's values. User changes made
|
|
53
|
+
* while gateway mode was active, plus unrelated filesystem/network keys, win.
|
|
54
|
+
*/
|
|
55
|
+
export function restoreClaudeSandbox(settings, snapshot, platform = process.platform) {
|
|
56
|
+
if (!isJsonObject(settings.sandbox)) return false;
|
|
57
|
+
const policy = impelClaudeSandboxPolicy(platform);
|
|
58
|
+
const prior = snapshot?.present === true && isJsonObject(snapshot.value)
|
|
59
|
+
? snapshot.value
|
|
60
|
+
: null;
|
|
61
|
+
const sandbox = { ...settings.sandbox };
|
|
62
|
+
let changed = false;
|
|
63
|
+
|
|
64
|
+
for (const key of MANAGED_SANDBOX_KEYS) {
|
|
65
|
+
if (!Object.hasOwn(sandbox, key) || !sameJsonValue(sandbox[key], policy[key])) continue;
|
|
66
|
+
if (prior && Object.hasOwn(prior, key)) sandbox[key] = prior[key];
|
|
67
|
+
else delete sandbox[key];
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!changed) return false;
|
|
72
|
+
if (Object.keys(sandbox).length > 0 || (snapshot?.present === true && prior)) {
|
|
73
|
+
settings.sandbox = sandbox;
|
|
74
|
+
} else if (snapshot?.present === true) {
|
|
75
|
+
settings.sandbox = snapshot.value;
|
|
76
|
+
} else {
|
|
77
|
+
delete settings.sandbox;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
package/src/claudeSetup.js
CHANGED
|
@@ -10,6 +10,11 @@ import os from "node:os";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
|
|
12
12
|
import { IMPEL_MANAGED_MCP_ENV, impelMcpInvocation } from "./selfInvocation.js";
|
|
13
|
+
import {
|
|
14
|
+
applyImpelClaudeSandbox,
|
|
15
|
+
captureClaudeSandbox,
|
|
16
|
+
restoreClaudeSandbox,
|
|
17
|
+
} from "./claudeSandbox.js";
|
|
13
18
|
|
|
14
19
|
export const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
15
20
|
export const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
@@ -123,12 +128,14 @@ export function applyClaudeGateway(gatewayUrl) {
|
|
|
123
128
|
|
|
124
129
|
const priorApiKeyHelper = settings.apiKeyHelper;
|
|
125
130
|
const priorBaseUrl = settings.env?.ANTHROPIC_BASE_URL;
|
|
131
|
+
const priorSandbox = captureClaudeSandbox(settings);
|
|
126
132
|
|
|
127
133
|
settings.apiKeyHelper = IMPEL_API_KEY_HELPER;
|
|
128
134
|
settings.env = {
|
|
129
135
|
...(settings.env || {}),
|
|
130
136
|
ANTHROPIC_BASE_URL: impelClaudeBaseUrl(gatewayUrl),
|
|
131
137
|
};
|
|
138
|
+
const sandbox = applyImpelClaudeSandbox(settings);
|
|
132
139
|
|
|
133
140
|
writeSettings(settings);
|
|
134
141
|
userConfig.mcpServers = {
|
|
@@ -143,6 +150,8 @@ export function applyClaudeGateway(gatewayUrl) {
|
|
|
143
150
|
baseUrl: settings.env.ANTHROPIC_BASE_URL,
|
|
144
151
|
priorApiKeyHelper,
|
|
145
152
|
priorBaseUrl,
|
|
153
|
+
priorSandbox,
|
|
154
|
+
sandbox,
|
|
146
155
|
};
|
|
147
156
|
}
|
|
148
157
|
|
|
@@ -179,6 +188,9 @@ export function revertClaudeGateway(gatewayUrl, backup = {}) {
|
|
|
179
188
|
changed = true;
|
|
180
189
|
}
|
|
181
190
|
|
|
191
|
+
const restoredSandbox = restoreClaudeSandbox(settings, backup.sandbox);
|
|
192
|
+
if (restoredSandbox) changed = true;
|
|
193
|
+
|
|
182
194
|
if (changed) writeSettings(settings);
|
|
183
195
|
|
|
184
196
|
if (isImpelMcpServer(userConfig.mcpServers?.[IMPEL_MCP_SERVER_NAME])) {
|
|
@@ -201,6 +213,7 @@ export function revertClaudeGateway(gatewayUrl, backup = {}) {
|
|
|
201
213
|
removedHelper,
|
|
202
214
|
removedBaseUrl,
|
|
203
215
|
removedMcpServer,
|
|
216
|
+
restoredSandbox,
|
|
204
217
|
restoredApiKeyHelper: removedHelper && backup.apiKeyHelper != null ? backup.apiKeyHelper : null,
|
|
205
218
|
restoredBaseUrl: removedBaseUrl && backup.ANTHROPIC_BASE_URL != null ? backup.ANTHROPIC_BASE_URL : null,
|
|
206
219
|
};
|
package/src/cliProfiles.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { CONFIG_DIR } from "./config.js";
|
|
12
12
|
import { normalizeTenantId } from "./tenants.js";
|
|
13
13
|
import { impelCliInvocation, impelMcpInvocation } from "./selfInvocation.js";
|
|
14
|
+
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
14
15
|
|
|
15
16
|
export const IMPEL_CLI_PROFILES_DIR = path.join(CONFIG_DIR, "cli");
|
|
16
17
|
|
|
@@ -118,6 +119,7 @@ export function ensureImpelClaudeProfile(gatewayUrl, tenantId, { crossAppModels
|
|
|
118
119
|
delete managedEnvironment.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY;
|
|
119
120
|
}
|
|
120
121
|
settings.env = managedEnvironment;
|
|
122
|
+
applyImpelClaudeSandbox(settings);
|
|
121
123
|
userConfig.mcpServers = {
|
|
122
124
|
...(userConfig.mcpServers &&
|
|
123
125
|
typeof userConfig.mcpServers === "object" &&
|
package/src/commands/use.js
CHANGED
|
@@ -76,9 +76,13 @@ async function useGateway({ target, app }) {
|
|
|
76
76
|
if (!isImpelClaudeBaseUrl(result.priorBaseUrl, gatewayUrl)) {
|
|
77
77
|
backup.ANTHROPIC_BASE_URL = result.priorBaseUrl ?? null;
|
|
78
78
|
}
|
|
79
|
+
if (!Object.hasOwn(backup, "sandbox")) {
|
|
80
|
+
backup.sandbox = result.priorSandbox;
|
|
81
|
+
}
|
|
79
82
|
console.log(`Claude Code -> GATEWAY (${result.path})`);
|
|
80
83
|
console.log(` apiKeyHelper = "${result.apiKeyHelper}"`);
|
|
81
84
|
console.log(` env.ANTHROPIC_BASE_URL = "${result.baseUrl}"`);
|
|
85
|
+
console.log(` sandbox = strict auto-allow (${result.sandbox.failIfUnavailable ? "fail closed" : "platform fallback"})`);
|
|
82
86
|
console.log(` MCP server = "impel" (direct Impel CLI invocation)`);
|
|
83
87
|
}
|
|
84
88
|
|
|
@@ -167,6 +171,9 @@ function useAccount({ target, app }) {
|
|
|
167
171
|
if (result.removedMcpServer) {
|
|
168
172
|
console.log(` removed Impel MCP server`);
|
|
169
173
|
}
|
|
174
|
+
if (result.restoredSandbox) {
|
|
175
|
+
console.log(` restored prior Claude sandbox settings`);
|
|
176
|
+
}
|
|
170
177
|
}
|
|
171
178
|
if (config?.backups) delete config.backups.claude;
|
|
172
179
|
}
|
package/src/gateway/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import readline from "node:readline";
|
|
6
6
|
|
|
7
7
|
import { nativeCommandInvocation } from "../nativeProcess.js";
|
|
8
|
+
import { applyImpelClaudeSandbox } from "../claudeSandbox.js";
|
|
8
9
|
|
|
9
10
|
const SAFE_ID = /^[a-z][a-z0-9-]{2,31}$/u;
|
|
10
11
|
const SAFE_NAMESPACE = /^[a-z][a-z0-9_-]{1,31}$/u;
|
|
@@ -301,6 +302,7 @@ export function createGatewayCli(options) {
|
|
|
301
302
|
};
|
|
302
303
|
delete settings.env.ANTHROPIC_API_KEY;
|
|
303
304
|
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
305
|
+
applyImpelClaudeSandbox(settings);
|
|
304
306
|
writePrivateFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
305
307
|
return { configDir, settingsPath };
|
|
306
308
|
}
|