impel-cli 0.16.3 → 0.16.5
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 +65 -18
- package/package.json +1 -1
- package/src/apps.js +8 -2
- package/src/claudeSandbox.js +81 -18
- package/src/cli.js +5 -0
- package/src/cliProfiles.js +5 -1
- package/src/codexSecurity.js +43 -1
- package/src/codexSetup.js +146 -3
- package/src/commands/sessions.js +114 -0
- package/src/commands/use.js +14 -0
- package/src/config.js +8 -3
- package/src/gateway/index.js +2 -1
- package/src/sessionCollector.js +996 -0
- package/src/sessionHooks.js +242 -0
package/README.md
CHANGED
|
@@ -25,6 +25,32 @@ of the protected Store directory and launches it with tenant-specific Codex and
|
|
|
25
25
|
browser profiles. Each approach keeps the user's normal app profile and
|
|
26
26
|
signed-in account untouched.
|
|
27
27
|
|
|
28
|
+
Every Impel-managed Claude Code, Codex CLI, Claude Desktop, and ChatGPT/Codex
|
|
29
|
+
Desktop profile also installs tenant-scoped lifecycle hooks for cloud session
|
|
30
|
+
persistence. Hook input is first written to a private local outbox, so an
|
|
31
|
+
offline network, interrupted hook, force-quit, or client restart cannot make a
|
|
32
|
+
provider turn fail. The next hook invocation retries any pending batches,
|
|
33
|
+
including batches left by an older session. Remote payloads are stored as
|
|
34
|
+
independent zstd frames by `impel-sessions`; the local collector sends bounded
|
|
35
|
+
newline-complete transcript deltas and a compact lifecycle ledger. Pending data
|
|
36
|
+
is capped at 256 MiB or 2,048 batches per session by default; corrupt or
|
|
37
|
+
permanently invalid batches are quarantined so they cannot block later data.
|
|
38
|
+
Set `IMPEL_SESSIONS_MAX_OUTBOX_BYTES` or `IMPEL_SESSIONS_MAX_OUTBOX_BATCHES` to
|
|
39
|
+
lower those local limits.
|
|
40
|
+
|
|
41
|
+
The service attributes each session to the PAT-resolved user and selected
|
|
42
|
+
tenant and records the provider, CLI/desktop surface, provider session ID, and
|
|
43
|
+
repository metadata. Set `IMPEL_TASK_ID` before launching a client to link new
|
|
44
|
+
sessions explicitly to an Impel task for cross-device discovery. The collector
|
|
45
|
+
never writes the configured PAT into hook configuration, and it redacts Impel
|
|
46
|
+
PAT/tenant credentials found in hook input or transcript content before
|
|
47
|
+
spooling. Task linkage is retried independently and cannot block payload
|
|
48
|
+
persistence when the current PAT lacks the `tasks` scope.
|
|
49
|
+
|
|
50
|
+
Codex hook trust is pinned to the exact generated handler and its array index.
|
|
51
|
+
If you manually reorder the managed group in `hooks.json`, rerun `impel setup`,
|
|
52
|
+
`impel app update`, or relaunch the isolated CLI to regenerate the trust entry.
|
|
53
|
+
|
|
28
54
|
## Gateway-only vendor package
|
|
29
55
|
|
|
30
56
|
White-labelled gateway launchers import the deliberately narrow
|
|
@@ -551,10 +577,11 @@ impel codex exec --json "review the current diff"
|
|
|
551
577
|
switch their native profiles in place; the isolated launchers are the safer
|
|
552
578
|
default when personal and Impel sessions should coexist.
|
|
553
579
|
|
|
554
|
-
Every Impel-managed Claude configuration uses the same strict sandbox defaults
|
|
580
|
+
Every Impel-managed Claude configuration uses the same strict sandbox defaults
|
|
581
|
+
with unrestricted outbound network access:
|
|
555
582
|
the isolated `impel claude` profile, each tenant's Claude Desktop Code profile,
|
|
556
583
|
the native profile while `impel use gateway claude` is active, and branded
|
|
557
|
-
gateway-only CLI profiles. App config version
|
|
584
|
+
gateway-only CLI profiles. App config version 15 also pins managed Claude
|
|
558
585
|
Desktop profiles to third-party deployment mode, enables the 3P Chat tab so
|
|
559
586
|
Code's "Back to Claude" route has a valid destination, and removes the unusable
|
|
560
587
|
Claude.ai sign-in path. Existing tenant profiles refresh onto these policies
|
|
@@ -566,12 +593,16 @@ Turns **on** gateway mode for the given tool (default `all`). Requires `impel au
|
|
|
566
593
|
first.
|
|
567
594
|
|
|
568
595
|
**Claude Code** — merges into `~/.claude/settings.json` (creating it if needed,
|
|
569
|
-
leaving
|
|
570
|
-
rules
|
|
596
|
+
leaving existing permission rules, hooks, other env vars, and custom sandbox
|
|
597
|
+
filesystem/network rules intact). Gateway mode enables Claude's strict auto-allow sandbox by
|
|
571
598
|
default: Bash runs inside the OS sandbox without routine prompts, the
|
|
572
599
|
`dangerouslyDisableSandbox` escape hatch is disabled, and supported platforms
|
|
573
600
|
fail closed if isolation cannot initialize. Native Windows keeps startup
|
|
574
601
|
available because Claude's sandbox is supported only on macOS, Linux, and WSL2.
|
|
602
|
+
A global `WebFetch(domain:*)` allow rule opens every outbound destination for Bash commands
|
|
603
|
+
while retaining filesystem isolation. Explicit deny rules remain authoritative.
|
|
604
|
+
On macOS, `enableWeakerNetworkIsolation` lets Go-based network tools reach the
|
|
605
|
+
system TLS trust service while remaining sandboxed.
|
|
575
606
|
|
|
576
607
|
```json
|
|
577
608
|
{
|
|
@@ -579,12 +610,16 @@ available because Claude's sandbox is supported only on macOS, Linux, and WSL2.
|
|
|
579
610
|
"env": {
|
|
580
611
|
"ANTHROPIC_BASE_URL": "https://gateway.useimpel.com/anthropic"
|
|
581
612
|
},
|
|
613
|
+
"permissions": {
|
|
614
|
+
"allow": ["WebFetch(domain:*)"]
|
|
615
|
+
},
|
|
582
616
|
"sandbox": {
|
|
583
617
|
"enabled": true,
|
|
584
618
|
"failIfUnavailable": true,
|
|
585
619
|
"autoAllowBashIfSandboxed": true,
|
|
586
620
|
"allowUnsandboxedCommands": false,
|
|
587
|
-
"excludedCommands": []
|
|
621
|
+
"excludedCommands": [],
|
|
622
|
+
"enableWeakerNetworkIsolation": true
|
|
588
623
|
}
|
|
589
624
|
}
|
|
590
625
|
```
|
|
@@ -596,7 +631,9 @@ available because Claude's sandbox is supported only on macOS, Linux, and WSL2.
|
|
|
596
631
|
as Claude Code's `apiKeyHelper`), so we reuse `impel token` there too instead of
|
|
597
632
|
writing the PAT into `config.toml` or `~/.codex/auth.json`. The block written
|
|
598
633
|
(fenced by markers, so everything else in your `config.toml` — other
|
|
599
|
-
`model_providers`, `mcp_servers`, sandbox policy — is left alone)
|
|
634
|
+
`model_providers`, `mcp_servers`, and unrelated sandbox policy — is left alone).
|
|
635
|
+
Codex defaults to the `workspace-write` filesystem sandbox with unrestricted
|
|
636
|
+
direct outbound access; its optional network proxy stays disabled:
|
|
600
637
|
|
|
601
638
|
```toml
|
|
602
639
|
model_provider = "impel"
|
|
@@ -604,6 +641,9 @@ model_provider = "impel"
|
|
|
604
641
|
# >>> impel-cli managed block (model_providers.impel) >>>
|
|
605
642
|
# Generated by `impel use gateway codex`. Safe to re-run; do not hand-edit
|
|
606
643
|
# the lines between the markers above/below, they'll be overwritten.
|
|
644
|
+
sandbox_mode = "workspace-write"
|
|
645
|
+
sandbox_workspace_write.network_access = true
|
|
646
|
+
|
|
607
647
|
[model_providers.impel]
|
|
608
648
|
name = "Impel Gateway"
|
|
609
649
|
base_url = "https://gateway.useimpel.com/chatgpt_passthrough/backend-api/codex"
|
|
@@ -618,8 +658,9 @@ refresh_interval_ms = 300000
|
|
|
618
658
|
```
|
|
619
659
|
|
|
620
660
|
Before overwriting anything, `impel` **backs up** what was there (any prior
|
|
621
|
-
`apiKeyHelper`, `ANTHROPIC_BASE_URL`, Claude sandbox policy, and the
|
|
622
|
-
`model_provider
|
|
661
|
+
`apiKeyHelper`, `ANTHROPIC_BASE_URL`, Claude sandbox/permission policy, and the
|
|
662
|
+
prior Codex `model_provider`, sandbox mode, and workspace network values) into
|
|
663
|
+
`~/.config/impel/config.json`, so `impel use account`
|
|
623
664
|
can restore it exactly. If `config.toml` already has a `[model_providers.impel]` table that
|
|
624
665
|
wasn't written by this tool, `impel` refuses to touch the file and tells you to
|
|
625
666
|
resolve it manually rather than risk emitting invalid/duplicate TOML.
|
|
@@ -632,11 +673,14 @@ removes **only** the Impel-added bits, never your own config:
|
|
|
632
673
|
- **Claude Code** — removes `apiKeyHelper` only if it's `"impel token"`, and
|
|
633
674
|
`env.ANTHROPIC_BASE_URL` only if it equals `<gateway>/anthropic`. Then restores
|
|
634
675
|
whatever those keys held before gateway mode (from the backup). If there was
|
|
635
|
-
nothing before, they're deleted (and an emptied `env` is dropped).
|
|
676
|
+
nothing before, they're deleted (and an emptied `env` is dropped). It also
|
|
677
|
+
removes only the Impel-added global `WebFetch(domain:*)` allow rule.
|
|
636
678
|
- **Codex** — removes the marker-fenced `[model_providers.impel]` block, and
|
|
637
679
|
resets `model_provider` **only if it currently reads `"impel"`** — back to its
|
|
638
680
|
backed-up prior value, or, if there was none, removes the line entirely so
|
|
639
|
-
Codex falls back to its built-in default.
|
|
681
|
+
Codex falls back to its built-in default. The prior
|
|
682
|
+
`sandbox_mode` and `sandbox_workspace_write.network_access` values are
|
|
683
|
+
restored. All other tables are untouched.
|
|
640
684
|
|
|
641
685
|
`impel use account` doesn't require the PAT and is safe to run repeatedly (if a
|
|
642
686
|
tool is already in account mode it just says so and changes nothing).
|
|
@@ -929,8 +973,10 @@ impel status # see where each tool stands right now
|
|
|
929
973
|
```
|
|
930
974
|
|
|
931
975
|
How the clean revert is guaranteed: `impel on` records the pre-existing values
|
|
932
|
-
(your own `apiKeyHelper` / `ANTHROPIC_BASE_URL` / Claude sandbox
|
|
933
|
-
`model_provider
|
|
976
|
+
(your own `apiKeyHelper` / `ANTHROPIC_BASE_URL` / Claude sandbox and permission
|
|
977
|
+
policy, and Codex's prior `model_provider`, sandbox mode, and workspace network
|
|
978
|
+
value) into
|
|
979
|
+
`~/.config/impel/config.json` **before** overwriting them,
|
|
934
980
|
and only ever captures a real *account-mode* value (re-running `impel on` never
|
|
935
981
|
clobbers the backup with Impel's own values). `impel off` removes only keys that
|
|
936
982
|
still point at Impel and restores those backups — so a full `on` → `off`
|
|
@@ -939,9 +985,10 @@ they started, with all your unrelated keys and tables intact.
|
|
|
939
985
|
|
|
940
986
|
If you'd rather revert by hand: delete `apiKeyHelper`, the Impel
|
|
941
987
|
`ANTHROPIC_BASE_URL`, and the Impel-managed sandbox keys from
|
|
942
|
-
`~/.claude/settings.json
|
|
988
|
+
`~/.claude/settings.json`, plus the Impel-added `WebFetch(domain:*)` allow rule; delete the block between the
|
|
943
989
|
`# >>> impel-cli managed block ... >>>` / `# <<< impel-cli managed block <<<`
|
|
944
|
-
markers in `~/.codex/config.toml
|
|
990
|
+
markers in `~/.codex/config.toml`, reset `model_provider`, and restore or remove
|
|
991
|
+
`sandbox_mode` and `sandbox_workspace_write.network_access`; and
|
|
945
992
|
`rm -rf ~/.config/impel` to forget the PAT + gateway entirely.
|
|
946
993
|
|
|
947
994
|
## Codex compatibility route
|
|
@@ -980,9 +1027,9 @@ OpenAI Responses front door for SDK/Eve traffic and may normalize the body
|
|
|
980
1027
|
`impel token` rather than copying it into their own config files — one token,
|
|
981
1028
|
one rotation path, no stale copies.
|
|
982
1029
|
- **The `config.toml` merge is intentionally narrow**, not a general TOML
|
|
983
|
-
parser: it
|
|
984
|
-
own fenced `[model_providers.impel*]` block, leaving unrelated
|
|
985
|
-
providers, MCP servers, sandbox policy, etc.) untouched.
|
|
1030
|
+
parser: it touches the top-level `model_provider`, sandbox mode, the
|
|
1031
|
+
workspace-write network toggle, and its own fenced `[model_providers.impel*]` block, leaving unrelated
|
|
1032
|
+
tables (other providers, MCP servers, sandbox policy, etc.) untouched.
|
|
986
1033
|
|
|
987
1034
|
## Sources consulted for the Codex config
|
|
988
1035
|
|
|
@@ -995,7 +1042,7 @@ OpenAI Responses front door for SDK/Eve traffic and may normalize the body
|
|
|
995
1042
|
|
|
996
1043
|
- https://code.claude.com/docs/en/settings#sandbox-settings
|
|
997
1044
|
- https://code.claude.com/docs/en/sandboxing#configure-the-sandbox-for-your-organization
|
|
998
|
-
- https://github.com/anthropics/claude-code/blob/v2.1.
|
|
1045
|
+
- https://github.com/anthropics/claude-code/blob/v2.1.214/examples/settings/settings-bash-sandbox.json
|
|
999
1046
|
|
|
1000
1047
|
## Sources consulted for concurrent tenant apps
|
|
1001
1048
|
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { normalizeTenantId } from "./tenants.js";
|
|
13
13
|
import { impelCliInvocation } from "./selfInvocation.js";
|
|
14
14
|
import { crossAppModelsEnabled, redactSecretText } from "./config.js";
|
|
15
|
+
import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
|
|
15
16
|
import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
|
|
16
17
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
17
18
|
|
|
@@ -163,7 +164,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
163
164
|
|
|
164
165
|
// Bump when the written config/manifest schema changes; a mismatch forces the
|
|
165
166
|
// slow open path (and thus a full config rewrite) after a CLI update.
|
|
166
|
-
export const CURRENT_CONFIG_VERSION =
|
|
167
|
+
export const CURRENT_CONFIG_VERSION = 16;
|
|
167
168
|
|
|
168
169
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
169
170
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -617,8 +618,12 @@ export function installManagedAppFiles({
|
|
|
617
618
|
writeClaudeDefaultApp(paths);
|
|
618
619
|
writeClaudeCodeSettings(paths);
|
|
619
620
|
writeClaudeConfig(paths, config, models);
|
|
621
|
+
ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
writeChatGPTConfig(paths, config, models, vendorCodexModels, chatgptInvocations);
|
|
625
|
+
ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
|
|
620
626
|
}
|
|
621
|
-
else writeChatGPTConfig(paths, config, models, vendorCodexModels, chatgptInvocations);
|
|
622
627
|
// Non-bundle targets are the background-refresh / fast-open path: configs,
|
|
623
628
|
// token helper, catalog, and manifest only. Bundle swaps require the app
|
|
624
629
|
// to be closed (see quitBlockingApps), so those paths never attempt one.
|
|
@@ -1024,6 +1029,7 @@ function writeChatGPTConfig(paths, config, models, vendorCodexModels, invocation
|
|
|
1024
1029
|
const managedToml = [
|
|
1025
1030
|
CHATGPT_CONFIG_START,
|
|
1026
1031
|
"# Managed by `impel app update`. This profile is isolated from ~/.codex.",
|
|
1032
|
+
"# Impel session-hook trust is pinned separately by exact handler hash.",
|
|
1027
1033
|
`model = ${tomlString(selectedModel.slug)}`,
|
|
1028
1034
|
'model_provider = "impel"',
|
|
1029
1035
|
`chatgpt_base_url = ${tomlString(`${config.gatewayUrl}/chatgpt_passthrough/backend-api`)}`,
|
package/src/claudeSandbox.js
CHANGED
|
@@ -4,8 +4,11 @@ const MANAGED_SANDBOX_KEYS = Object.freeze([
|
|
|
4
4
|
"autoAllowBashIfSandboxed",
|
|
5
5
|
"allowUnsandboxedCommands",
|
|
6
6
|
"excludedCommands",
|
|
7
|
+
"enableWeakerNetworkIsolation",
|
|
7
8
|
]);
|
|
8
9
|
|
|
10
|
+
export const IMPEL_CLAUDE_NETWORK_PERMISSION = "WebFetch(domain:*)";
|
|
11
|
+
|
|
9
12
|
function isJsonObject(value) {
|
|
10
13
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
11
14
|
}
|
|
@@ -28,24 +31,79 @@ export function impelClaudeSandboxPolicy(platform = process.platform) {
|
|
|
28
31
|
autoAllowBashIfSandboxed: true,
|
|
29
32
|
allowUnsandboxedCommands: false,
|
|
30
33
|
excludedCommands: [],
|
|
34
|
+
...(platform === "darwin" ? { enableWeakerNetworkIsolation: true } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applyImpelClaudeNetworkPermission(permissions) {
|
|
39
|
+
const current = isJsonObject(permissions) ? permissions : {};
|
|
40
|
+
const allow = Array.isArray(current.allow) ? current.allow : [];
|
|
41
|
+
return {
|
|
42
|
+
...current,
|
|
43
|
+
allow: [...new Set([...allow, IMPEL_CLAUDE_NETWORK_PERMISSION])],
|
|
31
44
|
};
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
/** Apply the managed sandbox keys while preserving unrelated sandbox config. */
|
|
35
48
|
export function applyImpelClaudeSandbox(settings, platform = process.platform) {
|
|
36
49
|
const current = isJsonObject(settings.sandbox) ? settings.sandbox : {};
|
|
50
|
+
const policy = impelClaudeSandboxPolicy(platform);
|
|
37
51
|
settings.sandbox = {
|
|
38
52
|
...current,
|
|
39
|
-
...
|
|
53
|
+
...policy,
|
|
40
54
|
};
|
|
55
|
+
settings.permissions = applyImpelClaudeNetworkPermission(settings.permissions);
|
|
41
56
|
return settings.sandbox;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
59
|
/** Capture the exact pre-Impel value for reversible native-profile switching. */
|
|
45
60
|
export function captureClaudeSandbox(settings) {
|
|
46
|
-
|
|
61
|
+
const snapshot = Object.hasOwn(settings, "sandbox")
|
|
47
62
|
? { present: true, value: settings.sandbox }
|
|
48
63
|
: { present: false };
|
|
64
|
+
snapshot.permissions = Object.hasOwn(settings, "permissions")
|
|
65
|
+
? { present: true, value: settings.permissions }
|
|
66
|
+
: { present: false };
|
|
67
|
+
return snapshot;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function restoreClaudeNetworkPermission(settings, snapshot) {
|
|
71
|
+
if (!Object.hasOwn(settings, "permissions")) return false;
|
|
72
|
+
const priorSnapshot = snapshot?.permissions;
|
|
73
|
+
const priorPresent = priorSnapshot?.present === true;
|
|
74
|
+
const priorValue = priorPresent ? priorSnapshot.value : undefined;
|
|
75
|
+
const appliedPrior = applyImpelClaudeNetworkPermission(priorValue);
|
|
76
|
+
|
|
77
|
+
if (sameJsonValue(settings.permissions, appliedPrior)) {
|
|
78
|
+
if (priorPresent) settings.permissions = priorValue;
|
|
79
|
+
else delete settings.permissions;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (!isJsonObject(settings.permissions) || !Array.isArray(settings.permissions.allow)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const priorPermissions = isJsonObject(priorValue) ? priorValue : null;
|
|
87
|
+
const priorAllow = priorPermissions && Array.isArray(priorPermissions.allow)
|
|
88
|
+
? priorPermissions.allow
|
|
89
|
+
: [];
|
|
90
|
+
if (priorAllow.includes(IMPEL_CLAUDE_NETWORK_PERMISSION)) return false;
|
|
91
|
+
|
|
92
|
+
const allow = settings.permissions.allow.filter(
|
|
93
|
+
(rule) => rule !== IMPEL_CLAUDE_NETWORK_PERMISSION,
|
|
94
|
+
);
|
|
95
|
+
if (sameJsonValue(allow, settings.permissions.allow)) return false;
|
|
96
|
+
|
|
97
|
+
const permissions = { ...settings.permissions };
|
|
98
|
+
if (allow.length > 0 || (priorPermissions && Object.hasOwn(priorPermissions, "allow"))) {
|
|
99
|
+
permissions.allow = allow;
|
|
100
|
+
} else {
|
|
101
|
+
delete permissions.allow;
|
|
102
|
+
}
|
|
103
|
+
if (Object.keys(permissions).length > 0) settings.permissions = permissions;
|
|
104
|
+
else if (priorPresent) settings.permissions = priorValue;
|
|
105
|
+
else delete settings.permissions;
|
|
106
|
+
return true;
|
|
49
107
|
}
|
|
50
108
|
|
|
51
109
|
/**
|
|
@@ -53,28 +111,33 @@ export function captureClaudeSandbox(settings) {
|
|
|
53
111
|
* while gateway mode was active, plus unrelated filesystem/network keys, win.
|
|
54
112
|
*/
|
|
55
113
|
export function restoreClaudeSandbox(settings, snapshot, platform = process.platform) {
|
|
56
|
-
if (!isJsonObject(settings.sandbox)) return false;
|
|
57
114
|
const policy = impelClaudeSandboxPolicy(platform);
|
|
58
115
|
const prior = snapshot?.present === true && isJsonObject(snapshot.value)
|
|
59
116
|
? snapshot.value
|
|
60
117
|
: null;
|
|
61
|
-
const sandbox = { ...settings.sandbox };
|
|
62
118
|
let changed = false;
|
|
63
119
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
120
|
+
if (isJsonObject(settings.sandbox)) {
|
|
121
|
+
const sandbox = { ...settings.sandbox };
|
|
122
|
+
let sandboxChanged = false;
|
|
123
|
+
for (const key of MANAGED_SANDBOX_KEYS) {
|
|
124
|
+
if (!Object.hasOwn(sandbox, key) || !sameJsonValue(sandbox[key], policy[key])) continue;
|
|
125
|
+
if (prior && Object.hasOwn(prior, key)) sandbox[key] = prior[key];
|
|
126
|
+
else delete sandbox[key];
|
|
127
|
+
sandboxChanged = true;
|
|
128
|
+
}
|
|
70
129
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
130
|
+
if (sandboxChanged) {
|
|
131
|
+
if (Object.keys(sandbox).length > 0 || (snapshot?.present === true && prior)) {
|
|
132
|
+
settings.sandbox = sandbox;
|
|
133
|
+
} else if (snapshot?.present === true) {
|
|
134
|
+
settings.sandbox = snapshot.value;
|
|
135
|
+
} else {
|
|
136
|
+
delete settings.sandbox;
|
|
137
|
+
}
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
78
140
|
}
|
|
79
|
-
|
|
141
|
+
|
|
142
|
+
return restoreClaudeNetworkPermission(settings, snapshot) || changed;
|
|
80
143
|
}
|
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { cmdAgents } from "./commands/agents.js";
|
|
|
15
15
|
import { cmdTenant } from "./commands/tenant.js";
|
|
16
16
|
import { cmdDoctor } from "./commands/doctor.js";
|
|
17
17
|
import { cmdSetup } from "./commands/setup.js";
|
|
18
|
+
import { cmdSessions } from "./commands/sessions.js";
|
|
18
19
|
import { cmdUpdate } from "./commands/update.js";
|
|
19
20
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
20
21
|
|
|
@@ -121,6 +122,10 @@ export async function main(argv) {
|
|
|
121
122
|
case "mcp":
|
|
122
123
|
return cmdMcp(rest);
|
|
123
124
|
|
|
125
|
+
// Hidden transport used by managed Claude/Codex lifecycle hooks.
|
|
126
|
+
case "sessions":
|
|
127
|
+
return cmdSessions(rest);
|
|
128
|
+
|
|
124
129
|
case "claude":
|
|
125
130
|
case "codex":
|
|
126
131
|
return cmdLaunch(cmd, rest);
|
package/src/cliProfiles.js
CHANGED
|
@@ -12,6 +12,7 @@ import { CONFIG_DIR } from "./config.js";
|
|
|
12
12
|
import { normalizeTenantId } from "./tenants.js";
|
|
13
13
|
import { impelCliInvocation, impelMcpInvocation } from "./selfInvocation.js";
|
|
14
14
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
15
|
+
import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
|
|
15
16
|
|
|
16
17
|
export const IMPEL_CLI_PROFILES_DIR = path.join(CONFIG_DIR, "cli");
|
|
17
18
|
|
|
@@ -131,6 +132,7 @@ export function ensureImpelClaudeProfile(gatewayUrl, tenantId, { crossAppModels
|
|
|
131
132
|
|
|
132
133
|
writePrivateFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
133
134
|
writePrivateFile(userConfigPath, `${JSON.stringify(userConfig, null, 2)}\n`);
|
|
135
|
+
ensureClaudeSessionHooks(configDir, tenantId, "claude_cli");
|
|
134
136
|
|
|
135
137
|
return { configDir, settingsPath, userConfigPath };
|
|
136
138
|
}
|
|
@@ -203,9 +205,10 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
|
203
205
|
|
|
204
206
|
const { preamble, rest } = splitTomlPreamble(withoutManagedBlock);
|
|
205
207
|
const providerLine = 'model_provider = "impel"';
|
|
206
|
-
|
|
208
|
+
let nextPreamble = CODEX_PROVIDER_LINE_RE.test(preamble)
|
|
207
209
|
? preamble.replace(CODEX_PROVIDER_LINE_RE, providerLine)
|
|
208
210
|
: `${preamble.trimEnd()}${preamble.trim() ? "\n" : ""}${providerLine}\n`;
|
|
211
|
+
nextPreamble = nextPreamble.replace(/^bypass_hook_trust[ \t]*=[ \t]*(?:true|false)[ \t]*\n?/gm, "");
|
|
209
212
|
const restText = rest.trim();
|
|
210
213
|
const next = [nextPreamble.trimEnd(), codexManagedBlock(gatewayUrl, tenantId), restText]
|
|
211
214
|
.filter(Boolean)
|
|
@@ -213,6 +216,7 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
|
213
216
|
.concat("\n");
|
|
214
217
|
|
|
215
218
|
writePrivateFile(configPath, hardenManagedCodexToml(next, configPath));
|
|
219
|
+
ensureCodexSessionHooks(codexHome, tenantId, "codex_cli");
|
|
216
220
|
secureManagedCodexHome(codexHome);
|
|
217
221
|
return { codexHome, configPath };
|
|
218
222
|
}
|
package/src/codexSecurity.js
CHANGED
|
@@ -80,19 +80,61 @@ function upsertManagedBoolean(toml, table, key, value, configPath) {
|
|
|
80
80
|
return `${base}${base ? "\n\n" : ""}${MANAGED_COMMENT}\n[${table}]\n${key} = ${value}\n`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function upsertManagedRootString(toml, key, value, configPath) {
|
|
84
|
+
const lines = String(toml || "").replace(/\r\n/gu, "\n").split("\n");
|
|
85
|
+
const firstTable = lines.findIndex((line) => /^\s*\[/u.test(line));
|
|
86
|
+
const preambleEnd = firstTable === -1 ? lines.length : firstTable;
|
|
87
|
+
const pattern = new RegExp(`^(\\s*)${escapeRegex(key)}\\s*=.*$`, "u");
|
|
88
|
+
const indices = lines
|
|
89
|
+
.slice(0, preambleEnd)
|
|
90
|
+
.flatMap((line, index) => (pattern.test(line) ? [index] : []));
|
|
91
|
+
if (indices.length > 1) {
|
|
92
|
+
throw new Error(`${configPath} defines ${key} more than once; resolve it before re-running.`);
|
|
93
|
+
}
|
|
94
|
+
if (indices.length === 1) {
|
|
95
|
+
const index = indices[0];
|
|
96
|
+
const indent = lines[index].match(pattern)?.[1] || "";
|
|
97
|
+
lines[index] = `${indent}${key} = ${JSON.stringify(value)}`;
|
|
98
|
+
} else {
|
|
99
|
+
lines.splice(preambleEnd, 0, `${key} = ${JSON.stringify(value)}`, "");
|
|
100
|
+
}
|
|
101
|
+
return `${lines.join("\n").replace(/\s*$/u, "")}\n`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Enable unrestricted outbound access while Codex remains in workspace-write. */
|
|
105
|
+
export function enableManagedCodexNetworkAccess(
|
|
106
|
+
toml,
|
|
107
|
+
configPath = "managed Codex config",
|
|
108
|
+
) {
|
|
109
|
+
const workspaceSandbox = upsertManagedRootString(
|
|
110
|
+
toml,
|
|
111
|
+
"sandbox_mode",
|
|
112
|
+
"workspace-write",
|
|
113
|
+
configPath,
|
|
114
|
+
);
|
|
115
|
+
return upsertManagedBoolean(
|
|
116
|
+
workspaceSandbox,
|
|
117
|
+
"sandbox_workspace_write",
|
|
118
|
+
"network_access",
|
|
119
|
+
"true",
|
|
120
|
+
configPath,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
/**
|
|
84
125
|
* Disable Codex shell snapshots and keep its default KEY/SECRET/TOKEN filter
|
|
85
126
|
* enabled. Existing unrelated keys in either table are retained.
|
|
86
127
|
*/
|
|
87
128
|
export function hardenManagedCodexToml(toml, configPath = "managed Codex config") {
|
|
88
129
|
const withoutSnapshots = upsertManagedBoolean(toml, "features", "shell_snapshot", "false", configPath);
|
|
89
|
-
|
|
130
|
+
const withoutCredentialSnapshots = upsertManagedBoolean(
|
|
90
131
|
withoutSnapshots,
|
|
91
132
|
"shell_environment_policy",
|
|
92
133
|
"ignore_default_excludes",
|
|
93
134
|
"false",
|
|
94
135
|
configPath,
|
|
95
136
|
);
|
|
137
|
+
return enableManagedCodexNetworkAccess(withoutCredentialSnapshots, configPath);
|
|
96
138
|
}
|
|
97
139
|
|
|
98
140
|
function assertSafeManagedPath(target, expectedType) {
|