@runuai/host 0.9.51 → 0.9.53

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.
@@ -190,6 +190,10 @@ interface LoginOperation {
190
190
  /** ADR-116: when set, the captured credential lands in a labeled extra
191
191
  * account instead of the default slot. */
192
192
  accountLabel: string | null;
193
+ /** Diagnosis breadcrumb: has ANY output arrived after the one-time code
194
+ * was written? Distinguishes "stdin never delivered" from "CLI answered
195
+ * and we ignored it" without reading the PTY transcript. */
196
+ postInputOutputSeen: boolean;
193
197
  }
194
198
 
195
199
  const defaultTimers: EngineLoginTimers = {
@@ -391,6 +395,7 @@ export class EngineLoginManager {
391
395
  pendingInput: null,
392
396
  callbackTarget: null,
393
397
  accountLabel: label ?? null,
398
+ postInputOutputSeen: false,
394
399
  };
395
400
  this.operations.set(opId, operation);
396
401
  this.engineOperations.set(engine, operation);
@@ -664,6 +669,29 @@ export class EngineLoginManager {
664
669
  );
665
670
 
666
671
  if (operation.engine === "claude") {
672
+ if (operation.inputUsed && !operation.postInputOutputSeen) {
673
+ operation.postInputOutputSeen = true;
674
+ console.log(
675
+ `[engine-login] output resumed after input for ${operation.opId} — the code reached the login TUI`,
676
+ );
677
+ }
678
+ // The TUI answers a bad/expired/mismatched code with an error AND an
679
+ // open prompt — no exit, no token. Detect it and fail visibly instead
680
+ // of the silent wait-for-timeout that hid the 2026-08-20 hangs.
681
+ if (operation.inputUsed && !operation.finishing) {
682
+ const cliError = detectClaudeCliError(operation.outputTail);
683
+ if (cliError) {
684
+ console.warn(
685
+ `[engine-login] login CLI rejected the code for ${operation.opId}: ${cliError}`,
686
+ );
687
+ void this.fail(
688
+ operation,
689
+ "invalid_input",
690
+ `The sign-in CLI rejected the code: ${cliError} Make sure the code comes from THIS attempt's authorization page, then retry.`,
691
+ );
692
+ return;
693
+ }
694
+ }
667
695
  // Before the one-time input is consumed, token-shaped output could only
668
696
  // be an authorization URL/detail or hostile terminal content. This also
669
697
  // prevents an input echo from becoming the credential boundary.
@@ -1047,6 +1075,27 @@ export function createEngineLoginManager(
1047
1075
  return new EngineLoginManager(options);
1048
1076
  }
1049
1077
 
1078
+ /** Detect the TUI's post-input rejection ("OAuth error: Invalid code…",
1079
+ * expired codes, …) in the sanitized output. ANSI stripping can eat spaces
1080
+ * ("Invalidcode"), so the patterns tolerate missing whitespace. Exported for
1081
+ * tests. */
1082
+ export function detectClaudeCliError(tail: string): string | null {
1083
+ const lines = tail
1084
+ .replace(/\u001b\[[0-9;?]*[ -\/]*[@-~]/g, "")
1085
+ .replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, "")
1086
+ .split("\n")
1087
+ .map((line) => line.trim())
1088
+ .filter(Boolean);
1089
+ const match = lines.find((line) =>
1090
+ /oauth\s*error|invalid\s*cod|code\s*.{0,12}expired|authentication\s*failed/i.test(
1091
+ line,
1092
+ ),
1093
+ );
1094
+ if (!match) return null;
1095
+ if (/sk-ant-|Bearer |[A-Za-z0-9_-]{40,}/.test(match)) return null;
1096
+ return match.slice(0, 200);
1097
+ }
1098
+
1050
1099
  /** The last visible, sanitized line of the login CLI's output — bounded and
1051
1100
  * secret-checked so it can ride a terminal event message (ADR-116). Anything
1052
1101
  * token-shaped disqualifies the whole line rather than risking a partial. */
@@ -1440,7 +1489,12 @@ export function engineLoginContainerArgs(
1440
1489
  // a client id (live 2026-08-18). stty targets the PTY `script`
1441
1490
  // just allocated.
1442
1491
  "stty cols 500 rows 50 2>/dev/null; exec claude setup-token",
1443
- "/dev/null",
1492
+ // The PTY transcript lands in the op's private bind leaf (same
1493
+ // trust class as the codex auth.json captured there; the leaf is
1494
+ // 0700 and deleted at cleanup). A stuck flow can then be read
1495
+ // VERBATIM from the host — the 2026-08-20 hang was undiagnosable
1496
+ // because the typescript went to /dev/null.
1497
+ `${LOGIN_MOUNT}/typescript`,
1444
1498
  ]
1445
1499
  : ["codex", "login"];
1446
1500
  if (engineLoginBackend().apple) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.51",
3
+ "version": "0.9.53",
4
4
  "description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",
@@ -376,7 +376,18 @@ function inspectSystemdState(execute: StepExecutor): {
376
376
  );
377
377
  }
378
378
  const enabled = enabledResult.status === 0 && enabledState === "enabled";
379
- if (!enabled && !DISABLED_SYSTEMD_STATES.has(enabledState)) {
379
+ // A unit that was never installed answers differently across systemd
380
+ // versions: newer ones print "not-found", older ones (Amazon Linux 2023's
381
+ // systemd, live 2026-08-20) print NOTHING on stdout and put "Failed to get
382
+ // unit file state … No such file or directory" on stderr. Both mean the
383
+ // same thing on a first install: not enabled.
384
+ const neverInstalled =
385
+ enabledResult.status !== 0 &&
386
+ enabledState === "" &&
387
+ /no such file or directory|failed to get unit file state/i.test(
388
+ enabledResult.stderr,
389
+ );
390
+ if (!enabled && !neverInstalled && !DISABLED_SYSTEMD_STATES.has(enabledState)) {
380
391
  throw stepFailure(
381
392
  "systemctl",
382
393
  enabledArgs,