@runuai/host 0.9.171 → 0.9.172

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/src/main.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env tsx
2
+ import "./load-env";
3
+ import { discoverCustomConnection, saveCustomConnection, saveCustomConnectionManual } from "../lib/custom-connection-capture";
4
+ import { OpenRouterBudgetPoller } from "../lib/openrouter-budget";
5
+ import { onLeasedEnginesChanged, leasedEnginesFor, isLeasedEngineAccount } from "../lib/credential-consumers/engines";
6
+ import { isOwnedInternalConnectionRuntime } from "../lib/internal-connections";
7
+ import { CUSTOM_CONNECTIONS_FEATURE, ENGINE_CONNECT_CUSTOM_FEATURE, ENGINE_CONNECT_CUSTOM_MANUAL_FEATURE, INTERNAL_CONNECTIONS_FEATURE, connectionOpId, parseCustomConnectionMaterial, parseCustomConnectionEnabled, parseEngineConnectCustomDiscoverFrame, parseEngineConnectCustomSaveFrame, parseEngineConnectCustomManualSaveFrame } from "./custom-connections";
2
8
  import type { AssignmentDelivery, DeliveryOrigin } from "./protocol";
9
+ import { parseCaptureFlowWire } from "./cloud-credentials";
3
10
 
4
- import "./load-env";
5
11
  import { sampleDiskPressure } from "../lib/disk-pressure";
6
12
  import { TASK_SECRETS_FEATURE, isTaskSecretStagingPut } from "./task-secrets";
7
13
  import { writeTaskSecretStaging } from "../lib/task-secret-staging";
@@ -181,7 +187,7 @@ import {
181
187
  rejectContainerRuntimeSelection,
182
188
  reprobeContainerRuntimeMachineIdentity,
183
189
  } from "../lib/container-runtime";
184
- import { persistedTaskEnvironmentsMatchMachine, reconstructPersistedTaskEnvironment } from "../lib/task-environment";
190
+ import { machineTasksEnabled, persistedTaskEnvironmentsMatchMachine, reconstructPersistedTaskEnvironment } from "../lib/task-environment";
185
191
  import { reconcileRunningTaskPersonCredentials } from "../lib/task-person-credentials";
186
192
  import { logVnodePressure } from "../lib/vnode-pressure";
187
193
  import { resolveAppleTunnelRoute } from "./apple-tunnel-route";
@@ -509,6 +515,15 @@ const bridgeUrl =
509
515
 
510
516
  let ws: WebSocket | null = null;
511
517
  let stopping = false;
518
+ const openRouterBudgets = new OpenRouterBudgetPoller({ emit: frame => {
519
+ if (!stopping && credentialFramesReady && ws?.readyState === WebSocket.OPEN) send(ws, frame);
520
+ } });
521
+ onLeasedEnginesChanged(taskId => {
522
+ if (stopping) { openRouterBudgets.stop(taskId); return; }
523
+ const lease = leasedEnginesFor(taskId);
524
+ openRouterBudgets.sync(taskId, lease?.open ? lease.accounts.flatMap(account => isLeasedEngineAccount(account) && account.authKind === "custom-connection" && account.connectionMaterial?.providerType === "openrouter" ? [{ credentialId: account.credentialId, revision: account.revision, apiKey: account.connectionMaterial.apiKey }] : []) : []);
525
+ });
526
+
512
527
  let fatal = false;
513
528
  /** Auth-rejected (4001) retry cadence. A genuinely revoked credential costs
514
529
  * one connection attempt per interval; a transient rejection (the bridge's
@@ -523,6 +538,17 @@ let shutdownRequested = false;
523
538
  let shutdownExitCode = 0;
524
539
  const managedMutationDrain = new ManagedOperationDrain(maybeExit);
525
540
  const hostLogManager = createHostLogManager();
541
+ const customConnectionOperations = new Map<AbortController, Promise<void>>();
542
+
543
+ function runCustomConnectionOperation(operation: (signal: AbortSignal) => Promise<void>): void {
544
+ void managedMutationDrain.track(async () => {
545
+ const controller = new AbortController();
546
+ const work = Promise.resolve().then(() => operation(controller.signal));
547
+ customConnectionOperations.set(controller, work);
548
+ try { await work; } finally { customConnectionOperations.delete(controller); }
549
+ });
550
+ }
551
+
526
552
  const pendingRemoteOperationCleanups = new Set<Promise<void>>();
527
553
  let stopActiveRemoteOperations = (): Promise<void> => Promise.resolve();
528
554
 
@@ -545,8 +571,10 @@ function trackRemoteOperationCleanup(cleanup: Promise<void>): Promise<void> {
545
571
  function stopRemoteOperationManagers(): Promise<void> {
546
572
  hostLogManager.stopAll();
547
573
  engineConnectManager.stopAll();
574
+ for (const controller of customConnectionOperations.keys()) controller.abort();
548
575
  return trackRemoteOperationCleanup(
549
576
  Promise.all([
577
+ ...customConnectionOperations.values(),
550
578
  engineLoginManager.stopAll(),
551
579
  // Tunnel upstreams are generation-scoped resources just like login
552
580
  // containers; a bridge replacement or shutdown must join their
@@ -1033,6 +1061,7 @@ const taskFiles = sidecarMode()
1033
1061
  : null;
1034
1062
  const cloudCredentials = new CloudCredentialRuntime({
1035
1063
  runnerKind: sidecarMode() ? "task-machine" : "owned",
1064
+ internalRuntimeProvider: (assignment) => internalRuntimeProvider(assignment.taskId),
1036
1065
  send: sendCredentialFrame,
1037
1066
  quiesceProcesses: (request) => getOrchestrator().quiesceLeasedEngineAgents(request),
1038
1067
  mcpServedChanged: (taskId) => getOrchestrator().mcpServedChanged(taskId),
@@ -1492,12 +1521,30 @@ void refreshDiskSnapshot();
1492
1521
  const diskTimer = setInterval(() => void refreshDiskSnapshot(), 60_000);
1493
1522
  diskTimer.unref?.();
1494
1523
 
1524
+ /** A persisted provider wins over this runner's default selection. Legacy
1525
+ * resources without a proven provider are unknown; only a new/empty task
1526
+ * may use the selected provisioning backend as its authoritative plan. */
1527
+ function internalRuntimeProvider(taskId?: string): string | null {
1528
+ if (sidecarMode() || signInBoxMode()) return null;
1529
+ const task = taskId ? getHostTask(taskId) : undefined;
1530
+ if (task?.environmentProvider !== null && task?.environmentProvider !== undefined) return task.environmentProvider;
1531
+ if (task && (task.composeProject !== null || task.worktreePath !== null)) return null;
1532
+ if (machineTasksEnabled()) return null;
1533
+ const runtime = containerRuntimeCapabilityState();
1534
+ if (runtime.status !== "ready") return null;
1535
+ return runtime.provider === "docker" ? "docker-compose" : runtime.provider === "apple-container" ? "apple-container" : null;
1536
+ }
1537
+
1495
1538
  /** Build the current host capability advertisement (ADR-021). */
1496
1539
  function buildCapabilities(): HostCapabilities {
1497
1540
  const runtime = containerRuntimeCapabilityState();
1498
1541
  const projectEnvBindings = projectEnvBindingCapability();
1499
1542
  return {
1500
1543
  version: packageVersion(),
1544
+ customConnections: credentialsMode() === "active",
1545
+ engineConnectCustom: signInBoxMode(),
1546
+ engineConnectCustomManual: signInBoxMode(),
1547
+ internalConnections: isOwnedInternalConnectionRuntime(internalRuntimeProvider()),
1501
1548
  ...(diskSnapshot ? { disk: diskSnapshot } : {}),
1502
1549
  protocolFeatures: [
1503
1550
  "host-workspace-file-read-v1",
@@ -1543,6 +1590,9 @@ function buildCapabilities(): HostCapabilities {
1543
1590
  // ADR-161 §1.11: this image's `uai files` reads cloud links. The cloud
1544
1591
  // captures the advertisement at container creation.
1545
1592
  HOST_AGENT_FILES_FEATURE,
1593
+ ...(credentialsMode() === "active" ? [CUSTOM_CONNECTIONS_FEATURE] : []),
1594
+ ...(signInBoxMode() ? [ENGINE_CONNECT_CUSTOM_FEATURE, ENGINE_CONNECT_CUSTOM_MANUAL_FEATURE] : []),
1595
+ ...(isOwnedInternalConnectionRuntime(internalRuntimeProvider()) ? [INTERNAL_CONNECTIONS_FEATURE] : []),
1546
1596
  // ADR-154: only the sign-in box writes cloud credentials.
1547
1597
  ...(signInBoxMode()
1548
1598
  ? [
@@ -3058,7 +3108,7 @@ async function connect(): Promise<void> {
3058
3108
  }
3059
3109
 
3060
3110
  const frame = parseCloudFrame(data);
3061
- if (!frame) return;
3111
+ if (!frame) { replyToInvalidCustomConnectionFrame(socket, data); return; }
3062
3112
  if (stopping && rejectFrameDuringShutdown(socket, frame)) return;
3063
3113
  if (signInBoxMode() && rejectFrameOnSignInBox(socket, frame)) return;
3064
3114
  if (!onOwnCredentials() && rejectCredentialFrameOffOwnCredentials(socket, frame)) return;
@@ -3609,6 +3659,21 @@ async function connect(): Promise<void> {
3609
3659
  );
3610
3660
  break;
3611
3661
  }
3662
+ case "engine.connect.custom.discover": {
3663
+ if (!signInBoxMode()) { send(socket, { kind: "engine.connect.custom.discovered", opId: frame.opId, result: { ok: false, reason: "unsupported" } }); break; }
3664
+ runCustomConnectionOperation(signal => discoverCustomConnection(frame, { signal }).catch(() => ({ ok: false as const, reason: "upstream-error" as const })).then(result => send(socket, { kind: "engine.connect.custom.discovered", opId: frame.opId, result })));
3665
+ break;
3666
+ }
3667
+ case "engine.connect.custom.manual-save": {
3668
+ if (!signInBoxMode()) { send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result: { ok: false, reason: "unsupported" } }); break; }
3669
+ runCustomConnectionOperation(signal => saveCustomConnectionManual(frame, configuredCredentialStore(), { signal }).catch(() => ({ ok: false as const, reason: "upstream-error" as const })).then(result => send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result })));
3670
+ break;
3671
+ }
3672
+ case "engine.connect.custom.save": {
3673
+ if (!signInBoxMode()) { send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result: { ok: false, reason: "unsupported" } }); break; }
3674
+ runCustomConnectionOperation(signal => saveCustomConnection(frame, configuredCredentialStore(), { signal }).catch(() => ({ ok: false as const, reason: "upstream-error" as const })).then(result => send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result })));
3675
+ break;
3676
+ }
3612
3677
  case "mcp.capture.probe":
3613
3678
  case "mcp.capture.complete": {
3614
3679
  // ADR-154 §11: only the sign-in box runs an MCP handshake as a
@@ -4831,6 +4896,24 @@ const PICKER_FAILURE_TEXT: Record<GitHubRepositoryAccessErrorCode, string> = {
4831
4896
  invalid_response: "GitHub answered with something unexpected",
4832
4897
  };
4833
4898
 
4899
+ function replyToInvalidCustomConnectionFrame(socket: WebSocket, data: RawData): void {
4900
+ let value: unknown;
4901
+ try { value = JSON.parse(rawDataToBuffer(data).toString("utf8")); } catch { return; }
4902
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
4903
+ const frame = value as Record<string, unknown>;
4904
+ if (!connectionOpId(frame.opId)) return;
4905
+ if (frame.kind === "engine.connect.custom.discover") {
4906
+ send(socket, { kind: "engine.connect.custom.discovered", opId: frame.opId, result: { ok: false, reason: "unsupported" } });
4907
+ } else if (frame.kind === "engine.connect.custom.save" || frame.kind === "engine.connect.custom.manual-save") {
4908
+ const settings = parseCustomConnectionMaterial(frame.settings);
4909
+ const flow = parseCaptureFlowWire(frame.flow);
4910
+ const supported = settings && (frame.kind !== "engine.connect.custom.manual-save" || (settings.providerType === "custom" && settings.internal === true)) && parseCustomConnectionEnabled(frame.enabled) && flow &&
4911
+ flow.location.class === "engine" && flow.location.engineKind === settings.engine && flow.brokerParameter === null;
4912
+ send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId,
4913
+ result: { ok: false, reason: supported ? "invalid-models" : "unsupported" } });
4914
+ }
4915
+ }
4916
+
4834
4917
  function parseCloudFrame(data: RawData): CloudToHost | null {
4835
4918
  let parsed: unknown;
4836
4919
  try {
@@ -5092,6 +5175,9 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
5092
5175
  // ADR-154: the v1 lease, credential and task env replica frames, parsed
5093
5176
  // to their exact shapes here; only a runner on cloud credentials acts on
5094
5177
  // them.
5178
+ if (frame.kind === "engine.connect.custom.discover") return parseEngineConnectCustomDiscoverFrame(frame);
5179
+ if (frame.kind === "engine.connect.custom.manual-save") return parseEngineConnectCustomManualSaveFrame(frame);
5180
+ if (frame.kind === "engine.connect.custom.save") return parseEngineConnectCustomSaveFrame(frame);
5095
5181
  if (frame.kind === "lease.grant.v1") return parseLeaseGrantV1Frame(frame);
5096
5182
  if (frame.kind === "lease.unavailable") return parseLeaseUnavailableFrame(frame);
5097
5183
  if (frame.kind === "lease.stale") return parseLeaseStaleFrame(frame);
@@ -5213,6 +5299,13 @@ function rejectFrameDuringShutdown(
5213
5299
  ): boolean {
5214
5300
  const error = "managed host is restarting; retry the operation";
5215
5301
  switch (frame.kind) {
5302
+ case "engine.connect.custom.discover":
5303
+ send(socket, { kind: "engine.connect.custom.discovered", opId: frame.opId, result: { ok: false, reason: "unreachable" } });
5304
+ return true;
5305
+ case "engine.connect.custom.manual-save":
5306
+ case "engine.connect.custom.save":
5307
+ send(socket, { kind: "engine.connect.custom.saved", opId: frame.opId, result: { ok: false, reason: "unreachable" } });
5308
+ return true;
5216
5309
  case "command":
5217
5310
  send(socket, {
5218
5311
  kind: "result",
@@ -5626,6 +5719,12 @@ function expectTaskAgents(value: unknown): TaskAgent[] {
5626
5719
  ) {
5627
5720
  out.accountId = row.accountId;
5628
5721
  }
5722
+ if (row.accountBinding !== undefined) {
5723
+ if (row.accountBinding !== "required" && row.accountBinding !== "preferred") throw new Error("invalid command args: account binding");
5724
+ if (row.accountBinding === "required" && row.kind !== "claude" && row.kind !== "codex") throw new Error("invalid command args: unsupported required account engine");
5725
+ if (row.accountBinding === "required" && !out.accountId) throw new Error("invalid command args: required account missing");
5726
+ out.accountBinding = row.accountBinding;
5727
+ }
5629
5728
  if (row.uaiProvider === "anthropic" || row.uaiProvider === "openai") {
5630
5729
  out.uaiProvider = row.uaiProvider;
5631
5730
  }
@@ -6107,6 +6206,7 @@ function reconnectDelay(): number {
6107
6206
  function requestShutdown(signal: NodeJS.Signals): void {
6108
6207
  console.log(`[host-agent] ${signal} received; shutting down`);
6109
6208
  stopping = true;
6209
+ openRouterBudgets.stop();
6110
6210
  // Cancel human-paced operations without invalidating this socket's exact
6111
6211
  // generation: already-admitted one-shot config writes may still ack while
6112
6212
  // the ordinary in-flight drain completes.
@@ -6119,6 +6219,7 @@ function requestShutdown(signal: NodeJS.Signals): void {
6119
6219
  // it). Exit waits for it.
6120
6220
  void cloudCredentials
6121
6221
  .shutdown()
6222
+ .then(() => openRouterBudgets.drain())
6122
6223
  .catch((error: unknown) =>
6123
6224
  console.warn(`[credentials] shutdown failed: ${error instanceof Error ? error.message : String(error)}`),
6124
6225
  )
package/src/protocol.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export * from "./custom-connections";
2
+ import type { AccountBinding, EngineConnectCustomDiscoverFrame, EngineConnectCustomDiscoveredFrame, EngineConnectCustomSaveFrame, EngineConnectCustomManualSaveFrame, EngineConnectCustomSavedFrame, OpenRouterBudgetFrame } from "./custom-connections";
1
3
  import type { TaskSecretApplied, TaskSecretStagingPut, TaskSecretStagingAck } from "./task-secrets";
2
4
  import {
3
5
  parseCaptureFlowWire,
@@ -3154,6 +3156,7 @@ export interface TaskAgent {
3154
3156
  effort?: string;
3155
3157
  /** ADR-157 addendum §7: preferred cloud-leased AI account. */
3156
3158
  accountId?: string;
3159
+ accountBinding?: AccountBinding;
3157
3160
  defaultPrompt?: string;
3158
3161
  initialPrompt?: string;
3159
3162
  /** ADR-046 reference material resolved from the agent's persona + team.
@@ -4068,6 +4071,10 @@ export function isHostConfigAckFrame(
4068
4071
  * changes. The cloud caches it per hostId to drive the task-creation pickers.
4069
4072
  */
4070
4073
  export interface HostCapabilities {
4074
+ customConnections?: boolean;
4075
+ engineConnectCustom?: boolean;
4076
+ engineConnectCustomManual?: boolean;
4077
+ internalConnections?: boolean;
4071
4078
  /** Filesystem capacity, refreshed each minute. Null means unmeasurable
4072
4079
  * (for example Docker in a VM); available excludes root-reserved blocks. */
4073
4080
  disk?: {
@@ -4904,6 +4911,9 @@ export interface TaskMachineRetireAckFrame {
4904
4911
  }
4905
4912
 
4906
4913
  export type CloudToHost =
4914
+ | EngineConnectCustomDiscoverFrame
4915
+ | EngineConnectCustomSaveFrame
4916
+ | EngineConnectCustomManualSaveFrame
4907
4917
  | {
4908
4918
  kind: "command";
4909
4919
  commandId: string;
@@ -5083,6 +5093,9 @@ export interface AssignmentDelivery {
5083
5093
  recipientGeneration: string | null;
5084
5094
  }
5085
5095
  export interface AssignmentTurnEvidence {
5096
+ credentialId?: string | null;
5097
+ credentialRevision?: string | null;
5098
+ connection?: boolean | null;
5086
5099
  leadRunId?: string | null;
5087
5100
  turnId: string;
5088
5101
  assignmentId: string | null;
@@ -5156,6 +5169,9 @@ export type McpOp =
5156
5169
  | { kind: "disconnect"; connectionId: string };
5157
5170
 
5158
5171
  export type HostToCloud =
5172
+ | EngineConnectCustomDiscoveredFrame
5173
+ | EngineConnectCustomSavedFrame
5174
+ | OpenRouterBudgetFrame
5159
5175
  // ADR-103: `bootId` scopes event seqs to one host process lifetime — the
5160
5176
  // cloud's replay watermark only applies within a matching boot. Optional
5161
5177
  // for pre-103 hosts (which also never send seq'd events).
@@ -5358,6 +5374,8 @@ export type HostEvent =
5358
5374
  * `credentialId` is the leased AI account the turn ran on (host
5359
5375
  * 0.9.167+); older runners omit it and the cloud stores null. */
5360
5376
  usage?: {
5377
+ connection?: boolean;
5378
+ credentialRevision?: string;
5361
5379
  model?: string;
5362
5380
  inputTokens?: number;
5363
5381
  outputTokens?: number;