@runuai/host 0.9.89 → 0.9.91
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/agents/factory.ts +1 -0
- package/lib/agents/types.ts +3 -0
- package/lib/agents/uai.ts +87 -0
- package/lib/codex-auth.ts +26 -7
- package/lib/machine-provider-aws.ts +42 -1
- package/package.json +1 -1
- package/src/main.ts +3 -0
- package/src/protocol.ts +2 -0
package/lib/agents/factory.ts
CHANGED
package/lib/agents/types.ts
CHANGED
|
@@ -54,6 +54,9 @@ export const RosterAgent = z.object({
|
|
|
54
54
|
initialPrompt: z.string().optional(), // first-turn instructions, this task only
|
|
55
55
|
skills: z.array(RosterSkill).optional(), // ADR-046 reference material
|
|
56
56
|
permissions: z.array(z.string()).optional(), // ADR-048 `uai` CLI capabilities
|
|
57
|
+
// ADR-128: which upstream protocol a Uai-engine model speaks — resolved
|
|
58
|
+
// cloud-side from the model catalog; selects the delegated adapter.
|
|
59
|
+
uaiProvider: z.enum(["anthropic", "openai"]).optional(),
|
|
57
60
|
});
|
|
58
61
|
export type RosterAgent = z.infer<typeof RosterAgent>;
|
|
59
62
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Uai Managed Engine (ADR-128) — not a harness, a delegation.
|
|
3
|
+
*
|
|
4
|
+
* The customer picks "Uai Managed Engine" and one of Uai's models; under
|
|
5
|
+
* the hood this adapter spawns the EXISTING claude or codex adapter
|
|
6
|
+
* (chosen by the model's provider, resolved cloud-side into
|
|
7
|
+
* `agent.uaiProvider`) with the environment pointed at the inference
|
|
8
|
+
* gateway: base URL swapped, the agent's own task token as the API
|
|
9
|
+
* credential, and the host's real provider credentials explicitly
|
|
10
|
+
* BLANKED so a BYO host's Claude subscription can never be billed for a
|
|
11
|
+
* Uai-engine turn. The gateway meters usage server-side and the cloud
|
|
12
|
+
* debits credits — this file never sees money.
|
|
13
|
+
*
|
|
14
|
+
* The model slug passes through verbatim: the underlying CLI sends it as
|
|
15
|
+
* the request's `model`, and the gateway maps slug → upstream id.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { factoryFor, register } from "./registry";
|
|
19
|
+
import type { AgentSession, AgentSessionFactory } from "./types";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_GATEWAY_URL = "https://inference.runuai.com";
|
|
22
|
+
|
|
23
|
+
export function gatewayUrl(): string {
|
|
24
|
+
return process.env.UAI_INFERENCE_GATEWAY_URL || DEFAULT_GATEWAY_URL;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The env override set for a delegated session. Exported for tests. */
|
|
28
|
+
export function uaiGatewayEnv(
|
|
29
|
+
provider: "anthropic" | "openai",
|
|
30
|
+
url: string,
|
|
31
|
+
taskToken: string,
|
|
32
|
+
): Record<string, string> {
|
|
33
|
+
if (provider === "openai") {
|
|
34
|
+
return {
|
|
35
|
+
OPENAI_BASE_URL: url,
|
|
36
|
+
OPENAI_API_KEY: taskToken,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ANTHROPIC_BASE_URL: url,
|
|
41
|
+
ANTHROPIC_AUTH_TOKEN: taskToken,
|
|
42
|
+
// Explicit blanks WIN over the claude adapter's passEnv inheritance
|
|
43
|
+
// (same mechanism its communicator profile uses to scrub) — the host's
|
|
44
|
+
// real credential must never ride a gateway-billed session.
|
|
45
|
+
ANTHROPIC_API_KEY: "",
|
|
46
|
+
CLAUDE_CODE_OAUTH_TOKEN: "",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const create: AgentSessionFactory["create"] = async (args): Promise<AgentSession> => {
|
|
51
|
+
const provider = args.agent.uaiProvider === "openai" ? "openai" : "anthropic";
|
|
52
|
+
const delegateKind = provider === "openai" ? "codex" : "claude";
|
|
53
|
+
const delegate = factoryFor(delegateKind);
|
|
54
|
+
if (!delegate) {
|
|
55
|
+
throw new Error(`uai engine: no ${delegateKind} adapter registered to delegate to`);
|
|
56
|
+
}
|
|
57
|
+
const taskToken = args.agentEnv?.UAI_TASK_TOKEN;
|
|
58
|
+
if (!taskToken) {
|
|
59
|
+
// agentCliEnv mints no token for a permissionless non-secretary agent;
|
|
60
|
+
// the gateway authenticates with exactly that token, so the engine
|
|
61
|
+
// needs it. The default crew grants permissions, so this is rare.
|
|
62
|
+
throw new Error(
|
|
63
|
+
"uai engine: this agent has no task token (no CLI permissions) — grant it at least one permission or use a direct engine",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return delegate.create({
|
|
67
|
+
...args,
|
|
68
|
+
agentEnv: {
|
|
69
|
+
...(args.agentEnv ?? {}),
|
|
70
|
+
...uaiGatewayEnv(provider, gatewayUrl(), taskToken),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
register({
|
|
76
|
+
kind: "uai",
|
|
77
|
+
label: "Uai Managed Engine",
|
|
78
|
+
// The model catalog is cloud data (ADR-128); the composer sources it from
|
|
79
|
+
// /api/org/inference/models, never from host capabilities. Advertising an
|
|
80
|
+
// empty list here simply marks the kind spawnable on this host.
|
|
81
|
+
supportedModels: () => [],
|
|
82
|
+
supportedEfforts: () => [],
|
|
83
|
+
// Always available: the engine needs no host-side credential — auth is
|
|
84
|
+
// the per-agent task token, billing is the org's credits.
|
|
85
|
+
available: () => true,
|
|
86
|
+
create,
|
|
87
|
+
});
|
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
|
@@ -2919,6 +2919,9 @@ function expectTaskAgents(value: unknown): TaskAgent[] {
|
|
|
2919
2919
|
// effort was previously dropped here even though the protocol + adapters
|
|
2920
2920
|
// carry it — fixed alongside skills (ADR-046).
|
|
2921
2921
|
if (typeof row.effort === "string") out.effort = row.effort;
|
|
2922
|
+
if (row.uaiProvider === "anthropic" || row.uaiProvider === "openai") {
|
|
2923
|
+
out.uaiProvider = row.uaiProvider;
|
|
2924
|
+
}
|
|
2922
2925
|
if (typeof row.defaultPrompt === "string") {
|
|
2923
2926
|
out.defaultPrompt = row.defaultPrompt;
|
|
2924
2927
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -173,6 +173,8 @@ export type PermissionDecision = { kind: "accept" } | { kind: "decline" };
|
|
|
173
173
|
* `effort` (when set) is the CLI reasoning level, passed through like `model`.
|
|
174
174
|
*/
|
|
175
175
|
export interface TaskAgent {
|
|
176
|
+
/** ADR-128: upstream protocol of a Uai-engine model (kind "uai"). */
|
|
177
|
+
uaiProvider?: "anthropic" | "openai";
|
|
176
178
|
id: string;
|
|
177
179
|
label: string;
|
|
178
180
|
kind: string;
|