@runuai/host 0.9.88 → 0.9.90
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/codex-auth.ts +26 -7
- package/lib/machine-provider-aws.ts +42 -1
- package/package.json +1 -1
- package/src/main.ts +33 -22
package/lib/codex-auth.ts
CHANGED
|
@@ -3,13 +3,32 @@
|
|
|
3
3
|
* in {@link ./github-tokens} (ADR-027/033 → host 0.8.1).
|
|
4
4
|
*
|
|
5
5
|
* The host owner's `~/.codex` is docker-cp'd into each task container ONCE, at
|
|
6
|
-
* task-up (`scripts/agent/task-up.sh`). When the owner re-logs into Codex
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* task-up (`scripts/agent/task-up.sh`). When the owner re-logs into Codex,
|
|
7
|
+
* `codex login` revokes and rewrites `~/.codex/auth.json` on the HOST —
|
|
8
|
+
* already-running containers keep their stale copy, so a re-login otherwise
|
|
9
|
+
* only helps NEW tasks. The live sweep refreshes the credential-bearing copy
|
|
10
|
+
* while deliberately preserving the container's `config.toml`: browser/MCP
|
|
11
|
+
* setup owns that live file and serializes its updates with agent spawning.
|
|
12
|
+
*
|
|
13
|
+
* That revoke-then-rewrite is CLIENT-SIDE and scoped to the LOCAL codex_home —
|
|
14
|
+
* not a server-side "one session per account" policy. `codex login` calls
|
|
15
|
+
* `clear_existing_auth_before_login` (openai/codex `cli/src/login.rs`), which
|
|
16
|
+
* revokes only whatever token `revocable_token()` (`login/src/auth/revoke.rs`)
|
|
17
|
+
* finds in THIS SAME codex_home's `auth.json` via `logout_with_revoke`
|
|
18
|
+
* (`login/src/auth/manager.rs`) — it never looks at, let alone reaches, another
|
|
19
|
+
* login's session. Separate logins are independent authorization_code+PKCE
|
|
20
|
+
* grants with independent refresh lineages (source analysis corroborated by a
|
|
21
|
+
* community-verified workaround, not an empirical run — full writeup:
|
|
22
|
+
* `docs/pricing-economics.md` R8/R10).
|
|
23
|
+
*
|
|
24
|
+
* What DOES matter for concurrency: refresh rotation is single-use, and a
|
|
25
|
+
* reused refresh token comes back `refresh_token_reused` — a TERMINAL
|
|
26
|
+
* `Exhausted` failure the client never retries. This file's docker-cp pattern
|
|
27
|
+
* already gives every task container its own independent filesystem copy of
|
|
28
|
+
* ONE lineage with nothing to reconcile them, so the instant any one
|
|
29
|
+
* container's own `codex` process refreshes, every sibling copy is a stale
|
|
30
|
+
* member of a now-rotated family and its next refresh is terminal, not
|
|
31
|
+
* self-healing — containers must never refresh this credential themselves.
|
|
13
32
|
*
|
|
14
33
|
* Three triggers, covering every path:
|
|
15
34
|
* - {@link watchCodexAuth} — fs.watch on `~/.codex`, for `codex login` run
|
|
@@ -403,10 +403,51 @@ export function createAwsMachineProvider(
|
|
|
403
403
|
try {
|
|
404
404
|
const existing = JSON.parse(found.stdout);
|
|
405
405
|
if (typeof existing === "string" && existing.startsWith("sg-")) {
|
|
406
|
+
// The group admits only host addresses — and it may predate THIS
|
|
407
|
+
// host (a second managed host in the fleet account, or this host
|
|
408
|
+
// rebooting onto a new public IP). Ensure our egress /32 is
|
|
409
|
+
// present; a duplicate rule is AWS's problem to dedupe, not ours
|
|
410
|
+
// to pre-check. Without this, every machine of a newer host sat
|
|
411
|
+
// unreachable behind the first host's rule ("machine never became
|
|
412
|
+
// reachable over ssh", live 2026-08-31).
|
|
413
|
+
const ensureEgress = await hostEgressIp();
|
|
414
|
+
const ensured = await runner([
|
|
415
|
+
"ec2",
|
|
416
|
+
"authorize-security-group-ingress",
|
|
417
|
+
"--group-id",
|
|
418
|
+
existing,
|
|
419
|
+
"--ip-permissions",
|
|
420
|
+
JSON.stringify([
|
|
421
|
+
{
|
|
422
|
+
IpProtocol: "tcp",
|
|
423
|
+
FromPort: 0,
|
|
424
|
+
ToPort: 65535,
|
|
425
|
+
IpRanges: [
|
|
426
|
+
{
|
|
427
|
+
CidrIp: `${ensureEgress}/32`,
|
|
428
|
+
Description: "uai orchestrating host",
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
]),
|
|
433
|
+
]);
|
|
434
|
+
if (
|
|
435
|
+
ensured.status !== 0 &&
|
|
436
|
+
!/InvalidPermission\.Duplicate/.test(ensured.stderr)
|
|
437
|
+
) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`could not admit this host into ${MANAGED_SECURITY_GROUP}: ${
|
|
440
|
+
ensured.stderr.trim() || `exit ${ensured.status ?? "killed"}`
|
|
441
|
+
}`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
406
444
|
resolvedSecurityGroup = existing;
|
|
407
445
|
return existing;
|
|
408
446
|
}
|
|
409
|
-
} catch {
|
|
447
|
+
} catch (err) {
|
|
448
|
+
if (err instanceof Error && /could not admit this host/.test(err.message)) {
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
410
451
|
// fall through to creation
|
|
411
452
|
}
|
|
412
453
|
}
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -2614,7 +2614,11 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
|
|
|
2614
2614
|
) {
|
|
2615
2615
|
return { kind: "mcp.op", opId: frame.opId, op: frame.op };
|
|
2616
2616
|
}
|
|
2617
|
-
|
|
2617
|
+
// Names only — never payloads (frames can carry tokens).
|
|
2618
|
+
console.warn(
|
|
2619
|
+
`[host-agent] dropping unknown frame kind=${String(frame.kind)}` +
|
|
2620
|
+
(frame.kind === "command" ? ` command=${String(frame.command)}` : ""),
|
|
2621
|
+
);
|
|
2618
2622
|
return null;
|
|
2619
2623
|
}
|
|
2620
2624
|
|
|
@@ -2861,28 +2865,35 @@ function rawDataToBuffer(data: RawData): Buffer {
|
|
|
2861
2865
|
return Buffer.from(data);
|
|
2862
2866
|
}
|
|
2863
2867
|
|
|
2868
|
+
// Exhaustive BY TYPE: `satisfies Record<keyof HostCommands, true>` makes a
|
|
2869
|
+
// command missing from this map a compile error. The previous hand-written
|
|
2870
|
+
// array silently dropped any frame it lagged behind on — hostUpdate and
|
|
2871
|
+
// hostRestart shipped in 0.9.62 and every remote Update/Restart click since
|
|
2872
|
+
// died here as "dropping unknown frame" / a cloud-side command timeout.
|
|
2873
|
+
const HOST_COMMAND_NAMES = {
|
|
2874
|
+
hostUpdate: true,
|
|
2875
|
+
hostRestart: true,
|
|
2876
|
+
taskUp: true,
|
|
2877
|
+
taskDown: true,
|
|
2878
|
+
taskStatus: true,
|
|
2879
|
+
taskStop: true,
|
|
2880
|
+
channelEnsure: true,
|
|
2881
|
+
channelTeardown: true,
|
|
2882
|
+
channelDeliver: true,
|
|
2883
|
+
channelResolvePermission: true,
|
|
2884
|
+
cloneRepo: true,
|
|
2885
|
+
taskDiff: true,
|
|
2886
|
+
attachmentWrite: true,
|
|
2887
|
+
attachmentRead: true,
|
|
2888
|
+
filesOp: true,
|
|
2889
|
+
channelInterrupt: true,
|
|
2890
|
+
appendTranscript: true,
|
|
2891
|
+
previewEnsure: true,
|
|
2892
|
+
engineAccountRemove: true,
|
|
2893
|
+
} satisfies Record<keyof HostCommands, true>;
|
|
2894
|
+
|
|
2864
2895
|
function isHostCommand(command: string): command is keyof HostCommands {
|
|
2865
|
-
|
|
2866
|
-
// manual allow-list (TS can't derive it from the type), so a missing entry
|
|
2867
|
-
// silently drops the frame and hangs the cloud command.
|
|
2868
|
-
return [
|
|
2869
|
-
"taskUp",
|
|
2870
|
-
"taskDown",
|
|
2871
|
-
"taskStatus",
|
|
2872
|
-
"channelEnsure",
|
|
2873
|
-
"channelTeardown",
|
|
2874
|
-
"channelDeliver",
|
|
2875
|
-
"channelResolvePermission",
|
|
2876
|
-
"cloneRepo",
|
|
2877
|
-
"taskDiff",
|
|
2878
|
-
"attachmentWrite",
|
|
2879
|
-
"attachmentRead",
|
|
2880
|
-
"filesOp",
|
|
2881
|
-
"channelInterrupt",
|
|
2882
|
-
"appendTranscript",
|
|
2883
|
-
"previewEnsure",
|
|
2884
|
-
"engineAccountRemove",
|
|
2885
|
-
].includes(command);
|
|
2896
|
+
return Object.hasOwn(HOST_COMMAND_NAMES, command);
|
|
2886
2897
|
}
|
|
2887
2898
|
|
|
2888
2899
|
function expectString(args: unknown[], index: number): string {
|