@bridge4dev/runner 0.66.0 → 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,9 +1,9 @@
1
- import type { AccountLoginCodeResult, AccountLoginTarget, LoginStartResult } from './auth-relay.js';
1
+ import type { AccountLoginCodeResult, AccountLoginTarget, LoginStartResult, RelayAgent } from './auth-relay.js';
2
2
  /**
3
3
  * Several logins of one agent, on the wire (#422 S2, plan §8).
4
4
  *
5
- * Five commands, each a thin door over what `claude-homes.ts` already does on
6
- * the machine. New NAMES rather than new arguments of `login_start`: an older
5
+ * Five commands, each a thin door over what `claude-homes.ts` (Claude) and
6
+ * `codex-accounts.ts` (Codex, S4) already do on the machine. New NAMES rather than new arguments of `login_start`: an older
7
7
  * runner silently ignores an argument it does not know and would sign in over
8
8
  * the machine login while the API believed a saved account was being added; an
9
9
  * unknown name it answers «Unknown command» at once.
@@ -21,10 +21,9 @@ export declare const ACCOUNT_COMMANDS: readonly ["agent_accounts", "agent_accoun
21
21
  export type AccountCommand = (typeof ACCOUNT_COMMANDS)[number];
22
22
  /**
23
23
  * The agents this runner keeps several logins for (`capabilities.accounts`,
24
- * R11). Codex joins in S4; until then its account commands are refused with a
25
- * sentence rather than half-served.
24
+ * R11). Codex joined in S4 – the same five commands and the same card.
26
25
  */
27
- export declare const ACCOUNT_AGENTS: readonly ["claude"];
26
+ export declare const ACCOUNT_AGENTS: readonly ["claude", "codex"];
28
27
  /** What `supervisor.ts` sends back as `command_result`, minus the envelope. */
29
28
  export type AccountCommandReply = {
30
29
  ok: true;
@@ -36,7 +35,7 @@ export type AccountCommandReply = {
36
35
  };
37
36
  /** The two sign-in steps the commands drive – `AuthRelay`, or a test double. */
38
37
  export interface AccountSignIn {
39
- startAccountLogin(target: AccountLoginTarget): Promise<LoginStartResult>;
38
+ startAccountLogin(target: AccountLoginTarget, agent?: RelayAgent): Promise<LoginStartResult>;
40
39
  submitAccountCode(code: string): Promise<AccountLoginCodeResult>;
41
40
  }
42
41
  export declare function isAccountCommand(name: string): name is AccountCommand;
@@ -1,11 +1,12 @@
1
- import { AccountError, forgetAccount, isAccountId, listAccounts, setActiveAccount, } from './claude-homes.js';
2
- import { MACHINE_ACCOUNT_ID } from './login-marks.js';
1
+ import { forgetAccount, listAccounts, setActiveAccount } from './claude-homes.js';
2
+ import { activateCodexAccount, forgetCodexAccount, listCodexAccounts } from './codex-accounts.js';
3
+ import { AccountError, MACHINE_ACCOUNT_ID, isAccountId } from './login-marks.js';
3
4
  import { maskString } from './policy.js';
4
5
  /**
5
6
  * Several logins of one agent, on the wire (#422 S2, plan §8).
6
7
  *
7
- * Five commands, each a thin door over what `claude-homes.ts` already does on
8
- * the machine. New NAMES rather than new arguments of `login_start`: an older
8
+ * Five commands, each a thin door over what `claude-homes.ts` (Claude) and
9
+ * `codex-accounts.ts` (Codex, S4) already do on the machine. New NAMES rather than new arguments of `login_start`: an older
9
10
  * runner silently ignores an argument it does not know and would sign in over
10
11
  * the machine login while the API believed a saved account was being added; an
11
12
  * unknown name it answers «Unknown command» at once.
@@ -28,10 +29,9 @@ export const ACCOUNT_COMMANDS = [
28
29
  ];
29
30
  /**
30
31
  * The agents this runner keeps several logins for (`capabilities.accounts`,
31
- * R11). Codex joins in S4; until then its account commands are refused with a
32
- * sentence rather than half-served.
32
+ * R11). Codex joined in S4 – the same five commands and the same card.
33
33
  */
34
- export const ACCOUNT_AGENTS = ['claude'];
34
+ export const ACCOUNT_AGENTS = ['claude', 'codex'];
35
35
  export function isAccountCommand(name) {
36
36
  return ACCOUNT_COMMANDS.includes(name);
37
37
  }
@@ -48,9 +48,8 @@ function accountIdArg(value) {
48
48
  }
49
49
  export async function runAccountCommand(name, args, signIn) {
50
50
  const agent = args['agent'];
51
- if (agent === 'codex') {
52
- return { ok: false, error: 'several Codex logins are not supported by this runner yet' };
53
- }
51
+ if (agent === 'codex')
52
+ return runCodexAccountCommand(name, args, signIn);
54
53
  if (agent !== 'claude')
55
54
  return { ok: false, error: 'agent must be claude or codex' };
56
55
  try {
@@ -96,4 +95,48 @@ export async function runAccountCommand(name, args, signIn) {
96
95
  return refusal(error);
97
96
  }
98
97
  }
98
+ /**
99
+ * The same five doors for Codex (#422 S4). What differs is said where it differs:
100
+ * the machine login is the host user's own file (no sign-in into it, R13), and a
101
+ * device sign-in finishes on the provider's page – there is no code to send; the
102
+ * window learns the result from the list (§8).
103
+ */
104
+ async function runCodexAccountCommand(name, args, signIn) {
105
+ try {
106
+ switch (name) {
107
+ case 'agent_accounts':
108
+ return { ok: true, result: listCodexAccounts() };
109
+ case 'agent_account_login_start': {
110
+ const target = args['target'];
111
+ if (target !== 'machine' && target !== 'saved') {
112
+ return { ok: false, error: 'target must be machine or saved' };
113
+ }
114
+ return { ok: true, result: await signIn.startAccountLogin(target, 'codex') };
115
+ }
116
+ case 'agent_account_login_code': {
117
+ const result = {
118
+ ok: false,
119
+ detail: 'a Codex sign-in finishes on the provider page – there is no code to send; the account list shows it once it is done',
120
+ };
121
+ return { ok: false, error: result.detail, result };
122
+ }
123
+ case 'agent_account_activate': {
124
+ const id = accountIdArg(args['id']);
125
+ if (!id)
126
+ return { ok: false, error: 'no such account on this server' };
127
+ return { ok: true, result: { active: activateCodexAccount(id) } };
128
+ }
129
+ case 'agent_account_forget': {
130
+ const id = accountIdArg(args['id']);
131
+ if (!id)
132
+ return { ok: false, error: 'no such account on this server' };
133
+ const { active } = forgetCodexAccount(id);
134
+ return { ok: true, result: { ok: true, active } };
135
+ }
136
+ }
137
+ }
138
+ catch (error) {
139
+ return refusal(error);
140
+ }
141
+ }
99
142
  //# sourceMappingURL=account-commands.js.map
@@ -1,24 +1,90 @@
1
1
  export type CodexAuthMode = 'link' | 'own';
2
2
  export interface CodexHome {
3
3
  path: string;
4
- /** How the home is authenticated — reported in notices, not a secret. */
5
- auth: 'linked' | 'own' | 'missing';
4
+ /**
5
+ * How the home is authenticated — reported in notices, not a secret.
6
+ * `account` – a saved login is linked (`accountId`); `own` – a real file the
7
+ * runner could not move into the store and keeps using where it is.
8
+ */
9
+ auth: 'linked' | 'account' | 'own' | 'missing';
10
+ /** The saved login the home is set to – present with `account`, and with `missing` under it. */
11
+ accountId?: string;
6
12
  }
7
13
  export declare function codexHomePath(): string;
14
+ /** Where the saved Codex logins live – inside the one home, beside what they share. */
15
+ export declare function codexAccountsDir(home?: string): string;
16
+ /** The directory of one saved login. Throws on anything that is not an account id. */
17
+ export declare function codexAccountDir(id: string, home?: string): string;
18
+ /** The login file of one saved account. */
19
+ export declare function codexAccountAuthFile(id: string, home?: string): string;
20
+ /** The machine login of Codex – the host user's own file, never written by DevBridge (R13). */
21
+ export declare function hostCodexAuthFile(homedir?: string): string;
8
22
  /**
9
- * Throwaway home for a device-code login. The flow runs here and is promoted
10
- * into the real home only on success, so an abandoned or timed-out sign-in
11
- * cannot destroy a credential that was working.
23
+ * Throwaway homes for device-code logins, one per attempt. The flow runs there
24
+ * and its login is moved into the store only on success, so an abandoned or
25
+ * timed-out sign-in cannot destroy a credential that was working.
12
26
  */
13
27
  export declare function stagingCodexHomePath(): string;
28
+ /**
29
+ * `[codex] auth` as the machine's owner WROTE it – `undefined` when the key is
30
+ * not in `config.toml`. Set once at daemon start.
31
+ *
32
+ * Only a mode that is really written may be forced, and even then not over a
33
+ * saved login (S4 item 2): the runner used to pass `link` by default from three
34
+ * places – the daemon start, every session start and the minute probe – and each
35
+ * of them would have undone a switch of account within a minute (К4).
36
+ */
37
+ export declare function configureCodexAuth(mode: CodexAuthMode | undefined): void;
38
+ export declare function configuredCodexAuth(): CodexAuthMode | undefined;
39
+ /** Which saved login the home is set to, if any – the mark, read only. */
40
+ export declare function markedCodexAccount(home?: string): string | null;
41
+ /** Who a Codex login is, read locally from its own file – never a token. */
42
+ export interface CodexIdentity {
43
+ email?: string;
44
+ /** `tokens.account_id` – the key of a Codex subscription (plan §8, S4 item 5). */
45
+ orgId?: string;
46
+ /** `chatgpt_plan_type` – `plus`, `pro`, `team`, … */
47
+ plan?: string;
48
+ }
49
+ export interface CodexCredential {
50
+ /**
51
+ * `ok` – a login the CLI can use; `expired` – a login past its date with no
52
+ * refresh token; `missing` – no file; `unreadable` – a file that is not a login
53
+ * (torn, hand-edited); `unknown` – we could not look (EACCES).
54
+ */
55
+ status: 'ok' | 'expired' | 'missing' | 'unreadable' | 'unknown';
56
+ /** A ChatGPT login (tokens) or an API key – only for a file that is a login. */
57
+ kind?: 'chatgpt' | 'apikey';
58
+ identity: CodexIdentity;
59
+ /** Only when the access token's own date is the login's date (no refresh token). */
60
+ expiresAt?: string;
61
+ /** Last write of the file, whole ms – a refusal mark older than this no longer holds. */
62
+ writtenMs?: number;
63
+ }
64
+ /**
65
+ * Read a Codex login file without letting a token out of this function.
66
+ *
67
+ * The identity comes from the claims of `id_token`, decoded locally (S4 item 8):
68
+ * `email`, and under `https://api.openai.com/auth` the plan and the ChatGPT
69
+ * account; `tokens.account_id` first for the key, the claim when it is absent. An
70
+ * internal file of the CLI (§11): whatever cannot be read is simply not there –
71
+ * no identity is invented, and a row without a key merges with nothing.
72
+ *
73
+ * The verdict is the pre-S4 one (`readCodexCredential`): a refresh token means
74
+ * the CLI renews the access token on its own, so only a lone access token past
75
+ * its `exp` is `expired`.
76
+ */
77
+ export declare function readCodexCredentialFile(file: string): CodexCredential;
14
78
  /**
15
79
  * Create (or refresh) the runner's CODEX_HOME and return it.
16
80
  *
17
- * `auth: 'link'` (default) symlinks the host user's `~/.codex/auth.json` so the
18
- * runner uses their ChatGPT subscription and — importantly — shares one
19
- * credential store with their own CLI, so a token refresh on either side keeps
20
- * both working. `auth: 'own'` leaves the home unauthenticated until a
21
- * device-code login writes into it, which is the choice for full isolation.
81
+ * `link` (the default when nothing else is decided) symlinks the host user's
82
+ * `~/.codex/auth.json` so the runner uses their ChatGPT subscription and —
83
+ * importantly — shares one credential store with their own CLI, so a token
84
+ * refresh on either side keeps both working. `own` leaves the home
85
+ * unauthenticated until a device-code login is stored for it, which is the
86
+ * choice for full isolation. A saved login (`account:<id>`) is kept whatever
87
+ * the configured mode says (S4 item 2).
22
88
  *
23
89
  * A home under the OS temp dir still works, but codex then refuses to install
24
90
  * its helper binaries ("Refusing to create helper binaries under temporary
@@ -35,6 +101,9 @@ export declare function ensureCodexHome(options?: {
35
101
  * it only answers "is the credential still where we left it, and if not, can we
36
102
  * put it back". This is what makes a credential disappearing under a running
37
103
  * daemon self-healing instead of permanent.
104
+ *
105
+ * Forces nothing it was not told to by the machine's owner (S4 item 2): with no
106
+ * `[codex] auth` in `config.toml` the mark decides, and a saved login stays.
38
107
  */
39
108
  export declare function repairCodexAuth(options?: {
40
109
  auth?: CodexAuthMode;
@@ -42,20 +111,73 @@ export declare function repairCodexAuth(options?: {
42
111
  }): CodexHome;
43
112
  /**
44
113
  * Drop a linked auth.json so a device-code login writes our own file instead.
45
- *
46
- * Only ever called AFTER a login has actually succeeded in the staging home —
47
- * never as a pre-step. Detaching first meant an abandoned or timed-out sign-in
48
- * left the server permanently "not signed in", recoverable only by restarting
49
- * the daemon.
114
+ * Never a real credential: only a symlink is removed.
50
115
  */
51
116
  export declare function detachLinkedAuth(dir?: string): void;
117
+ export interface StoredCodexLogin {
118
+ id: string;
119
+ /** The saved row whose login this one replaced – the same subscription (D19). */
120
+ replaced?: string;
121
+ }
122
+ /**
123
+ * Move a login file into the store (§8 `agent_account_login_code` for Codex, S4
124
+ * items 4 and 5) – by rename, and one subscription, one row.
125
+ *
126
+ * A known `account_id` that a saved row already has REPLACES that row's login:
127
+ * the row keeps its id (refusal marks and a live session are keyed by it, R15)
128
+ * and its `addedAt` becomes now – the date is that of the login the row holds,
129
+ * and a second sign-in of the same subscription must change something the
130
+ * window can see (S3 hands this over: `deviceSignInResult`). An unknown key
131
+ * merges with nothing. The machine row takes no part: it is the host's own file.
132
+ *
133
+ * The record is written BEFORE the file moves, so a failure never leaves a login
134
+ * nobody can list or forget; a move that fails takes its new record back.
135
+ */
136
+ export declare function storeCodexLogin(source: string, options: {
137
+ activate: boolean;
138
+ home?: string;
139
+ replaceActive?: boolean;
140
+ }): StoredCodexLogin;
52
141
  /**
53
- * Promote a credential produced by a staging login into the real home, and
54
- * record that this home now owns its own login.
142
+ * Make a row the one the home uses: the mark first, then the link.
143
+ *
144
+ * In that order on purpose – a daemon that dies between the two leaves a mark
145
+ * the next repair finishes (it points the link where the mark says), never a
146
+ * link that the next repair would undo.
147
+ *
148
+ * A real file in place of the link is dealt with FIRST (§8 `activate`, S4 item
149
+ * 7): the CLI may have refreshed the login in use a moment ago, and repointing
150
+ * over it would throw that token away.
151
+ */
152
+ export declare function markActiveCodexLogin(id: string, dir?: string): void;
153
+ export declare function setCodexActiveLogin(id: string, dir?: string): CodexHome;
154
+ /**
155
+ * A fresh throwaway home for one device-code sign-in (config only, no credential).
156
+ *
157
+ * One directory per attempt, and every other one is removed first: only one
158
+ * sign-in runs on a machine at a time (the relay cancels the previous one before
159
+ * it gets here), and the pre-S4 runner kept a single fixed staging home that was
160
+ * never cleaned up – on this machine it had lain there since 01.08.2026.
55
161
  */
56
- export declare function adoptLoginResult(stagingDir: string, dir?: string): boolean;
57
- /** Seed a throwaway home for the device-code flow (config only, no credential). */
58
162
  export declare function prepareStagingHome(): string;
59
- /** Remove the staging home whatever the outcome — it may hold a credential. */
60
- export declare function discardStagingHome(): void;
163
+ /** Remove one sign-in's home whatever the outcome — it may hold a credential. */
164
+ export declare function discardStagingHome(dir: string): void;
165
+ /**
166
+ * Remove every sign-in home – at daemon start (nothing can be signing in yet)
167
+ * and before a new sign-in. Includes the fixed-path staging home of runners
168
+ * before S4.
169
+ */
170
+ export declare function discardAbandonedCodexStagingHomes(): number;
171
+ /**
172
+ * A device-code sign-in in `stagingDir` finished: its login becomes a saved
173
+ * account – by rename, deduplicated by subscription – and the one in use (R15).
174
+ * The staging home is removed either way.
175
+ *
176
+ * `replaceActive` – the sign-in came through the one-login window, where it has
177
+ * always meant «this login from now on»: it takes the place of the login in use
178
+ * rather than adding a row (D27, for an organization without several logins).
179
+ */
180
+ export declare function adoptLoginResult(stagingDir: string, options?: {
181
+ replaceActive?: boolean;
182
+ }): StoredCodexLogin;
61
183
  //# sourceMappingURL=codex-home.d.ts.map