@runuai/host 0.9.59 → 0.9.61
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 +85 -12
- package/lib/mcp-gateway.ts +13 -9
- package/package.json +1 -1
- package/src/protocol.ts +4 -0
package/lib/engine-login.ts
CHANGED
|
@@ -169,7 +169,11 @@ export interface EngineLoginManagerOptions {
|
|
|
169
169
|
seams?: Partial<EngineLoginSeams>;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
type OperationStage =
|
|
172
|
+
type OperationStage =
|
|
173
|
+
| "starting"
|
|
174
|
+
| "awaiting_input"
|
|
175
|
+
| "awaiting_callback"
|
|
176
|
+
| "awaiting_confirmation";
|
|
173
177
|
|
|
174
178
|
interface CodexCallbackTarget {
|
|
175
179
|
readonly origin: string;
|
|
@@ -555,20 +559,34 @@ export class EngineLoginManager {
|
|
|
555
559
|
/** Validate and replay a Codex callback only to this operation's listener. */
|
|
556
560
|
async callback(opId: string, value: string): Promise<boolean> {
|
|
557
561
|
const operation = this.operations.get(opId);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
!operation.callbackTarget
|
|
567
|
-
) {
|
|
562
|
+
// Same named-rejection contract as input() (0.9.51): every silent exit
|
|
563
|
+
// here left an operator staring at "Waiting for the host…" with zero
|
|
564
|
+
// evidence on either side (live 2026-08-21 — the callback path was the
|
|
565
|
+
// last breadcrumb-free room in the login pipeline).
|
|
566
|
+
const refuse = (reason: string): false => {
|
|
567
|
+
console.log(
|
|
568
|
+
`[engine-login] callback refused for ${opId}: ${reason}`,
|
|
569
|
+
);
|
|
568
570
|
return false;
|
|
571
|
+
};
|
|
572
|
+
if (!operation) return refuse("unknown opId");
|
|
573
|
+
if (!this.isCurrent(operation)) return refuse("superseded operation");
|
|
574
|
+
if (operation.engine !== "codex") return refuse("wrong engine");
|
|
575
|
+
if (operation.stage !== "awaiting_callback") {
|
|
576
|
+
return refuse(`wrong stage (${operation.stage})`);
|
|
569
577
|
}
|
|
578
|
+
if (operation.callbackUsed) return refuse("callback already used");
|
|
579
|
+
if (operation.finishing) return refuse("operation finishing");
|
|
580
|
+
if (!operation.process) return refuse("no login process");
|
|
581
|
+
if (!operation.callbackTarget) return refuse("no callback target");
|
|
582
|
+
console.log(
|
|
583
|
+
`[engine-login] callback accepted for ${opId} (${value.length} chars); validating`,
|
|
584
|
+
);
|
|
570
585
|
const query = validateCodexCallback(value, operation.callbackTarget);
|
|
571
586
|
if (!query) {
|
|
587
|
+
console.warn(
|
|
588
|
+
`[engine-login] callback failed validation for ${opId} (state/shape mismatch with this attempt's authorize URL)`,
|
|
589
|
+
);
|
|
572
590
|
await this.fail(
|
|
573
591
|
operation,
|
|
574
592
|
"invalid_callback",
|
|
@@ -576,6 +594,9 @@ export class EngineLoginManager {
|
|
|
576
594
|
);
|
|
577
595
|
return false;
|
|
578
596
|
}
|
|
597
|
+
console.log(
|
|
598
|
+
`[engine-login] callback validated for ${opId}; replaying into the login container`,
|
|
599
|
+
);
|
|
579
600
|
operation.callbackUsed = true;
|
|
580
601
|
const callbackUrl = callbackReplayUrl(operation.callbackTarget, query);
|
|
581
602
|
const replay = (async () => {
|
|
@@ -584,6 +605,9 @@ export class EngineLoginManager {
|
|
|
584
605
|
operation.process!.containerName,
|
|
585
606
|
callbackUrl,
|
|
586
607
|
);
|
|
608
|
+
console.log(
|
|
609
|
+
`[engine-login] callback replay delivered for ${operation.opId}; waiting for the CLI to finish`,
|
|
610
|
+
);
|
|
587
611
|
} catch {
|
|
588
612
|
if (this.isCurrent(operation)) {
|
|
589
613
|
await this.fail(
|
|
@@ -799,6 +823,25 @@ export class EngineLoginManager {
|
|
|
799
823
|
}
|
|
800
824
|
|
|
801
825
|
if (operation.stage === "starting") {
|
|
826
|
+
const device = findCodexDeviceAuthorization(operation.outputTail);
|
|
827
|
+
if (device) {
|
|
828
|
+
console.log(
|
|
829
|
+
`[engine-login] codex device authorization found for ${operation.opId}; awaiting operator confirmation`,
|
|
830
|
+
);
|
|
831
|
+
operation.stage = "awaiting_confirmation";
|
|
832
|
+
safeEmit(
|
|
833
|
+
operation.emit,
|
|
834
|
+
authorizeEvent(operation, device.verificationUrl),
|
|
835
|
+
);
|
|
836
|
+
safeEmit(operation.emit, {
|
|
837
|
+
kind: "engine.login.event",
|
|
838
|
+
opId: operation.opId,
|
|
839
|
+
engine: operation.engine,
|
|
840
|
+
phase: "awaiting_confirmation",
|
|
841
|
+
message: `Enter code ${device.userCode} on the verification page. Nothing to paste here — this completes on its own.`,
|
|
842
|
+
});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
802
845
|
const authorization = findCodexAuthorization(operation.outputTail);
|
|
803
846
|
if (authorization) {
|
|
804
847
|
// Symmetric with the claude branch: this breadcrumb's ABSENCE was
|
|
@@ -1469,6 +1512,24 @@ export function findSafeHttpsUrl(value: string): string | null {
|
|
|
1469
1512
|
return null;
|
|
1470
1513
|
}
|
|
1471
1514
|
|
|
1515
|
+
/**
|
|
1516
|
+
* Codex `--device-auth` banner: a verification URL plus a short one-time
|
|
1517
|
+
* code ("AC57-MDV9G" shape). Both the phrase gate and the code shape are
|
|
1518
|
+
* required so container names, hashes, or URL fragments cannot fake a
|
|
1519
|
+
* device prompt. Reads the merged tail — codex prints to stderr.
|
|
1520
|
+
*/
|
|
1521
|
+
export function findCodexDeviceAuthorization(
|
|
1522
|
+
value: string,
|
|
1523
|
+
): { verificationUrl: string; userCode: string } | null {
|
|
1524
|
+
const plain = stripTerminalControl(value);
|
|
1525
|
+
if (!/one-time code|device code/i.test(plain)) return null;
|
|
1526
|
+
const verificationUrl = findSafeHttpsUrl(plain);
|
|
1527
|
+
if (!verificationUrl) return null;
|
|
1528
|
+
const code = /(?:^|\s)([A-Z0-9]{4,8}-[A-Z0-9]{4,10})(?:\s|$)/m.exec(plain);
|
|
1529
|
+
if (!code) return null;
|
|
1530
|
+
return { verificationUrl, userCode: code[1]! };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1472
1533
|
export function extractClaudeOAuthToken(value: string): string | null {
|
|
1473
1534
|
const plain = stripTerminalControl(value);
|
|
1474
1535
|
// The FULL `sk-ant-oat01-` prefix and a realistic minimum length are both
|
|
@@ -1901,7 +1962,12 @@ export function engineLoginContainerArgs(
|
|
|
1901
1962
|
// because the typescript went to /dev/null.
|
|
1902
1963
|
`${LOGIN_MOUNT}/typescript`,
|
|
1903
1964
|
]
|
|
1904
|
-
:
|
|
1965
|
+
: // Device-code flow: no localhost callback, no paste — the CLI prints
|
|
1966
|
+
// a verification URL + one-time code, the operator confirms on the
|
|
1967
|
+
// provider's page, and the CLI polls to completion (ADR-116 v2,
|
|
1968
|
+
// 2026-08-24). The callback machinery below stays as the fallback
|
|
1969
|
+
// for CLIs that still print the localhost authorize URL.
|
|
1970
|
+
["codex", "login", "--device-auth"];
|
|
1905
1971
|
if (engineLoginBackend().apple) {
|
|
1906
1972
|
// Same containment contract, Apple dialect: read-only root, all caps
|
|
1907
1973
|
// dropped, a plain tmpfs at /tmp (the Apple CLI takes no mount options —
|
|
@@ -2036,6 +2102,13 @@ export function codexCallbackReplayDockerRequest(
|
|
|
2036
2102
|
"--fail",
|
|
2037
2103
|
"--silent",
|
|
2038
2104
|
"--show-error",
|
|
2105
|
+
// Follow the callback's redirect to the CLI's /success page: codex
|
|
2106
|
+
// writes auth.json on the callback but EXITS only after serving that
|
|
2107
|
+
// page — a real browser follows; a replay that stops at the redirect
|
|
2108
|
+
// leaves a completed login waiting forever (live 2026-08-24: the
|
|
2109
|
+
// first-ever completed pane codex login needed a manual /success
|
|
2110
|
+
// fetch to let the CLI exit).
|
|
2111
|
+
"--location",
|
|
2039
2112
|
"--max-time",
|
|
2040
2113
|
"10",
|
|
2041
2114
|
"--output",
|
package/lib/mcp-gateway.ts
CHANGED
|
@@ -73,21 +73,25 @@ function gatewayPort(value: string | undefined): number {
|
|
|
73
73
|
export const MCP_GATEWAY_PORT = gatewayPort(
|
|
74
74
|
process.env.UAI_MCP_GATEWAY_PORT,
|
|
75
75
|
);
|
|
76
|
-
/** Loopback
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* the
|
|
80
|
-
*
|
|
81
|
-
*
|
|
76
|
+
/** Loopback works only where the container runtime forwards
|
|
77
|
+
* host.docker.internal INTO the host loopback — macOS Docker
|
|
78
|
+
* (Desktop/OrbStack). Native Linux docker resolves host.docker.internal to
|
|
79
|
+
* the bridge gateway IP, where a loopback bind is unreachable: every task's
|
|
80
|
+
* MCP config carried a dead URL and the gateway showed connection-refused
|
|
81
|
+
* from inside containers (live 2026-08-24, first Linux host with an MCP
|
|
82
|
+
* connection). Apple vmnet guests cannot reach host loopback either. Both
|
|
83
|
+
* non-mac-docker cases therefore bind all interfaces — the unguessable
|
|
84
|
+
* per-task path token remains the auth (same posture the operator override
|
|
85
|
+
* has always allowed). Resolved per call: the runtime pin lands before the
|
|
86
|
+
* production listener starts, and a pin never changes within a process. */
|
|
82
87
|
function gatewayBindAddress(): string {
|
|
83
88
|
const configured = process.env.UAI_MCP_GATEWAY_BIND;
|
|
84
89
|
if (configured) return configured;
|
|
85
90
|
// The pin, not the ready-gated identity: the production listener starts
|
|
86
91
|
// during boot while activation still reports `checking`, and a loopback
|
|
87
92
|
// bind chosen then would strand every vmnet guest.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
: "127.0.0.1";
|
|
93
|
+
if (pinnedContainerRuntimeProvider() === "apple-container") return "0.0.0.0";
|
|
94
|
+
return process.platform === "darwin" ? "127.0.0.1" : "0.0.0.0";
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
/** Host address a task container dials to reach this gateway. Docker guests
|
package/package.json
CHANGED
package/src/protocol.ts
CHANGED
|
@@ -345,6 +345,9 @@ export type EngineLoginEventFrame =
|
|
|
345
345
|
| "starting"
|
|
346
346
|
| "awaiting_input"
|
|
347
347
|
| "awaiting_callback"
|
|
348
|
+
// Device-code flow (codex `--device-auth`): the operator confirms a
|
|
349
|
+
// short one-time code on the provider's page; nothing is pasted back.
|
|
350
|
+
| "awaiting_confirmation"
|
|
348
351
|
| "succeeded"
|
|
349
352
|
| "cancelled";
|
|
350
353
|
url?: never;
|
|
@@ -787,6 +790,7 @@ export function isEngineLoginEventFrame(
|
|
|
787
790
|
(frame.phase === "starting" ||
|
|
788
791
|
frame.phase === "awaiting_input" ||
|
|
789
792
|
frame.phase === "awaiting_callback" ||
|
|
793
|
+
frame.phase === "awaiting_confirmation" ||
|
|
790
794
|
frame.phase === "succeeded" ||
|
|
791
795
|
frame.phase === "cancelled") &&
|
|
792
796
|
!Object.hasOwn(frame, "errorCode")
|