@runuai/host 0.9.98 → 0.9.100

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.
@@ -0,0 +1,7 @@
1
+ -- ADR-131 Decision 3: engine accounts carry a scope — 'host' (the owner's,
2
+ -- today's rows) or 'org:<orgId>' (a workspace's account on a shared host).
3
+ -- Tasks resolve accounts by their scope; org scopes never see the default
4
+ -- slot or the host's rows.
5
+ ALTER TABLE `host_engine_accounts` ADD `scope` text NOT NULL DEFAULT 'host';--> statement-breakpoint
6
+ CREATE INDEX `host_engine_accounts_scope_kind_idx` ON `host_engine_accounts` (`scope`,`kind`);--> statement-breakpoint
7
+ ALTER TABLE `uai_host_tasks` ADD `engine_account_scope` text;
@@ -120,6 +120,13 @@
120
120
  "when": 1786406402000,
121
121
  "tag": "0016_task_environment",
122
122
  "breakpoints": true
123
+ },
124
+ {
125
+ "idx": 17,
126
+ "version": "6",
127
+ "when": 1788820000000,
128
+ "tag": "0017_engine_account_scope",
129
+ "breakpoints": true
123
130
  }
124
131
  ]
125
132
  }
package/db/schema.ts CHANGED
@@ -37,6 +37,10 @@ export const hostTasks = sqliteTable(
37
37
  ownerEmail: text("owner_email"),
38
38
  ownerName: text("owner_name"),
39
39
  projectSlugs: text("project_slugs").notNull().default("[]"),
40
+ // ADR-131 D3: which engine-account scope this task resolves credentials
41
+ // from ("org:<orgId>" on a shared host; null = "host"). Persisted so a
42
+ // host restart re-provisions the same scope before the cloud re-sends it.
43
+ engineAccountScope: text("engine_account_scope"),
40
44
  lockedAt: integer("locked_at", { mode: "number" }),
41
45
  startedAt: integer("started_at", { mode: "number" }),
42
46
  endedAt: integer("ended_at", { mode: "number" }),
@@ -271,6 +275,9 @@ export const engineAccounts = sqliteTable("host_engine_accounts", {
271
275
  authKind: text("auth_kind").notNull(), // "env" | "config-dir"
272
276
  secretEnc: text("secret_enc"), // sealed JSON (env kind); null for config-dir
273
277
  configDir: text("config_dir"), // host dir (config-dir kind); null for env
278
+ // ADR-131 D3: "host" (owner-level, default) or "org:<orgId>" (a workspace's
279
+ // account on a shared host). Resolution is by exact scope.
280
+ scope: text("scope").notNull().default("host"),
274
281
  createdAt: integer("created_at", { mode: "number" }).notNull(),
275
282
  });
276
283
 
@@ -22,6 +22,10 @@
22
22
  * caps reset over time).
23
23
  */
24
24
 
25
+ import {
26
+ HOST_ACCOUNT_SCOPE,
27
+ isEngineAccountScope,
28
+ } from "../src/protocol";
25
29
  import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
26
30
  import { homedir } from "node:os";
27
31
  import { join } from "node:path";
@@ -85,6 +89,8 @@ export interface EngineAccountInfo {
85
89
  label: string;
86
90
  authKind: AccountAuthKind;
87
91
  isDefault: boolean;
92
+ /** ADR-131 D3: "host" or "org:<orgId>". */
93
+ scope: string;
88
94
  }
89
95
 
90
96
  /** A fully-resolved account, ready to inject / provision (host-trusted; the
@@ -94,6 +100,8 @@ export interface ResolvedEngineAccount {
94
100
  kind: string;
95
101
  label: string;
96
102
  isDefault: boolean;
103
+ /** ADR-131 D3: "host" or "org:<orgId>". */
104
+ scope: string;
97
105
  authKind: AccountAuthKind;
98
106
  /** Env merged into the agent's docker exec. Empty for the default env
99
107
  * account (the ambient `.env.local` credential is account #1). */
@@ -141,6 +149,27 @@ function unpackSecret(enc: string): string {
141
149
  return openAesGcm(ct, nonce);
142
150
  }
143
151
 
152
+ // ---------------------------------------------------------------------------
153
+ // Scopes (ADR-131 Decision 3).
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /**
157
+ * Every account has a scope. `host` is the owner's: the default slot plus
158
+ * every extra account connected before scopes existed (and every one an
159
+ * owner connects on a BYO/dedicated host). `org:<orgId>` is a workspace's
160
+ * account on a shared host: resolved ONLY for that workspace's tasks, and
161
+ * never joined by the default slot — a tenant sees exactly what it connected.
162
+ */
163
+ export { HOST_ACCOUNT_SCOPE, isEngineAccountScope };
164
+
165
+ /** A row's scope (legacy rows read as `host`) matches the requested one. */
166
+ export function accountInScope(
167
+ rowScope: string | null | undefined,
168
+ scope: string,
169
+ ): boolean {
170
+ return (rowScope ?? HOST_ACCOUNT_SCOPE) === scope;
171
+ }
172
+
144
173
  // ---------------------------------------------------------------------------
145
174
  // Resolution.
146
175
  // ---------------------------------------------------------------------------
@@ -177,6 +206,7 @@ function synthesizeDefault(
177
206
  label: "Default",
178
207
  isDefault: true,
179
208
  authKind: "env",
209
+ scope: HOST_ACCOUNT_SCOPE,
180
210
  execEnv: {},
181
211
  };
182
212
  }
@@ -187,6 +217,7 @@ function synthesizeDefault(
187
217
  label: "Default",
188
218
  isDefault: true,
189
219
  authKind: "config-dir",
220
+ scope: HOST_ACCOUNT_SCOPE,
190
221
  execEnv: d.selectVar ? { [d.selectVar]: containerDir } : {},
191
222
  configDir: {
192
223
  hostDir: (d.defaultHostDir as (o: string) => string)(s.ownerHome()),
@@ -196,14 +227,22 @@ function synthesizeDefault(
196
227
  }
197
228
 
198
229
  /** DB extras for a kind, resolved into injectable form. */
199
- function resolveExtras(kind: string, s: AccountSeams): ResolvedEngineAccount[] {
230
+ function resolveExtras(
231
+ kind: string,
232
+ s: AccountSeams,
233
+ scope: string,
234
+ ): ResolvedEngineAccount[] {
200
235
  const d = KINDS[kind];
201
236
  if (!d) return [];
202
237
  const rows = getDb()
203
238
  .select()
204
239
  .from(schema.engineAccounts)
205
240
  .all()
206
- .filter((r) => r.kind === kind);
241
+ .filter(
242
+ (r) =>
243
+ r.kind === kind &&
244
+ accountInScope((r as { scope?: string | null }).scope, scope),
245
+ );
207
246
  const out: ResolvedEngineAccount[] = [];
208
247
  for (const row of rows) {
209
248
  if (row.authKind === "env") {
@@ -226,6 +265,7 @@ function resolveExtras(kind: string, s: AccountSeams): ResolvedEngineAccount[] {
226
265
  label: row.label,
227
266
  isDefault: false,
228
267
  authKind: "env",
268
+ scope,
229
269
  execEnv,
230
270
  });
231
271
  } else if (row.authKind === "config-dir" && row.configDir && d.selectVar) {
@@ -236,6 +276,7 @@ function resolveExtras(kind: string, s: AccountSeams): ResolvedEngineAccount[] {
236
276
  label: row.label,
237
277
  isDefault: false,
238
278
  authKind: "config-dir",
279
+ scope,
239
280
  execEnv: { [d.selectVar]: containerDir },
240
281
  configDir: { hostDir: row.configDir, containerDir },
241
282
  });
@@ -248,12 +289,16 @@ function resolveExtras(kind: string, s: AccountSeams): ResolvedEngineAccount[] {
248
289
  export function resolveEngineAccounts(
249
290
  kind: string,
250
291
  seams: Partial<AccountSeams> = {},
292
+ scope: string = HOST_ACCOUNT_SCOPE,
251
293
  ): ResolvedEngineAccount[] {
252
294
  const s = withDefaults(seams);
253
295
  const accounts: ResolvedEngineAccount[] = [];
254
- const def = synthesizeDefault(kind, s);
255
- if (def) accounts.push(def);
256
- accounts.push(...resolveExtras(kind, s));
296
+ // The default slot is the OWNER's credential: it joins the host scope only.
297
+ if (scope === HOST_ACCOUNT_SCOPE) {
298
+ const def = synthesizeDefault(kind, s);
299
+ if (def) accounts.push(def);
300
+ }
301
+ accounts.push(...resolveExtras(kind, s, scope));
257
302
  return accounts;
258
303
  }
259
304
 
@@ -261,22 +306,47 @@ export function resolveEngineAccounts(
261
306
  export function listEngineAccounts(
262
307
  kind: string,
263
308
  seams: Partial<AccountSeams> = {},
309
+ scope: string = HOST_ACCOUNT_SCOPE,
264
310
  ): EngineAccountInfo[] {
265
- return resolveEngineAccounts(kind, seams).map((a) => ({
311
+ return resolveEngineAccounts(kind, seams, scope).map((a) => ({
266
312
  id: a.id,
267
313
  kind: a.kind,
268
314
  label: a.label,
269
315
  authKind: a.authKind,
270
316
  isDefault: a.isDefault,
317
+ scope: a.scope,
271
318
  }));
272
319
  }
273
320
 
321
+ /** Every scope holding at least one stored account for the kind, plus
322
+ * `host` (which may consist of the default slot alone). */
323
+ export function engineAccountScopes(kind: string): string[] {
324
+ const scopes = new Set<string>([HOST_ACCOUNT_SCOPE]);
325
+ for (const row of getDb().select().from(schema.engineAccounts).all()) {
326
+ if (row.kind !== kind) continue;
327
+ scopes.add((row as { scope?: string | null }).scope ?? HOST_ACCOUNT_SCOPE);
328
+ }
329
+ return [...scopes];
330
+ }
331
+
332
+ /** Account list across ALL scopes — the capabilities advertisement. The
333
+ * cloud filters per caller; labels only, never credentials. */
334
+ export function listEngineAccountsAllScopes(
335
+ kind: string,
336
+ seams: Partial<AccountSeams> = {},
337
+ ): EngineAccountInfo[] {
338
+ return engineAccountScopes(kind).flatMap((scope) =>
339
+ listEngineAccounts(kind, seams, scope),
340
+ );
341
+ }
342
+
274
343
  /** How many accounts a kind has right now (default + extras). */
275
344
  export function engineAccountCount(
276
345
  kind: string,
277
346
  seams: Partial<AccountSeams> = {},
347
+ scope: string = HOST_ACCOUNT_SCOPE,
278
348
  ): number {
279
- return resolveEngineAccounts(kind, seams).length;
349
+ return resolveEngineAccounts(kind, seams, scope).length;
280
350
  }
281
351
 
282
352
  // ---------------------------------------------------------------------------
@@ -339,9 +409,19 @@ export function selectAccount(
339
409
  /** Pick an account for a kind (default + extras), applying rotation state. */
340
410
  export function pickEngineAccount(
341
411
  kind: string,
342
- opts: { exclude?: Set<string>; seams?: Partial<AccountSeams>; now?: number } = {},
412
+ opts: {
413
+ exclude?: Set<string>;
414
+ seams?: Partial<AccountSeams>;
415
+ now?: number;
416
+ /** ADR-131 D3: the task's scope; default = the host's own accounts. */
417
+ scope?: string;
418
+ } = {},
343
419
  ): ResolvedEngineAccount | null {
344
- const accounts = resolveEngineAccounts(kind, opts.seams ?? {});
420
+ const accounts = resolveEngineAccounts(
421
+ kind,
422
+ opts.seams ?? {},
423
+ opts.scope ?? HOST_ACCOUNT_SCOPE,
424
+ );
345
425
  return selectAccount(accounts, { exclude: opts.exclude, now: opts.now });
346
426
  }
347
427
 
@@ -495,6 +575,7 @@ export async function provisionEngineAccounts(
495
575
  kinds: Iterable<string>,
496
576
  seams: Partial<AccountSeams> = {},
497
577
  environment?: Pick<TaskEnvironmentHandle, "exec" | "copy">,
578
+ scope: string = HOST_ACCOUNT_SCOPE,
498
579
  ): Promise<ProvisionEngineAccountsResult> {
499
580
  const s = withDefaults(seams);
500
581
  const seen = new Set<string>();
@@ -504,7 +585,7 @@ export async function provisionEngineAccounts(
504
585
  for (const kind of kinds) {
505
586
  if (seen.has(kind)) continue;
506
587
  seen.add(kind);
507
- for (const account of resolveEngineAccounts(kind, s)) {
588
+ for (const account of resolveEngineAccounts(kind, s, scope)) {
508
589
  if (account.isDefault || !account.configDir) continue;
509
590
  if (!s.fileExists(account.configDir.hostDir)) {
510
591
  failed += 1;
@@ -554,8 +635,12 @@ export function addEngineAccount(
554
635
  label: string,
555
636
  creds: { apiKey?: string; token?: string },
556
637
  seams: Partial<AccountSeams> = {},
638
+ scope: string = HOST_ACCOUNT_SCOPE,
557
639
  ): AddAccountResult {
558
640
  const s = withDefaults(seams);
641
+ if (!isEngineAccountScope(scope)) {
642
+ return { ok: false, message: "The account scope is invalid." };
643
+ }
559
644
  const trimmedLabel = label.trim();
560
645
  if (!trimmedLabel) return { ok: false, message: "Give the account a label." };
561
646
  const secret = (creds.token ?? creds.apiKey ?? "").trim();
@@ -581,6 +666,7 @@ export function addEngineAccount(
581
666
  secretEnc: packSecret(JSON.stringify({ [envVar]: secret })),
582
667
  configDir: null,
583
668
  createdAt: now,
669
+ scope,
584
670
  })
585
671
  .run();
586
672
  return { ok: true, message: `Added Claude account "${trimmedLabel}".`, id };
@@ -621,6 +707,7 @@ export function addEngineAccount(
621
707
  secretEnc: null,
622
708
  configDir: hostDir,
623
709
  createdAt: now,
710
+ scope,
624
711
  })
625
712
  .run();
626
713
  return { ok: true, message: `Added Codex account "${trimmedLabel}".`, id };
@@ -642,7 +729,11 @@ export function addCodexAccountFromAuthJson(
642
729
  label: string,
643
730
  authJson: string,
644
731
  seams: Partial<AccountSeams> = {},
732
+ scope: string = HOST_ACCOUNT_SCOPE,
645
733
  ): AddAccountResult {
734
+ if (!isEngineAccountScope(scope)) {
735
+ return { ok: false, message: "The account scope is invalid." };
736
+ }
646
737
  const s = withDefaults(seams);
647
738
  const trimmedLabel = label.trim();
648
739
  if (!trimmedLabel) return { ok: false, message: "Give the account a label." };
@@ -690,6 +781,7 @@ export function addCodexAccountFromAuthJson(
690
781
  secretEnc: null,
691
782
  configDir: hostDir,
692
783
  createdAt: Date.now(),
784
+ scope,
693
785
  })
694
786
  .run();
695
787
  return { ok: true, message: `Added Codex account "${trimmedLabel}".`, id };
@@ -698,7 +790,19 @@ export function addCodexAccountFromAuthJson(
698
790
  /** Remove an EXTRA account (idempotent). The synthesized default can't be
699
791
  * removed here (use the engine disconnect flow for the legacy slot). The
700
792
  * isolated host config dir of a config-dir account is deleted too. */
701
- export function removeEngineAccount(id: string): { ok: boolean; message: string } {
793
+ /** Whether a caller acting in `scope` may remove `row`. `undefined` is the
794
+ * host's own local UI, which may remove anything. */
795
+ export function accountRemovableInScope(
796
+ row: { scope?: string | null },
797
+ scope: string | undefined,
798
+ ): boolean {
799
+ return scope === undefined || accountInScope(row.scope, scope);
800
+ }
801
+
802
+ export function removeEngineAccount(
803
+ id: string,
804
+ scope?: string,
805
+ ): { ok: boolean; message: string } {
702
806
  if (id.startsWith("default:")) {
703
807
  return {
704
808
  ok: false,
@@ -711,6 +815,11 @@ export function removeEngineAccount(id: string): { ok: boolean; message: string
711
815
  .where(eq(schema.engineAccounts.id, id))
712
816
  .get();
713
817
  if (!row) return { ok: true, message: "Account already removed." };
818
+ // ADR-131 D3: a workspace removes only its own accounts — never the host's
819
+ // and never another workspace's, whatever id it names.
820
+ if (!accountRemovableInScope(row, scope)) {
821
+ return { ok: false, message: "That account belongs to a different scope." };
822
+ }
714
823
  getDb().delete(schema.engineAccounts).where(eq(schema.engineAccounts.id, id)).run();
715
824
  if (row.authKind === "config-dir" && row.configDir) {
716
825
  try {
@@ -10,7 +10,9 @@
10
10
  */
11
11
 
12
12
  import {
13
+ HOST_ACCOUNT_SCOPE,
13
14
  isEngineAccountLabel,
15
+ isEngineAccountScope,
14
16
  isHostOpId,
15
17
  MAX_ENGINE_CONNECT_LINE_CHARS,
16
18
  MAX_ENGINE_CONNECT_SECRET_CHARS,
@@ -57,8 +59,10 @@ export class EngineConnectManager {
57
59
  secret: string | undefined,
58
60
  label: string | undefined,
59
61
  emit: EmitEngineConnectEvent,
62
+ scope: string | undefined = undefined,
60
63
  ): boolean {
61
64
  if (!isHostOpId(opId) || this.operations.has(opId)) return false;
65
+ if (scope !== undefined && !isEngineAccountScope(scope)) return false;
62
66
  if (
63
67
  secret !== undefined &&
64
68
  (secret.length < 1 || secret.length > MAX_ENGINE_CONNECT_SECRET_CHARS)
@@ -93,7 +97,7 @@ export class EngineConnectManager {
93
97
  this.finish(operation, false, "Engine connect timed out.");
94
98
  }, CONNECT_TIMEOUT_MS);
95
99
  operation.timer.unref?.();
96
- void this.run(operation, secret, label).catch((error: unknown) => {
100
+ void this.run(operation, secret, label, scope).catch((error: unknown) => {
97
101
  this.finish(
98
102
  operation,
99
103
  false,
@@ -118,6 +122,7 @@ export class EngineConnectManager {
118
122
  operation: ConnectOperation,
119
123
  secret: string | undefined,
120
124
  label: string | undefined,
125
+ scope: string | undefined,
121
126
  ): Promise<void> {
122
127
  const { engine } = operation;
123
128
  const emitLine = (line: string): void => {
@@ -153,11 +158,28 @@ export class EngineConnectManager {
153
158
 
154
159
  // ADR-116 labeled extra account — the sink validates kind support.
155
160
  if (label !== undefined && secret !== undefined) {
156
- const added = addEngineAccount(engine, label, { token: secret });
161
+ const added = addEngineAccount(
162
+ engine,
163
+ label,
164
+ { token: secret },
165
+ {},
166
+ scope ?? HOST_ACCOUNT_SCOPE,
167
+ );
157
168
  if (added.ok) this.afterMutation(engine, false);
158
169
  this.finish(operation, added.ok, added.message);
159
170
  return;
160
171
  }
172
+ // ADR-131 D3: everything below writes the host's DEFAULT slot or re-detects
173
+ // a host-side login — the owner's credential, never a workspace's. A
174
+ // tenant scope must not reach it.
175
+ if (scope !== undefined && scope !== HOST_ACCOUNT_SCOPE) {
176
+ this.finish(
177
+ operation,
178
+ false,
179
+ "Workspace accounts need a label and a pasted secret; the host's own sign-in is not available to workspaces.",
180
+ );
181
+ return;
182
+ }
161
183
 
162
184
  const result = await connectEngine(
163
185
  engine as EngineKind,
@@ -27,7 +27,9 @@ import { homedir, tmpdir } from "node:os";
27
27
  import { basename, dirname, join, resolve } from "node:path";
28
28
 
29
29
  import {
30
+ HOST_ACCOUNT_SCOPE,
30
31
  isEngineAccountLabel,
32
+ isEngineAccountScope,
31
33
  isHostOpId,
32
34
  MAX_ENGINE_LOGIN_QUERY_CHARS,
33
35
  MAX_ENGINE_LOGIN_TEXT_CHARS,
@@ -145,8 +147,16 @@ export interface EngineLoginSeams {
145
147
  persistClaudeToken(token: string): Promise<void>;
146
148
  persistCodexAuth(sourceFile: string): Promise<void>;
147
149
  /** ADR-116: labeled extra-account captures (never the default slots). */
148
- addClaudeAccount(label: string, token: string): { ok: boolean; message: string };
149
- addCodexAccount(label: string, authJson: string): { ok: boolean; message: string };
150
+ addClaudeAccount(
151
+ label: string,
152
+ token: string,
153
+ scope: string,
154
+ ): { ok: boolean; message: string };
155
+ addCodexAccount(
156
+ label: string,
157
+ authJson: string,
158
+ scope: string,
159
+ ): { ok: boolean; message: string };
150
160
  refreshEngineAccountAgents(engine: "claude"): Promise<void>;
151
161
  reinjectCodexRunningTasks(): Promise<void>;
152
162
  ownerHome(): string;
@@ -209,6 +219,8 @@ interface LoginOperation {
209
219
  /** ADR-116: when set, the captured credential lands in a labeled extra
210
220
  * account instead of the default slot. */
211
221
  accountLabel: string | null;
222
+ /** ADR-131 D3: the scope a labeled capture is stored under. */
223
+ accountScope: string;
212
224
  /** Diagnosis breadcrumb: has ANY output arrived after the one-time code
213
225
  * was written? Distinguishes "stdin never delivered" from "CLI answered
214
226
  * and we ignored it" without reading the PTY transcript. */
@@ -299,10 +311,10 @@ function defaultSeams(options: EngineLoginManagerOptions): EngineLoginSeams {
299
311
  },
300
312
  persistClaudeToken: (token) => persistClaudeTokenAtomic(token),
301
313
  persistCodexAuth: (sourceFile) => persistCodexAuthAtomic(sourceFile),
302
- addClaudeAccount: (label, token) =>
303
- addEngineAccount("claude", label, { token }),
304
- addCodexAccount: (label, authJson) =>
305
- addCodexAccountFromAuthJson(label, authJson),
314
+ addClaudeAccount: (label, token, scope) =>
315
+ addEngineAccount("claude", label, { token }, {}, scope),
316
+ addCodexAccount: (label, authJson, scope) =>
317
+ addCodexAccountFromAuthJson(label, authJson, {}, scope),
306
318
  refreshEngineAccountAgents: options.refreshEngineAccountAgents,
307
319
  reinjectCodexRunningTasks: () => reinjectCodexRunningTasks(),
308
320
  ownerHome,
@@ -370,8 +382,25 @@ export class EngineLoginManager {
370
382
  engine: EngineLoginKind,
371
383
  emit: EmitEngineLoginEvent,
372
384
  label?: string,
385
+ scope?: string,
373
386
  ): boolean {
374
387
  if (!isHostOpId(opId)) return false;
388
+ if (scope !== undefined && !isEngineAccountScope(scope)) {
389
+ safeEmit(
390
+ emit,
391
+ failedEvent(opId, engine, "start_failed", "The account scope is invalid."),
392
+ );
393
+ return false;
394
+ }
395
+ // A workspace's sign-in is always a labeled account: the default slot is
396
+ // the host owner's and must never be written on a tenant's behalf.
397
+ if (scope !== undefined && scope !== HOST_ACCOUNT_SCOPE && label === undefined) {
398
+ safeEmit(
399
+ emit,
400
+ failedEvent(opId, engine, "start_failed", "A workspace account needs a label."),
401
+ );
402
+ return false;
403
+ }
375
404
  if (label !== undefined && !isEngineAccountLabel(label)) {
376
405
  safeEmit(
377
406
  emit,
@@ -426,6 +455,7 @@ export class EngineLoginManager {
426
455
  pendingInput: null,
427
456
  callbackTarget: null,
428
457
  accountLabel: label ?? null,
458
+ accountScope: scope ?? HOST_ACCOUNT_SCOPE,
429
459
  postInputOutputSeen: false,
430
460
  };
431
461
  this.operations.set(opId, operation);
@@ -941,7 +971,11 @@ export class EngineLoginManager {
941
971
  // ADR-116: labeled capture — an extra account, not the default slot.
942
972
  // Extras are picked up at the next agent spawn / rotation, so no
943
973
  // running-agent refresh is needed (mirrors the local accounts API).
944
- const added = this.seams.addClaudeAccount(operation.accountLabel, token);
974
+ const added = this.seams.addClaudeAccount(
975
+ operation.accountLabel,
976
+ token,
977
+ operation.accountScope,
978
+ );
945
979
  terminal = added.ok
946
980
  ? {
947
981
  kind: "engine.login.event",
@@ -1011,7 +1045,11 @@ export class EngineLoginManager {
1011
1045
  join(tempDir, "codex", "auth.json"),
1012
1046
  "utf8",
1013
1047
  );
1014
- added = this.seams.addCodexAccount(operation.accountLabel, contents);
1048
+ added = this.seams.addCodexAccount(
1049
+ operation.accountLabel,
1050
+ contents,
1051
+ operation.accountScope,
1052
+ );
1015
1053
  } catch {
1016
1054
  added = {
1017
1055
  ok: false,