@bridge4dev/runner 0.65.1 → 0.67.0

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.
@@ -1,6 +1,24 @@
1
1
  import { type CodexHome } from './codex-home.js';
2
2
  import { AppServerClient, type ServerRequest } from './codex-protocol.js';
3
3
  import { type AgentAdapter, type AgentQuestion, type AgentSession, type EffortOption, type SessionSpec } from './types.js';
4
+ /** One entry of `/`, as `writableRootsFor` needs it. */
5
+ export interface RootEntry {
6
+ path: string;
7
+ /** Where a link leads; null when it leads nowhere. */
8
+ realPath: string | null;
9
+ isDirectory: boolean;
10
+ }
11
+ /** Lists `/`; throws when it cannot. Replaced only by tests. */
12
+ export type RootLister = () => RootEntry[];
13
+ /**
14
+ * The top-level directories a widened sandbox may write to, or null when `/`
15
+ * could not be read.
16
+ *
17
+ * Links are followed to where they lead and counted once (`/bin` → `/usr/bin`
18
+ * adds nothing `/usr` does not); a directory inside another root is dropped for
19
+ * the same reason; files (a swap file) and dangling links are skipped.
20
+ */
21
+ export declare function topLevelWritableRoots(listRoot?: RootLister): string[] | null;
4
22
  export interface CodexAdapterDeps {
5
23
  /** Injected in tests to drive a scripted app-server. */
6
24
  spawnClient?: (options: {
@@ -28,11 +46,15 @@ export interface CodexAdapterDeps {
28
46
  repairHome?: () => CodexHome;
29
47
  /**
30
48
  * How this runner's CODEX_HOME is authenticated (`[codex] auth` in
31
- * config.toml). Carried here because every repair has to honour it — a
32
- * repair that defaults to `link` would undo an owner's decision to keep the
33
- * runner's credential separate.
49
+ * config.toml) – ONLY when the owner wrote it. Every repair honours it, and a
50
+ * repair that defaulted to `link` would undo both an owner's decision to keep
51
+ * the runner's credential separate and a switch to a saved login (#422 S4 item
52
+ * 2). Production leaves it unset: the daemon records the configured mode once
53
+ * (`configureCodexAuth`), and the home reads it from there.
34
54
  */
35
55
  authMode?: 'link' | 'own';
56
+ /** What `/` holds, for the widened sandbox roots – a test seam (`codex-auto-dev-null`). */
57
+ listRoot?: RootLister;
36
58
  /** #382: how often live helpers are re-checked against Codex – a test seam. */
37
59
  subagentReconcileMs?: number;
38
60
  /** #382: how long a just-started helper's `idle` is disbelieved – a test seam. */
@@ -1,9 +1,12 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import { AsyncQueue } from '../async-queue.js';
2
4
  import { log } from '../log.js';
3
5
  import { evaluateToolUse, maskSecrets, maskString, } from '../policy.js';
4
6
  import { memoryDeathSentence, sessionMemoryEnv, sessionMemoryPromptLine } from '../session-cage.js';
5
7
  import { RUNNER_VERSION } from '../version.js';
6
8
  import { repairCodexAuth } from './codex-home.js';
9
+ import { codexSessionAccount, noteCodexSessionAccount, noteCodexUsage, releaseCodexSessionAccount, } from '../codex-accounts.js';
7
10
  import { AppServerClient, asRecord, num, RpcError, RpcTimeoutError, str, } from './codex-protocol.js';
8
11
  import { truncate } from './claude.js';
9
12
  import { AgentTaskTray } from './agent-tasks.js';
@@ -87,11 +90,79 @@ const MODE_POLICY = {
87
90
  *
88
91
  * Read on every turn rather than captured at launch, because the setting can be
89
92
  * switched while the session runs.
93
+ *
94
+ * **Never `/` itself** (gotcha 541, plan `codex-auto-dev-null`). Codex mounts a
95
+ * writable root without devices, and `/` laid over its own `/dev` left
96
+ * `/dev/null` visible but unopenable: every `bash -lc` printed «Permission
97
+ * denied» and `git status` exited 128. The widened roots are the machine's real
98
+ * top-level directories instead, minus the kernel's own (`/dev`, `/proc`,
99
+ * `/sys`) – checked on codex-cli 0.154.0 through `command/exec`: `/dev/null`
100
+ * opens, `git status` passes, a write outside the project is allowed, and, unlike
101
+ * `/`, a list of directories leaves no empty `.git`/`.codex`/`.agents` behind
102
+ * (17.09.2026, on throwaway folders – never on this production host's `/`).
90
103
  */
91
- function writableRootsFor(spec) {
104
+ function writableRootsFor(spec, listRoot = listRootEntries) {
92
105
  if (spec.trustMode === 'STRICT')
93
106
  return [spec.cwd];
94
- return spec.gitPolicy?.agentAllowOutsideFolder === true ? ['/'] : [spec.cwd];
107
+ if (spec.gitPolicy?.agentAllowOutsideFolder !== true)
108
+ return [spec.cwd];
109
+ const roots = topLevelWritableRoots(listRoot);
110
+ if (roots === null || roots.length === 0) {
111
+ // Unknown means confined (ADR 0007, «polarity»): a step outside asks, as it
112
+ // did before the permission existed.
113
+ return [spec.cwd];
114
+ }
115
+ return roots;
116
+ }
117
+ /** The kernel's own file systems – never a place a sandbox should bind over. */
118
+ const NEVER_WRITABLE_ROOTS = ['/dev', '/proc', '/sys'];
119
+ function listRootEntries() {
120
+ // `withFileTypes`, so a top-level entry is judged by what `readdir` already
121
+ // knows. A `stat` per entry would reach INTO every mount point, and a dead
122
+ // network mount at the top level would then block the daemon's only thread on
123
+ // every turn (found by the independent check of S4). Only a symbolic link is
124
+ // resolved, and those are few (`/bin`, `/lib`, …).
125
+ return fs.readdirSync('/', { withFileTypes: true }).map((entry) => {
126
+ const full = path.join('/', entry.name);
127
+ if (!entry.isSymbolicLink()) {
128
+ return { path: full, realPath: full, isDirectory: entry.isDirectory() };
129
+ }
130
+ try {
131
+ const realPath = fs.realpathSync(full);
132
+ return { path: full, realPath, isDirectory: fs.statSync(realPath).isDirectory() };
133
+ }
134
+ catch {
135
+ return { path: full, realPath: null, isDirectory: false };
136
+ }
137
+ });
138
+ }
139
+ /**
140
+ * The top-level directories a widened sandbox may write to, or null when `/`
141
+ * could not be read.
142
+ *
143
+ * Links are followed to where they lead and counted once (`/bin` → `/usr/bin`
144
+ * adds nothing `/usr` does not); a directory inside another root is dropped for
145
+ * the same reason; files (a swap file) and dangling links are skipped.
146
+ */
147
+ export function topLevelWritableRoots(listRoot = listRootEntries) {
148
+ let entries;
149
+ try {
150
+ entries = listRoot();
151
+ }
152
+ catch (error) {
153
+ log.warn('codex: could not read the top-level folders – the sandbox stays on the project folder', {
154
+ error: String(error).slice(0, 200),
155
+ });
156
+ return null;
157
+ }
158
+ const kernel = (dir) => NEVER_WRITABLE_ROOTS.some((root) => dir === root || dir.startsWith(`${root}/`));
159
+ const unique = [
160
+ ...new Set(entries
161
+ .filter((entry) => entry.isDirectory && entry.realPath !== null)
162
+ .map((entry) => path.resolve(entry.realPath))
163
+ .filter((dir) => dir !== '/' && !kernel(dir))),
164
+ ].sort();
165
+ return unique.filter((dir) => !unique.some((other) => other !== dir && dir.startsWith(`${other}/`)));
95
166
  }
96
167
  /**
97
168
  * DevBridge's own rules — composed per session since session 18, and kept
@@ -214,6 +285,15 @@ class CodexSession {
214
285
  queuedInput = [];
215
286
  /** Null when the home was injected — an injected home is the whole truth. */
216
287
  repairHome;
288
+ /**
289
+ * The login this process started under (#422 S4), kept to its end: the machine
290
+ * may be switched while it runs, and its limits, its card and a refusal it meets
291
+ * all belong to THIS account (R14). Codex itself keeps a running session on the
292
+ * account it started with – it will not refresh into another account's file.
293
+ */
294
+ account;
295
+ accountId;
296
+ listRoot;
217
297
  /** Open elicitations, oldest first — Codex may have several in flight. */
218
298
  questions = new Map();
219
299
  threadId = null;
@@ -279,6 +359,12 @@ class CodexSession {
279
359
  this.model = spec.model;
280
360
  this.effort = spec.effort;
281
361
  this.repairHome = deps.repairHome ?? (deps.codexHome ? null : repairCodexAuth);
362
+ this.listRoot = deps.listRoot;
363
+ this.account = codexSessionAccount(home);
364
+ this.accountId = this.account.id;
365
+ // Released in `finish`, and only by THIS session: «forget» refuses a login a
366
+ // live process refreshes into.
367
+ noteCodexSessionAccount(spec.sessionId, this.accountId, this);
282
368
  const wiring = {
283
369
  // The cage's two numbers ride along (#398 S5). Built here rather than
284
370
  // inside `scrubbedEnv`, which has no access to the session — and merged
@@ -346,6 +432,13 @@ class CodexSession {
346
432
  throw new Error(`codex is using an unexpected CODEX_HOME (${reportedHome ?? 'not reported'}) — refusing to start the session`);
347
433
  }
348
434
  this.client.notify('initialized', {});
435
+ // The app-server reads `auth.json` while it starts, so the login it ended
436
+ // up with is the one the link points at NOW, not the one this object read
437
+ // before the process existed. A switch in that window is rare and would
438
+ // otherwise put the previous account's name on this session's card and its
439
+ // limits (found by the independent check of S4). The row it is noted under
440
+ // moves with it, so «forget» refuses the right one.
441
+ this.rereadAccount();
349
442
  // The helper set is per PROCESS (#113, #382): a session relaunched after a
350
443
  // runner restart must not go on showing the helpers of its previous life,
351
444
  // and nothing else would say so until the next one starts or ends.
@@ -675,7 +768,7 @@ class CodexSession {
675
768
  input: [{ type: 'text', text, text_elements: [] }],
676
769
  approvalPolicy: policy.approvalPolicy,
677
770
  // #418: the roots live here and only here — see `writableRootsFor`.
678
- sandboxPolicy: sandboxPolicyFor(policy.sandbox, this.spec),
771
+ sandboxPolicy: sandboxPolicyFor(policy.sandbox, this.spec, this.listRoot),
679
772
  ...(this.model ? { model: this.model } : {}),
680
773
  // "Override the reasoning effort for this turn and subsequent turns" —
681
774
  // the same sticky-override channel the model uses (there is still no
@@ -1835,6 +1928,13 @@ class CodexSession {
1835
1928
  this.emitRateLimits();
1836
1929
  }
1837
1930
  emitRateLimits(blocked = null) {
1931
+ // Whose figures these are (#422 §8): the subscription this session started
1932
+ // under. Codex reports the windows of the account its process is signed in
1933
+ // with, and that is the account captured at the start – so the signature is
1934
+ // true for the whole life of the session. No key (an API-key login) – no
1935
+ // signature, and the panel draws as before #422 (R12).
1936
+ const orgId = this.account.orgId;
1937
+ noteCodexUsage(orgId, this.rateLimitWindows);
1838
1938
  // An empty snapshot is «no plan», not «nothing happened»: Codex sends this
1839
1939
  // for API-key accounts too, and the popup has to be able to say so.
1840
1940
  this.emit({
@@ -1845,6 +1945,7 @@ class CodexSession {
1845
1945
  planType: this.ratePlanType,
1846
1946
  measuredAt: new Date().toISOString(),
1847
1947
  windows: this.rateLimitWindows,
1948
+ ...(orgId ? { orgId } : {}),
1848
1949
  },
1849
1950
  });
1850
1951
  }
@@ -2194,6 +2295,27 @@ class CodexSession {
2194
2295
  this.capabilitiesInFlight = false;
2195
2296
  });
2196
2297
  }
2298
+ /** Who the home belongs to now – after the app-server has read its credential. */
2299
+ rereadAccount() {
2300
+ if (!this.repairHome)
2301
+ return;
2302
+ try {
2303
+ const now = codexSessionAccount(this.repairHome());
2304
+ if (now.id === this.account.id)
2305
+ return;
2306
+ log.info('codex: the machine changed its Codex login while this session was starting', {
2307
+ sessionId: this.spec.sessionId,
2308
+ from: this.account.id,
2309
+ to: now.id,
2310
+ });
2311
+ this.account = now;
2312
+ this.accountId = now.id;
2313
+ noteCodexSessionAccount(this.spec.sessionId, now.id, this);
2314
+ }
2315
+ catch (error) {
2316
+ log.warn('codex: could not re-read the account after start', { error: describe(error) });
2317
+ }
2318
+ }
2197
2319
  async publishCapabilities() {
2198
2320
  // Every probe carries its own catch: an unknown method on a newer or older
2199
2321
  // codex must degrade a picker, not kill the session.
@@ -2218,7 +2340,7 @@ class CodexSession {
2218
2340
  currentMode: this.mode,
2219
2341
  ...(currentModel ? { currentModel } : {}),
2220
2342
  ...(currentEffort ? { currentEffort } : {}),
2221
- ...(account ? { account } : {}),
2343
+ account: this.sessionAccountCard(account),
2222
2344
  mcpServers,
2223
2345
  interrupt: true,
2224
2346
  };
@@ -2358,6 +2480,21 @@ class CodexSession {
2358
2480
  }
2359
2481
  return options;
2360
2482
  }
2483
+ /**
2484
+ * The card's account: what the live process says about itself (`account/read`),
2485
+ * under the row and the subscription the session started with (#422 §8) – the
2486
+ * `id` a «forget» is refused for, the `orgId` the limits panel matches against.
2487
+ */
2488
+ sessionAccountCard(live) {
2489
+ const plan = live?.plan ?? (this.account.plan ? `ChatGPT ${this.account.plan}` : undefined);
2490
+ const email = live?.email ?? this.account.email;
2491
+ return {
2492
+ id: this.account.id,
2493
+ ...(email ? { email } : {}),
2494
+ ...(plan ? { plan } : {}),
2495
+ ...(this.account.orgId ? { orgId: this.account.orgId } : {}),
2496
+ };
2497
+ }
2361
2498
  async readAccount() {
2362
2499
  const result = asRecord(await this.client.request('account/read', {}, 15_000));
2363
2500
  const account = asRecord(result['account']);
@@ -2505,6 +2642,7 @@ class CodexSession {
2505
2642
  return home.auth === 'missing' ? 'missing' : 'expired';
2506
2643
  }
2507
2644
  finish() {
2645
+ releaseCodexSessionAccount(this.spec.sessionId, this);
2508
2646
  this.stopped = true;
2509
2647
  this.clearLimitSettle();
2510
2648
  this.subagents.close();
@@ -2514,7 +2652,7 @@ class CodexSession {
2514
2652
  }
2515
2653
  class ResumeFailed extends Error {
2516
2654
  }
2517
- function sandboxPolicyFor(mode, spec) {
2655
+ function sandboxPolicyFor(mode, spec, listRoot) {
2518
2656
  // turn/start takes the structured SandboxPolicy, while thread/start takes the
2519
2657
  // CLI-style SandboxMode string. Same intent, two shapes — and only this one
2520
2658
  // can name the writable roots, which is why #418 lives here.
@@ -2526,7 +2664,7 @@ function sandboxPolicyFor(mode, spec) {
2526
2664
  default:
2527
2665
  return {
2528
2666
  type: 'workspaceWrite',
2529
- writableRoots: writableRootsFor(spec),
2667
+ writableRoots: writableRootsFor(spec, listRoot),
2530
2668
  // Unchanged by #418, on purpose: the permission is about the file
2531
2669
  // system, and letting the network out with it would be a second
2532
2670
  // boundary nobody asked to move. A network command still fails in the
@@ -2796,11 +2934,11 @@ export class CodexAdapter {
2796
2934
  // Re-assert rather than reuse: `repairCodexAuth` is a couple of lstat calls
2797
2935
  // and it puts a credential link back if something removed it since boot.
2798
2936
  //
2799
- // The configured mode has to travel with it. Repairing with the default
2800
- // would silently re-link the host user's credential into a home the owner
2801
- // explicitly asked to keep isolated — turning off a security control by
2802
- // accident.
2803
- const repair = () => repairCodexAuth({ auth: this.deps.authMode ?? 'link' });
2937
+ // The configured mode has to travel with it – and only a configured one.
2938
+ // Repairing with a default would silently re-link the host user's credential
2939
+ // into a home the owner explicitly asked to keep isolated, and undo a switch
2940
+ // to a saved login at every session start (#422 S4 item 2).
2941
+ const repair = () => repairCodexAuth(this.deps.authMode ? { auth: this.deps.authMode } : {});
2804
2942
  const home = this.deps.codexHome ?? repair();
2805
2943
  return new CodexSession(spec, home, {
2806
2944
  ...this.deps,
@@ -87,9 +87,17 @@ export interface CommandOption {
87
87
  argumentHint?: string;
88
88
  }
89
89
  export interface AgentAccountInfo {
90
+ /**
91
+ * Which of the machine's accounts THIS session started under (#422 §8) –
92
+ * `machine` or a saved account's id. Kept to the session's end: switching the
93
+ * machine does not move a running session.
94
+ */
95
+ id?: string;
90
96
  email?: string;
91
97
  organization?: string;
92
98
  plan?: string;
99
+ /** The subscription key (D19) – what the limits panel matches figures against. */
100
+ orgId?: string;
93
101
  }
94
102
  export interface McpServerStatusInfo {
95
103
  name: string;
@@ -197,6 +205,15 @@ export interface AgentRateLimits {
197
205
  key: AgentRateLimitWindow['key'];
198
206
  resetsAt: string | null;
199
207
  } | null;
208
+ /**
209
+ * Whose figures these are (#422 §8) – the account the session runs under.
210
+ * `orgId` is the subscription key (`claude auth status`), `accountUuid` the
211
+ * signature the CLI itself writes next to a `/usage` reading. Both optional:
212
+ * a session nobody could identify sends none, and so does every runner from
213
+ * before #422 – the panel then draws exactly as it did (R12).
214
+ */
215
+ accountUuid?: string;
216
+ orgId?: string;
200
217
  }
201
218
  /** Everything the dashboard needs to render agent controls, live from the agent. */
202
219
  export interface AgentCapabilities {
@@ -727,6 +744,11 @@ export type InterruptOutcome =
727
744
  export interface AgentSession {
728
745
  /** Ends when the underlying agent process is gone. */
729
746
  events: AsyncIterable<AgentEvent>;
747
+ /**
748
+ * The account this session's process runs under (#422) – what a refusal or a
749
+ * successful turn is attributed to (R14). Absent: the machine login.
750
+ */
751
+ readonly accountId?: string;
730
752
  answerPermission(requestId: string, allow: boolean, note?: string): void;
731
753
  /**
732
754
  * Answer (or discuss) an open question. Returns false when the ask is no
@@ -1,7 +1,96 @@
1
1
  export declare function agentAuthPath(): string;
2
+ /**
3
+ * Who a Claude home last said it was – the answer of `claude auth status` in it.
4
+ *
5
+ * Kept on disk (#422 R14) so a restarted runner does not forget whose numbers a
6
+ * usage reading is and whom `auth_status` names as active. Not a verdict on the
7
+ * login: `auth status` says `loggedIn: true` over a dead credential, which is why
8
+ * the verdict stays with the credentials file.
9
+ */
10
+ export interface ClaudeIdentity {
11
+ email?: string;
12
+ /** The subscription key (D19, R1): one subscription – one row. */
13
+ orgId?: string;
14
+ orgName?: string;
15
+ /** `subscriptionType` – `max`, `pro`, … */
16
+ plan?: string;
17
+ /** When this identity was read, ISO. */
18
+ at: string;
19
+ }
20
+ /** One saved Claude login of this machine (#422). Files live in its home, not here. */
21
+ export interface ClaudeAccountRecord {
22
+ id: string;
23
+ addedAt: string;
24
+ lastSeenIdentity?: ClaudeIdentity;
25
+ /**
26
+ * A session under this account was refused (D2): the row is marked, never
27
+ * removed. Read against the credentials file – a file written after the mark
28
+ * (the CLI refreshed, somebody signed in again) outranks it.
29
+ */
30
+ loginExpiredAt?: string;
31
+ }
32
+ /**
33
+ * One saved Codex login of this machine (#422 S4) – the same shape as a Claude
34
+ * record. `lastSeenIdentity.orgId` carries `tokens.account_id` of its `auth.json`:
35
+ * the key of a Codex subscription (plan §8), one subscription – one row.
36
+ */
37
+ export type CodexAccountRecord = ClaudeAccountRecord;
38
+ /** What the runner remembers about the machine row itself – it has no record of its own. */
39
+ export interface ClaudeMachineRecord {
40
+ lastSeenIdentity?: ClaudeIdentity;
41
+ /**
42
+ * A refusal held against the token this runner captured (`claudeOauthToken`)
43
+ * – set only when the refused session actually ran with that token. It
44
+ * replaces the old «discard the token after a refusal», which erased the whole
45
+ * file and with it every saved account (#422 S1 item 3).
46
+ */
47
+ loginExpiredAt?: string;
48
+ }
49
+ export interface AgentAuthFile {
50
+ claudeOauthToken?: string;
51
+ /**
52
+ * When `claudeOauthToken` was stored – the token's age, and ONLY that. Every
53
+ * other write leaves it alone, or saving an account would make a
54
+ * months-old token look freshly captured.
55
+ */
56
+ updatedAt?: string;
57
+ claudeAccounts?: ClaudeAccountRecord[];
58
+ /** Absent means the machine login – the pre-#422 file reads exactly as before. */
59
+ claudeActiveAccount?: string;
60
+ claudeMachine?: ClaudeMachineRecord;
61
+ /**
62
+ * The saved Codex logins of this machine (#422 S4). Their files live in the
63
+ * runner's CODEX_HOME (`accounts/<id>/auth.json`); WHICH one is active is not
64
+ * here but in the home itself – the `auth.json` link and its mark – because
65
+ * that is what the CLI reads.
66
+ */
67
+ codexAccounts?: CodexAccountRecord[];
68
+ }
69
+ /** Everything this runner keeps in `agent-auth.json`, read forgivingly. */
70
+ export declare function readAgentAuth(): AgentAuthFile;
71
+ /**
72
+ * Change some fields of the file and keep every other one.
73
+ *
74
+ * Read → change → write, never «write what I know». The two writers this file
75
+ * had before #422 wrote the WHOLE file (`storeClaudeToken`) or deleted it
76
+ * (`clearStoredClaudeToken`), and with a list of accounts beside the token either
77
+ * would have erased every saved login's record on its way past (S1 item 3).
78
+ *
79
+ * A file that exists but cannot be parsed is moved aside, not overwritten: it may
80
+ * be the only record of which home belongs to which account, and «unreadable»
81
+ * must not quietly become «empty». A file that cannot be READ (EACCES) throws –
82
+ * writing over something we could not look at is the same mistake.
83
+ *
84
+ * Synchronous from read to rename, so two changes inside this process cannot
85
+ * interleave. Same write-then-rename as the config, 0600.
86
+ */
87
+ export declare function updateAgentAuth(change: (file: AgentAuthFile) => void): AgentAuthFile;
2
88
  /** The token we hold for Claude, or null once it is too old to trust. */
3
89
  export declare function storedClaudeToken(): string | null;
90
+ /** When the stored token was captured, in ms – null without one. */
91
+ export declare function storedClaudeTokenAtMs(): number | null;
4
92
  export declare function storeClaudeToken(token: string): void;
93
+ /** Drop the stored token – and nothing else: the accounts beside it stay (#422). */
5
94
  export declare function clearStoredClaudeToken(): void;
6
95
  /**
7
96
  * Put a stored token into this process's environment, unless the operator
@@ -15,6 +104,17 @@ export declare function clearStoredClaudeToken(): void;
15
104
  * Returns true when it applied one.
16
105
  */
17
106
  export declare function applyStoredClaudeToken(): boolean;
107
+ /**
108
+ * Was a session refused with the token this runner captured – after it was
109
+ * captured? A token stored after the refusal is newer evidence and outranks it.
110
+ * A token with no readable capture date is judged by the mark alone.
111
+ */
112
+ export declare function storedClaudeTokenRefused(): boolean;
113
+ /**
114
+ * The daemon's environment carries exactly the token this runner captured – so
115
+ * a machine-login session refused now was refused WITH it.
116
+ */
117
+ export declare function environmentCarriesStoredToken(): boolean;
18
118
  /**
19
119
  * The OAuth token `claude setup-token` printed, reassembled out of pty output.
20
120
  *