@rubytech/create-maxy 1.0.799 → 1.0.800

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/dist/index.js CHANGED
@@ -1765,11 +1765,17 @@ function installTunnelScripts() {
1765
1765
  // $HOME symlink — it's a helper, not a top-level operator command. We do
1766
1766
  // chmod +x defensively so `ls -l` and any ad-hoc `~/setup-tunnel.sh` copy
1767
1767
  // flow sees it as executable (Task 588).
1768
+ //
1769
+ // _cdp-authorize-matcher.mjs is imported by _cdp-authorize.mjs (Task 855)
1770
+ // — the tri-state matcher's MATCH_EXPR + findMatch live in this side
1771
+ // module so JSDOM tests run identical logic to the live page. chmod +x
1772
+ // is defensive symmetry; the file is read by Node, not exec'd.
1768
1773
  const cdpAuthorizeSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/_cdp-authorize.mjs");
1774
+ const cdpMatcherSrc = join(INSTALL_DIR, "platform/plugins/cloudflare/scripts/_cdp-authorize-matcher.mjs");
1769
1775
  const setupLink = resolve(process.env.HOME ?? "/root", "setup-tunnel.sh");
1770
1776
  const resetLink = resolve(process.env.HOME ?? "/root", "reset-tunnel.sh");
1771
1777
  const listLink = resolve(process.env.HOME ?? "/root", "list-cf-domains.sh");
1772
- for (const src of [setupSrc, resetSrc, listSrc, cdpAuthorizeSrc]) {
1778
+ for (const src of [setupSrc, resetSrc, listSrc, cdpAuthorizeSrc, cdpMatcherSrc]) {
1773
1779
  try {
1774
1780
  chmodSync(src, 0o755);
1775
1781
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.799",
3
+ "version": "1.0.800",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -131,6 +131,18 @@ ls /tmp/.X11-unix/
131
131
 
132
132
  You should see `X99`. If not, start VNC before retrying Step 1.
133
133
 
134
+ ### Three states the consent page can render
135
+
136
+ The dashboard at `dash.cloudflare.com/argotunnel?...` shows one of three states. The automation script's CDP helper (`_cdp-authorize.mjs`) detects each one; the same triage applies when you walk the flow manually.
137
+
138
+ 1. **Pre-authorize — the Authorize button is visible.** This is the normal first run. Click the zone you want, then click **Authorize**. Cloudflare's callback fires and `~/.cloudflared/cert.pem` lands a second or two later. Continue with the `mv` above.
139
+
140
+ 2. **Post-success — "Cloudflared has installed a certificate" modal.** The bound account already has a cert authorised for the zones the dashboard knows about. The OAuth callback is idempotent — navigating to a fresh `cloudflared tunnel login` URL still fires the callback, so `~/.cloudflared/cert.pem` will land. Wait a second or two, then run the `mv`.
141
+
142
+ If the cert *also* exists at `${CFG_DIR}/cert.pem` from a prior partial run, Step 1 is already complete — skip to Step 2.
143
+
144
+ 3. **Blank or button-less — neither the Authorize button nor the success modal is on the page.** The browser may be mid-load, signed out of Cloudflare, blocking on captcha, or showing some other state. Sign in to Cloudflare in the same browser, navigate back to the URL `cloudflared tunnel login` printed, and complete the click manually. If the URL has expired, kill `cloudflared` (`Ctrl+C`) and re-run Step 1 to get a fresh URL.
145
+
134
146
  ---
135
147
 
136
148
  ## Step 2 — List existing tunnels
@@ -0,0 +1,74 @@
1
+ // Tri-state DOM matcher for the Cloudflare argotunnel consent page (Task 855).
2
+ //
3
+ // `dash.cloudflare.com/argotunnel?...` legitimately renders three observable
4
+ // states for our flow:
5
+ //
6
+ // 1. Pre-authorize — a <button> or <input type="submit"> whose trimmed text
7
+ // matches /^(authorize|connect)$/i (and is not disabled). Click it; the
8
+ // callback fires, cloudflared writes ~/.cloudflared/cert.pem.
9
+ //
10
+ // 2. Post-success — the dashboard renders a Success modal containing the
11
+ // stable substring "Cloudflared has installed a certificate". This shape
12
+ // is reached when the user already authorised this account before (or
13
+ // did so manually in VNC between cloudflared spawn and helper poll).
14
+ // The OAuth callback is idempotent on the cert side: when the helper
15
+ // sees this state, the running cloudflared subprocess will still write
16
+ // ~/.cloudflared/cert.pem because the callback URL is exercised by the
17
+ // navigation. The wrapper drops into the existing cert-poll afterwards.
18
+ //
19
+ // 3. Neither — the page is mid-load, blank, or in some other state. The
20
+ // caller polls again until BUTTON_POLL_TIMEOUT_MS, then exits 1.
21
+ //
22
+ // The matcher returns a discriminated union from a single DOM read:
23
+ //
24
+ // { kind: 'button', descriptor: {tag,text,disabled} } | { kind: 'success' } | null
25
+ //
26
+ // PRIORITY: button > success modal. Page transitions can briefly render both
27
+ // (e.g. during the success animation). If a clickable Authorize button exists,
28
+ // click it — the click produces a fresh callback regardless of any leftover
29
+ // modal text. Only when no button is present do we trust the modal as the
30
+ // terminal state.
31
+ //
32
+ // EXPORTED SHAPES:
33
+ //
34
+ // * findMatch(doc) — JS function form. Used by JSDOM-based unit tests
35
+ // (platform/ui/scripts/__tests__/cdp-authorize-matcher.test.ts) so future
36
+ // matcher edits replay against captured DOM fixtures offline (Success
37
+ // criterion 8 of Task 855).
38
+ //
39
+ // * MATCH_EXPR — string form, evaluated by Chrome DevTools Protocol's
40
+ // Runtime.evaluate in _cdp-authorize.mjs's polling loop. Built from
41
+ // findMatch.toString() so the live page and the tests run identical
42
+ // logic — single source of truth.
43
+ //
44
+ // CONTRACT — DO NOT loosen the success-modal anchor. Tighter anchors regress
45
+ // on Cloudflare copy edits; "Cloudflared has installed a certificate" is the
46
+ // load-bearing claim of the modal. Wider anchors (e.g. matching just
47
+ // "certificate") would false-positive on the pre-authorize page.
48
+
49
+ export function findMatch(doc) {
50
+ const candidates = Array.from(doc.querySelectorAll('button, input[type="submit"]'));
51
+ const match = candidates.find((el) => {
52
+ const text = (el.textContent ?? el.value ?? '').trim();
53
+ return /^(authorize|connect)$/i.test(text) && !el.disabled;
54
+ });
55
+ if (match) {
56
+ const descriptor = {
57
+ tag: match.tagName.toLowerCase(),
58
+ text: (match.textContent ?? match.value ?? '').trim().slice(0, 40),
59
+ disabled: Boolean(match.disabled),
60
+ };
61
+ match.click();
62
+ return { kind: 'button', descriptor };
63
+ }
64
+ // textContent over innerText: jsdom's innerText is partial; the dashboard's
65
+ // success-modal text is plain DOM content (not visibility-gated by CSS for
66
+ // anyone reading the page) so textContent finds it on both runtimes.
67
+ const bodyText = doc.body ? doc.body.textContent || '' : '';
68
+ if (bodyText.includes('Cloudflared has installed a certificate')) {
69
+ return { kind: 'success' };
70
+ }
71
+ return null;
72
+ }
73
+
74
+ export const MATCH_EXPR = `(${findMatch.toString()})(document)`;
@@ -1,33 +1,51 @@
1
1
  #!/usr/bin/env node
2
- // Drive the Cloudflare argotunnel Authorize click via CDP WebSocket (Task 588).
2
+ // Drive the Cloudflare argotunnel consent page to a deterministic terminal
3
+ // state via CDP WebSocket. Originally Task 588 (Authorize click from code,
4
+ // collapsing a 180 s human-latency window). Task 855 widens the contract to
5
+ // three states because the dashboard does not always render a button — once
6
+ // the bound account has authorized this device's zones, navigating the same
7
+ // (or a fresh) consent URL renders a Success modal verbatim, and the
8
+ // pre-855 helper conflated "no button visible yet" with "no button will
9
+ // ever appear" and exited error.
3
10
  //
4
- // Why this exists: setup-tunnel.sh navigates the VNC browser to the argotunnel
5
- // consent URL via `PUT /json/new?<url>`. That opens the page but does not
6
- // press the Authorize button. Pre-Task 588 the script then polled for
7
- // cert.pem for up to 180 seconds, waiting for a human click in VNC. This
8
- // helper does the click from code, collapsing the wait to ~1-3 seconds.
9
- //
10
- // Protocol: Chrome DevTools Protocol over WebSocket (https://chromedevtools.github.io/devtools-protocol/).
11
+ // Protocol: Chrome DevTools Protocol over WebSocket
12
+ // (https://chromedevtools.github.io/devtools-protocol/):
11
13
  // 1. GET http://127.0.0.1:9222/json/list → find the target with matching id;
12
14
  // extract webSocketDebuggerUrl.
13
15
  // 2. Open WS, enable Page + Runtime domains.
14
16
  // 3. Wait for Page.loadEventFired (or observe document.readyState==='complete').
15
- // 4. Poll Runtime.evaluate every 200 ms for up to ~3 s, looking for a
16
- // button or input[type=submit] whose trimmed text/value matches
17
- // /^(authorize|connect)$/i. When found, click via JS (click() on the
18
- // matched node). Exit 0 on success, 1 with structured stderr otherwise.
17
+ // 4. Poll Runtime.evaluate every 200 ms for up to ~3 s, evaluating the
18
+ // tri-state matcher from _cdp-authorize-matcher.mjs. The matcher
19
+ // returns one of:
20
+ // - { kind:'button', descriptor } Authorize/Connect button found,
21
+ // click() fired in-page; emit
22
+ // reason=clicked, exit 0.
23
+ // - { kind:'success' } — Success modal text detected
24
+ // ("Cloudflared has installed a
25
+ // certificate"); cert is bound to
26
+ // the account. Emit reason=
27
+ // cert-already-installed, exit 0.
28
+ // Caller's existing cert-poll picks
29
+ // up the cert (callback is
30
+ // idempotent on this navigation).
31
+ // - null — Neither anchor matched yet; loop.
32
+ // After BUTTON_POLL_TIMEOUT_MS with no terminal match, emit
33
+ // reason=authorize-button-not-found, exit 1.
19
34
  //
20
35
  // Exit codes:
21
- // 0 - clicked successfully
22
- // 1 - authorize-button-not-found (the happy-path loud failure)
36
+ // 0 - terminal success (reason=clicked OR reason=cert-already-installed)
37
+ // 1 - authorize-button-not-found (loud failure button AND modal absent)
23
38
  // 2 - cdp-ws-unreachable or node-websocket-unavailable
24
39
  // 3 - target-not-found (the target_id isn't in /json/list)
25
40
  // 4 - click-evaluate-threw (Runtime.evaluate returned wasThrown=true)
26
41
  // 5 - protocol-error (malformed CDP response)
27
42
  //
28
- // Each structured failure prints ONE line to stdout in phase_line-compatible
29
- // shape so setup-tunnel.sh's tee_subprocess picks it up into the stream log:
43
+ // Stdout contract (read by setup-tunnel.sh's awk-based reason parser):
30
44
  // cdp-authorize result=<ok|error> reason=<…> elapsed_ms=<…> [detail=<…>]
45
+ // Exactly ONE such line per invocation; the wrapper takes the LAST
46
+ // `result=ok` line via awk so future debug-line additions cannot mis-route.
47
+
48
+ import { MATCH_EXPR } from './_cdp-authorize-matcher.mjs';
31
49
 
32
50
  const CDP_HOST = '127.0.0.1';
33
51
  const CDP_PORT = 9222;
@@ -211,35 +229,16 @@ try {
211
229
  die(1, 'page-load-timeout', { detail: err.message, elapsed_ms: Date.now() - started });
212
230
  }
213
231
 
214
- // Poll for the Authorize button. The expression finds the first
215
- // button/input[type=submit] whose trimmed textContent/value matches
216
- // /^(authorize|connect)$/i, clicks it, and returns a descriptor. Returns
217
- // null when no match yet.
218
- const CLICK_EXPR = `
219
- (() => {
220
- const candidates = Array.from(document.querySelectorAll('button, input[type="submit"]'));
221
- const match = candidates.find((el) => {
222
- const text = (el.textContent ?? el.value ?? '').trim();
223
- return /^(authorize|connect)$/i.test(text) && !el.disabled;
224
- });
225
- if (!match) return null;
226
- const descriptor = {
227
- tag: match.tagName.toLowerCase(),
228
- text: (match.textContent ?? match.value ?? '').trim().slice(0, 40),
229
- disabled: Boolean(match.disabled),
230
- };
231
- match.click();
232
- return descriptor;
233
- })()
234
- `;
235
-
236
- const clickDeadline = Date.now() + BUTTON_POLL_TIMEOUT_MS;
237
- let clicked = null;
238
- while (Date.now() < clickDeadline) {
232
+ // Poll the consent page until one of the three terminal states resolves.
233
+ // MATCH_EXPR is built from _cdp-authorize-matcher.mjs's findMatch so the
234
+ // JSDOM unit tests and the live CDP path execute literally identical logic.
235
+ const matchDeadline = Date.now() + BUTTON_POLL_TIMEOUT_MS;
236
+ let matched = null;
237
+ while (Date.now() < matchDeadline) {
239
238
  let r;
240
239
  try {
241
240
  r = await send('Runtime.evaluate', {
242
- expression: CLICK_EXPR,
241
+ expression: MATCH_EXPR,
243
242
  returnByValue: true,
244
243
  awaitPromise: false,
245
244
  });
@@ -253,22 +252,33 @@ while (Date.now() < clickDeadline) {
253
252
  });
254
253
  }
255
254
  const val = r?.result?.value;
256
- if (val && typeof val === 'object') {
257
- clicked = val;
255
+ if (val && typeof val === 'object' && (val.kind === 'button' || val.kind === 'success')) {
256
+ matched = val;
258
257
  break;
259
258
  }
260
259
  await new Promise((res) => setTimeout(res, BUTTON_POLL_INTERVAL_MS));
261
260
  }
262
261
 
263
- if (!clicked) {
262
+ if (!matched) {
264
263
  die(1, 'authorize-button-not-found', { elapsed_ms: Date.now() - started });
265
264
  }
266
265
 
267
- emit('ok', 'clicked', {
268
- tag: clicked.tag,
269
- text: clicked.text,
270
- elapsed_ms: Date.now() - started,
271
- });
266
+ if (matched.kind === 'button') {
267
+ const d = matched.descriptor ?? {};
268
+ emit('ok', 'clicked', {
269
+ tag: d.tag,
270
+ text: d.text,
271
+ elapsed_ms: Date.now() - started,
272
+ });
273
+ } else {
274
+ // matched.kind === 'success' — Success modal text is on the page; the
275
+ // bound account already has a cert. Caller drops into the cert-poll loop;
276
+ // the in-flight cloudflared subprocess receives the idempotent callback
277
+ // and writes ~/.cloudflared/cert.pem.
278
+ emit('ok', 'cert-already-installed', {
279
+ elapsed_ms: Date.now() - started,
280
+ });
281
+ }
272
282
 
273
283
  try { ws.close(); } catch { /* best-effort */ }
274
284
  process.exit(0);
@@ -60,22 +60,74 @@ CFG_DIR="${HOME}/.${BRAND}/cloudflared"
60
60
  mkdir -p "${CFG_DIR}"
61
61
 
62
62
  # --------------------------------------------------------------------------
63
- # Step 1: OAuth login (only if cert.pem missing). Corresponds to runbook Step 1.
63
+ # Step 1: OAuth login. Corresponds to runbook Step 1.
64
64
  #
65
- # Control flow, rewritten per Task 556:
65
+ # Step 1 is a state machine over three observable variables — brand-scoped
66
+ # cert path, default cert path, and the helper's outcome (Task 855):
67
+ #
68
+ # ${CFG_DIR}/cert.pem present → Step 1 already done; skip.
69
+ # ${CFG_DIR}/cert.pem missing AND
70
+ # ~/.cloudflared/cert.pem present → pre-flight: a prior run
71
+ # reached the OAuth callback
72
+ # but failed before the mv;
73
+ # promote and skip the
74
+ # helper. Idempotent.
75
+ # both missing → spawn cloudflared, drive
76
+ # the consent page via CDP,
77
+ # poll for cert.pem, mv.
78
+ #
79
+ # Control flow when both certs are missing (Task 556 / 588 / 855):
66
80
  # 1. CDP precheck on 127.0.0.1:9222 — loud failure if Chromium isn't up.
67
81
  # 2. Spawn cloudflared with stdout+stderr teed line-by-line to
68
82
  # $STREAM_LOG_PATH with prefix [script:setup-tunnel:cloudflared]
69
83
  # (Task 605's chat-surface namespace — see _stream-log.sh header).
70
84
  # 3. Extract the authorize URL with a tolerant regex as it streams.
71
85
  # 4. Drive the VNC Chromium to that URL via CDP `PUT /json/new?<url>`.
72
- # 5. Wait for ~/.cloudflared/cert.pem to land with bounded timeout.
73
- # 6. Move cert.pem into the brand-scoped path.
86
+ # 5. Run _cdp-authorize.mjs tri-state matcher returns one of:
87
+ # reason=clicked → button matched + clicked
88
+ # reason=cert-already-installed → Success modal detected
89
+ # reason=authorize-button-not-found → loud failure, exit 1
90
+ # (a)+(b) drop into the cert-pem poll; (c) bubbles up.
91
+ # 6. Wait for ~/.cloudflared/cert.pem to land with bounded timeout.
92
+ # 7. Move cert.pem into the brand-scoped path.
74
93
  #
75
- # Every branch exits 1 loudly naming the failure — no silent retries,
94
+ # Every failure branch exits 1 loudly naming the cause — no silent retries,
76
95
  # no xdg-open race, no cloudflared-internal browser-spawn path.
77
96
  # --------------------------------------------------------------------------
78
97
 
98
+ # Pre-flight cert-promotion (Task 855). When ${CFG_DIR}/cert.pem is missing
99
+ # but the OAuth-default ~/.cloudflared/cert.pem is present, a prior run
100
+ # reached the cloudflared callback but did not survive to the brand-scoped
101
+ # mv at the end of Step 1. The cert is bound to the account already; the
102
+ # only remediation is the move. Doing it before re-spawning cloudflared is
103
+ # what stops Step 1 from looping forever on a dashboard that has moved past
104
+ # a button-bearing state.
105
+ #
106
+ # The mv exit code is checked: if it fails (EACCES, ENOSPC, weird FS state),
107
+ # loud-fail with reason=cert-promote-failed instead of pretending Step 1
108
+ # succeeded — the next steps would die opaquely on the missing brand cert.
109
+ # stderr is captured into the phase_line so the operator sees the actual
110
+ # failure cause (mv exit-code 1 alone cannot disambiguate EACCES from ENOSPC).
111
+ if [ ! -f "${CFG_DIR}/cert.pem" ] && [ -f "${HOME}/.cloudflared/cert.pem" ]; then
112
+ MV_ERR="$(mktemp -t maxy-cert-promote-err.XXXXXX)"
113
+ if mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem" 2>"${MV_ERR}"; then
114
+ rm -f "${MV_ERR}"
115
+ phase_line setup-tunnel step=oauth-login result=ok \
116
+ reason=cert-promoted-from-default-path waited="0s"
117
+ else
118
+ MV_RC=$?
119
+ MV_STDERR="$(tr '\n' ' ' < "${MV_ERR}" 2>/dev/null | head -c 200 || echo unavailable)"
120
+ rm -f "${MV_ERR}"
121
+ phase_line setup-tunnel step=oauth-login result=error \
122
+ reason=cert-promote-failed mv_rc="${MV_RC}" stderr="${MV_STDERR}" \
123
+ from="${HOME}/.cloudflared/cert.pem" to="${CFG_DIR}/cert.pem"
124
+ echo "ERROR: failed to promote ~/.cloudflared/cert.pem to ${CFG_DIR}/cert.pem (mv exit=${MV_RC})." >&2
125
+ echo " mv stderr: ${MV_STDERR}" >&2
126
+ echo " Step 1 cannot proceed without the cert in the brand-scoped path." >&2
127
+ exit 1
128
+ fi
129
+ fi
130
+
79
131
  if [ ! -f "${CFG_DIR}/cert.pem" ]; then
80
132
  phase_line setup-tunnel step=oauth-login cert_path="${CFG_DIR}/cert.pem" display="${DISPLAY:-:99}"
81
133
 
@@ -215,22 +267,61 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
215
267
  echo "ERROR: _cdp-authorize.mjs helper missing or not executable at ${AUTH_HELPER}" >&2
216
268
  exit 1
217
269
  fi
218
- # tee_subprocess pipes the helper's cdp-authorize result= line into the
219
- # stream log under the setup-tunnel:cdp-click tag so the chat UI renders
220
- # the click outcome in real time. Exit code drives control flow.
221
- if tee_subprocess setup-tunnel:cdp-click -- \
222
- node "${AUTH_HELPER}" "${CDP_TARGET_ID}"; then
223
- phase_line setup-tunnel step=oauth-login result=authorize-clicked \
224
- target_id="${CDP_TARGET_ID}"
270
+ # tee_subprocess_capture pipes the helper's `cdp-authorize result=…` line
271
+ # into the stream log under the setup-tunnel:cdp-click tag (live-tailable)
272
+ # AND through this shell's stdout so the wrapper can capture it for the
273
+ # tri-state reason discriminator (Task 855). The helper writes exactly
274
+ # one such line per invocation and exits; the shape is its contract:
275
+ # `cdp-authorize result=<ok|error> reason=<token> [k=v …]`.
276
+ # The wrapper's awk takes the LAST `cdp-authorize result=ok ` line so that
277
+ # any future emit() additions cannot mis-route the discriminator — the
278
+ # success line is always the last one before exit.
279
+ HELPER_OUT="$(mktemp -t maxy-cdp-authorize-out.XXXXXX)"
280
+ if tee_subprocess_capture setup-tunnel:cdp-click -- \
281
+ node "${AUTH_HELPER}" "${CDP_TARGET_ID}" \
282
+ > "${HELPER_OUT}"; then
283
+ HELPER_REASON="$(awk '/^cdp-authorize result=ok / {m=$0} END {if (m) {match(m, /reason=[a-z0-9-]+/); print substr(m, RSTART+7, RLENGTH-7)}}' "${HELPER_OUT}")"
284
+ rm -f "${HELPER_OUT}"
285
+ case "${HELPER_REASON}" in
286
+ clicked)
287
+ phase_line setup-tunnel step=browser-drive result=ok \
288
+ reason=clicked target_id="${CDP_TARGET_ID}"
289
+ ;;
290
+ cert-already-installed)
291
+ # The dashboard rendered the Success modal — the bound account
292
+ # already has a cert. The OAuth callback URL was exercised by this
293
+ # navigation, so the in-flight cloudflared subprocess receives the
294
+ # idempotent callback and writes ~/.cloudflared/cert.pem; the poll
295
+ # below picks it up.
296
+ phase_line setup-tunnel step=browser-drive result=ok \
297
+ reason=cert-already-installed target_id="${CDP_TARGET_ID}"
298
+ ;;
299
+ *)
300
+ # Helper exited 0 but emitted a reason this wrapper does not
301
+ # recognise — protocol drift between helper and wrapper. Loud-fail
302
+ # rather than silently fall through to the cert-poll, which would
303
+ # mask the cause if the cert never lands.
304
+ kill "${CF_PIPELINE_PID}" 2>/dev/null || true
305
+ phase_line setup-tunnel step=browser-drive result=error \
306
+ reason=helper-unknown-success-reason raw="${HELPER_REASON:-empty}"
307
+ echo "ERROR: CDP helper exited 0 with unrecognised reason: ${HELPER_REASON:-empty}" >&2
308
+ echo " Expected reason=clicked or reason=cert-already-installed." >&2
309
+ echo " Helper / wrapper protocol drift — investigate stream log." >&2
310
+ exit 1
311
+ ;;
312
+ esac
225
313
  else
226
314
  CLICK_RC=$?
315
+ rm -f "${HELPER_OUT}"
227
316
  kill "${CF_PIPELINE_PID}" 2>/dev/null || true
228
- # Helper exit codes: 1=authorize-button-not-found (loud, expected failure
229
- # mode when the VNC browser isn't signed into Cloudflare), 2=cdp-ws-unreachable,
230
- # 3=target-not-found, 4=click-evaluate-threw, 5=protocol-error. All are
231
- # final no retry, no silent fallback. Map the exit code 1:1 to a reason
232
- # string so the stream-log `reason=<…>` is greppable by exact cause; a
233
- # single catch-all reason would defeat the observability contract.
317
+ # Helper exit codes (mapped 1:1 to reason= so stream-log grep is precise;
318
+ # a catch-all reason would defeat the observability contract):
319
+ # 1 = authorize-button-not-found (loud neither button nor success
320
+ # modal matched before BUTTON_POLL_TIMEOUT_MS; the form surfaces
321
+ # a verbatim quote of references/dashboard-guide.md "Authorise a
322
+ # new tunnel" so the operator can recover without SSH).
323
+ # 2 = cdp-ws-unreachable, 3 = target-not-found, 4 = click-evaluate-threw,
324
+ # 5 = protocol-error. All are final — no retry, no silent fallback.
234
325
  case "${CLICK_RC}" in
235
326
  1) CLICK_REASON=authorize-button-not-found ;;
236
327
  2) CLICK_REASON=cdp-ws-unreachable ;;
@@ -242,8 +333,13 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
242
333
  phase_line setup-tunnel step=browser-drive result=error \
243
334
  reason="${CLICK_REASON}" click_rc="${CLICK_RC}"
244
335
  echo "ERROR: CDP-driven Authorize click failed (helper exit=${CLICK_RC}, reason=${CLICK_REASON})." >&2
245
- echo " If the VNC browser is not signed into Cloudflare, sign in manually," >&2
246
- echo " close the sign-in tab, and re-run setup or run ~/reset-tunnel.sh first." >&2
336
+ if [ "${CLICK_REASON}" = "authorize-button-not-found" ]; then
337
+ echo " The dashboard did not present an Authorize button or a Success modal." >&2
338
+ echo " The form will surface the dashboard click-path for manual recovery." >&2
339
+ else
340
+ echo " The CDP helper failed before reaching a terminal page state." >&2
341
+ echo " Re-run setup — or run ~/reset-tunnel.sh first if state is corrupt." >&2
342
+ fi
247
343
  exit 1
248
344
  fi
249
345
  fi # end CDP-available branch (Task 664)
@@ -281,7 +377,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
281
377
  echo "ERROR: Timed out after ${LOGIN_WAIT}s waiting for cert.pem to land." >&2
282
378
  exit 1
283
379
  fi
284
- # Heartbeat every 2 s after the click. t=0 is the authorize-clicked
380
+ # Heartbeat every 2 s after the click. t=0 is the browser-drive
285
381
  # phase line above; the first heartbeat fires at t=2. Without this line
286
382
  # the tailer sees silence for the full 1-20 s round-trip — the exact
287
383
  # state Task 588 forbids.
@@ -294,7 +390,7 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
294
390
  done
295
391
 
296
392
  mv "${HOME}/.cloudflared/cert.pem" "${CFG_DIR}/cert.pem"
297
- phase_line setup-tunnel step=oauth-login result=cert-received \
393
+ phase_line setup-tunnel step=oauth-login result=ok \
298
394
  path="${CFG_DIR}/cert.pem" waited="${LOGIN_WAIT}s"
299
395
  fi
300
396
 
@@ -22,6 +22,8 @@ Any Cloudflare action outside these four surfaces is a discipline violation —
22
22
 
23
23
  Use this when the operator wants Cloudflare set up (or re-set up) end-to-end on the device. The script handles OAuth login, tunnel creation, DNS routing for each subdomain, config.yml + tunnel.state, and dispatches the `${BRAND}.service` restart to a transient `systemd-run` unit (Task 558) — all in one invocation. The restart fires a few seconds after the script exits so the script does not kill its own cgroup when invoked via the Bash tool; the chat UI receives a `server_shutdown` SSE frame and reconnects automatically. Post-restart hostname verification is out of scope for the script (connector is not up when the script exits) — verify via the next admin turn or manually with `curl -I https://<hostname>`. Apex hostnames cannot be routed by the CLI; when one is passed, the script prints an `ACTION REQUIRED` block naming the exact dashboard record to edit.
24
24
 
25
+ Step 1's OAuth flow is a state machine over three observable variables: the brand-scoped cert path (`${CFG_DIR}/cert.pem`), the OAuth-default cert path (`~/.cloudflared/cert.pem`), and the consent-page DOM via the CDP helper `_cdp-authorize.mjs` (Task 855). When the brand-scoped cert is missing but the default-path cert is present from any prior partial run, the wrapper promotes it (`mv`) and emits `step=oauth-login result=ok reason=cert-promoted-from-default-path` without re-spawning cloudflared. When both are missing, the helper polls `dash.cloudflare.com/argotunnel` and resolves to one of three terminal outcomes — `reason=clicked` (button matched and clicked), `reason=cert-already-installed` (Success modal text detected — the bound account already has a cert, callback is idempotent so the in-flight cloudflared subprocess writes the cert anyway), or `reason=authorize-button-not-found` (neither anchor matched; loud failure, exit 1). The first two paths drop into the existing brand-scoped cert poll; the third surfaces a remediation card in the form so the operator can complete the consent click in their own browser without SSH or `~/reset-tunnel.sh`.
26
+
25
27
  ### How inputs reach the script
26
28
 
27
29
  Inputs arrive through the `cloudflare-setup-form` component, not agent Q&A. The onboarding skill renders the form, the user submits admin/public/apex labels and the admin password in one action, and the `/api/admin/cloudflare/setup` endpoint runs (in this order):
@@ -50,6 +52,8 @@ The agent does not invoke the script directly during onboarding — the endpoint
50
52
 
51
53
  The endpoint returns `{ ok: false, field: "script", message, output }` and the form surfaces the error inline. Relay the output to the user, name the exit code, and cite `references/reset-guide.md` for the next action. Offer to re-render the form after any manual steps the script's error output named. Do not attempt a second invocation outside the form, a Playwright-driven dashboard inspection, or an alternative `cloudflared` command sequence. The discipline rule below applies.
52
54
 
55
+ When the failure reason is `authorize-button-not-found` (Task 855), the form renders a verbatim quote of `references/dashboard-guide.md` § "Authorise a new tunnel (pick the right zone)" alongside a "Try again" button — the operator completes the consent click in their own browser, then re-submits the same form. No agent action is required; relay the form's chat acknowledgement when it arrives. Do not suggest `~/reset-tunnel.sh` for this reason — the cert path is intact and a fresh consent click is the only remediation needed.
56
+
53
57
  ---
54
58
 
55
59
  ## 2. Manual fallback — `references/manual-setup.md`