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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbound-cli",
3
- "version": "1.16.21",
3
+ "version": "1.16.22",
4
4
  "description": "CLI tool for Unbound - AI Gateway management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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(elevated('unbound doctor --fix'))} to repair ${mdmFix.length === 1 ? 'it' : 'them'}.`);
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(elevated('unbound doctor --fix'))} to repair ${mdmFix.length === 1 ? 'it' : 'them'}.`);
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(elevated('unbound doctor --fix'))} to repair ${mdmFix.length === 1 ? 'it' : 'them'}.`);
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(elevated('unbound doctor --fix'))} to repair ${mdmFix.length === 1 ? 'it' : 'them'} too.`);
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: `${mac}/Codex`, linux: '/etc/codex', win32: path.join(pf, 'Codex') },
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
- function detectTools({ gatewayUrl, _mdmDirs, _binaryPath } = {}) {
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', {}), '/Library/Application Support/Codex');
34
+ assert.equal(managedDir('codex', 'darwin', {}), '/etc/codex');
35
35
  assert.equal(managedDir('augment', 'darwin', {}), '/etc/augment');
36
36
  });
37
37