@runuai/host 0.9.56 → 0.9.57
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 +160 -10
- package/package.json +1 -1
package/lib/engine-login.ts
CHANGED
|
@@ -742,8 +742,11 @@ export class EngineLoginManager {
|
|
|
742
742
|
// Before the one-time input is consumed, token-shaped output could only
|
|
743
743
|
// be an authorization URL/detail or hostile terminal content. This also
|
|
744
744
|
// prevents an input echo from becoming the credential boundary.
|
|
745
|
+
// The scan reads the RECONSTRUCTED screen, not the raw stream: the
|
|
746
|
+
// TUI's diff renderer skips already-correct cells, so the raw bytes
|
|
747
|
+
// never carry the contiguous token (see renderTerminalScreenText).
|
|
745
748
|
const token = operation.inputUsed
|
|
746
|
-
? extractClaudeOAuthToken(operation.stdoutTail)
|
|
749
|
+
? extractClaudeOAuthToken(renderTerminalScreenText(operation.stdoutTail))
|
|
747
750
|
: null;
|
|
748
751
|
if (token) {
|
|
749
752
|
console.log(
|
|
@@ -1149,8 +1152,10 @@ export function detectClaudeCliError(tail: string): string | null {
|
|
|
1149
1152
|
* secret-checked so it can ride a terminal event message (ADR-116). Anything
|
|
1150
1153
|
* token-shaped disqualifies the whole line rather than risking a partial. */
|
|
1151
1154
|
function lastSignificantOutputLine(tail: string): string | null {
|
|
1152
|
-
|
|
1153
|
-
|
|
1155
|
+
// stripTerminalControl handles the shapes the old inline regex missed —
|
|
1156
|
+
// private-parameter CSI (kitty's ESC[<u, ESC[>4m) and charset selects
|
|
1157
|
+
// (ESC(B) — whose husks once surfaced verbatim as "(B[>4m[<u" evidence.
|
|
1158
|
+
const lines = stripTerminalControl(tail)
|
|
1154
1159
|
.replace(/[\u0000-\u0009\u000b-\u001f\u007f]/g, "")
|
|
1155
1160
|
.split("\n")
|
|
1156
1161
|
.map((line) => line.trim())
|
|
@@ -1232,6 +1237,146 @@ function stripTerminalControl(value: string): string {
|
|
|
1232
1237
|
.replace(/\r/g, "\n");
|
|
1233
1238
|
}
|
|
1234
1239
|
|
|
1240
|
+
const SCREEN_MAX_ROWS = 200;
|
|
1241
|
+
const SCREEN_MAX_COLS = 600;
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Rebuild the visible terminal screen from raw TUI output. The login CLI's
|
|
1245
|
+
* renderer diffs frames: a repainted line SKIPS cells that already show the
|
|
1246
|
+
* right character and jumps the cursor over them. The minted token paints
|
|
1247
|
+
* over the "Paste code here if prompted >" prompt, whose `code` leaves an
|
|
1248
|
+
* 'o' exactly where `sk-ant-oat01-`'s 'o' lands — so the byte stream carries
|
|
1249
|
+
* `sk-ant-` ESC[10G `at01-…` and NEVER the contiguous token (live
|
|
1250
|
+
* 2026-08-21, first Linux host; the Air's earlier "corrupted" capture was
|
|
1251
|
+
* the same hole, not stderr interleave). Only a spatial reconstruction sees
|
|
1252
|
+
* what the operator sees, so the credential scan reads this rendering, never
|
|
1253
|
+
* the raw stream. Cells no frame painted stay spaces — a hole can only be
|
|
1254
|
+
* filled by content some frame actually painted there, never closed up.
|
|
1255
|
+
*/
|
|
1256
|
+
export function renderTerminalScreenText(value: string): string {
|
|
1257
|
+
const rows: (string[] | undefined)[] = [];
|
|
1258
|
+
let row = 0;
|
|
1259
|
+
let col = 0;
|
|
1260
|
+
let savedRow = 0;
|
|
1261
|
+
let savedCol = 0;
|
|
1262
|
+
const clamp = () => {
|
|
1263
|
+
row = Math.min(Math.max(row, 0), SCREEN_MAX_ROWS - 1);
|
|
1264
|
+
col = Math.min(Math.max(col, 0), SCREEN_MAX_COLS);
|
|
1265
|
+
};
|
|
1266
|
+
const line = (): string[] => (rows[row] ??= []);
|
|
1267
|
+
const pattern =
|
|
1268
|
+
// OSC | CSI(params, final) | charset | bare ESC final | CR | LF | text
|
|
1269
|
+
/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b\[([0-9;:?<=>]*)[!-/]*([@-~])|\x1b[()][0-9A-B]|\x1b([78=>DEHM])|(\r)|(\n)|([^\x1b\r\n\x00-\x1f\x7f]+)|[\s\S]/g;
|
|
1270
|
+
for (const part of value.matchAll(pattern)) {
|
|
1271
|
+
const [, csiParams, csiFinal, escFinal, cr, lf, text] = part;
|
|
1272
|
+
if (text !== undefined) {
|
|
1273
|
+
const cells = line();
|
|
1274
|
+
for (const ch of text) {
|
|
1275
|
+
if (col < SCREEN_MAX_COLS) cells[col] = ch;
|
|
1276
|
+
col += 1;
|
|
1277
|
+
}
|
|
1278
|
+
clamp();
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (cr !== undefined) {
|
|
1282
|
+
col = 0;
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (lf !== undefined) {
|
|
1286
|
+
row += 1;
|
|
1287
|
+
clamp();
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
if (escFinal !== undefined) {
|
|
1291
|
+
if (escFinal === "7") [savedRow, savedCol] = [row, col];
|
|
1292
|
+
else if (escFinal === "8") [row, col] = [savedRow, savedCol];
|
|
1293
|
+
else if (escFinal === "D" || escFinal === "E") row += 1;
|
|
1294
|
+
else if (escFinal === "M") row -= 1;
|
|
1295
|
+
if (escFinal === "E") col = 0;
|
|
1296
|
+
clamp();
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
if (csiFinal === undefined) continue;
|
|
1300
|
+
const params = csiParams ?? "";
|
|
1301
|
+
// Private-parameter sequences (kitty keyboard pops, DEC modes) are not
|
|
1302
|
+
// cursor movement — never let e.g. `ESC[<u` restore a stale cursor.
|
|
1303
|
+
if (/[?<=>]/.test(params)) continue;
|
|
1304
|
+
const numbers = params.split(";").map((entry) => Number.parseInt(entry, 10));
|
|
1305
|
+
const n = (index: number, fallback: number): number => {
|
|
1306
|
+
const parsed = numbers[index];
|
|
1307
|
+
return Number.isFinite(parsed) && parsed! > 0 ? parsed! : fallback;
|
|
1308
|
+
};
|
|
1309
|
+
switch (csiFinal) {
|
|
1310
|
+
case "G":
|
|
1311
|
+
col = n(0, 1) - 1;
|
|
1312
|
+
break;
|
|
1313
|
+
case "A":
|
|
1314
|
+
row -= n(0, 1);
|
|
1315
|
+
break;
|
|
1316
|
+
case "B":
|
|
1317
|
+
row += n(0, 1);
|
|
1318
|
+
break;
|
|
1319
|
+
case "C":
|
|
1320
|
+
col += n(0, 1);
|
|
1321
|
+
break;
|
|
1322
|
+
case "D":
|
|
1323
|
+
col -= n(0, 1);
|
|
1324
|
+
break;
|
|
1325
|
+
case "E":
|
|
1326
|
+
row += n(0, 1);
|
|
1327
|
+
col = 0;
|
|
1328
|
+
break;
|
|
1329
|
+
case "F":
|
|
1330
|
+
row -= n(0, 1);
|
|
1331
|
+
col = 0;
|
|
1332
|
+
break;
|
|
1333
|
+
case "d":
|
|
1334
|
+
row = n(0, 1) - 1;
|
|
1335
|
+
break;
|
|
1336
|
+
case "H":
|
|
1337
|
+
case "f":
|
|
1338
|
+
row = n(0, 1) - 1;
|
|
1339
|
+
col = n(1, 1) - 1;
|
|
1340
|
+
break;
|
|
1341
|
+
case "s":
|
|
1342
|
+
[savedRow, savedCol] = [row, col];
|
|
1343
|
+
break;
|
|
1344
|
+
case "u":
|
|
1345
|
+
[row, col] = [savedRow, savedCol];
|
|
1346
|
+
break;
|
|
1347
|
+
case "K": {
|
|
1348
|
+
const cells = line();
|
|
1349
|
+
const mode = Number.isFinite(numbers[0]) ? numbers[0]! : 0;
|
|
1350
|
+
if (mode === 0) cells.length = Math.min(cells.length, col);
|
|
1351
|
+
else if (mode === 1) {
|
|
1352
|
+
for (let i = 0; i <= col && i < cells.length; i += 1) cells[i] = " ";
|
|
1353
|
+
} else if (mode === 2) cells.length = 0;
|
|
1354
|
+
break;
|
|
1355
|
+
}
|
|
1356
|
+
case "J": {
|
|
1357
|
+
const mode = Number.isFinite(numbers[0]) ? numbers[0]! : 0;
|
|
1358
|
+
if (mode === 0) {
|
|
1359
|
+
line().length = Math.min(line().length, col);
|
|
1360
|
+
rows.length = Math.min(rows.length, row + 1);
|
|
1361
|
+
} else if (mode === 1) {
|
|
1362
|
+
for (let i = 0; i < row; i += 1) rows[i] = undefined;
|
|
1363
|
+
const cells = line();
|
|
1364
|
+
for (let i = 0; i <= col && i < cells.length; i += 1) cells[i] = " ";
|
|
1365
|
+
} else {
|
|
1366
|
+
rows.length = 0;
|
|
1367
|
+
}
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
default:
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1373
|
+
clamp();
|
|
1374
|
+
}
|
|
1375
|
+
return rows
|
|
1376
|
+
.map((cells) => Array.from(cells ?? [], (cell) => cell ?? " ").join(""))
|
|
1377
|
+
.join("\n");
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1235
1380
|
function candidateHttpsUrls(value: string): string[] {
|
|
1236
1381
|
const plain = stripTerminalControl(value);
|
|
1237
1382
|
return [...plain.matchAll(/https:\/\/[^\s<>"'\x00-\x1f]+/g)]
|
|
@@ -1269,13 +1414,13 @@ export function findSafeHttpsUrl(value: string): string | null {
|
|
|
1269
1414
|
export function extractClaudeOAuthToken(value: string): string | null {
|
|
1270
1415
|
const plain = stripTerminalControl(value);
|
|
1271
1416
|
// The FULL `sk-ant-oat01-` prefix and a realistic minimum length are both
|
|
1272
|
-
// load-bearing: the
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1417
|
+
// load-bearing: the TUI's diff renderer skips screen cells that already
|
|
1418
|
+
// hold the right character, so naive control-stripping glues the runs into
|
|
1419
|
+
// `sk-ant-at01-…` — one character short, token-shaped, wrong — which a
|
|
1420
|
+
// permissive scan once persisted as a "successful" login whose agents then
|
|
1421
|
+
// 401'd forever (live 2026-08-21). The strict prefix makes any mangled
|
|
1422
|
+
// capture a non-match; the actual token match happens against the
|
|
1423
|
+
// reconstructed screen (renderTerminalScreenText), where it is contiguous.
|
|
1279
1424
|
const match =
|
|
1280
1425
|
/(?:^|[^A-Za-z0-9_-])(sk-ant-oat01-[A-Za-z0-9_-]{64,4096})(?![A-Za-z0-9_-])/.exec(
|
|
1281
1426
|
plain,
|
|
@@ -1956,6 +2101,11 @@ export async function createEngineLoginTempDir(
|
|
|
1956
2101
|
await marker.sync();
|
|
1957
2102
|
await marker.close();
|
|
1958
2103
|
marker = null;
|
|
2104
|
+
// The container is told HOME and CODEX_HOME live under the leaf, and
|
|
2105
|
+
// codex refuses to start when CODEX_HOME does not exist (claude
|
|
2106
|
+
// self-creates its HOME). Create both up front.
|
|
2107
|
+
await mkdir(join(path, "home"), { mode: 0o700 });
|
|
2108
|
+
await mkdir(join(path, "codex"), { mode: 0o700 });
|
|
1959
2109
|
await syncDirectory(path);
|
|
1960
2110
|
return path;
|
|
1961
2111
|
} catch (error) {
|