@runuai/host 0.9.50 → 0.9.52
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/lib/engine-login.ts +103 -10
- package/package.json +1 -1
package/lib/engine-login.ts
CHANGED
|
@@ -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);
|
|
@@ -413,17 +418,35 @@ export class EngineLoginManager {
|
|
|
413
418
|
/** Consume Claude's one-time code exactly once. */
|
|
414
419
|
input(opId: string, text: string): boolean {
|
|
415
420
|
const operation = this.operations.get(opId);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
421
|
+
// Every rejection names itself in the host log: a silently dropped code
|
|
422
|
+
// reads as "Waiting for the host…" forever on the cloud pane (live
|
|
423
|
+
// 2026-08-20, the guard chain gave the operator nothing to act on).
|
|
424
|
+
// Never log `text` — it is one-time OAuth material.
|
|
425
|
+
if (!operation) {
|
|
426
|
+
console.warn(`[engine-login] input dropped: unknown opId ${opId}`);
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
const rejection = !this.isCurrent(operation)
|
|
430
|
+
? "operation belongs to a previous bridge generation"
|
|
431
|
+
: operation.engine !== "claude"
|
|
432
|
+
? `engine is ${operation.engine}, not claude`
|
|
433
|
+
: operation.stage !== "awaiting_input"
|
|
434
|
+
? `stage is ${operation.stage}, not awaiting_input`
|
|
435
|
+
: operation.inputUsed
|
|
436
|
+
? "a code was already submitted for this operation"
|
|
437
|
+
: operation.finishing
|
|
438
|
+
? "the operation is already finishing"
|
|
439
|
+
: null;
|
|
440
|
+
if (rejection !== null) {
|
|
441
|
+
console.warn(
|
|
442
|
+
`[engine-login] input dropped for ${opId}: ${rejection}`,
|
|
443
|
+
);
|
|
424
444
|
return false;
|
|
425
445
|
}
|
|
426
446
|
if (!isValidOneTimeInput(text)) {
|
|
447
|
+
console.warn(
|
|
448
|
+
`[engine-login] input rejected for ${opId}: failed one-time-input validation (length ${text.length})`,
|
|
449
|
+
);
|
|
427
450
|
void this.fail(
|
|
428
451
|
operation,
|
|
429
452
|
"invalid_input",
|
|
@@ -431,6 +454,9 @@ export class EngineLoginManager {
|
|
|
431
454
|
);
|
|
432
455
|
return false;
|
|
433
456
|
}
|
|
457
|
+
console.log(
|
|
458
|
+
`[engine-login] input accepted for ${opId} (${text.length} chars)`,
|
|
459
|
+
);
|
|
434
460
|
operation.inputUsed = true;
|
|
435
461
|
operation.outputTail = "";
|
|
436
462
|
// CARRIAGE RETURN, not newline: `claude setup-token` is an Ink raw-mode
|
|
@@ -441,13 +467,20 @@ export class EngineLoginManager {
|
|
|
441
467
|
// hung on exactly this.
|
|
442
468
|
const value = `${text}\r`;
|
|
443
469
|
if (!operation.process) {
|
|
470
|
+
console.log(
|
|
471
|
+
`[engine-login] input buffered for ${opId} (login process still starting)`,
|
|
472
|
+
);
|
|
444
473
|
operation.pendingInput = value;
|
|
445
474
|
return true;
|
|
446
475
|
}
|
|
447
476
|
try {
|
|
448
477
|
operation.process.writeInput(value);
|
|
478
|
+
console.log(`[engine-login] input written to the login process for ${opId}`);
|
|
449
479
|
return true;
|
|
450
480
|
} catch {
|
|
481
|
+
console.warn(
|
|
482
|
+
`[engine-login] input write FAILED for ${opId}: the login process rejected stdin`,
|
|
483
|
+
);
|
|
451
484
|
void this.fail(
|
|
452
485
|
operation,
|
|
453
486
|
"invalid_input",
|
|
@@ -636,6 +669,29 @@ export class EngineLoginManager {
|
|
|
636
669
|
);
|
|
637
670
|
|
|
638
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
|
+
}
|
|
639
695
|
// Before the one-time input is consumed, token-shaped output could only
|
|
640
696
|
// be an authorization URL/detail or hostile terminal content. This also
|
|
641
697
|
// prevents an input echo from becoming the credential boundary.
|
|
@@ -643,6 +699,9 @@ export class EngineLoginManager {
|
|
|
643
699
|
? extractClaudeOAuthToken(operation.outputTail)
|
|
644
700
|
: null;
|
|
645
701
|
if (token) {
|
|
702
|
+
console.log(
|
|
703
|
+
`[engine-login] token detected in CLI output for ${operation.opId}; persisting`,
|
|
704
|
+
);
|
|
646
705
|
operation.outputTail = "";
|
|
647
706
|
this.beginClaudeFinish(operation, token);
|
|
648
707
|
return;
|
|
@@ -650,6 +709,9 @@ export class EngineLoginManager {
|
|
|
650
709
|
if (operation.stage === "starting") {
|
|
651
710
|
const authorizeUrl = findSafeHttpsUrl(operation.outputTail);
|
|
652
711
|
if (authorizeUrl) {
|
|
712
|
+
console.log(
|
|
713
|
+
`[engine-login] authorize URL found for ${operation.opId}; awaiting the one-time code`,
|
|
714
|
+
);
|
|
653
715
|
operation.stage = "awaiting_input";
|
|
654
716
|
safeEmit(operation.emit, authorizeEvent(operation, authorizeUrl));
|
|
655
717
|
safeEmit(operation.emit, {
|
|
@@ -694,12 +756,17 @@ export class EngineLoginManager {
|
|
|
694
756
|
if (operation.finishPromise) await operation.finishPromise;
|
|
695
757
|
return;
|
|
696
758
|
}
|
|
759
|
+
const evidence = lastSignificantOutputLine(operation.outputTail);
|
|
760
|
+
console.warn(
|
|
761
|
+
`[engine-login] login process for ${operation.opId} ended (exit ${result.code ?? "spawn-failed"}) before completion` +
|
|
762
|
+
(evidence ? `; last output: ${evidence}` : ""),
|
|
763
|
+
);
|
|
697
764
|
await this.fail(
|
|
698
765
|
operation,
|
|
699
766
|
result.spawnFailed ? "unavailable" : "start_failed",
|
|
700
767
|
result.spawnFailed
|
|
701
768
|
? "The login runtime is unavailable."
|
|
702
|
-
: `${engineLabel(operation.engine)} login did not complete
|
|
769
|
+
: `${engineLabel(operation.engine)} login did not complete.${evidence ? ` Last output: ${evidence}` : ""}`,
|
|
703
770
|
);
|
|
704
771
|
}
|
|
705
772
|
|
|
@@ -1008,6 +1075,27 @@ export function createEngineLoginManager(
|
|
|
1008
1075
|
return new EngineLoginManager(options);
|
|
1009
1076
|
}
|
|
1010
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
|
+
|
|
1011
1099
|
/** The last visible, sanitized line of the login CLI's output — bounded and
|
|
1012
1100
|
* secret-checked so it can ride a terminal event message (ADR-116). Anything
|
|
1013
1101
|
* token-shaped disqualifies the whole line rather than risking a partial. */
|
|
@@ -1401,7 +1489,12 @@ export function engineLoginContainerArgs(
|
|
|
1401
1489
|
// a client id (live 2026-08-18). stty targets the PTY `script`
|
|
1402
1490
|
// just allocated.
|
|
1403
1491
|
"stty cols 500 rows 50 2>/dev/null; exec claude setup-token",
|
|
1404
|
-
|
|
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`,
|
|
1405
1498
|
]
|
|
1406
1499
|
: ["codex", "login"];
|
|
1407
1500
|
if (engineLoginBackend().apple) {
|