@runuai/host 0.9.100 → 0.9.102
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 +21 -2
- package/src/protocol.ts +22 -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 {
|
|
@@ -225,6 +229,7 @@ import {
|
|
|
225
229
|
type TaskDownInput,
|
|
226
230
|
type TaskLaunchInput,
|
|
227
231
|
type FilesOpInput,
|
|
232
|
+
engineAccountScopeFromWire,
|
|
228
233
|
} from "./protocol";
|
|
229
234
|
|
|
230
235
|
setContainerRuntimeOperationalCheck(() => containerRuntimeProblem() === null);
|
|
@@ -697,7 +702,7 @@ function engineCatalogCapabilities(): EngineCatalogCapability[] {
|
|
|
697
702
|
/** ADR-116: the labeled-accounts advertisement — bounded, labels-only. */
|
|
698
703
|
function engineAccountSummaries(): EngineAccountSummary[] {
|
|
699
704
|
const summaries: EngineAccountSummary[] = [];
|
|
700
|
-
for (const kind of ["claude", "codex", "opencode"] as const) {
|
|
705
|
+
for (const kind of ["claude", "codex", "grok", "cursor", "opencode"] as const) {
|
|
701
706
|
try {
|
|
702
707
|
// ADR-131 D3: every scope — the cloud filters per caller.
|
|
703
708
|
for (const account of listEngineAccountsAllScopes(kind)) {
|
|
@@ -744,6 +749,9 @@ function flushStoppedTaskStatuses(socket: WebSocket | null = ws): void {
|
|
|
744
749
|
});
|
|
745
750
|
}
|
|
746
751
|
|
|
752
|
+
// ADR-131 D3: a stored account (any scope) makes a kind advertisable even
|
|
753
|
+
// when the host's own credential is absent — see hasStoredEngineAccounts.
|
|
754
|
+
setAdvertisementFallback((kind) => hasStoredEngineAccounts(kind));
|
|
747
755
|
onRegistryChange(() => sendCapabilities());
|
|
748
756
|
onContainerRuntimeChange((next, previous) => {
|
|
749
757
|
if (stopping) return;
|
|
@@ -3198,6 +3206,9 @@ function expectChannelEnsureInput(
|
|
|
3198
3206
|
if (typeof input.globalContext === "string") {
|
|
3199
3207
|
out.globalContext = input.globalContext;
|
|
3200
3208
|
}
|
|
3209
|
+
// ADR-131 D3: same scope the task launched with (refreshChannel adopts it).
|
|
3210
|
+
const channelScope = engineAccountScopeFromWire(input.engineAccountScope);
|
|
3211
|
+
if (channelScope !== undefined) out.engineAccountScope = channelScope;
|
|
3201
3212
|
// ADR-083: secretary identity drives the role-specific transcript preamble.
|
|
3202
3213
|
// Both are optional for wire compatibility with older clouds.
|
|
3203
3214
|
const mode = parseChannelMode(input.mode);
|
|
@@ -3300,6 +3311,14 @@ function expectTaskCommandTask(value: unknown): TaskCommandTask {
|
|
|
3300
3311
|
) {
|
|
3301
3312
|
out.sharedFiles = row.sharedFiles;
|
|
3302
3313
|
}
|
|
3314
|
+
// ADR-131 D3: the workspace whose engine accounts this task resolves.
|
|
3315
|
+
// Dropping it here silently re-scoped every shared-host task to the host's
|
|
3316
|
+
// own credentials (live 2026-09-07) — see engineAccountScopeFromWire.
|
|
3317
|
+
const taskScope = engineAccountScopeFromWire(
|
|
3318
|
+
row.engineAccountScope,
|
|
3319
|
+
"task.engineAccountScope",
|
|
3320
|
+
);
|
|
3321
|
+
if (taskScope !== undefined) out.engineAccountScope = taskScope;
|
|
3303
3322
|
return out;
|
|
3304
3323
|
}
|
|
3305
3324
|
|
package/src/protocol.ts
CHANGED
|
@@ -121,6 +121,26 @@ export function isEngineAccountScope(value: unknown): value is string {
|
|
|
121
121
|
(typeof value === "string" && ORG_ACCOUNT_SCOPE_RE.test(value))
|
|
122
122
|
);
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* ADR-131 D3: read an optional `engineAccountScope` off a wire object.
|
|
127
|
+
* Returns the scope when present and well-formed, undefined when absent, and
|
|
128
|
+
* THROWS on a malformed value — a tenant scope must never degrade silently
|
|
129
|
+
* to the host's credentials. LIVE 2026-09-07: the host's task-up and
|
|
130
|
+
* channel-ensure validators rebuilt their inputs field by field and never
|
|
131
|
+
* copied this key, so every shared-host task ran in the HOST scope — Codex
|
|
132
|
+
* had no account there and looped "Reconnecting" while the cloud had sent
|
|
133
|
+
* `org:<id>` all along. Every `expect*Input` that carries a scope must call
|
|
134
|
+
* this; `host-agent/src/protocol.test.ts` pins the shape.
|
|
135
|
+
*/
|
|
136
|
+
export function engineAccountScopeFromWire(
|
|
137
|
+
value: unknown,
|
|
138
|
+
field = "engineAccountScope",
|
|
139
|
+
): string | undefined {
|
|
140
|
+
if (value === undefined || value === null) return undefined;
|
|
141
|
+
if (isEngineAccountScope(value)) return value;
|
|
142
|
+
throw new Error(`invalid ${field}`);
|
|
143
|
+
}
|
|
124
144
|
export const MAX_ENGINE_CONNECT_SECRET_CHARS = 8_192;
|
|
125
145
|
export const MAX_ENGINE_CONNECT_LINE_CHARS = 512;
|
|
126
146
|
export const MAX_ENGINE_CATALOG_ENTRIES = 8;
|
|
@@ -398,7 +418,8 @@ export interface EngineLoginCapability {
|
|
|
398
418
|
* Operator-chosen label + shape metadata only — never credentials. */
|
|
399
419
|
export interface EngineAccountSummary {
|
|
400
420
|
id: string;
|
|
401
|
-
|
|
421
|
+
/** grok/cursor since host 0.9.101 (API-key accounts, ADR-131 D3). */
|
|
422
|
+
kind: "claude" | "codex" | "grok" | "cursor" | "opencode";
|
|
402
423
|
label: string;
|
|
403
424
|
authKind: "env" | "config-dir";
|
|
404
425
|
isDefault: boolean;
|