@runuai/host 0.9.60 → 0.9.62
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 +48 -2
- package/package.json +1 -1
- package/src/index.ts +61 -0
- package/src/main.ts +9 -1
- package/src/protocol.ts +26 -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;
|
|
@@ -819,6 +823,25 @@ export class EngineLoginManager {
|
|
|
819
823
|
}
|
|
820
824
|
|
|
821
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
|
+
}
|
|
822
845
|
const authorization = findCodexAuthorization(operation.outputTail);
|
|
823
846
|
if (authorization) {
|
|
824
847
|
// Symmetric with the claude branch: this breadcrumb's ABSENCE was
|
|
@@ -1489,6 +1512,24 @@ export function findSafeHttpsUrl(value: string): string | null {
|
|
|
1489
1512
|
return null;
|
|
1490
1513
|
}
|
|
1491
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
|
+
|
|
1492
1533
|
export function extractClaudeOAuthToken(value: string): string | null {
|
|
1493
1534
|
const plain = stripTerminalControl(value);
|
|
1494
1535
|
// The FULL `sk-ant-oat01-` prefix and a realistic minimum length are both
|
|
@@ -1921,7 +1962,12 @@ export function engineLoginContainerArgs(
|
|
|
1921
1962
|
// because the typescript went to /dev/null.
|
|
1922
1963
|
`${LOGIN_MOUNT}/typescript`,
|
|
1923
1964
|
]
|
|
1924
|
-
:
|
|
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"];
|
|
1925
1971
|
if (engineLoginBackend().apple) {
|
|
1926
1972
|
// Same containment contract, Apple dialect: read-only root, all caps
|
|
1927
1973
|
// dropped, a plain tmpfs at /tmp (the Apple CLI takes no mount options —
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -168,7 +168,68 @@ export const hostEvents: HostEventStream = {
|
|
|
168
168
|
},
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
+
/** Set by main.ts at boot: the drain-then-supervisor-respawn used for
|
|
172
|
+
* staged activation. A hook (not an import) because main imports this
|
|
173
|
+
* module. */
|
|
174
|
+
let managedHostRestartHook: (() => void) | null = null;
|
|
175
|
+
export function setManagedHostRestartHook(hook: () => void): void {
|
|
176
|
+
managedHostRestartHook = hook;
|
|
177
|
+
}
|
|
178
|
+
|
|
171
179
|
export const hostCommands: HostCommands = {
|
|
180
|
+
async hostUpdate(ctx) {
|
|
181
|
+
logCommand(ctx, "hostUpdate");
|
|
182
|
+
try {
|
|
183
|
+
const { updateManagedRuntime } = await import("../lib/managed-runtime");
|
|
184
|
+
const outcome = await updateManagedRuntime();
|
|
185
|
+
if (outcome.status === "not-managed") {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
code: HostErrorCode.Internal,
|
|
189
|
+
message:
|
|
190
|
+
"this host is not a managed install — update it where it runs",
|
|
191
|
+
retryable: false,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (outcome.status === "deferred") {
|
|
195
|
+
// A rollback hold is an operator verdict that the newer release is
|
|
196
|
+
// unsafe on this host; a remote click must not override it.
|
|
197
|
+
return {
|
|
198
|
+
ok: true,
|
|
199
|
+
value: { version: outcome.version, staged: false, deferred: true },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (outcome.status === "current") {
|
|
203
|
+
return { ok: true, value: { version: outcome.version, staged: false } };
|
|
204
|
+
}
|
|
205
|
+
managedHostRestartHook?.();
|
|
206
|
+
return { ok: true, value: { version: outcome.version, staged: true } };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
code: HostErrorCode.Internal,
|
|
211
|
+
message: `update failed: ${
|
|
212
|
+
error instanceof Error ? error.message : "unknown error"
|
|
213
|
+
}`,
|
|
214
|
+
retryable: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
async hostRestart(ctx) {
|
|
219
|
+
logCommand(ctx, "hostRestart");
|
|
220
|
+
if (!managedHostRestartHook) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
code: HostErrorCode.Internal,
|
|
224
|
+
message: "restart is unavailable in this process",
|
|
225
|
+
retryable: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// The result frame flushes before exit: shutdown drains in-flight
|
|
229
|
+
// commands (this one included) before the process terminates.
|
|
230
|
+
managedHostRestartHook();
|
|
231
|
+
return { ok: true, value: undefined };
|
|
232
|
+
},
|
|
172
233
|
async taskUp(ctx, input) {
|
|
173
234
|
logCommand(ctx, "taskUp", input.task.id);
|
|
174
235
|
const admissionFailure = managedMaintenanceUnavailable();
|
package/src/main.ts
CHANGED
|
@@ -161,7 +161,7 @@ import {
|
|
|
161
161
|
onAgentClisReady,
|
|
162
162
|
standardRuntimes,
|
|
163
163
|
} from "../lib/standard-image";
|
|
164
|
-
import { hostCommands, hostEvents } from "./index";
|
|
164
|
+
import { hostCommands, hostEvents, setManagedHostRestartHook } from "./index";
|
|
165
165
|
import { EventOutbox, transmitEventOutbox } from "./event-outbox";
|
|
166
166
|
import {
|
|
167
167
|
HostErrorCode,
|
|
@@ -171,6 +171,7 @@ import {
|
|
|
171
171
|
GITHUB_REPOSITORY_ACCESS_PROTOCOL_FEATURE,
|
|
172
172
|
HOST_MAINTENANCE_READINESS_PROTOCOL_FEATURE,
|
|
173
173
|
HOST_CONFIG_PROTOCOL_FEATURE,
|
|
174
|
+
HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE,
|
|
174
175
|
HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE,
|
|
175
176
|
HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
|
|
176
177
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
@@ -567,6 +568,7 @@ function buildCapabilities(): HostCapabilities {
|
|
|
567
568
|
HOST_LOGS_PROTOCOL_FEATURE,
|
|
568
569
|
HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
|
|
569
570
|
HOST_CONFIG_PROTOCOL_FEATURE,
|
|
571
|
+
HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE,
|
|
570
572
|
HOST_ENGINE_ACCOUNTS_PROTOCOL_FEATURE,
|
|
571
573
|
HOST_ENGINE_CONNECT_PROTOCOL_FEATURE,
|
|
572
574
|
// The echo adapter cannot execute the in-task CLI. Advertising typed
|
|
@@ -2284,6 +2286,10 @@ async function dispatchCommand(
|
|
|
2284
2286
|
expectString(args, 1),
|
|
2285
2287
|
expectNumberArg(args, 2),
|
|
2286
2288
|
);
|
|
2289
|
+
case "hostUpdate":
|
|
2290
|
+
return hostCommands.hostUpdate(ctx);
|
|
2291
|
+
case "hostRestart":
|
|
2292
|
+
return hostCommands.hostRestart(ctx);
|
|
2287
2293
|
case "engineAccountRemove": {
|
|
2288
2294
|
// ADR-116: mirror the local API's post-mutation behavior — the cloud's
|
|
2289
2295
|
// account list self-heals through the capability re-advertisement.
|
|
@@ -3263,6 +3269,8 @@ export function requestManagedHostRestart(): void {
|
|
|
3263
3269
|
shutdownExitCode = MANAGED_UPDATE_RESTART_EXIT_CODE;
|
|
3264
3270
|
requestShutdown("SIGTERM");
|
|
3265
3271
|
}
|
|
3272
|
+
// Remote hostUpdate/hostRestart commands reuse the exact activation restart.
|
|
3273
|
+
setManagedHostRestartHook(requestManagedHostRestart);
|
|
3266
3274
|
|
|
3267
3275
|
/** Automatic host updates wait only for in-flight work to drain. Running
|
|
3268
3276
|
* tasks are deliberately not counted: durable sessions survive the service
|
package/src/protocol.ts
CHANGED
|
@@ -86,6 +86,9 @@ export const MCP_GATEWAY_HEALTH_PROTOCOL_FEATURE = "mcp-gateway-health-v1";
|
|
|
86
86
|
export const HOST_LOGS_PROTOCOL_FEATURE = "host-logs-v1";
|
|
87
87
|
export const HOST_ENGINE_LOGIN_PROTOCOL_FEATURE = "host-engine-login-v1";
|
|
88
88
|
export const HOST_CONFIG_PROTOCOL_FEATURE = "host-config-v1";
|
|
89
|
+
/** Remote update/restart over the bridge (owner-gated cloud routes). */
|
|
90
|
+
export const HOST_REMOTE_LIFECYCLE_PROTOCOL_FEATURE =
|
|
91
|
+
"host-remote-lifecycle-v1";
|
|
89
92
|
/** ADR-116: labeled engine accounts on the cloud host page — account list in
|
|
90
93
|
* capabilities, account-targeted login (`label` on engine.login.start), and
|
|
91
94
|
* the engineAccountRemove command. */
|
|
@@ -345,6 +348,9 @@ export type EngineLoginEventFrame =
|
|
|
345
348
|
| "starting"
|
|
346
349
|
| "awaiting_input"
|
|
347
350
|
| "awaiting_callback"
|
|
351
|
+
// Device-code flow (codex `--device-auth`): the operator confirms a
|
|
352
|
+
// short one-time code on the provider's page; nothing is pasted back.
|
|
353
|
+
| "awaiting_confirmation"
|
|
348
354
|
| "succeeded"
|
|
349
355
|
| "cancelled";
|
|
350
356
|
url?: never;
|
|
@@ -787,6 +793,7 @@ export function isEngineLoginEventFrame(
|
|
|
787
793
|
(frame.phase === "starting" ||
|
|
788
794
|
frame.phase === "awaiting_input" ||
|
|
789
795
|
frame.phase === "awaiting_callback" ||
|
|
796
|
+
frame.phase === "awaiting_confirmation" ||
|
|
790
797
|
frame.phase === "succeeded" ||
|
|
791
798
|
frame.phase === "cancelled") &&
|
|
792
799
|
!Object.hasOwn(frame, "errorCode")
|
|
@@ -1246,7 +1253,26 @@ export type FilesOpValue =
|
|
|
1246
1253
|
| { dataBase64: string; size: number; eof: boolean } // read
|
|
1247
1254
|
| Record<string, never>; // write / mkdir / delete
|
|
1248
1255
|
|
|
1256
|
+
export interface HostUpdateOutcome {
|
|
1257
|
+
version: string;
|
|
1258
|
+
/** True when a newer signed release was staged and the host is about to
|
|
1259
|
+
* drain-restart into it; false when it was already current. */
|
|
1260
|
+
staged: boolean;
|
|
1261
|
+
/** Set when a rollback hold deferred the update (operator ran a rollback;
|
|
1262
|
+
* remote updates respect that verdict). */
|
|
1263
|
+
deferred?: boolean;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1249
1266
|
export interface HostCommands {
|
|
1267
|
+
/**
|
|
1268
|
+
* Stage a signed update from the release feed and drain-restart into it.
|
|
1269
|
+
* Managed installs only; the host verifies the signature itself, so the
|
|
1270
|
+
* remote trigger adds no trust surface. The reconnect on the new version
|
|
1271
|
+
* is the real confirmation.
|
|
1272
|
+
*/
|
|
1273
|
+
hostUpdate(ctx: CommandContext): Promise<HostCommandResult<HostUpdateOutcome>>;
|
|
1274
|
+
/** Drain in-flight work, then let the service supervisor respawn the host. */
|
|
1275
|
+
hostRestart(ctx: CommandContext): Promise<HostCommandResult<void>>;
|
|
1250
1276
|
taskUp(
|
|
1251
1277
|
ctx: CommandContext,
|
|
1252
1278
|
input: TaskLaunchInput,
|