@runuai/host 0.9.55 → 0.9.56

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.
@@ -125,7 +125,7 @@ export interface EngineLoginSeams {
125
125
  removeTempDir(path: string): Promise<void>;
126
126
  startContainer(
127
127
  request: EngineLoginContainerRequest,
128
- onOutput: (chunk: string) => void,
128
+ onOutput: (chunk: string, stream?: "stdout" | "stderr") => void,
129
129
  ): Promise<EngineLoginProcess>;
130
130
  replayCodexCallback(
131
131
  containerName: string,
@@ -186,6 +186,8 @@ interface LoginOperation {
186
186
  settlementPromise: Promise<void> | null;
187
187
  suppressTerminalEvent: boolean;
188
188
  outputTail: string;
189
+ /** stdout-only tail — the credential boundary (stderr never joins it). */
190
+ stdoutTail: string;
189
191
  inputUsed: boolean;
190
192
  callbackUsed: boolean;
191
193
  pendingInput: string | null;
@@ -393,6 +395,7 @@ export class EngineLoginManager {
393
395
  settlementPromise: null,
394
396
  suppressTerminalEvent: false,
395
397
  outputTail: "",
398
+ stdoutTail: "",
396
399
  inputUsed: false,
397
400
  callbackUsed: false,
398
401
  pendingInput: null,
@@ -462,6 +465,7 @@ export class EngineLoginManager {
462
465
  );
463
466
  operation.inputUsed = true;
464
467
  operation.outputTail = "";
468
+ operation.stdoutTail = "";
465
469
  if (!operation.process) {
466
470
  console.log(
467
471
  `[engine-login] input buffered for ${opId} (login process still starting)`,
@@ -650,7 +654,7 @@ export class EngineLoginManager {
650
654
  const containerName = `uai-login-${operation.engine}-${this.seams.randomHex(8)}`;
651
655
  processHandle = await this.seams.startContainer(
652
656
  { engine: operation.engine, containerName, tempDir },
653
- (chunk) => this.onOutput(operation, chunk),
657
+ (chunk, stream) => this.onOutput(operation, chunk, stream ?? "stdout"),
654
658
  );
655
659
  if (!this.isCurrent(operation)) {
656
660
  await this.stopDetachedProcess(processHandle);
@@ -688,13 +692,28 @@ export class EngineLoginManager {
688
692
  }
689
693
  }
690
694
 
691
- private onOutput(operation: LoginOperation, chunk: string): void {
695
+ private onOutput(
696
+ operation: LoginOperation,
697
+ chunk: string,
698
+ stream: "stdout" | "stderr" = "stdout",
699
+ ): void {
692
700
  if (!this.isCurrent(operation) || operation.finishing) return;
693
701
  operation.outputTail = appendUtf8Tail(
694
702
  operation.outputTail,
695
703
  chunk,
696
704
  MAX_OUTPUT_TAIL_BYTES,
697
705
  );
706
+ if (stream === "stdout") {
707
+ // The CREDENTIAL boundary reads stdout only: stderr chunks (the Apple
708
+ // runtime's XPC chatter) interleaving into a merged tail once produced
709
+ // a corrupted token-shaped capture (live 2026-08-21). The merged tail
710
+ // above stays for URL detection and human-facing evidence.
711
+ operation.stdoutTail = appendUtf8Tail(
712
+ operation.stdoutTail,
713
+ chunk,
714
+ MAX_OUTPUT_TAIL_BYTES,
715
+ );
716
+ }
698
717
 
699
718
  if (operation.engine === "claude") {
700
719
  if (operation.inputUsed && !operation.postInputOutputSeen) {
@@ -724,13 +743,14 @@ export class EngineLoginManager {
724
743
  // be an authorization URL/detail or hostile terminal content. This also
725
744
  // prevents an input echo from becoming the credential boundary.
726
745
  const token = operation.inputUsed
727
- ? extractClaudeOAuthToken(operation.outputTail)
746
+ ? extractClaudeOAuthToken(operation.stdoutTail)
728
747
  : null;
729
748
  if (token) {
730
749
  console.log(
731
750
  `[engine-login] token detected in CLI output for ${operation.opId}; persisting`,
732
751
  );
733
752
  operation.outputTail = "";
753
+ operation.stdoutTail = "";
734
754
  this.beginClaudeFinish(operation, token);
735
755
  return;
736
756
  }
@@ -990,6 +1010,7 @@ export class EngineLoginManager {
990
1010
  private detach(operation: LoginOperation): void {
991
1011
  operation.active = false;
992
1012
  operation.outputTail = "";
1013
+ operation.stdoutTail = "";
993
1014
  operation.pendingInput = null;
994
1015
  operation.callbackTarget = null;
995
1016
  this.clearOperationTimer(operation);
@@ -1247,9 +1268,18 @@ export function findSafeHttpsUrl(value: string): string | null {
1247
1268
 
1248
1269
  export function extractClaudeOAuthToken(value: string): string | null {
1249
1270
  const plain = stripTerminalControl(value);
1250
- const match = /(?:^|[^A-Za-z0-9_-])(sk-ant-[A-Za-z0-9_-]{16,4096})(?![A-Za-z0-9_-])/.exec(
1251
- plain,
1252
- );
1271
+ // The FULL `sk-ant-oat01-` prefix and a realistic minimum length are both
1272
+ // load-bearing: the login CLI's stderr (the Apple runtime's XPC chatter is
1273
+ // full of the word "token") interleaves with stdout in the captured tail,
1274
+ // and a permissive scan once matched a corrupted hybrid — `sk-ant-at01-…`,
1275
+ // one character short, token-shaped, wrong — which persisted as a
1276
+ // "successful" login whose agents then 401'd forever (live 2026-08-21).
1277
+ // A strict prefix makes a mangled interleave a non-match, and the CLI
1278
+ // prints the token again on its plain summary line, which then matches.
1279
+ const match =
1280
+ /(?:^|[^A-Za-z0-9_-])(sk-ant-oat01-[A-Za-z0-9_-]{64,4096})(?![A-Za-z0-9_-])/.exec(
1281
+ plain,
1282
+ );
1253
1283
  return match?.[1] ?? null;
1254
1284
  }
1255
1285
 
@@ -1388,7 +1418,7 @@ function callbackReplayUrl(
1388
1418
 
1389
1419
  async function startLoginContainer(
1390
1420
  request: EngineLoginContainerRequest,
1391
- onOutput: (chunk: string) => void,
1421
+ onOutput: (chunk: string, stream?: "stdout" | "stderr") => void,
1392
1422
  ): Promise<EngineLoginProcess> {
1393
1423
  const args = engineLoginContainerArgs(request);
1394
1424
  const child = spawn(engineLoginBackend().command, args, {
@@ -1397,8 +1427,8 @@ async function startLoginContainer(
1397
1427
  });
1398
1428
  child.stdout.setEncoding("utf8");
1399
1429
  child.stderr.setEncoding("utf8");
1400
- child.stdout.on("data", (chunk: string) => onOutput(chunk));
1401
- child.stderr.on("data", (chunk: string) => onOutput(chunk));
1430
+ child.stdout.on("data", (chunk: string) => onOutput(chunk, "stdout"));
1431
+ child.stderr.on("data", (chunk: string) => onOutput(chunk, "stderr"));
1402
1432
  child.stdin.on("error", () => {});
1403
1433
 
1404
1434
  let stopped = false;
@@ -2299,6 +2299,11 @@ export class Orchestrator {
2299
2299
  // Token revoked/expired: the env-injected credential only changes on
2300
2300
  // a fresh runner. Re-spawn on the re-resolved account (picks up a
2301
2301
  // reconnected token); bounded so a still-bad token can't loop.
2302
+ // Log the matched evidence — the 2026-08-21 corrupted-token loop was
2303
+ // diagnosed blind because the trigger text never appeared anywhere.
2304
+ console.warn(
2305
+ `[orchestrator] ${channel.taskId}/${agentId}: claude auth-revoked pattern matched: ${event.message.slice(0, 300)}`,
2306
+ );
2302
2307
  await this.refreshAgentToken(channel, agent);
2303
2308
  break;
2304
2309
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.55",
3
+ "version": "0.9.56",
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>",
@@ -545,7 +545,7 @@ export async function prepareLinuxDefinitionInstallRollback(
545
545
  }
546
546
  }
547
547
 
548
- function assertSystemdDisabledForRollback(
548
+ export function assertSystemdDisabledForRollback(
549
549
  execute: StepExecutor,
550
550
  allowMissing: boolean,
551
551
  ): void {
@@ -553,11 +553,25 @@ function assertSystemdDisabledForRollback(
553
553
  const disabled = execute(false, "systemctl", disableArgs);
554
554
  if (disabled.status === 0) return;
555
555
  if (allowMissing) {
556
+ // Disabling a unit whose FILE is already gone answers with stderr-only
557
+ // "does not exist" on AL2023 (live 2026-08-21: a poisoned rollback from
558
+ // an earlier failed install could never complete, wedging every later
559
+ // install at "could not begin"). Gone is as disabled as it gets.
560
+ if (/does not exist|no such file or directory/i.test(disabled.stderr)) {
561
+ return;
562
+ }
556
563
  const probeArgs = ["--user", "is-enabled", UNIT];
557
564
  const probe = execute(false, "systemctl", probeArgs);
565
+ const probeState = probe.stdout.trim();
566
+ const neverInstalled =
567
+ probe.status !== 0 &&
568
+ probeState === "" &&
569
+ /no such file or directory|failed to get unit file state/i.test(
570
+ probe.stderr,
571
+ );
558
572
  if (
559
573
  probe.status !== 0 &&
560
- DISABLED_SYSTEMD_STATES.has(probe.stdout.trim())
574
+ (DISABLED_SYSTEMD_STATES.has(probeState) || neverInstalled)
561
575
  ) {
562
576
  return;
563
577
  }