@ghl-ai/aw 0.1.44-beta.7 → 0.1.44-beta.9

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.
@@ -116,6 +116,12 @@ export function summarizeAsOneLine(state) {
116
116
  parts.push(`slashShim ${slashShimSummaryToken(state.slashShim.action)}`);
117
117
  }
118
118
 
119
+ // Proxy state — only surfaced when actually applied. Skipping the
120
+ // token on no-proxy harnesses keeps the summary line short.
121
+ if (state.proxy && state.proxy.applied && state.proxy.source) {
122
+ parts.push(`proxy ${state.proxy.source}`);
123
+ }
124
+
119
125
  const seconds = Number(state.durationMs ?? 0) / 1000;
120
126
  parts.push(`init ${seconds.toFixed(1)}s`);
121
127
  parts.push(state.didInit ? 'ready' : 'skipped');
@@ -484,6 +490,18 @@ export function dumpPostInitState(opts = {}) {
484
490
  lines.push('');
485
491
  }
486
492
 
493
+ // Proxy block (always emitted when opts.proxy is provided, so operators
494
+ // can confirm whether HTTPS_PROXY was honored. Codex Cloud diagnostics
495
+ // depends on this being present.)
496
+ if (opts.proxy && typeof opts.proxy === 'object') {
497
+ const proxy = opts.proxy;
498
+ lines.push('proxy:');
499
+ lines.push(` applied=${proxy.applied ? 'true' : 'false'}`);
500
+ if (proxy.source) lines.push(` source=${proxy.source}`);
501
+ if (proxy.proxyUrl) lines.push(` url=${proxy.proxyUrl}`);
502
+ lines.push('');
503
+ }
504
+
487
505
  lines.push('─── end ───');
488
506
  lines.push('');
489
507
 
package/c4/gitAuth.mjs CHANGED
@@ -349,12 +349,21 @@ export async function verifyAuth(mode, token, { shell = defaultShell, fetchImpl
349
349
  * c FAIL → 'rewrite-conflict' (the Cursor scenario)
350
350
  * else → 'ok'
351
351
  *
352
+ * Note: 'pat-invalid' is misnamed when `authStatus` is undefined (i.e. the
353
+ * REST `fetch()` errored out before getting an HTTP status). The Codex
354
+ * Cloud failure mode is exactly that: HTTPS_PROXY is set but Node fetch
355
+ * doesn't honor it, so the REST probe returns `{ ok:false, error:'fetch failed' }`
356
+ * with no status. Callers must distinguish the two by looking at
357
+ * `authStatus` and `lsRemoteWithAuthOk`.
358
+ *
352
359
  * @param {string} token
353
360
  * @param {object} [opts]
354
361
  * @returns {Promise<{
355
362
  * apiOk: boolean,
356
363
  * lsRemoteWithAuthOk: boolean,
357
364
  * lsRemoteViaRewriteOk: boolean,
365
+ * authStatus: number | undefined,
366
+ * authError: string | undefined,
358
367
  * diagnosis: 'ok' | 'pat-invalid' | 'rewrite-conflict' | 'helper-mismatch' | 'network'
359
368
  * }>}
360
369
  */
@@ -380,5 +389,12 @@ export async function preflightPlatformDocs(token, opts = {}) {
380
389
  diagnosis = 'ok';
381
390
  }
382
391
 
383
- return { apiOk, lsRemoteWithAuthOk, lsRemoteViaRewriteOk, diagnosis };
392
+ return {
393
+ apiOk,
394
+ lsRemoteWithAuthOk,
395
+ lsRemoteViaRewriteOk,
396
+ authStatus: a.status,
397
+ authError: a.error,
398
+ diagnosis,
399
+ };
384
400
  }
package/c4/index.mjs CHANGED
@@ -56,6 +56,7 @@ export { copyRepoRootInstructions } from './repoRootInstructions.mjs';
56
56
  export { ensureRepoLocalIgnore } from './repoLocalIgnore.mjs';
57
57
  export { jsonMergeWithDedup, claudeHooksMerge } from './jsonMerge.mjs';
58
58
  export { runPreflight } from './preflight.mjs';
59
+ export { configureUndiciProxy } from './proxyConfig.mjs';
59
60
  export {
60
61
  summarizeAsOneLine,
61
62
  diagnoseAwRouterView,
@@ -0,0 +1,127 @@
1
+ /**
2
+ * c4/proxyConfig.mjs — install undici proxy dispatcher from env.
3
+ *
4
+ * Why this exists:
5
+ * Codex Cloud (and other corporate-MITM cloud agents) force outbound
6
+ * HTTPS through a proxy via HTTPS_PROXY. Standard tools (curl, git, npm)
7
+ * honor that variable transparently. Node's native fetch (undici) does
8
+ * NOT honor proxy env vars before Node 24, so REST preflight in
9
+ * `aw c4` fails with `fetch failed: ENETUNREACH` even though the same
10
+ * PAT works fine via `git ls-remote` and `curl`.
11
+ *
12
+ * This module reads the proxy URL from env in the documented priority
13
+ * order and installs an undici ProxyAgent as the global dispatcher
14
+ * exactly once at orchestrator entry. After this runs, every fetch()
15
+ * call inside aw-c4 routes through the proxy.
16
+ *
17
+ * Why dynamic import:
18
+ * Keeps the no-proxy fast path zero-cost (no undici namespace resolution)
19
+ * and lets unit tests inject a synthetic { setGlobalDispatcher, ProxyAgent }
20
+ * pair via opts.undiciImpl without going through vi.mock module-graph
21
+ * plumbing.
22
+ *
23
+ * Failure isolation:
24
+ * If ProxyAgent construction or setGlobalDispatcher throws, we swallow
25
+ * it and report `applied: false`. A misconfigured proxy URL must NEVER
26
+ * crash `aw c4` — the orchestrator continues and surfaces the failure
27
+ * in diagnostics so the user can see what happened.
28
+ *
29
+ * Contract:
30
+ * .aw_docs/features/aw-c4-codex-proxy-fix/spec.md::§"libs/aw/c4/proxyConfig.mjs"
31
+ */
32
+
33
+ const PROXY_ENV_PRIORITY = Object.freeze([
34
+ 'HTTPS_PROXY',
35
+ 'https_proxy',
36
+ 'HTTP_PROXY',
37
+ 'http_proxy',
38
+ ]);
39
+
40
+ /**
41
+ * @typedef {object} ProxyConfigResult
42
+ * @property {boolean} applied True iff a global dispatcher was
43
+ * installed successfully.
44
+ * @property {string|null} proxyUrl The URL the agent was constructed
45
+ * with, or null when no proxy env
46
+ * var was set.
47
+ * @property {string|null} source Exact env-var name the URL was
48
+ * resolved from, or null. Useful
49
+ * for diagnostics (so the user can
50
+ * see "we used HTTPS_PROXY, not
51
+ * http_proxy").
52
+ */
53
+
54
+ /**
55
+ * Resolve the first non-empty proxy URL from env in priority order.
56
+ *
57
+ * @param {NodeJS.ProcessEnv} env
58
+ * @returns {{ url: string, source: string } | null}
59
+ */
60
+ function resolveProxyFromEnv(env) {
61
+ for (const name of PROXY_ENV_PRIORITY) {
62
+ const value = env?.[name];
63
+ if (typeof value === 'string' && value.length > 0) {
64
+ return { url: value, source: name };
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Install an undici ProxyAgent as the global fetch dispatcher when
72
+ * an HTTP/HTTPS proxy env var is set.
73
+ *
74
+ * Calling this with no proxy in env is safe — it returns
75
+ * `{ applied: false, ... }` without touching undici. Calling it twice
76
+ * is also safe; the second call replaces the first dispatcher.
77
+ *
78
+ * @param {NodeJS.ProcessEnv} [env]
79
+ * @param {object} [opts]
80
+ * @param {{ setGlobalDispatcher: Function, ProxyAgent: Function }} [opts.undiciImpl]
81
+ * Override for tests. When omitted, undici is dynamically imported.
82
+ * @returns {ProxyConfigResult}
83
+ */
84
+ export function configureUndiciProxy(env = process.env, opts = {}) {
85
+ const resolved = resolveProxyFromEnv(env);
86
+ if (!resolved) {
87
+ return { applied: false, proxyUrl: null, source: null };
88
+ }
89
+
90
+ const undici = opts.undiciImpl ?? loadUndiciSync();
91
+ if (!undici) {
92
+ return { applied: false, proxyUrl: resolved.url, source: resolved.source };
93
+ }
94
+
95
+ try {
96
+ const agent = new undici.ProxyAgent(resolved.url);
97
+ undici.setGlobalDispatcher(agent);
98
+ return { applied: true, proxyUrl: resolved.url, source: resolved.source };
99
+ } catch {
100
+ // ProxyAgent ctor or setGlobalDispatcher rejected the URL. The most
101
+ // common cause is a malformed URL ("proxy:8080" without scheme).
102
+ // Caller treats apply-failure same as no-proxy and continues.
103
+ return { applied: false, proxyUrl: resolved.url, source: resolved.source };
104
+ }
105
+ }
106
+
107
+ import { createRequire } from 'node:module';
108
+
109
+ /**
110
+ * Load the bundled `undici` synchronously via createRequire so we can
111
+ * keep `configureUndiciProxy` synchronous (orchestrator entry must not
112
+ * become async just to install a dispatcher).
113
+ *
114
+ * Falls back to `null` if the import is unavailable for any reason —
115
+ * in which case the caller treats it as a no-op.
116
+ */
117
+ let cachedUndici;
118
+ function loadUndiciSync() {
119
+ if (cachedUndici !== undefined) return cachedUndici;
120
+ try {
121
+ const localRequire = createRequire(import.meta.url);
122
+ cachedUndici = localRequire('undici');
123
+ } catch {
124
+ cachedUndici = null;
125
+ }
126
+ return cachedUndici;
127
+ }
package/commands/c4.mjs CHANGED
@@ -210,6 +210,21 @@ export async function c4Command(rawArgs, overrides = {}) {
210
210
 
211
211
  const startTs = now();
212
212
 
213
+ // Step 0 — undici proxy installation.
214
+ // Codex Cloud (and other corporate-MITM cloud agents) force outbound
215
+ // HTTPS through HTTPS_PROXY. Standard tools (curl, git, npm) honor it
216
+ // transparently; Node's native fetch (undici) does not before Node 24.
217
+ // Install a global ProxyAgent dispatcher BEFORE any other step so every
218
+ // subsequent fetch() (preflight REST, MCP smoke probe, etc.) routes
219
+ // through the proxy. Defensive try/catch — proxy install failure must
220
+ // never crash aw-c4.
221
+ let proxy = { applied: false, proxyUrl: null, source: null };
222
+ try {
223
+ proxy = c4.configureUndiciProxy(env);
224
+ } catch (err) {
225
+ writer.stderr(`[aw-c4] configureUndiciProxy failed (non-fatal): ${err?.message ?? err}\n`);
226
+ }
227
+
213
228
  // Step 1 — harness detection (override wins).
214
229
  const cliHarness = args['--harness'];
215
230
  const harness = SUPPORTED_HARNESSES.includes(cliHarness) ? cliHarness : c4.detectHarness({ env }).harness;
@@ -228,6 +243,7 @@ export async function c4Command(rawArgs, overrides = {}) {
228
243
  authOk: false,
229
244
  didInit: false,
230
245
  durationMs: now() - startTs,
246
+ proxy,
231
247
  }) + '\n');
232
248
  return exit(0);
233
249
  }
@@ -263,6 +279,7 @@ export async function c4Command(rawArgs, overrides = {}) {
263
279
  bridge: self.view ? { ok: self.view.ok } : undefined,
264
280
  injector: self.injector ? { ok: self.injector.ok } : undefined,
265
281
  diagnose: true,
282
+ proxy,
266
283
  }) + '\n');
267
284
  } catch (err) {
268
285
  writer.stderr(`[aw-c4] diagnose: summary line failed: ${err?.message ?? err}\n`);
@@ -282,18 +299,39 @@ export async function c4Command(rawArgs, overrides = {}) {
282
299
  // Step 5 — preflight.
283
300
  const preflight = await c4.preflightPlatformDocs(token);
284
301
  if (preflight.diagnosis === 'pat-invalid') {
285
- writer.stderr('[aw-c4] PAT invalid (REST 4xx). Refresh GITHUB_PAT and retry.\n');
286
- writer.stdout(c4.summarizeAsOneLine({
287
- harness,
288
- hasToken: true,
289
- tokenSource,
290
- authOk: false,
291
- didInit: false,
292
- durationMs: now() - startTs,
293
- }) + '\n');
294
- return exit(0);
302
+ // Disambiguate two failure modes that both end up tagged 'pat-invalid':
303
+ // (a) Real bad PAT — REST returned a 4xx HTTP status (`authStatus`
304
+ // is a number). Git ls-remote also fails. Exit 0 with the
305
+ // actionable "refresh PAT" message.
306
+ // (b) Codex Cloud proxy gap — REST `fetch()` failed BEFORE getting
307
+ // any HTTP status (`authStatus == null`, `authError` like
308
+ // "fetch failed: ENETUNREACH") because Node fetch can't see
309
+ // HTTPS_PROXY. Git ls-remote works (libcurl honors the proxy).
310
+ // The PAT is fine; proceed with install and surface the real
311
+ // cause on stderr.
312
+ const restStatusKnown = typeof preflight.authStatus === 'number';
313
+ if (!restStatusKnown && preflight.lsRemoteWithAuthOk) {
314
+ const cause = preflight.authError ?? 'unknown';
315
+ writer.stderr(
316
+ `[aw-c4] api.github.com unreachable from Node fetch (${cause}); ` +
317
+ `git auth OK, continuing.\n`,
318
+ );
319
+ // fall through into normal install flow
320
+ } else {
321
+ writer.stderr('[aw-c4] PAT invalid (REST 4xx). Refresh GITHUB_PAT and retry.\n');
322
+ writer.stdout(c4.summarizeAsOneLine({
323
+ harness,
324
+ hasToken: true,
325
+ tokenSource,
326
+ authOk: false,
327
+ didInit: false,
328
+ durationMs: now() - startTs,
329
+ proxy,
330
+ }) + '\n');
331
+ return exit(0);
332
+ }
295
333
  }
296
- if (preflight.diagnosis !== 'ok') {
334
+ if (preflight.diagnosis !== 'ok' && preflight.diagnosis !== 'pat-invalid') {
297
335
  writer.stderr(`[aw-c4] preflight diagnosis: ${preflight.diagnosis} (continuing)\n`);
298
336
  }
299
337
 
@@ -306,6 +344,7 @@ export async function c4Command(rawArgs, overrides = {}) {
306
344
  authOk: preflight.diagnosis === 'ok',
307
345
  didInit: false,
308
346
  durationMs: now() - startTs,
347
+ proxy,
309
348
  }) + '\n');
310
349
  return exit(0);
311
350
  }
@@ -385,6 +424,7 @@ export async function c4Command(rawArgs, overrides = {}) {
385
424
  awHome,
386
425
  configPaths: [],
387
426
  slashShim: slashShim?.ok ? slashShim.value : null,
427
+ proxy,
388
428
  });
389
429
 
390
430
  // Step 18 — one-line summary.
@@ -399,6 +439,7 @@ export async function c4Command(rawArgs, overrides = {}) {
399
439
  injector: branch?.injector,
400
440
  mcpProbe: mcpProbe?.value?.authStatus,
401
441
  slashShim: slashShim?.ok ? slashShim.value : null,
442
+ proxy,
402
443
  }) + '\n');
403
444
 
404
445
  return exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.44-beta.7",
3
+ "version": "0.1.44-beta.9",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {