impel-cli 0.17.1 → 0.17.3
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 +19 -1
- package/package.json +1 -1
- package/src/apps.js +60 -16
- package/src/cli.js +2 -0
- package/src/codesign.js +98 -24
- package/src/commands/apps.js +30 -14
- package/src/commands/converge.js +95 -2
- package/src/commands/setup.js +29 -30
- package/src/commands/update.js +5 -0
- package/src/installRecovery/engine.js +4 -1
- package/src/installRecovery/inference.js +3 -1
- package/src/installRecovery/tools.js +23 -5
- package/src/macSetup.js +260 -0
- package/src/nativeProcess.js +3 -3
- package/src/platformSetup.js +49 -0
- package/src/privileges.js +32 -0
- package/src/provisioning.js +10 -8
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Useful setup options:
|
|
|
56
56
|
```sh
|
|
57
57
|
impel setup --pat <pat> # non-interactive credential input
|
|
58
58
|
impel setup --skip-apps # prepare CLI profiles only
|
|
59
|
-
impel setup --skip-clis #
|
|
59
|
+
impel setup --skip-clis # do not install missing vendor CLIs
|
|
60
60
|
impel setup --no-recovery # disable local and assisted recovery for this run
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -133,6 +133,7 @@ Useful update options:
|
|
|
133
133
|
```sh
|
|
134
134
|
impel update --check # inspect npm availability without changing state
|
|
135
135
|
impel update --skip-apps # reconcile CLI profiles only
|
|
136
|
+
impel update --skip-clis # do not install missing vendor CLIs
|
|
136
137
|
impel update --no-recovery # disable recovery for this run
|
|
137
138
|
```
|
|
138
139
|
|
|
@@ -231,6 +232,8 @@ compatibility for one release, but no command can enable native routing.
|
|
|
231
232
|
|
|
232
233
|
### macOS
|
|
233
234
|
|
|
235
|
+
- Missing Claude Code and Codex CLIs can be installed for the signed-in user
|
|
236
|
+
through the vendors' checksum-verifying native installers after confirmation.
|
|
234
237
|
- Tenant apps live in `~/Applications`.
|
|
235
238
|
- LaunchServices registration is non-launching and idempotent.
|
|
236
239
|
- Existing bundle fingerprints and stable signing identity prevent unnecessary
|
|
@@ -308,3 +311,18 @@ run a real package manager, or modify native personal profiles.
|
|
|
308
311
|
|
|
309
312
|
CI runs on Ubuntu, macOS, and Windows with supported Node versions. See
|
|
310
313
|
[RELEASING.md](RELEASING.md) for the npm release process.
|
|
314
|
+
|
|
315
|
+
The macOS config-contract job downloads the exact ChatGPT archive declared in
|
|
316
|
+
`PINNED_VENDOR_APPS`, verifies its checksum, signature, app version, and
|
|
317
|
+
embedded Codex version, then validates a deterministic generated profile with
|
|
318
|
+
Codex's strict config loader. A ChatGPT pin update must also update the embedded
|
|
319
|
+
Codex version and the reviewed config fixture when its contract changes.
|
|
320
|
+
|
|
321
|
+
The pinned Claude contract job likewise force-downloads the exact desktop app,
|
|
322
|
+
verifies the embedded Claude Code release manifest, builds the managed bundle
|
|
323
|
+
with ad-hoc signing, and checks deterministic generated desktop and Claude Code
|
|
324
|
+
settings against a reviewed fixture. The manifest-selected Claude Code runtime
|
|
325
|
+
runs `doctor` in bare mode against an isolated profile; CI intercepts the
|
|
326
|
+
`security` command so the check cannot read, reset, unlock, or modify a
|
|
327
|
+
Keychain. A Claude pin update must also update its `claudeCodeVersion` and
|
|
328
|
+
`claudeCodeCommit` pins and reviewed contract fixture.
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -43,6 +43,8 @@ const APP_DEFINITIONS = {
|
|
|
43
43
|
export const PINNED_VENDOR_APPS = Object.freeze({
|
|
44
44
|
claude: Object.freeze({
|
|
45
45
|
version: "1.20186.9",
|
|
46
|
+
claudeCodeVersion: "2.1.209",
|
|
47
|
+
claudeCodeCommit: "0fe048596fd45e79e99353cbe2c3d1b1ac069568",
|
|
46
48
|
bundleName: "Claude.app",
|
|
47
49
|
downloads: Object.freeze({
|
|
48
50
|
arm64: Object.freeze({
|
|
@@ -57,6 +59,7 @@ export const PINNED_VENDOR_APPS = Object.freeze({
|
|
|
57
59
|
}),
|
|
58
60
|
chatgpt: Object.freeze({
|
|
59
61
|
version: "26.707.72221",
|
|
62
|
+
codexVersion: "0.144.2",
|
|
60
63
|
bundleName: "ChatGPT.app",
|
|
61
64
|
downloads: Object.freeze({
|
|
62
65
|
arm64: Object.freeze({
|
|
@@ -589,6 +592,7 @@ export function installManagedAppFiles({
|
|
|
589
592
|
writeTokenHelperFile = true,
|
|
590
593
|
claudeUserData = null,
|
|
591
594
|
chatgptInvocations = null,
|
|
595
|
+
generatedAt = new Date().toISOString(),
|
|
592
596
|
}) {
|
|
593
597
|
// true = rebuild every target's bundle; false = configs only; an array
|
|
594
598
|
// limits the expensive rebuild to just the stale targets.
|
|
@@ -621,7 +625,14 @@ export function installManagedAppFiles({
|
|
|
621
625
|
ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
|
|
622
626
|
}
|
|
623
627
|
else {
|
|
624
|
-
writeChatGPTConfig(
|
|
628
|
+
writeChatGPTConfig(
|
|
629
|
+
paths,
|
|
630
|
+
config,
|
|
631
|
+
models,
|
|
632
|
+
vendorCodexModels,
|
|
633
|
+
chatgptInvocations,
|
|
634
|
+
generatedAt,
|
|
635
|
+
);
|
|
625
636
|
ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
|
|
626
637
|
}
|
|
627
638
|
// Non-bundle targets are the background-refresh / fast-open path: configs,
|
|
@@ -666,7 +677,7 @@ export function installManagedAppFiles({
|
|
|
666
677
|
config,
|
|
667
678
|
),
|
|
668
679
|
models: models.map((model) => model.id),
|
|
669
|
-
updatedAt:
|
|
680
|
+
updatedAt: generatedAt,
|
|
670
681
|
}, null, 2) + "\n", 0o600);
|
|
671
682
|
return installed;
|
|
672
683
|
}
|
|
@@ -815,10 +826,13 @@ function installPinnedVendorApp(target, homeDir) {
|
|
|
815
826
|
}
|
|
816
827
|
}
|
|
817
828
|
|
|
818
|
-
export function ensureVendorApp(target, {
|
|
829
|
+
export function ensureVendorApp(target, {
|
|
830
|
+
homeDir = os.homedir(),
|
|
831
|
+
forceDownload = false,
|
|
832
|
+
} = {}) {
|
|
819
833
|
const definition = APP_DEFINITIONS[target];
|
|
820
834
|
const pin = PINNED_VENDOR_APPS[target];
|
|
821
|
-
const current = findVendorApp(target, homeDir);
|
|
835
|
+
const current = forceDownload ? null : findVendorApp(target, homeDir);
|
|
822
836
|
if (current) return { path: current, action: "existing", note: `verified ${pin.version}` };
|
|
823
837
|
|
|
824
838
|
// Name-collision transparency: e.g. OpenAI's native ChatGPT chat app
|
|
@@ -974,7 +988,14 @@ function writeClaudeDefaultApp(paths) {
|
|
|
974
988
|
writeAtomic(configPath, JSON.stringify(next, null, 2) + "\n", 0o600);
|
|
975
989
|
}
|
|
976
990
|
|
|
977
|
-
function writeChatGPTConfig(
|
|
991
|
+
function writeChatGPTConfig(
|
|
992
|
+
paths,
|
|
993
|
+
config,
|
|
994
|
+
models,
|
|
995
|
+
vendorCodexModels,
|
|
996
|
+
invocations = null,
|
|
997
|
+
generatedAt = new Date().toISOString(),
|
|
998
|
+
) {
|
|
978
999
|
const experimental = crossAppModelsEnabled(config);
|
|
979
1000
|
const orderedModels = experimental
|
|
980
1001
|
? [...models.filter((model) => model.provider === "codex"), ...models.filter((model) => model.provider === "claude")]
|
|
@@ -1008,7 +1029,7 @@ function writeChatGPTConfig(paths, config, models, vendorCodexModels, invocation
|
|
|
1008
1029
|
? configuredTier
|
|
1009
1030
|
: (configuredTier === "fast" && supportedTiers.includes("priority") ? "priority" : null);
|
|
1010
1031
|
const catalog = {
|
|
1011
|
-
fetched_at:
|
|
1032
|
+
fetched_at: generatedAt,
|
|
1012
1033
|
client_version: "impel-managed-2",
|
|
1013
1034
|
models: codexModels,
|
|
1014
1035
|
};
|
|
@@ -1209,18 +1230,27 @@ function codexCatalogEntry(model, index, vendorModel) {
|
|
|
1209
1230
|
function readVendorCodexModels(vendorPath) {
|
|
1210
1231
|
const binary = vendorPath && path.join(vendorPath, "Contents", "Resources", "codex");
|
|
1211
1232
|
if (!binary || !fs.existsSync(binary)) return new Map();
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1233
|
+
const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "impel-vendor-codex-models-"));
|
|
1234
|
+
try {
|
|
1235
|
+
// Vendor enrichment must come only from the exact bundled release. Reading
|
|
1236
|
+
// the operator's native CODEX_HOME here would make generated app configs
|
|
1237
|
+
// depend on unrelated local state and could trigger an online catalog read.
|
|
1238
|
+
const env = { ...process.env, CODEX_HOME: codexHome };
|
|
1239
|
+
const result = spawnSync(binary, ["debug", "models", "--bundled"], {
|
|
1240
|
+
encoding: "utf8",
|
|
1241
|
+
env,
|
|
1242
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
1243
|
+
});
|
|
1244
|
+
if (result.status !== 0) return new Map();
|
|
1217
1245
|
try {
|
|
1218
1246
|
const payload = JSON.parse(result.stdout);
|
|
1219
1247
|
const models = Array.isArray(payload) ? payload : payload.models;
|
|
1220
1248
|
if (Array.isArray(models)) return new Map(models.map((model) => [model.slug, model]));
|
|
1221
1249
|
} catch {
|
|
1222
|
-
// Fall through to the
|
|
1250
|
+
// Fall through to the Impel-generated schema.
|
|
1223
1251
|
}
|
|
1252
|
+
} finally {
|
|
1253
|
+
fs.rmSync(codexHome, { recursive: true, force: true });
|
|
1224
1254
|
}
|
|
1225
1255
|
return new Map();
|
|
1226
1256
|
}
|
|
@@ -1373,11 +1403,13 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1373
1403
|
});
|
|
1374
1404
|
|
|
1375
1405
|
const macos = path.join(staging, "Contents", "MacOS");
|
|
1406
|
+
const vendorExecutable = path.join(macos, executableName);
|
|
1407
|
+
const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
|
|
1376
1408
|
writeAtomic(path.join(staging, "Contents", "Resources", "impel-compatibility.json"), JSON.stringify({
|
|
1377
1409
|
schemaVersion: 4,
|
|
1378
1410
|
cliVersion: CLI_VERSION,
|
|
1379
1411
|
bundleFingerprint: BUNDLE_BUILD_FINGERPRINT,
|
|
1380
|
-
signingMode:
|
|
1412
|
+
signingMode: signingModeForIdentity(signingIdentity),
|
|
1381
1413
|
vendorVersion: bundleVersion(vendorPath),
|
|
1382
1414
|
sourceBundle: vendorPath,
|
|
1383
1415
|
bundleIdentifier: paths.claude.bundleIdentifier,
|
|
@@ -1403,7 +1435,7 @@ function writeVendoredClaudeBundle(paths, vendorPath, gatewayUrl, safeStorageNam
|
|
|
1403
1435
|
asarHeaderSha256: newAsarHash,
|
|
1404
1436
|
}, null, 2) + "\n", 0o644);
|
|
1405
1437
|
|
|
1406
|
-
signVendoredApp(staging,
|
|
1438
|
+
signVendoredApp(staging, vendorExecutable, "Claude", signingIdentity);
|
|
1407
1439
|
replaceDirectory(bundle, staging);
|
|
1408
1440
|
} catch (error) {
|
|
1409
1441
|
fs.rmSync(staging, { recursive: true, force: true });
|
|
@@ -1456,6 +1488,8 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
1456
1488
|
const runtimeAsarPath = path.join(staging, "Contents", "Resources", "app.asar");
|
|
1457
1489
|
const fastModePatchCount = patchFastModeAuthGate(runtimeAsarPath);
|
|
1458
1490
|
const asarHash = asarHeaderHash(runtimeAsarPath);
|
|
1491
|
+
const vendorExecutable = path.join(vendorBundle, "Contents", "MacOS", executableName);
|
|
1492
|
+
const signingIdentity = resolveVendoredAppSigningIdentity(vendorExecutable);
|
|
1459
1493
|
const launchHost = ensureChatGPTLaunchHost();
|
|
1460
1494
|
writeAtomic(
|
|
1461
1495
|
path.join(staging, "Contents", "Info.plist"),
|
|
@@ -1487,7 +1521,7 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
1487
1521
|
schemaVersion: 3,
|
|
1488
1522
|
cliVersion: CLI_VERSION,
|
|
1489
1523
|
bundleFingerprint: BUNDLE_BUILD_FINGERPRINT,
|
|
1490
|
-
signingMode:
|
|
1524
|
+
signingMode: signingModeForIdentity(signingIdentity),
|
|
1491
1525
|
vendorVersion: bundleVersion(vendorPath),
|
|
1492
1526
|
sourceBundle: vendorPath,
|
|
1493
1527
|
bundleIdentifier: paths.chatgpt.bundleIdentifier,
|
|
@@ -1505,7 +1539,7 @@ function writeVendoredChatGPTBundle(paths, vendorPath, gatewayUrl) {
|
|
|
1505
1539
|
asarHeaderSha256: asarHash,
|
|
1506
1540
|
}, null, 2) + "\n", 0o644);
|
|
1507
1541
|
|
|
1508
|
-
signVendoredApp(staging,
|
|
1542
|
+
signVendoredApp(staging, vendorExecutable, "ChatGPT", signingIdentity);
|
|
1509
1543
|
replaceDirectory(bundle, staging);
|
|
1510
1544
|
} catch (error) {
|
|
1511
1545
|
fs.rmSync(staging, { recursive: true, force: true });
|
|
@@ -2131,6 +2165,16 @@ if (process.type === "browser" && displayName) {
|
|
|
2131
2165
|
`;
|
|
2132
2166
|
}
|
|
2133
2167
|
|
|
2168
|
+
function resolveVendoredAppSigningIdentity(vendorExecutable) {
|
|
2169
|
+
if (process.platform !== "darwin" || !isMachO(vendorExecutable)) return null;
|
|
2170
|
+
return resolveSigningIdentity();
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
export function signingModeForIdentity(identity) {
|
|
2174
|
+
if (!identity) return desiredSigningMode();
|
|
2175
|
+
return identity.mode === "external" ? identity.descriptor : identity.mode;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2134
2178
|
function signVendoredApp(bundle, vendorExecutable, label, identity = null) {
|
|
2135
2179
|
if (process.platform !== "darwin" || !isMachO(vendorExecutable)) return;
|
|
2136
2180
|
// Resolve (and, on first use, create) the signing identity only once we know
|
package/src/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { cmdSessions } from "./commands/sessions.js";
|
|
|
19
19
|
import { cmdUpdate } from "./commands/update.js";
|
|
20
20
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
21
21
|
import { cmdConverge } from "./commands/converge.js";
|
|
22
|
+
import { refuseElevatedMacExecution } from "./privileges.js";
|
|
22
23
|
|
|
23
24
|
const HELP = `impel — isolated Impel workspaces for every tenant
|
|
24
25
|
|
|
@@ -75,6 +76,7 @@ function normalizeTarget(token) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export async function main(argv) {
|
|
79
|
+
if (refuseElevatedMacExecution(argv)) return;
|
|
78
80
|
const [cmd, ...rest] = argv;
|
|
79
81
|
|
|
80
82
|
switch (cmd) {
|
package/src/codesign.js
CHANGED
|
@@ -117,7 +117,10 @@ export function resolveSigningIdentity(deps = {}) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
try {
|
|
120
|
-
const identity =
|
|
120
|
+
const identity = withSigningIdentityLock(
|
|
121
|
+
{ configDir, fsImpl },
|
|
122
|
+
() => ensureLocalSigningIdentity({ run, configDir, fsImpl, randomBytes }),
|
|
123
|
+
);
|
|
121
124
|
if (cache) cachedIdentity = identity;
|
|
122
125
|
return identity;
|
|
123
126
|
} catch (error) {
|
|
@@ -142,25 +145,33 @@ function ensureLocalSigningIdentity({ run, configDir, fsImpl, randomBytes }) {
|
|
|
142
145
|
// therefore the designated requirement) stays stable across updates.
|
|
143
146
|
if (fsImpl.existsSync(keychain) && fsImpl.existsSync(passwordFile)) {
|
|
144
147
|
const password = fsImpl.readFileSync(passwordFile, "utf8").trim();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
148
|
+
try {
|
|
149
|
+
if (!password) throw new Error("stored signing keychain password is empty");
|
|
150
|
+
unlockKeychain(run, keychain, password);
|
|
151
|
+
runOrThrow(run, "/usr/bin/security", ["set-keychain-settings", keychain], "configure signing keychain");
|
|
152
|
+
ensureKeychainInSearchList(run, keychain);
|
|
153
|
+
let existing = findLocalIdentitySha(run, keychain);
|
|
154
|
+
if (existing) return localIdentity(existing, keychain);
|
|
155
|
+
|
|
156
|
+
// Repair identities created before trust was established, plus imports
|
|
157
|
+
// interrupted after the private key reached the keychain.
|
|
158
|
+
authorizeCodesignKey(run, keychain, password);
|
|
159
|
+
trustExistingLocalCertificate({ run, keychain, fsImpl });
|
|
160
|
+
existing = findLocalIdentitySha(run, keychain);
|
|
161
|
+
if (existing) return localIdentity(existing, keychain);
|
|
162
|
+
throw new Error("existing signing identity could not be trusted");
|
|
163
|
+
} catch {
|
|
164
|
+
// This keychain and password file are entirely Impel-owned. A mismatched,
|
|
165
|
+
// empty, or half-imported pair can never provide stable identity, so reset
|
|
166
|
+
// it atomically instead of falling back forever on every future rebuild.
|
|
167
|
+
resetLocalSigningIdentity({ run, keychain, passwordFile, fsImpl });
|
|
168
|
+
}
|
|
169
|
+
} else if (fsImpl.existsSync(keychain) || fsImpl.existsSync(passwordFile)) {
|
|
170
|
+
resetLocalSigningIdentity({ run, keychain, passwordFile, fsImpl });
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
const password = randomBytes(24).toString("hex");
|
|
162
|
-
createLocalSigningIdentity({ run, dir, keychain, password, fsImpl });
|
|
163
|
-
fsImpl.writeFileSync(passwordFile, `${password}\n`, { mode: 0o600 });
|
|
174
|
+
createLocalSigningIdentity({ run, dir, keychain, passwordFile, password, fsImpl });
|
|
164
175
|
|
|
165
176
|
ensureKeychainInSearchList(run, keychain);
|
|
166
177
|
const sha = findLocalIdentitySha(run, keychain);
|
|
@@ -183,7 +194,7 @@ function findLocalIdentitySha(run, keychain) {
|
|
|
183
194
|
return parseIdentitySha(result.stdout, SIGNING_IDENTITY_NAME);
|
|
184
195
|
}
|
|
185
196
|
|
|
186
|
-
function createLocalSigningIdentity({ run, dir, keychain, password, fsImpl }) {
|
|
197
|
+
function createLocalSigningIdentity({ run, dir, keychain, passwordFile, password, fsImpl }) {
|
|
187
198
|
const work = fsImpl.mkdtempSync(path.join(os.tmpdir(), "impel-codesign-"));
|
|
188
199
|
const keyPath = path.join(work, "key.pem");
|
|
189
200
|
const certPath = path.join(work, "cert.pem");
|
|
@@ -211,26 +222,89 @@ function createLocalSigningIdentity({ run, dir, keychain, password, fsImpl }) {
|
|
|
211
222
|
"-name", SIGNING_IDENTITY_NAME,
|
|
212
223
|
], "package signing certificate");
|
|
213
224
|
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
runOrThrow(run, "/usr/bin/security", ["create-keychain", "-p", password, keychain], "create signing keychain");
|
|
226
|
+
try {
|
|
227
|
+
writePrivateAtomic(fsImpl, passwordFile, `${password}\n`);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
resetLocalSigningIdentity({ run, keychain, passwordFile, fsImpl });
|
|
230
|
+
throw error;
|
|
216
231
|
}
|
|
217
232
|
// No auto-lock: codesign must reach the private key non-interactively.
|
|
218
|
-
run
|
|
233
|
+
runOrThrow(run, "/usr/bin/security", ["set-keychain-settings", keychain], "configure signing keychain");
|
|
219
234
|
unlockKeychain(run, keychain, password);
|
|
220
235
|
runOrThrow(run, "/usr/bin/security", [
|
|
221
236
|
"import", p12Path, "-k", keychain, "-P", p12Pass, "-T", "/usr/bin/codesign",
|
|
222
237
|
], "import signing identity");
|
|
223
238
|
// Grant codesign non-interactive access to the key so signing never pops a
|
|
224
239
|
// "codesign wants to use a key" GUI prompt. We own the keychain password.
|
|
225
|
-
|
|
226
|
-
"set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", password, keychain,
|
|
227
|
-
], "authorize codesign for the signing key");
|
|
240
|
+
authorizeCodesignKey(run, keychain, password);
|
|
228
241
|
trustLocalCertificate(run, keychain, certPath);
|
|
229
242
|
} finally {
|
|
230
243
|
fsImpl.rmSync(work, { recursive: true, force: true });
|
|
231
244
|
}
|
|
232
245
|
}
|
|
233
246
|
|
|
247
|
+
function authorizeCodesignKey(run, keychain, password) {
|
|
248
|
+
runOrThrow(run, "/usr/bin/security", [
|
|
249
|
+
"set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", password, keychain,
|
|
250
|
+
], "authorize codesign for the signing key");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function writePrivateAtomic(fsImpl, filePath, contents) {
|
|
254
|
+
const temporary = `${filePath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
|
255
|
+
try {
|
|
256
|
+
fsImpl.writeFileSync(temporary, contents, { mode: 0o600 });
|
|
257
|
+
fsImpl.renameSync(temporary, filePath);
|
|
258
|
+
fsImpl.chmodSync?.(filePath, 0o600);
|
|
259
|
+
} finally {
|
|
260
|
+
fsImpl.rmSync(temporary, { force: true });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resetLocalSigningIdentity({ run, keychain, passwordFile, fsImpl }) {
|
|
265
|
+
if (fsImpl.existsSync(keychain)) {
|
|
266
|
+
run("/usr/bin/security", ["delete-keychain", keychain]);
|
|
267
|
+
}
|
|
268
|
+
fsImpl.rmSync(keychain, { force: true });
|
|
269
|
+
fsImpl.rmSync(passwordFile, { force: true });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const SIGNING_LOCK_STALE_MS = 5 * 60 * 1_000;
|
|
273
|
+
const SIGNING_LOCK_WAIT_MS = 15_000;
|
|
274
|
+
const SIGNING_LOCK_POLL_MS = 100;
|
|
275
|
+
const SIGNING_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
276
|
+
|
|
277
|
+
function withSigningIdentityLock({ configDir, fsImpl }, run) {
|
|
278
|
+
const dir = path.join(configDir, "codesign");
|
|
279
|
+
const lock = path.join(dir, ".identity-lock");
|
|
280
|
+
fsImpl.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
281
|
+
const deadline = Date.now() + SIGNING_LOCK_WAIT_MS;
|
|
282
|
+
while (true) {
|
|
283
|
+
try {
|
|
284
|
+
fsImpl.mkdirSync(lock, { mode: 0o700 });
|
|
285
|
+
break;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error?.code !== "EEXIST") throw error;
|
|
288
|
+
try {
|
|
289
|
+
if (Date.now() - fsImpl.statSync(lock).mtimeMs > SIGNING_LOCK_STALE_MS) {
|
|
290
|
+
fsImpl.rmSync(lock, { recursive: true, force: true });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
} catch (statError) {
|
|
294
|
+
if (statError?.code !== "ENOENT") throw statError;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (Date.now() >= deadline) throw new Error("timed out waiting for another Impel signing operation");
|
|
298
|
+
Atomics.wait(SIGNING_LOCK_SLEEP, 0, 0, SIGNING_LOCK_POLL_MS);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
return run();
|
|
303
|
+
} finally {
|
|
304
|
+
fsImpl.rmSync(lock, { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
234
308
|
function unlockKeychain(run, keychain, password) {
|
|
235
309
|
runOrThrow(
|
|
236
310
|
run,
|
package/src/commands/apps.js
CHANGED
|
@@ -43,6 +43,7 @@ import { syncAgentProfilesSafe } from "../agents.js";
|
|
|
43
43
|
import { secureManagedCodexHome } from "../codexSecurity.js";
|
|
44
44
|
import { impelCliInvocation } from "../selfInvocation.js";
|
|
45
45
|
import { createProgressLogger, withProgress } from "../progress.js";
|
|
46
|
+
import { findNativeBinary } from "../nativeProcess.js";
|
|
46
47
|
import {
|
|
47
48
|
ensureWindowsClaudeApp,
|
|
48
49
|
ensureWindowsChatGPTApp,
|
|
@@ -325,6 +326,7 @@ export async function reconcileWindowsTenantApps({
|
|
|
325
326
|
syncSkills: syncSkillsSafe,
|
|
326
327
|
syncAgents: syncAgentProfilesSafe,
|
|
327
328
|
existsSync: fs.existsSync,
|
|
329
|
+
findBinary: findNativeBinary,
|
|
328
330
|
log: (message) => console.log(message),
|
|
329
331
|
...overrides,
|
|
330
332
|
};
|
|
@@ -375,17 +377,23 @@ export async function reconcileWindowsTenantApps({
|
|
|
375
377
|
homeDir,
|
|
376
378
|
environment,
|
|
377
379
|
});
|
|
380
|
+
const agentTargets = [];
|
|
378
381
|
for (const target of actionTargets) {
|
|
379
382
|
const { client, env, label } = appSkillTarget(target, paths);
|
|
380
|
-
|
|
383
|
+
if (io.findBinary(client, environment, "win32")) {
|
|
384
|
+
await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label });
|
|
385
|
+
agentTargets.push(target);
|
|
386
|
+
}
|
|
381
387
|
if (target === "chatgpt") secureManagedCodexHome(paths.chatgpt.codexHome);
|
|
382
388
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
+
if (agentTargets.length) {
|
|
390
|
+
await io.syncAgents({
|
|
391
|
+
profiles: agentTargets.map((target) => appAgentProfile(target, paths)),
|
|
392
|
+
gatewayUrl: config.gatewayUrl,
|
|
393
|
+
credential: config.pat,
|
|
394
|
+
tenantId: config.tenantId,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
389
397
|
const verification = {
|
|
390
398
|
claude: !actionTargets.includes("claude") || io.existsSync(path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`)),
|
|
391
399
|
chatgpt: !actionTargets.includes("chatgpt") || io.existsSync(path.join(paths.chatgpt.codexHome, "config.toml")),
|
|
@@ -404,6 +412,7 @@ export async function reconcileMacTenantApps({
|
|
|
404
412
|
skipVendorTargets = [],
|
|
405
413
|
confirmVendorInstall = async () => true,
|
|
406
414
|
homeDir = os.homedir(),
|
|
415
|
+
environment = process.env,
|
|
407
416
|
} = {}, overrides = {}) {
|
|
408
417
|
const io = {
|
|
409
418
|
status: appStatus,
|
|
@@ -417,6 +426,7 @@ export async function reconcileMacTenantApps({
|
|
|
417
426
|
secureCodexHome: secureManagedCodexHome,
|
|
418
427
|
openLauncher: openManagedLauncher,
|
|
419
428
|
existsSync: fs.existsSync,
|
|
429
|
+
findBinary: findNativeBinary,
|
|
420
430
|
log: (message) => console.log(message),
|
|
421
431
|
...overrides,
|
|
422
432
|
};
|
|
@@ -462,17 +472,23 @@ export async function reconcileMacTenantApps({
|
|
|
462
472
|
writeBundles: staleTargets,
|
|
463
473
|
});
|
|
464
474
|
const paths = appPaths(homeDir, config.tenantId, { tenantName: config.tenantName });
|
|
475
|
+
const agentItems = [];
|
|
465
476
|
for (const item of installed) {
|
|
466
477
|
const { client, env, label } = appSkillTarget(item.target, paths);
|
|
467
|
-
|
|
478
|
+
if (io.findBinary(client, environment, "darwin")) {
|
|
479
|
+
await io.syncSkills({ client, gatewayUrl: resolveSkillsGateway(config.gatewayUrl), env, label });
|
|
480
|
+
agentItems.push(item);
|
|
481
|
+
}
|
|
468
482
|
if (item.target === "chatgpt") io.secureCodexHome(paths.chatgpt.codexHome);
|
|
469
483
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
484
|
+
if (agentItems.length) {
|
|
485
|
+
await io.syncAgents({
|
|
486
|
+
profiles: agentItems.map((item) => appAgentProfile(item.target, paths)),
|
|
487
|
+
gatewayUrl: config.gatewayUrl,
|
|
488
|
+
credential: config.pat,
|
|
489
|
+
tenantId: config.tenantId,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
476
492
|
for (const item of installed) {
|
|
477
493
|
if (reopenTargets.has(item.target)) io.openLauncher(item.launcher);
|
|
478
494
|
}
|
package/src/commands/converge.js
CHANGED
|
@@ -5,6 +5,7 @@ import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
|
5
5
|
import { printReconciliationSummary, reconcileAllTenants, selectDefaultTenant } from "../provisioning.js";
|
|
6
6
|
import { fetchTenants } from "../tenants.js";
|
|
7
7
|
import { promptText } from "../prompt.js";
|
|
8
|
+
import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
|
|
8
9
|
import { restoreNativeProfiles } from "./use.js";
|
|
9
10
|
|
|
10
11
|
function confirmed(answer) {
|
|
@@ -24,6 +25,7 @@ function mergeTenantReport(report, replacement) {
|
|
|
24
25
|
|
|
25
26
|
export async function cmdConverge(argv = [], overrides = {}) {
|
|
26
27
|
const skipApps = argv.includes("--skip-apps");
|
|
28
|
+
const skipClis = argv.includes("--skip-clis");
|
|
27
29
|
const noRecovery = argv.includes("--no-recovery");
|
|
28
30
|
const io = {
|
|
29
31
|
loadConfig,
|
|
@@ -36,8 +38,12 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
36
38
|
isTTY: process.stdin.isTTY,
|
|
37
39
|
platform: process.platform,
|
|
38
40
|
environment: process.env,
|
|
41
|
+
preparePlatformClis,
|
|
39
42
|
...overrides,
|
|
40
43
|
};
|
|
44
|
+
if (overrides.prepareWindowsClis && !overrides.preparePlatformClis) {
|
|
45
|
+
io.preparePlatformClis = overrides.prepareWindowsClis;
|
|
46
|
+
}
|
|
41
47
|
const config = io.loadConfig();
|
|
42
48
|
if (!config?.pat) {
|
|
43
49
|
console.error("impel update: not authenticated; run `impel setup` first.");
|
|
@@ -93,8 +99,94 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
93
99
|
environment: io.environment,
|
|
94
100
|
confirmVendorInstall: confirmInstall,
|
|
95
101
|
}, overrides.reconcileOverrides || {});
|
|
102
|
+
|
|
103
|
+
let sharedFailure = null;
|
|
104
|
+
const prepareSharedClis = async ({ confirmedTools = null, inspectOnly = false } = {}) => {
|
|
105
|
+
try {
|
|
106
|
+
const inspected = await io.preparePlatformClis({
|
|
107
|
+
gatewayUrl: config.gatewayUrl,
|
|
108
|
+
tenantId: selected.id,
|
|
109
|
+
platform: io.platform,
|
|
110
|
+
skipInstall: true,
|
|
111
|
+
});
|
|
112
|
+
let prepared = inspected;
|
|
113
|
+
if (inspected.missingAfter.length && !inspectOnly && !skipClis) {
|
|
114
|
+
let installTools = confirmedTools;
|
|
115
|
+
if (installTools === null) {
|
|
116
|
+
installTools = [];
|
|
117
|
+
if (io.isTTY) {
|
|
118
|
+
for (const tool of inspected.missingAfter) {
|
|
119
|
+
if (!inspected.installCommands?.[tool]) continue;
|
|
120
|
+
const label = tool === "claude" ? "Claude Code" : "Codex";
|
|
121
|
+
if (confirmed(await io.promptText(`Install the official ${label} CLI for this user? [y/N] `))) {
|
|
122
|
+
installTools.push(tool);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (installTools.length) {
|
|
128
|
+
prepared = await io.preparePlatformClis({
|
|
129
|
+
gatewayUrl: config.gatewayUrl,
|
|
130
|
+
tenantId: selected.id,
|
|
131
|
+
platform: io.platform,
|
|
132
|
+
skipInstall: false,
|
|
133
|
+
installTools,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
sharedFailure = prepared.missingAfter.length ? describeCliFailure(prepared) : null;
|
|
138
|
+
return prepared;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
sharedFailure = redactSecretText(error?.message || error);
|
|
141
|
+
return { binaries: {}, missingAfter: ["claude", "codex"] };
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
await prepareSharedClis();
|
|
96
146
|
let report = await run();
|
|
97
|
-
if (
|
|
147
|
+
if (sharedFailure && !noRecovery) {
|
|
148
|
+
let sharedReady = false;
|
|
149
|
+
const recovery = await io.recoverInstall({
|
|
150
|
+
failure: {
|
|
151
|
+
scope: "shared",
|
|
152
|
+
platform: io.platform,
|
|
153
|
+
architecture: process.arch,
|
|
154
|
+
step: "update.shared_vendor_clis",
|
|
155
|
+
message: sharedFailure,
|
|
156
|
+
},
|
|
157
|
+
config,
|
|
158
|
+
goals: [{
|
|
159
|
+
id: "shared-vendor-clis-ready",
|
|
160
|
+
description: "Shared vendor CLI prerequisites are installed and discoverable",
|
|
161
|
+
run: () => sharedReady,
|
|
162
|
+
}],
|
|
163
|
+
explicit: true,
|
|
164
|
+
noRecovery: false,
|
|
165
|
+
actionContext: {
|
|
166
|
+
tenantId: selected.id,
|
|
167
|
+
installVendorClis: async (tool) => {
|
|
168
|
+
await prepareSharedClis({ confirmedTools: [tool] });
|
|
169
|
+
sharedReady = !sharedFailure;
|
|
170
|
+
return io.preparePlatformClis({
|
|
171
|
+
gatewayUrl: config.gatewayUrl,
|
|
172
|
+
tenantId: selected.id,
|
|
173
|
+
platform: io.platform,
|
|
174
|
+
skipInstall: true,
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
retryStep: async () => {
|
|
178
|
+
await prepareSharedClis({ inspectOnly: true });
|
|
179
|
+
sharedReady = !sharedFailure;
|
|
180
|
+
return sharedReady;
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
}, overrides.recoveryOverrides || {});
|
|
184
|
+
if (recovery.fixed) {
|
|
185
|
+
await prepareSharedClis({ inspectOnly: true });
|
|
186
|
+
report = await run();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!sharedFailure && !report.passed && !noRecovery) {
|
|
98
190
|
for (const failed of report.tenants.filter((tenant) => tenant.status === "failed")) {
|
|
99
191
|
const tenant = listing.tenants.find((candidate) => candidate.id === failed.tenantId);
|
|
100
192
|
if (!tenant) continue;
|
|
@@ -165,7 +257,8 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
165
257
|
}
|
|
166
258
|
printReconciliationSummary(report);
|
|
167
259
|
console.log(`CLI tenant: ${selected.id} (change with \`impel tenant use <tenant>\`)`);
|
|
168
|
-
if (!report.passed) {
|
|
260
|
+
if (!report.passed || sharedFailure) {
|
|
261
|
+
if (sharedFailure) console.error(`Shared update failure: ${sharedFailure}`);
|
|
169
262
|
console.error("impel update: one or more tenant surfaces remain incomplete.");
|
|
170
263
|
process.exitCode = 1;
|
|
171
264
|
return false;
|