@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.
- package/lib/agents/cursor.ts +6 -1
- package/lib/agents/registry.ts +20 -1
- package/lib/engine-accounts.ts +60 -0
- package/package.json +1 -1
- package/src/main.ts +9 -2
- package/src/protocol.ts +2 -1
package/lib/agents/cursor.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/lib/agents/registry.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/lib/engine-accounts.ts
CHANGED
|
@@ -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
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 {
|
|
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
|
-
|
|
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;
|