@runuai/host 0.9.100 → 0.9.101

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.
@@ -192,7 +192,12 @@ export class CursorSession implements AgentSession {
192
192
  this.agentEnv = args.agentEnv ?? {};
193
193
  this.spawner =
194
194
  args.spawner ?? ((request) => spawnEnvironmentProcess(this.environment, request));
195
- this.apiKey = args.apiKey ?? process.env.CURSOR_API_KEY ?? "";
195
+ // An account-bound key (ADR-076/ADR-131 D3, injected through agentEnv)
196
+ // beats the host's own .env.local key — a workspace's Cursor account must
197
+ // never fall through to the operator's, and on a shared host the ambient
198
+ // key is usually absent anyway.
199
+ this.apiKey =
200
+ args.apiKey ?? this.agentEnv.CURSOR_API_KEY ?? process.env.CURSOR_API_KEY ?? "";
196
201
  }
197
202
 
198
203
  onEvent(handler: AgentEventHandler): () => void {
@@ -72,6 +72,21 @@ const adapters = new Map<string, RegisteredAdapter>();
72
72
  /** Listeners fired after any registry mutation (manual re-advertise hook). */
73
73
  const changeListeners = new Set<() => void>();
74
74
 
75
+ /**
76
+ * ADR-131 D3: a second source of "usable on this host" beyond the adapter's
77
+ * own `available()`, which only sees the host's default credential. The host
78
+ * agent installs the engine-account store here so a kind with a stored
79
+ * (workspace- or host-scoped) account is advertised even when the owner never
80
+ * signed in to it. Installed by main.ts; absent in unit tests.
81
+ */
82
+ let advertisementFallback: (kind: string) => boolean = () => false;
83
+
84
+ export function setAdvertisementFallback(
85
+ fallback: ((kind: string) => boolean) | null,
86
+ ): void {
87
+ advertisementFallback = fallback ?? (() => false);
88
+ }
89
+
75
90
  /**
76
91
  * Register (or replace) an adapter for a kind. Called at module load by the
77
92
  * adapter modules. Replacing is allowed so a later import wins deterministically.
@@ -107,7 +122,10 @@ export function supportsExecutionProfile(kind: string, profileId: string): boole
107
122
  */
108
123
  export function capabilities(): AgentKindCapability[] {
109
124
  return list()
110
- .filter((adapter) => adapter.available?.() ?? true)
125
+ .filter(
126
+ (adapter) =>
127
+ (adapter.available?.() ?? true) || advertisementFallback(adapter.kind),
128
+ )
111
129
  .map((adapter) => {
112
130
  const out: AgentKindCapability = {
113
131
  kind: adapter.kind,
@@ -143,4 +161,5 @@ export function onChange(listener: () => void): () => void {
143
161
  export function reset(): void {
144
162
  adapters.clear();
145
163
  changeListeners.clear();
164
+ advertisementFallback = () => false;
146
165
  }
@@ -64,6 +64,11 @@ interface KindDescriptor {
64
64
 
65
65
  const KINDS: Record<string, KindDescriptor> = {
66
66
  claude: { authKind: "env" },
67
+ // API-key engines: an extra account is one env var. The default slot stays
68
+ // the host's own `.env.local` key (the adapters read process.env), so these
69
+ // never synthesize a default — `defaultAvailable` is false for them.
70
+ grok: { authKind: "env" },
71
+ cursor: { authKind: "env" },
67
72
  codex: {
68
73
  authKind: "config-dir",
69
74
  defaultHostDir: (owner) => join(owner, ".codex"),
@@ -82,6 +87,39 @@ const KINDS: Record<string, KindDescriptor> = {
82
87
  },
83
88
  };
84
89
 
90
+ /** The env var an extra API-key account injects, per kind (the same vars the
91
+ * adapters read for the host's own key). Claude is special-cased in
92
+ * `addEngineAccount` (token vs. key); Codex writes an auth.json instead. */
93
+ const API_KEY_ENV_VARS: Record<string, string> = {
94
+ grok: "XAI_API_KEY",
95
+ cursor: "CURSOR_API_KEY",
96
+ };
97
+ const API_KEY_ENGINE_LABELS: Record<string, string> = {
98
+ grok: "Grok",
99
+ cursor: "Cursor",
100
+ };
101
+
102
+ /**
103
+ * Whether ANY stored account exists for `kind`, in any scope. The
104
+ * advertisement fallback (ADR-131 D3): an adapter's `available()` only sees
105
+ * the host's own credential, so a shared host whose owner never signed in to
106
+ * Codex hid the kind from every workspace that had connected its own account
107
+ * — the cloud then refused the crew with "host does not support agent kind".
108
+ * A stored account is a usable credential; the cloud scopes it per caller.
109
+ */
110
+ export function hasStoredEngineAccounts(kind: string): boolean {
111
+ if (!KINDS[kind]) return false;
112
+ try {
113
+ return getDb()
114
+ .select()
115
+ .from(schema.engineAccounts)
116
+ .all()
117
+ .some((row) => row.kind === kind);
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
85
123
  /** Public (secret-free) account descriptor for the host UI. */
86
124
  export interface EngineAccountInfo {
87
125
  id: string;
@@ -672,6 +710,28 @@ export function addEngineAccount(
672
710
  return { ok: true, message: `Added Claude account "${trimmedLabel}".`, id };
673
711
  }
674
712
 
713
+ const envKeyVar = API_KEY_ENV_VARS[kind];
714
+ if (envKeyVar) {
715
+ getDb()
716
+ .insert(schema.engineAccounts)
717
+ .values({
718
+ id,
719
+ kind,
720
+ label: trimmedLabel,
721
+ authKind: "env",
722
+ secretEnc: packSecret(JSON.stringify({ [envKeyVar]: secret })),
723
+ configDir: null,
724
+ createdAt: now,
725
+ scope,
726
+ })
727
+ .run();
728
+ return {
729
+ ok: true,
730
+ message: `Added ${API_KEY_ENGINE_LABELS[kind] ?? kind} account "${trimmedLabel}".`,
731
+ id,
732
+ };
733
+ }
734
+
675
735
  if (kind === "codex") {
676
736
  const d = KINDS.codex as KindDescriptor;
677
737
  const hostDir = (d.extraHostDir as (o: string, i: string) => string)(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.100",
3
+ "version": "0.9.101",
4
4
  "description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",
package/src/main.ts CHANGED
@@ -132,6 +132,7 @@ import { newId } from "../lib/ulid";
132
132
  import {
133
133
  capabilities as agentKindCapabilities,
134
134
  onChange as onRegistryChange,
135
+ setAdvertisementFallback,
135
136
  } from "../lib/agents/registry";
136
137
  import { canAdvertiseTypedSecretaryDispatch } from "../lib/agents/mode";
137
138
  import {
@@ -150,7 +151,10 @@ import {
150
151
  patchHostConfig,
151
152
  type HostConfigResult,
152
153
  } from "../lib/host-config";
153
- import { listEngineAccountsAllScopes } from "../lib/engine-accounts";
154
+ import {
155
+ hasStoredEngineAccounts,
156
+ listEngineAccountsAllScopes,
157
+ } from "../lib/engine-accounts";
154
158
  import { createEngineConnectManager } from "../lib/engine-connect";
155
159
  import { createEngineLoginManager } from "../lib/engine-login";
156
160
  import {
@@ -697,7 +701,7 @@ function engineCatalogCapabilities(): EngineCatalogCapability[] {
697
701
  /** ADR-116: the labeled-accounts advertisement — bounded, labels-only. */
698
702
  function engineAccountSummaries(): EngineAccountSummary[] {
699
703
  const summaries: EngineAccountSummary[] = [];
700
- for (const kind of ["claude", "codex", "opencode"] as const) {
704
+ for (const kind of ["claude", "codex", "grok", "cursor", "opencode"] as const) {
701
705
  try {
702
706
  // ADR-131 D3: every scope — the cloud filters per caller.
703
707
  for (const account of listEngineAccountsAllScopes(kind)) {
@@ -744,6 +748,9 @@ function flushStoppedTaskStatuses(socket: WebSocket | null = ws): void {
744
748
  });
745
749
  }
746
750
 
751
+ // ADR-131 D3: a stored account (any scope) makes a kind advertisable even
752
+ // when the host's own credential is absent — see hasStoredEngineAccounts.
753
+ setAdvertisementFallback((kind) => hasStoredEngineAccounts(kind));
747
754
  onRegistryChange(() => sendCapabilities());
748
755
  onContainerRuntimeChange((next, previous) => {
749
756
  if (stopping) return;
package/src/protocol.ts CHANGED
@@ -398,7 +398,8 @@ export interface EngineLoginCapability {
398
398
  * Operator-chosen label + shape metadata only — never credentials. */
399
399
  export interface EngineAccountSummary {
400
400
  id: string;
401
- kind: "claude" | "codex" | "opencode";
401
+ /** grok/cursor since host 0.9.101 (API-key accounts, ADR-131 D3). */
402
+ kind: "claude" | "codex" | "grok" | "cursor" | "opencode";
402
403
  label: string;
403
404
  authKind: "env" | "config-dir";
404
405
  isDefault: boolean;