unbound-cli 1.16.21 → 1.16.22
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/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -130,7 +130,7 @@ Examples:
|
|
|
130
130
|
const reasons = [];
|
|
131
131
|
if (t.conflict) reasons.push('two modes installed');
|
|
132
132
|
for (const c of t.checks) if (!c.ok) reasons.push(c.summary || c.name);
|
|
133
|
-
s = C.red(`✗ Tampered — ${reasons.join(', ')}`);
|
|
133
|
+
s = C.red(`✗ ${t.notEnforced ? 'Not enforced' : 'Tampered'} — ${reasons.join(', ')}`);
|
|
134
134
|
} else s = C.dim('○ Not set up');
|
|
135
135
|
console.log(` ${labelOf(t).padEnd(W)} ${s}`);
|
|
136
136
|
}
|
|
@@ -153,6 +153,8 @@ Examples:
|
|
|
153
153
|
const userNames = userTampered.map((t) => t.label).join(', ');
|
|
154
154
|
const mdmNames = mdmTampered.map((t) => t.label).join(', ');
|
|
155
155
|
const allFix = tampered.map((t) => t.key);
|
|
156
|
+
// A codex policy can live only in the user's cache, which a sudo re-detection may not see.
|
|
157
|
+
const mdmCmd = elevated(mdmTampered.some((t) => t.notEnforced) ? `unbound setup ${mdmFix.join(' ')}` : 'unbound doctor --fix');
|
|
156
158
|
const anyInstalled = tools.some((t) => t.status !== 'not-installed');
|
|
157
159
|
|
|
158
160
|
if (opts.fix) {
|
|
@@ -176,7 +178,7 @@ Examples:
|
|
|
176
178
|
// tools only, and point at sudo for whatever's org-managed.
|
|
177
179
|
if (!root && !userFix.length) {
|
|
178
180
|
console.log(`${mdmNames} ${mdmFix.length === 1 ? 'is' : 'are'} set up by your organization.`);
|
|
179
|
-
console.log(` Run ${C.bold(
|
|
181
|
+
console.log(` Run ${C.bold(mdmCmd)} to repair ${mdmFix.length === 1 ? 'it' : 'them'}.`);
|
|
180
182
|
console.log('');
|
|
181
183
|
process.exitCode = 1;
|
|
182
184
|
return;
|
|
@@ -186,7 +188,7 @@ Examples:
|
|
|
186
188
|
output.info(`Reinstalling: ${fixDisplay}${root ? ' (org-wide, all users)' : ''}`);
|
|
187
189
|
console.log('');
|
|
188
190
|
const r = spawnSync(process.argv[0], [process.argv[1], 'setup', ...fixNow], { stdio: 'inherit' });
|
|
189
|
-
if (!root && mdmFix.length) console.error(` ${mdmNames} ${mdmFix.length === 1 ? 'is' : 'are'} set up by your organization — run ${C.bold(
|
|
191
|
+
if (!root && mdmFix.length) console.error(` ${mdmNames} ${mdmFix.length === 1 ? 'is' : 'are'} set up by your organization — run ${C.bold(mdmCmd)} to repair ${mdmFix.length === 1 ? 'it' : 'them'}.`);
|
|
190
192
|
process.exitCode = fixExitCode(r.status, root, mdmFix.length > 0) || ((configMismatch || blankConfigDir) ? 1 : 0);
|
|
191
193
|
return;
|
|
192
194
|
}
|
|
@@ -213,11 +215,11 @@ Examples:
|
|
|
213
215
|
console.log(` Run ${C.bold('unbound doctor --fix')} to repair ${userNames}.`);
|
|
214
216
|
} else if (!userFix.length) {
|
|
215
217
|
console.log(` ${mdmNames} ${mdmFix.length === 1 ? 'is' : 'are'} set up by your organization.`);
|
|
216
|
-
console.log(` Run ${C.bold(
|
|
218
|
+
console.log(` Run ${C.bold(mdmCmd)} to repair ${mdmFix.length === 1 ? 'it' : 'them'}.`);
|
|
217
219
|
} else {
|
|
218
220
|
// Both: a user without sudo can still fix their own tools; sudo fixes everything.
|
|
219
221
|
console.log(` Run ${C.bold('unbound doctor --fix')} to repair ${userNames}.`);
|
|
220
|
-
console.log(` ${mdmNames} ${mdmFix.length === 1 ? 'is' : 'are'} set up by your organization — run ${C.bold(
|
|
222
|
+
console.log(` ${mdmNames} ${mdmFix.length === 1 ? 'is' : 'are'} set up by your organization — run ${C.bold(mdmCmd)} to repair ${mdmFix.length === 1 ? 'it' : 'them'} too.`);
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
225
|
if (key.state === 'invalid') {
|
package/src/toolHealth.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const path = require('path');
|
|
19
|
-
const { spawnSync } = require('child_process');
|
|
19
|
+
const { spawnSync, execFileSync } = require('child_process');
|
|
20
20
|
const TOML = require('@iarna/toml');
|
|
21
21
|
const claudePaths = require('./claudePaths');
|
|
22
22
|
|
|
@@ -238,7 +238,7 @@ function managedDir(family, platform = process.platform, env = process.env) {
|
|
|
238
238
|
const dirs = {
|
|
239
239
|
cursor: { darwin: `${mac}/Cursor`, linux: '/etc/cursor', win32: path.join(pd, 'Cursor') },
|
|
240
240
|
'claude-code': { darwin: `${mac}/ClaudeCode`, linux: '/etc/claude-code', win32: path.join(pf, 'ClaudeCode') },
|
|
241
|
-
codex: { darwin:
|
|
241
|
+
codex: { darwin: '/etc/codex', linux: '/etc/codex', win32: path.join(pf, 'Codex') },
|
|
242
242
|
augment: { darwin: '/etc/augment', linux: '/etc/augment', win32: path.join(pd, 'Augment') },
|
|
243
243
|
};
|
|
244
244
|
if (!dirs[family]) return null;
|
|
@@ -371,7 +371,7 @@ function buildVariants(gatewayUrl, binaryPath) {
|
|
|
371
371
|
const features = cfg.features;
|
|
372
372
|
if (!features || typeof features !== 'object') return false;
|
|
373
373
|
return features.hooks === undefined ? features.codex_hooks === true : features.hooks === true;
|
|
374
|
-
} }, { short: 'codex hooks not enabled' }),
|
|
374
|
+
} }, { short: 'codex hooks not enabled', kind: 'aux' }), // aux: an org install leaves it set with no user hook
|
|
375
375
|
scriptCheck('Hook script', '~/.codex/hooks/unbound.py'),
|
|
376
376
|
envKeyCheck('API key env', 'UNBOUND_CODEX_API_KEY'),
|
|
377
377
|
],
|
|
@@ -436,6 +436,23 @@ function detectVariant(variant) {
|
|
|
436
436
|
return { key: variant.key, label: variant.label, family: variant.family, mode: variant.mode, status: statusOf(checks), checks };
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
// Where codex's requirements set allow_managed_hooks_only (codex then skips user-level hooks), or null.
|
|
440
|
+
// Reads the host's real policy files, like mdmDetect reads its managed dirs; tests pass _codexManagedOnly.
|
|
441
|
+
function codexManagedHooksOnly() {
|
|
442
|
+
const on = (text) => /^[ \t]*["']?allow_managed_hooks_only["']?[ \t]*=[ \t]*true\b/m.test((text || '').replace(/^\uFEFF/, ''));
|
|
443
|
+
const strings = (v) => (typeof v === 'string' ? [v] : v && typeof v === 'object' ? Object.values(v).flatMap(strings) : []);
|
|
444
|
+
const system = process.platform === 'win32' ? path.join(process.env.ProgramData || 'C:\\ProgramData', 'OpenAI', 'Codex', 'requirements.toml') : '/etc/codex/requirements.toml';
|
|
445
|
+
if (on(readText(system))) return system;
|
|
446
|
+
// The cloud cache is JSON; its TOML lives in string values.
|
|
447
|
+
const cache = path.join(process.env.CODEX_HOME || path.join(HOME, '.codex'), 'cloud-config-bundle-cache.json');
|
|
448
|
+
if (strings(readJson(cache)).some(on)) return cache;
|
|
449
|
+
const plist = '/Library/Managed Preferences/com.openai.codex.plist';
|
|
450
|
+
try {
|
|
451
|
+
const b64 = execFileSync('/usr/bin/plutil', ['-extract', 'requirements_toml_base64', 'raw', '-o', '-', plist], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
452
|
+
return on(Buffer.from(b64, 'base64').toString()) ? plist : null;
|
|
453
|
+
} catch { return null; }
|
|
454
|
+
}
|
|
455
|
+
|
|
439
456
|
// Collapse the per-(tool,mode) variants to one entry per product family. Picks
|
|
440
457
|
// the installed/tampered mode if any; otherwise reports managed-by-mdm or
|
|
441
458
|
// not-installed. This is what both `doctor` and `status` render.
|
|
@@ -443,7 +460,8 @@ function detectVariant(variant) {
|
|
|
443
460
|
// org-managed scenarios can be exercised without writing under /Library or /etc.
|
|
444
461
|
// `_binaryPath` (test-only) overrides the system hook-binary path so binary-mode
|
|
445
462
|
// scenarios can be exercised without writing under /opt.
|
|
446
|
-
|
|
463
|
+
// `_codexManagedOnly` (test-only) replaces the codexManagedHooksOnly() result.
|
|
464
|
+
function detectTools({ gatewayUrl, _mdmDirs, _binaryPath, _codexManagedOnly } = {}) {
|
|
447
465
|
const variants = buildVariants(gatewayUrl, _binaryPath).map(detectVariant);
|
|
448
466
|
const families = [];
|
|
449
467
|
const seen = new Set();
|
|
@@ -472,6 +490,16 @@ function detectTools({ gatewayUrl, _mdmDirs, _binaryPath } = {}) {
|
|
|
472
490
|
}
|
|
473
491
|
}
|
|
474
492
|
}
|
|
493
|
+
// A working org hook in codex's system folder runs regardless, so only a user-level install is at risk.
|
|
494
|
+
// Gateway mode proxies traffic and is unaffected; a dual-mode install already reports 'two modes installed'.
|
|
495
|
+
const codex = families.find((t) => t.key === 'codex-subscription' && t.status !== 'not-installed');
|
|
496
|
+
const atRisk = codex && mdmDetect('codex', _mdmDirs && _mdmDirs.codex, _binaryPath).status !== 'healthy';
|
|
497
|
+
const managedOnly = atRisk && (_codexManagedOnly !== undefined ? _codexManagedOnly : codexManagedHooksOnly());
|
|
498
|
+
if (managedOnly) {
|
|
499
|
+
codex.checks.push({ name: 'Managed hooks only', ok: false, kind: 'structural', summary: 'Codex only runs managed hooks',
|
|
500
|
+
detail: `allow_managed_hooks_only is set in ${managedOnly}` });
|
|
501
|
+
Object.assign(codex, { status: 'tampered', notEnforced: true, scope: 'mdm' });
|
|
502
|
+
}
|
|
475
503
|
return families;
|
|
476
504
|
}
|
|
477
505
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function detectCodex({ userHook = true, mdmHook = false, managedOnly = null, cache, real = false } = {}) {
|
|
8
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-codex-'));
|
|
9
|
+
const write = (p, text) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); };
|
|
10
|
+
const saved = { HOME: process.env.HOME, KEY: process.env.UNBOUND_CODEX_API_KEY, CODEX_HOME: process.env.CODEX_HOME };
|
|
11
|
+
try {
|
|
12
|
+
process.env.HOME = home;
|
|
13
|
+
process.env.UNBOUND_CODEX_API_KEY = 'k';
|
|
14
|
+
process.env.CODEX_HOME = path.join(home, '.codex');
|
|
15
|
+
if (cache) write(path.join(home, '.codex', 'cloud-config-bundle-cache.json'), JSON.stringify(cache));
|
|
16
|
+
write(path.join(home, '.codex', 'config.toml'), '[features]\nhooks = true\n');
|
|
17
|
+
if (userHook) {
|
|
18
|
+
write(path.join(home, '.codex', 'hooks.json'), JSON.stringify({ hooks: { Stop: [{ hooks: [{ command: 'unbound.py' }] }] } }));
|
|
19
|
+
write(path.join(home, '.codex', 'hooks', 'unbound.py'), '');
|
|
20
|
+
}
|
|
21
|
+
const mdm = path.join(home, 'etc-codex');
|
|
22
|
+
if (mdmHook) {
|
|
23
|
+
write(path.join(mdm, 'hooks.json'), JSON.stringify({ hooks: { Stop: [{ hooks: [{ command: 'unbound.py' }] }] } }));
|
|
24
|
+
write(path.join(mdm, 'hooks', 'unbound.py'), '');
|
|
25
|
+
}
|
|
26
|
+
delete require.cache[require.resolve('../src/toolHealth')];
|
|
27
|
+
delete require.cache[require.resolve('../src/claudePaths')];
|
|
28
|
+
const { detectTools } = require('../src/toolHealth');
|
|
29
|
+
return detectTools({ _mdmDirs: { codex: mdm }, _codexManagedOnly: real ? undefined : managedOnly }).find((t) => t.family === 'codex');
|
|
30
|
+
} finally {
|
|
31
|
+
process.env.HOME = saved.HOME;
|
|
32
|
+
if (saved.CODEX_HOME === undefined) delete process.env.CODEX_HOME; else process.env.CODEX_HOME = saved.CODEX_HOME;
|
|
33
|
+
if (saved.KEY === undefined) delete process.env.UNBOUND_CODEX_API_KEY; else process.env.UNBOUND_CODEX_API_KEY = saved.KEY;
|
|
34
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('a user-level codex hook under allow_managed_hooks_only is not enforced and routed org-wide', () => {
|
|
39
|
+
const t = detectCodex({ managedOnly: '/etc/codex/requirements.toml' });
|
|
40
|
+
assert.equal(t.status, 'tampered');
|
|
41
|
+
assert.equal(t.notEnforced, true);
|
|
42
|
+
assert.equal(t.scope, 'mdm');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('an org install that left the feature flag behind reads as managed, not tampered', () => {
|
|
46
|
+
const t = detectCodex({ userHook: false, mdmHook: true, managedOnly: '/etc/codex/requirements.toml' });
|
|
47
|
+
assert.equal(t.status, 'managed-by-mdm');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('the setting on the first line of the cloud policy cache is detected', () => {
|
|
51
|
+
const t = detectCodex({ real: true, cache: { toml: 'allow_managed_hooks_only = true\n' } });
|
|
52
|
+
assert.equal(t.notEnforced, true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('a working org hook means the user install is not flagged', () => {
|
|
56
|
+
const t = detectCodex({ mdmHook: true, managedOnly: '/etc/codex/requirements.toml' });
|
|
57
|
+
assert.ok(!t.notEnforced);
|
|
58
|
+
});
|
|
@@ -31,7 +31,7 @@ test('managedDir win32: honors ProgramFiles/ProgramData overrides, falls back wh
|
|
|
31
31
|
test('managedDir darwin: unchanged', () => {
|
|
32
32
|
assert.equal(managedDir('cursor', 'darwin', {}), '/Library/Application Support/Cursor');
|
|
33
33
|
assert.equal(managedDir('claude-code', 'darwin', {}), '/Library/Application Support/ClaudeCode');
|
|
34
|
-
assert.equal(managedDir('codex', 'darwin', {}), '/
|
|
34
|
+
assert.equal(managedDir('codex', 'darwin', {}), '/etc/codex');
|
|
35
35
|
assert.equal(managedDir('augment', 'darwin', {}), '/etc/augment');
|
|
36
36
|
});
|
|
37
37
|
|