johns-harness 2026.9.10 → 2026.9.12

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/README.md CHANGED
@@ -45,6 +45,14 @@ The wizard sets up credentials, workspace, gateway, and channels. It asks for yo
45
45
  | Connect | `johnness channels add --channel telegram --token "<bot token>"` | Connects a chat channel. Discord, Slack, WhatsApp, Signal, iMessage, Matrix, and more are supported. |
46
46
  | Check | `johnness doctor` | Health checks and quick fixes for the gateway and channels. |
47
47
 
48
+ <!-- JOHNNESS_PATCH_DEVICE_LOGIN_60B -->
49
+ ### Signing in from chat
50
+ Owners and allowlisted senders can send `/login` (or `/relogin`) in Telegram, Discord, or another chat channel.
51
+ Open the link, sign in to ChatGPT if asked, then enter the device code on the same page and tap Approve. In Telegram, tap the code to copy it.
52
+ Enable "Device code authorization for Codex" once in chatgpt.com > Settings > Security; if it was disabled, send `/login` again for a fresh code.
53
+ For headless installs, run `johnness models auth login --device`; no browser or localhost callback needed.
54
+ Expired ChatGPT credentials trigger an owner notice with the `/login` recovery command.
55
+
48
56
  ### From source
49
57
 
50
58
  ```bash
@@ -0,0 +1,229 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ // Native OpenAI device flow, verified against Codex 0.154.0 device_code_auth.rs.
4
+ // OpenAI uses deviceauth + an authorization-code/PKCE exchange, NOT RFC 8628.
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { canonicalPath, acquireOwnedLock, profileLockPath, readAuthStore, transactAuthStore } from './auth-store-transactions.js';
9
+ import { readManagedCodexStore, mutateAuthProfile, resolveCodexProfileOwner } from './codex-oauth-owner.js';
10
+ import { validateRefreshedCredential } from './codex-oauth-token.js';
11
+ const PROVIDER = 'openai-codex';
12
+ const ISSUER = 'https://auth.openai.com';
13
+ const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
14
+ const TTL = 15 * 60 * 1000;
15
+ const active = globalThis[Symbol.for('johnness.codexDeviceLogin.v60')] ??= new Map();
16
+ export class DeviceLoginError extends Error {
17
+ constructor(reason, status) { super(`Codex device login: ${reason}${status ? ` (HTTP ${status})` : ''}`); this.reason = reason; }
18
+ }
19
+ const fail = (reason, status) => { throw new DeviceLoginError(reason, status); };
20
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
21
+ function claims(token) { try { return JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()); } catch { return {}; } }
22
+ export function redactLoginEmail(email) {
23
+ if (typeof email !== 'string' || !email.includes('@')) return 'ChatGPT account';
24
+ const [local, domain] = email.split('@');
25
+ return `${local.slice(0, 1)}***@${domain.slice(0, 1)}***`;
26
+ }
27
+ export const DEVICE_LOGIN_SECURITY_FIX = "Go to chatgpt.com > Settings > Security, turn on 'Device code authorization for Codex'";
28
+ export function formatDeviceLoginInstructions(login, { email, chat = false } = {}) {
29
+ const account = typeof email === 'string' && email.includes('@') ? redactLoginEmail(email) : 'the same account as before';
30
+ const code = chat ? `\`${login.userCode}\`` : login.userCode;
31
+ const retry = chat ? 'send /login again' : 'run johnness models auth login --device again';
32
+ const minutes = Math.max(1, Math.ceil((login.expiresAt - Date.now()) / 60000));
33
+ return `Let's sign you back into ChatGPT.
34
+ 1. Open ${login.verificationUri} on your phone or computer.
35
+ 2. Sign in with your ChatGPT account (${account}) if asked.
36
+ 3. On the same page, enter this code${chat ? ' (tap the code to copy it)' : ''}:
37
+ ${code}
38
+ 4. Tap Approve.
39
+ Code is good for ${minutes} minute${minutes === 1 ? '' : 's'}. If you see 'Device code authorization is disabled', ${DEVICE_LOGIN_SECURITY_FIX.charAt(0).toLowerCase() + DEVICE_LOGIN_SECURITY_FIX.slice(1)}, then ${retry} for a fresh code.`;
40
+ }
41
+ function hasDeviceLoginSecurityFix(error) {
42
+ return ['device_login_unavailable', 'device_code_authorization_disabled'].includes(error?.reason);
43
+ }
44
+ export async function startDeviceLogin({ provider = PROVIDER, fetch: fetchImpl = globalThis.fetch, now = Date.now, sleep: pause = sleep, signal } = {}) {
45
+ if (provider !== PROVIDER) fail('unsupported_provider');
46
+ async function request(url, body, form = false, remaining = 30000) {
47
+ if (signal?.aborted) fail('cancelled');
48
+ const timeout = AbortSignal.timeout(Math.max(1, Math.min(30000, remaining)));
49
+ let response;
50
+ try { response = await fetchImpl(url, { method: 'POST', headers: { 'Content-Type': form ? 'application/x-www-form-urlencoded' : 'application/json' }, body: form ? new URLSearchParams(body) : JSON.stringify(body), signal: signal ? AbortSignal.any([signal, timeout]) : timeout }); }
51
+ catch { fail(signal?.aborted ? 'cancelled' : 'network_error'); }
52
+ let data; try { data = await response.json(); } catch { data = {}; }
53
+ return { response, data };
54
+ }
55
+ const started = now();
56
+ const { response, data } = await request(`${ISSUER}/api/accounts/deviceauth/usercode`, { client_id: CLIENT_ID });
57
+ if (!response.ok) {
58
+ // Only classify the initial request, never reinterpret polling's bare 403/404.
59
+ const messages = [data?.message, data?.error_description, data?.error?.message, data?.error];
60
+ if (response.status === 403 && messages.some(message => typeof message === 'string' && /device code authorization is disabled|security settings do not allow device code authorization for codex/i.test(message))) fail('device_code_authorization_disabled', response.status);
61
+ fail(response.status === 404 ? 'device_login_unavailable' : 'request_failed', response.status);
62
+ }
63
+ const userCode = data.user_code ?? data.usercode;
64
+ if (typeof data.device_auth_id !== 'string' || !data.device_auth_id || typeof userCode !== 'string' || !/^[A-Za-z0-9-]{4,32}$/.test(userCode)) fail('invalid_response');
65
+ const expiresAt = started + TTL;
66
+ let interval = Math.max(1000, Math.min(60000, (Number(data.interval) || 5) * 1000));
67
+ let promise;
68
+ async function pollLoop() {
69
+ while (now() < expiresAt) {
70
+ const { response, data: result } = await request(`${ISSUER}/api/accounts/deviceauth/token`, { device_auth_id: data.device_auth_id, user_code: userCode }, false, expiresAt - now());
71
+ if (now() >= expiresAt) fail('expired');
72
+ const code = typeof result.error === 'string' ? result.error : result.error?.code;
73
+ if (['access_denied', 'authorization_declined', 'denied'].includes(code)) fail('denied');
74
+ if (['expired_token', 'expired', 'device_code_expired'].includes(code)) fail('expired');
75
+ if (response.ok) {
76
+ if (typeof result.authorization_code !== 'string' || !result.authorization_code || typeof result.code_verifier !== 'string' || !result.code_verifier) fail('invalid_response');
77
+ const exchanged = await request(`${ISSUER}/oauth/token`, { grant_type: 'authorization_code', client_id: CLIENT_ID, redirect_uri: `${ISSUER}/deviceauth/callback`, code: result.authorization_code, code_verifier: result.code_verifier }, true, expiresAt - now());
78
+ if (!exchanged.response.ok) fail('exchange_failed', exchanged.response.status);
79
+ const tokens = exchanged.data, accessClaims = claims(tokens.access_token), idClaims = claims(tokens.id_token);
80
+ const accountId = accessClaims['https://api.openai.com/auth']?.chatgpt_account_id ?? idClaims['https://api.openai.com/auth']?.chatgpt_account_id;
81
+ const expires = Number.isFinite(tokens.expires_in) && tokens.expires_in > 0 ? Date.now() + tokens.expires_in * 1000 : accessClaims.exp * 1000;
82
+ const email = idClaims.email ?? accessClaims['https://api.openai.com/profile']?.email;
83
+ try { return validateRefreshedCredential({ type: 'oauth', provider: PROVIDER, access: tokens.access_token, refresh: tokens.refresh_token, expires, accountId, ...(typeof email === 'string' ? { email } : {}) }); }
84
+ catch { fail('invalid_response'); }
85
+ }
86
+ // Codex treats bare 403 and 404 as pending, not denial.
87
+ if (response.status === 429) {
88
+ const retry = Number(response.headers?.get('retry-after'));
89
+ interval = Math.max(interval, Number.isFinite(retry) && retry > 0 ? retry * 1000 : 5000);
90
+ } else if (code === 'slow_down') interval += 5000;
91
+ else if (![403, 404].includes(response.status) && code !== 'authorization_pending') fail('poll_failed', response.status);
92
+ await pause(Math.min(interval, Math.max(0, expiresAt - now())));
93
+ }
94
+ fail('expired');
95
+ }
96
+ return { userCode, verificationUri: `${ISSUER}/codex/device`, expiresAt, poll() { return promise ??= pollLoop(); } };
97
+ }
98
+ function ownerContext(params = {}) {
99
+ const stateDir = canonicalPath(params.stateDir || process.env.JOHNNESS_STATE_DIR || path.join(os.homedir(), '.johnness'));
100
+ const mainAgentDir = canonicalPath(params.mainAgentDir || process.env.JOHNNESS_AGENT_DIR || process.env.PI_CODING_AGENT_DIR || path.join(stateDir, 'agents/main/agent'));
101
+ return { ...params, stateDir, mainAgentDir, agentDir: canonicalPath(params.agentDir || mainAgentDir) };
102
+ }
103
+ export function selectDeviceLoginProfile(params = {}) {
104
+ const ctx = ownerContext(params), store = readManagedCodexStore(ctx);
105
+ const ids = Object.keys(store.profiles).filter(id => store.profiles[id]?.provider === PROVIDER && store.profiles[id]?.type === 'oauth');
106
+ const preferred = [params.profileId, store.lastGood?.[PROVIDER], ...(store.order?.[PROVIDER] ?? []), `${PROVIDER}:default`, ...ids].find(id => ids.includes(id));
107
+ const owner = preferred ? resolveCodexProfileOwner({ ...ctx, profileId: preferred }) : ctx;
108
+ return { ...owner, profileId: preferred, previous: preferred ? store.profiles[preferred] : undefined };
109
+ }
110
+ export async function handoffDeviceCredential(params, credential) {
111
+ const selected = selectDeviceLoginProfile(params);
112
+ const profileId = selected.profileId ?? `${PROVIDER}:${credential.email?.trim() || 'default'}`;
113
+ let next;
114
+ try { next = validateRefreshedCredential(credential, selected.previous, { profileId }); }
115
+ catch (error) {
116
+ if (error.reason !== 'account_mismatch') fail('invalid_response');
117
+ next = validateRefreshedCredential(credential, undefined, { profileId });
118
+ (params.log ?? console.info)('[codex-device-login] Owner selected a different ChatGPT account; replacing the existing profile (identities redacted).');
119
+ }
120
+ // A new login is a clean credential, not a merge of old failure/token metadata.
121
+ const clean = { type: 'oauth', provider: PROVIDER, access: next.access, refresh: next.refresh, expires: next.expires, accountId: next.accountId, ...(credential.email ? { email: credential.email } : {}) };
122
+ await mutateAuthProfile({ ...selected, profileId, credential: clean, expected: selected.previous, clearLoginFailures: true });
123
+ return { profileId, credential: clean };
124
+ }
125
+ export async function getOrStartDeviceLogin(params = {}) {
126
+ const selected = selectDeviceLoginProfile(params);
127
+ const slot = selected.profileId ?? PROVIDER;
128
+ const file = path.join(selected.agentDir, 'auth-profiles.json');
129
+ const key = `${file}\0${slot}`;
130
+ if (active.has(key)) return active.get(key);
131
+ // The profile owner handles token writes; a separate device lock spans the
132
+ // human authorization window, including CLI/gateway and bundle boundaries.
133
+ const pending = (async () => {
134
+ const lock = profileLockPath(selected.stateDir, 'openai-codex-device', `${file}:${slot}`);
135
+ let release;
136
+ try { release = await acquireOwnedLock(lock, { waitMs: 1, pollMs: 1 }); }
137
+ catch (error) {
138
+ if (error.reason !== 'lock_contention') throw error;
139
+ let record;
140
+ for (let attempt = 0; attempt < 150; attempt++) {
141
+ record = readAuthStore(file).codexDeviceLogins?.[slot];
142
+ if (record?.status === 'pending' && record.expiresAt > Date.now()) break;
143
+ await sleep(200);
144
+ }
145
+ if (record?.status !== 'pending' || record.expiresAt <= Date.now()) fail('login_already_pending');
146
+ let completion;
147
+ return { userCode: record.userCode, verificationUri: record.verificationUri, expiresAt: record.expiresAt, complete() {
148
+ return completion ??= (async () => {
149
+ while (Date.now() < record.expiresAt) {
150
+ const store = readAuthStore(file), current = store.codexDeviceLogins?.[slot];
151
+ if (current?.id !== record.id) fail('cancelled');
152
+ if (current.status === 'success') return { profileId: current.profileId, credential: store.profiles[current.profileId] };
153
+ if (current.status === 'failed') fail(current.reason === 'expired' ? 'expired' : 'denied');
154
+ await sleep(1000);
155
+ }
156
+ fail('expired');
157
+ })().finally(() => { if (active.get(key) === pending) active.delete(key); });
158
+ } };
159
+ }
160
+ try {
161
+ const login = await (params.start ?? startDeviceLogin)({ ...params, provider: PROVIDER });
162
+ const id = randomUUID();
163
+ await transactAuthStore(file, store => {
164
+ store.codexDeviceLogins ??= {};
165
+ // Only the public prompt is stored. The device-auth id and PKCE material
166
+ // remain in the issuing process, and tokens only go through the owner.
167
+ store.codexDeviceLogins[slot] = { id, userCode: login.userCode, verificationUri: login.verificationUri, expiresAt: login.expiresAt, status: 'pending' };
168
+ });
169
+ let completion;
170
+ return { ...login, complete() {
171
+ return completion ??= (async () => {
172
+ try {
173
+ const result = await handoffDeviceCredential(selected, await login.poll());
174
+ await transactAuthStore(file, store => {
175
+ store.codexDeviceLogins[slot] = { id, status: 'success', profileId: result.profileId };
176
+ });
177
+ return result;
178
+ } catch (error) {
179
+ await transactAuthStore(file, store => { store.codexDeviceLogins[slot] = { id, status: 'failed', reason: error.reason === 'expired' ? 'expired' : 'denied' }; });
180
+ throw error;
181
+ } finally { await release(); if (active.get(key) === pending) active.delete(key); }
182
+ })();
183
+ } };
184
+ } catch (error) { await release(); throw error; }
185
+ })();
186
+ active.set(key, pending);
187
+ pending.catch(() => { if (active.get(key) === pending) active.delete(key); });
188
+ return pending;
189
+ }
190
+ export async function runDeviceLoginCli(opts, runtime, params = {}) {
191
+ if (opts.provider && opts.provider !== PROVIDER) fail('unsupported_provider');
192
+ try {
193
+ const login = await getOrStartDeviceLogin(params);
194
+ runtime.log(formatDeviceLoginInstructions(login, { email: selectDeviceLoginProfile(params).previous?.email }));
195
+ const result = await login.complete();
196
+ runtime.log(`Signed in as ${redactLoginEmail(result.credential.email)}`);
197
+ return result;
198
+ } catch (error) {
199
+ if (hasDeviceLoginSecurityFix(error)) runtime.log(`${DEVICE_LOGIN_SECURITY_FIX}, then run johnness models auth login --device again for a fresh code.`);
200
+ throw error;
201
+ }
202
+ }
203
+ export async function handleDeviceLoginCommand(params, allowTextCommands, { routeReply, callGateway, log = console.info, ...options } = {}) {
204
+ if (!allowTextCommands || !/^\/(login|relogin)(?:\s|$)/i.test(params.command.commandBodyNormalized)) return null;
205
+ const reply = text => ({ shouldContinue: false, reply: { text } });
206
+ // Same authoritative sender check as config-changing commands. No model call needed.
207
+ if (!params.command.isAuthorizedSender) return reply('Only the owner can do that.');
208
+ try {
209
+ const login = await getOrStartDeviceLogin({ ...options, agentDir: params.agentDir, log });
210
+ const route = { channel: params.ctx.OriginatingChannel || params.command.channel, to: params.ctx.OriginatingTo || params.command.from || params.command.to, sessionKey: params.sessionKey, accountId: params.ctx.AccountId, threadId: params.ctx.MessageThreadId, cfg: params.cfg };
211
+ const routeKey = JSON.stringify([route.channel, route.to, route.accountId, route.threadId]);
212
+ login.notifiedRoutes ??= new Set();
213
+ if (!login.notifiedRoutes.has(routeKey)) {
214
+ login.notifiedRoutes.add(routeKey);
215
+ // Never keep the command lane occupied for the 15-minute authorization window.
216
+ void login.complete().then(() => 'Signed in. I\'m back.', error => error.reason === 'expired' ? "That code expired (they last 15 minutes). Send /login and I'll give you a fresh one." : error.reason === 'denied' ? 'That sign-in was declined. Send /login to try again.' : 'Login failed. Send /login to try again.').then(async text => {
217
+ const result = ["webchat", "internal"].includes(route.channel) && callGateway
218
+ ? await callGateway({ method: "chat.inject", params: { sessionKey: route.sessionKey, message: text } })
219
+ : await routeReply({ ...route, payload: { text } });
220
+ if (result?.ok === false) throw new Error('delivery_failed');
221
+ log('[codex-device-login] completion delivered');
222
+ }).catch(() => log('[codex-device-login] completion delivery failed; send /login again.'));
223
+ }
224
+ log('[codex-device-login] /login recognized; code issued (code and identity redacted)');
225
+ return reply(formatDeviceLoginInstructions(login, { email: selectDeviceLoginProfile({ ...options, agentDir: params.agentDir }).previous?.email, chat: true }));
226
+ } catch (error) {
227
+ return reply(hasDeviceLoginSecurityFix(error) ? `${DEVICE_LOGIN_SECURITY_FIX}, then send /login again for a fresh code.` : 'Could not start ChatGPT login. Send /login to try again.');
228
+ }
229
+ }
@@ -0,0 +1,61 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { transactAuthStore, sameCredential } from './auth-store-transactions.js';
6
+ export const LOGIN_NUDGE_TEXT = "My ChatGPT login expired. Send /login and I'll walk you through signing back in (takes about a minute).";
7
+ const DEAD = new Set(['refresh_token_reused', 'invalid_grant', 'refresh_token_expired', 'refresh_token_invalidated', 'expired_no_refresh']);
8
+ export function resolveLoginOwnerRoute(cfg = {}) {
9
+ const owners = cfg.commands?.ownerAllowFrom ?? [];
10
+ // Primary channel: first enabled configured transport; choose only a concrete owner,
11
+ // never a wildcard or group target. Account-specific allowFrom wins over base config.
12
+ for (const [channel, base] of Object.entries(cfg.channels ?? {})) {
13
+ if (!base || base.enabled === false || !['telegram', 'discord', 'slack', 'signal', 'whatsapp', 'imessage', 'mattermost', 'msteams', 'googlechat'].includes(channel)) continue;
14
+ const accounts = base.accounts ? Object.entries(base.accounts) : [[undefined, base]];
15
+ for (const [accountId, account] of accounts) {
16
+ if (account.enabled === false) continue;
17
+ const scoped = owners.filter(owner => typeof owner === 'string' && owner.startsWith(`${channel}:`)).map(owner => owner.slice(channel.length + 1));
18
+ const unscoped = owners.filter(owner => typeof owner === 'string' && !owner.includes(':'));
19
+ const candidates = scoped.length ? scoped : unscoped.length ? unscoped : account.allowFrom ?? account.dm?.allowFrom ?? base.allowFrom ?? base.dm?.allowFrom ?? [];
20
+ const to = candidates.map(String).map(id => id.replace(new RegExp(`^${channel}:`), '')).find(id => id && id !== '*' && !/^(?:group|channel|chat):/.test(id) && (channel !== 'telegram' || /^\d+$/.test(id)));
21
+ if (to) return { channel, to: channel === 'discord' && /^\d+$/.test(to) ? `user:${to}` : to, accountId };
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ export async function notifyDeadCodexChain(params = {}) {
27
+ if (!DEAD.has(params.reason)) return false;
28
+ const now = params.now?.() ?? Date.now();
29
+ // Persist reservation before sending: concurrent bundles/processes and restarts
30
+ // cannot produce a notification storm. Never mutate a credential in this path.
31
+ const result = await transactAuthStore(params.ownerPath, store => {
32
+ if (params.attempted && !sameCredential(store.profiles[params.profileId], params.attempted)) return false;
33
+ const prior = store.codexLoginNudges?.[params.profileId]?.at;
34
+ if (Number.isFinite(prior) && now - prior < 6 * 60 * 60 * 1000) return false;
35
+ store.codexLoginNudges ??= {};
36
+ store.codexLoginNudges[params.profileId] = { at: now };
37
+ return true;
38
+ }, params);
39
+ if (!result.result) return false;
40
+ const log = params.nudgeLog ?? console.info;
41
+ try {
42
+ let cfg = params.cfg;
43
+ if (!cfg) {
44
+ // Do not resolve some other installation's config when inspecting a fixture.
45
+ const configPath = process.env.JOHNNESS_CONFIG_PATH || path.join(params.stateDir, 'johnness.json');
46
+ if (fs.existsSync(configPath) && path.resolve(path.dirname(configPath)) === path.resolve(params.stateDir)) {
47
+ const { H: loadConfig } = await import('./auth-profiles-5CHn7vq1.js');
48
+ cfg = loadConfig();
49
+ }
50
+ }
51
+ const route = resolveLoginOwnerRoute(cfg);
52
+ if (!route) { log(`[codex-device-login] ${LOGIN_NUDGE_TEXT} (No owner route resolved.)`); return true; }
53
+ if (params.send) await params.send({ ...route, text: LOGIN_NUDGE_TEXT, cfg });
54
+ else {
55
+ const { deliverOutboundPayloads } = await import('./deliver-runtime-JZdz0coW.js');
56
+ await deliverOutboundPayloads({ ...route, cfg, payloads: [{ text: LOGIN_NUDGE_TEXT }] });
57
+ }
58
+ log('[codex-device-login] login-expired owner notice delivered');
59
+ } catch { log('[codex-device-login] Login-expired notice delivery failed. Send /login to fix it.'); }
60
+ return true;
61
+ }
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { notifyDeadCodexChain } from './codex-oauth-nudge.js';
1
4
  // JOHNNESS_PATCH_CODEX_OAUTH_OWNERSHIP_58
2
5
  import fs from 'node:fs';
3
6
  import os from 'node:os';
@@ -31,6 +34,10 @@ function resolveOwner(ctx,profileId) {
31
34
  if(credentialAccount(a) && credentialAccount(b))return {file:ctx.localPath,credential:b,main,local};
32
35
  throw new AuthOwnershipError('owner_conflict',{profileId});
33
36
  }
37
+ export function resolveCodexProfileOwner(params={}) {
38
+ const ctx=context(params),owner=resolveOwner(ctx,params.profileId);
39
+ return {...ctx,agentDir:path.dirname(owner.file)};
40
+ }
34
41
  function storeFiles(ctx) {
35
42
  const files=new Set([ctx.mainPath,ctx.localPath]);
36
43
  try {for(const ent of fs.readdirSync(path.join(ctx.stateDir,'agents'),{withFileTypes:true})){if(ent.isDirectory())files.add(canonicalPath(path.join(ctx.stateDir,'agents',ent.name,'agent/auth-profiles.json')));}}
@@ -62,6 +69,10 @@ function effective(ctx,store) {
62
69
  if(isManagedCodexCredential(owner.credential)) {
63
70
  // Read-only observation must not discard a positively bound newer local chain.
64
71
  const credential=owner.duplicate && owner.localCredential.expires>owner.credential.expires?owner.localCredential:owner.credential;
72
+ if(!sameCredential(result.profiles[id],credential) && result.usageStats){
73
+ const usage=readAuthStore(owner.file).usageStats?.[id];
74
+ if(usage)result.usageStats[id]=clone(usage);else delete result.usageStats[id];
75
+ }
65
76
  result.profiles[id]=clone(credential);
66
77
  const failure=readAuthStore(owner.file).codexAuthFailures?.[id];
67
78
  if(failure){result.codexAuthFailures??={};result.codexAuthFailures[id]=clone(failure);}
@@ -124,6 +135,7 @@ async function unavailable(ctx,profileId,attempted,reason) {
124
135
  if(store.lastGood)for(const provider of Object.keys(store.lastGood))if(store.lastGood[provider]===profileId)delete store.lastGood[provider];
125
136
  },ctx);
126
137
  publish(ctx,profileId);
138
+ void notifyDeadCodexChain({...ctx,profileId,attempted,reason}).catch(()=>{});
127
139
  }
128
140
  export async function refreshManagedCodexProfile(params={}) {
129
141
  const ctx=context(params),profileId=params.profileId;registerSnapshot(ctx,params.store);
@@ -138,8 +150,15 @@ export async function refreshManagedCodexProfile(params={}) {
138
150
  const store=readAuthStore(ctx.ownerPath),current=store.profiles[profileId];
139
151
  if(!compatible(attempted,current))throw new AuthOwnershipError('credential_changed',{profileId});
140
152
  attempted=clone(current);
141
- if(store.codexAuthFailures?.[profileId])throw new AuthOwnershipError('auth_unavailable',{profileId});
153
+ if(store.codexAuthFailures?.[profileId]){
154
+ void notifyDeadCodexChain({...ctx,profileId,attempted,reason:store.codexAuthFailures[profileId].reason}).catch(()=>{});
155
+ throw new AuthOwnershipError('auth_unavailable',{profileId});
156
+ }
142
157
  if(usable(attempted,ctx)){publish(ctx,profileId);log('adopted');return {apiKey:attempted.access,newCredentials:attempted};}
158
+ if(!attempted.refresh){
159
+ await unavailable(ctx,profileId,attempted,'expired_no_refresh');
160
+ throw new AuthOwnershipError('auth_unavailable',{profileId});
161
+ }
143
162
  let next;
144
163
  try {
145
164
  const result=ctx.refresh?await withRefreshDeadline(signal=>ctx.refresh(clone(attempted),{signal,profileId}),ctx):await refreshCodexToken({...ctx,credential:attempted,profileId});
@@ -220,6 +239,10 @@ export async function mutateAuthProfile(params={}) {
220
239
  const {store}=await transactAuthStore(owner.file,fresh=>{
221
240
  if(Object.hasOwn(params,'expected')&&!sameCredential(fresh.profiles[profileId],params.expected))throw new AuthOwnershipError('credential_changed',{profileId});
222
241
  if(params.credential===null)delete fresh.profiles[profileId];else fresh.profiles[profileId]=clone(params.credential);
242
+ if(params.clearLoginFailures){
243
+ if(fresh.usageStats)delete fresh.usageStats[profileId];
244
+ if(fresh.lastGood?.[PROVIDER]===profileId)delete fresh.lastGood[PROVIDER];
245
+ }
223
246
  if(fresh.codexAuthFailures)delete fresh.codexAuthFailures[profileId];
224
247
  },ctx);
225
248
  publish(ctx,profileId);return store;
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { l as escapeRegExp } from "./utils-DC4zYvW0.js";
2
4
  import { i as listThinkingLevels } from "./thinking-BYwvlJ3S.js";
3
5
  import { Gi as DEFAULT_PROVIDER, Wi as DEFAULT_MODEL, h as resolveConfiguredModelRef } from "./model-selection-L7RMwsG-.js";
@@ -179,6 +181,13 @@ function buildChatCommands() {
179
181
  captureRemaining: true
180
182
  }]
181
183
  }),
184
+ defineChatCommand({
185
+ key: "login",
186
+ nativeName: "login",
187
+ description: "Sign in to ChatGPT with a device code (owner only).",
188
+ textAliases: ["/login", "/relogin"],
189
+ category: "management"
190
+ }),
182
191
  defineChatCommand({
183
192
  key: "status",
184
193
  nativeName: "status",
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { D as isPlainObject, u as escapeRegExp } from "./logger-Bj0Xl6pn.js";
2
4
  import { Fr as DEFAULT_MODEL, Ir as DEFAULT_PROVIDER, l as resolveConfiguredModelRef, nn as getActivePluginRegistry } from "./model-selection-BGlGpPgM.js";
3
5
  import { f as listChannelDocks, r as listThinkingLevels } from "./thinking-CAzdgmNV.js";
@@ -216,6 +218,13 @@ function buildChatCommands() {
216
218
  captureRemaining: true
217
219
  }]
218
220
  }),
221
+ defineChatCommand({
222
+ key: "login",
223
+ nativeName: "login",
224
+ description: "Sign in to ChatGPT with a device code (owner only).",
225
+ textAliases: ["/login", "/relogin"],
226
+ category: "management"
227
+ }),
219
228
  defineChatCommand({
220
229
  key: "status",
221
230
  nativeName: "status",
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { D as isPlainObject, u as escapeRegExp } from "./logger-BW8uLq6f.js";
2
4
  import { Mr as DEFAULT_PROVIDER, jr as DEFAULT_MODEL, tn as getActivePluginRegistry, u as resolveConfiguredModelRef } from "./model-selection-BU6wl1le.js";
3
5
  import { f as listChannelDocks, r as listThinkingLevels } from "./thinking-B5B36ffe.js";
@@ -216,6 +218,13 @@ function buildChatCommands() {
216
218
  captureRemaining: true
217
219
  }]
218
220
  }),
221
+ defineChatCommand({
222
+ key: "login",
223
+ nativeName: "login",
224
+ description: "Sign in to ChatGPT with a device code (owner only).",
225
+ textAliases: ["/login", "/relogin"],
226
+ category: "management"
227
+ }),
219
228
  defineChatCommand({
220
229
  key: "status",
221
230
  nativeName: "status",
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { Bi as DEFAULT_PROVIDER, Or as resolveConfiguredModelRef, zi as DEFAULT_MODEL } from "./auth-profiles-5CHn7vq1.js";
2
4
  import { c as escapeRegExp } from "./utils-C5WN6czr.js";
3
5
  import { u as getActivePluginRegistry } from "./registry-DoLLbW4m.js";
@@ -179,6 +181,13 @@ function buildChatCommands() {
179
181
  captureRemaining: true
180
182
  }]
181
183
  }),
184
+ defineChatCommand({
185
+ key: "login",
186
+ nativeName: "login",
187
+ description: "Sign in to ChatGPT with a device code (owner only).",
188
+ textAliases: ["/login", "/relogin"],
189
+ category: "management"
190
+ }),
182
191
  defineChatCommand({
183
192
  key: "status",
184
193
  nativeName: "status",
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "./codex-oauth-device.js";
1
4
  import { E as normalizeLogLevel, a as logVerbose, c as shouldLogVerbose, k as resolvePreferredJohnnessTmpDir, m as getChildLogger, p as theme, t as danger, u as warn } from "./globals-Bv4ZcVWM.js";
2
5
  import { g as resolveStateDir, r as STATE_DIR, u as resolveGatewayPort, y as resolveRequiredHomeDir } from "./paths-BfR2LXbA.js";
3
6
  import { c as sanitizeForLog, d as createNonExitingRuntime, f as defaultRuntime, t as createSubsystemLogger } from "./subsystem-Cf9yS0UI.js";
@@ -40974,6 +40977,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
40974
40977
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
40975
40978
  }
40976
40979
  async function handleCommands(params) {
40980
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
40981
+ if (deviceLoginReply) return deviceLoginReply;
40977
40982
  if (HANDLERS === null) HANDLERS = [
40978
40983
  handlePluginCommand,
40979
40984
  handleBashCommand,
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { runDeviceLoginCli } from "./codex-oauth-device.js";
1
4
  import { t as __exportAll } from "./rolldown-runtime-DUslC3ob.js";
2
5
  import { d as colorize, f as isRich$1, p as theme } from "./globals-Bv4ZcVWM.js";
3
6
  import { t as CONFIG_PATH } from "./paths-BfR2LXbA.js";
@@ -336,6 +339,10 @@ async function runBuiltInOpenAICodexLogin(params) {
336
339
  else params.runtime.log(`Default model available: ${OPENAI_CODEX_DEFAULT_MODEL} (use --set-default to apply)`);
337
340
  }
338
341
  async function modelsAuthLoginCommand(opts, runtime) {
342
+ if (opts.device) {
343
+ const config = await loadValidConfigOrThrow();
344
+ return await runDeviceLoginCli(opts, runtime, { agentDir: resolveAgentDir(config, resolveDefaultAgentId(config)) });
345
+ }
339
346
  if (!process.stdin.isTTY) throw new Error("models auth login requires an interactive TTY.");
340
347
  const config = await loadValidConfigOrThrow();
341
348
  const defaultAgentId = resolveDefaultAgentId(config);
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { runDeviceLoginCli } from "./codex-oauth-device.js";
1
4
  import { t as CONFIG_PATH } from "./paths-BJV7vkaX.js";
2
5
  import { d as colorize, f as isRich$1, p as theme } from "./globals-BM8hKFm0.js";
3
6
  import { S as shortenHomePath } from "./utils-DC4zYvW0.js";
@@ -437,6 +440,10 @@ async function runBuiltInOpenAICodexLogin(params) {
437
440
  else params.runtime.log(`Default model available: ${OPENAI_CODEX_DEFAULT_MODEL} (use --set-default to apply)`);
438
441
  }
439
442
  async function modelsAuthLoginCommand(opts, runtime) {
443
+ if (opts.device) {
444
+ const config = await loadValidConfigOrThrow();
445
+ return await runDeviceLoginCli(opts, runtime, { agentDir: resolveAgentDir(config, resolveDefaultAgentId(config)) });
446
+ }
440
447
  if (!process.stdin.isTTY) throw new Error("models auth login requires an interactive TTY.");
441
448
  const config = await loadValidConfigOrThrow();
442
449
  const defaultAgentId = resolveDefaultAgentId(config);
@@ -2586,9 +2593,10 @@ function registerModelsCli(program) {
2586
2593
  await modelsAuthAddCommand({}, defaultRuntime);
2587
2594
  });
2588
2595
  });
2589
- auth.command("login").description("Run a provider plugin auth flow (OAuth/API key)").option("--provider <id>", "Provider id registered by a plugin").option("--method <id>", "Provider auth method id").option("--set-default", "Apply the provider's default model recommendation", false).action(async (opts) => {
2596
+ auth.command("login").description("Run a provider plugin auth flow (OAuth/API key)").option("--device", "Sign in to ChatGPT using a headless device code", false).option("--provider <id>", "Provider id registered by a plugin").option("--method <id>", "Provider auth method id").option("--set-default", "Apply the provider's default model recommendation", false).action(async (opts) => {
2590
2597
  await runModelsCommand(async () => {
2591
2598
  await modelsAuthLoginCommand({
2599
+ device: Boolean(opts.device),
2592
2600
  provider: opts.provider,
2593
2601
  method: opts.method,
2594
2602
  setDefault: Boolean(opts.setDefault)
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { p as theme } from "./globals-Bv4ZcVWM.js";
2
4
  import "./paths-BfR2LXbA.js";
3
5
  import { f as defaultRuntime } from "./subsystem-Cf9yS0UI.js";
@@ -246,9 +248,10 @@ function registerModelsCli(program) {
246
248
  await modelsAuthAddCommand({}, defaultRuntime);
247
249
  });
248
250
  });
249
- auth.command("login").description("Run a provider plugin auth flow (OAuth/API key)").option("--provider <id>", "Provider id registered by a plugin").option("--method <id>", "Provider auth method id").option("--set-default", "Apply the provider's default model recommendation", false).action(async (opts) => {
251
+ auth.command("login").description("Run a provider plugin auth flow (OAuth/API key)").option("--device", "Sign in to ChatGPT using a headless device code", false).option("--provider <id>", "Provider id registered by a plugin").option("--method <id>", "Provider auth method id").option("--set-default", "Apply the provider's default model recommendation", false).action(async (opts) => {
250
252
  await runModelsCommand(async () => {
251
253
  await modelsAuthLoginCommand({
254
+ device: Boolean(opts.device),
252
255
  provider: opts.provider,
253
256
  method: opts.method,
254
257
  setDefault: Boolean(opts.setDefault)
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "./codex-oauth-device.js";
1
4
  // JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59
2
5
  import { _ as normalizeStringEntries, a as resolveAgentDir, b as resolveAgentModelPrimaryValue, c as resolveAgentWorkspaceDir, d as resolveRunModelFallbacksOverride, f as resolveSessionAgentId, g as normalizeHyphenSlug, h as normalizeAtHashSlug, i as resolveAgentConfig, l as resolveDefaultAgentId, n as hasConfiguredModelFallbacks, p as resolveSessionAgentIds, r as listAgentIds, s as resolveAgentSkillsFilter, t as runTasksWithConcurrency, u as resolveEffectiveModelFallbacks, v as normalizeStringEntriesLower, x as toAgentModelListLike, y as resolveAgentModelFallbackValues } from "./run-with-concurrency-Cuc1THN9.js";
3
6
  import { a as resolveGatewayPort, c as resolveStateDir, d as resolveRequiredHomeDir, l as expandHomePrefix, n as STATE_DIR, r as resolveConfigPath } from "./paths-hfkBoC7i.js";
@@ -33951,6 +33954,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
33951
33954
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
33952
33955
  }
33953
33956
  async function handleCommands(params) {
33957
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
33958
+ if (deviceLoginReply) return deviceLoginReply;
33954
33959
  if (HANDLERS === null) HANDLERS = [
33955
33960
  handlePluginCommand,
33956
33961
  handleBashCommand,
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "./codex-oauth-device.js";
1
4
  // JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59
2
5
  import { $ as isValidAgentId, C as filterBootstrapFilesForSession, D as runCommandWithTimeout, G as normalizeStringEntries, J as DEFAULT_MAIN_KEY, K as normalizeStringEntriesLower, M as openBoundaryFileSync, O as runExec, Q as classifySessionKeyShape, S as ensureAgentWorkspace, T as resolveJohnnessPackageRoot, U as normalizeAtHashSlug, V as normalizeWindowsPathForComparison, W as normalizeHyphenSlug, X as buildAgentPeerSessionKey, Y as buildAgentMainSessionKey, _t as resolveAgentModelFallbackValues, a as resolveAgentDir, at as scopedHeartbeatWakeOptions, c as resolveAgentWorkspaceDir, ct as normalizeOptionalAccountId, d as resolveRunModelFallbacksOverride, dt as getSubagentDepth, et as normalizeAgentId, f as resolveSessionAgentId, ft as isAcpSessionKey, gt as resolveThreadParentSessionKey, h as DEFAULT_AGENT_WORKSPACE_DIR, ht as parseAgentSessionKey, i as resolveAgentConfig, it as sanitizeAgentId, j as openBoundaryFile, k as spawnWithFallback, l as resolveDefaultAgentId, lt as isBlockedObjectKey, mt as isSubagentSessionKey, n as hasConfiguredModelFallbacks, nt as resolveAgentIdFromSessionKey, ot as DEFAULT_ACCOUNT_ID, p as resolveSessionAgentIds, pt as isCronSessionKey, q as DEFAULT_AGENT_ID, r as listAgentIds, rt as resolveThreadSessionKeys, s as resolveAgentSkillsFilter, st as normalizeAccountId$2, t as runTasksWithConcurrency, tt as normalizeMainKey, u as resolveEffectiveModelFallbacks, ut as deriveSessionChatType, v as DEFAULT_IDENTITY_FILENAME, vt as resolveAgentModelPrimaryValue, w as loadWorkspaceBootstrapFiles, yt as toAgentModelListLike } from "./run-with-concurrency-BXSUl5Nj.js";
3
6
  import { a as resolveGatewayPort, c as resolveStateDir, d as resolveRequiredHomeDir, l as expandHomePrefix, n as STATE_DIR, r as resolveConfigPath } from "./paths-GBpjI3o0.js";
@@ -33947,6 +33950,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
33947
33950
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
33948
33951
  }
33949
33952
  async function handleCommands(params) {
33953
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
33954
+ if (deviceLoginReply) return deviceLoginReply;
33950
33955
  if (HANDLERS === null) HANDLERS = [
33951
33956
  handlePluginCommand,
33952
33957
  handleBashCommand,
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { It as getActivePluginRegistry, Nr as DEFAULT_MODEL, Pr as DEFAULT_PROVIDER, or as resolveConfiguredModelRef } from "./model-auth-Ciehz0x5.js";
2
4
  import { D as isPlainObject, u as escapeRegExp } from "./logger-Cxu-Klb_.js";
3
5
  import { f as listChannelDocks, r as listThinkingLevels } from "./thinking-WQ4Zq8Nl.js";
@@ -216,6 +218,13 @@ function buildChatCommands() {
216
218
  captureRemaining: true
217
219
  }]
218
220
  }),
221
+ defineChatCommand({
222
+ key: "login",
223
+ nativeName: "login",
224
+ description: "Sign in to ChatGPT with a device code (owner only).",
225
+ textAliases: ["/login", "/relogin"],
226
+ category: "management"
227
+ }),
219
228
  defineChatCommand({
220
229
  key: "status",
221
230
  nativeName: "status",
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { D as isPlainObject, u as escapeRegExp } from "./logger-3VRkTvTU.js";
2
4
  import { f as listChannelDocks, r as listThinkingLevels } from "./thinking-B-rIrY_n.js";
3
5
  import { Dn as resolveConfiguredModelRef, Xn as DEFAULT_PROVIDER, Yn as DEFAULT_MODEL, mr as getActivePluginRegistry } from "./model-auth-CX9cPHdC.js";
@@ -216,6 +218,13 @@ function buildChatCommands() {
216
218
  captureRemaining: true
217
219
  }]
218
220
  }),
221
+ defineChatCommand({
222
+ key: "login",
223
+ nativeName: "login",
224
+ description: "Sign in to ChatGPT with a device code (owner only).",
225
+ textAliases: ["/login", "/relogin"],
226
+ category: "management"
227
+ }),
219
228
  defineChatCommand({
220
229
  key: "status",
221
230
  nativeName: "status",
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { Ir as DEFAULT_MODEL, Lr as DEFAULT_PROVIDER, Rt as getActivePluginRegistry, cr as resolveConfiguredModelRef } from "./model-auth-DVyo6JSX.js";
2
4
  import { D as isPlainObject, u as escapeRegExp } from "./logger-By5InJr-.js";
3
5
  import { f as listChannelDocks, r as listThinkingLevels } from "./thinking-WXqkGljb.js";
@@ -216,6 +218,13 @@ function buildChatCommands() {
216
218
  captureRemaining: true
217
219
  }]
218
220
  }),
221
+ defineChatCommand({
222
+ key: "login",
223
+ nativeName: "login",
224
+ description: "Sign in to ChatGPT with a device code (owner only).",
225
+ textAliases: ["/login", "/relogin"],
226
+ category: "management"
227
+ }),
219
228
  defineChatCommand({
220
229
  key: "status",
221
230
  nativeName: "status",
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { C as isPlainObject, o as escapeRegExp } from "./utils-B0IyLNx9.js";
2
4
  import { r as normalizeChannelId } from "./plugins-BQilE2mk.js";
3
5
  import { Ar as DEFAULT_MODEL, Mt as getActivePluginRegistry, jr as DEFAULT_PROVIDER, nr as resolveConfiguredModelRef } from "./model-auth-kiHYHGrs.js";
@@ -216,6 +218,13 @@ function buildChatCommands() {
216
218
  captureRemaining: true
217
219
  }]
218
220
  }),
221
+ defineChatCommand({
222
+ key: "login",
223
+ nativeName: "login",
224
+ description: "Sign in to ChatGPT with a device code (owner only).",
225
+ textAliases: ["/login", "/relogin"],
226
+ category: "management"
227
+ }),
219
228
  defineChatCommand({
220
229
  key: "status",
221
230
  nativeName: "status",
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { Dt as getActivePluginRegistry, Rn as resolveConfiguredModelRef, ii as DEFAULT_PROVIDER, ri as DEFAULT_MODEL } from "./config-4Fgm-yEH.js";
2
4
  import { F as isPlainObject, v as escapeRegExp } from "./logger-U3s76KST.js";
3
5
  import { f as listChannelDocks, r as listThinkingLevels } from "./thinking-pTdxk2dr.js";
@@ -216,6 +218,13 @@ function buildChatCommands() {
216
218
  captureRemaining: true
217
219
  }]
218
220
  }),
221
+ defineChatCommand({
222
+ key: "login",
223
+ nativeName: "login",
224
+ description: "Sign in to ChatGPT with a device code (owner only).",
225
+ textAliases: ["/login", "/relogin"],
226
+ category: "management"
227
+ }),
219
228
  defineChatCommand({
220
229
  key: "status",
221
230
  nativeName: "status",
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { Kt as resolveConfiguredModelRef, Xn as DEFAULT_MODEL, Zn as DEFAULT_PROVIDER, yr as getActivePluginRegistry } from "./config-Di-lyCdh.js";
2
4
  import { D as isPlainObject, u as escapeRegExp } from "./logger-Blr-bUxJ.js";
3
5
  import { r as normalizeChannelId } from "./plugins-Bv1h7dAV.js";
@@ -216,6 +218,13 @@ function buildChatCommands() {
216
218
  captureRemaining: true
217
219
  }]
218
220
  }),
221
+ defineChatCommand({
222
+ key: "login",
223
+ nativeName: "login",
224
+ description: "Sign in to ChatGPT with a device code (owner only).",
225
+ textAliases: ["/login", "/relogin"],
226
+ category: "management"
227
+ }),
219
228
  defineChatCommand({
220
229
  key: "status",
221
230
  nativeName: "status",
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "../codex-oauth-device.js";
1
4
  // JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59
2
5
  import { $ as buildAgentPeerSessionKey, C as filterBootstrapFilesForSession, D as runCommandWithTimeout, G as normalizeStringEntries, J as resolveAgentModelPrimaryValue, K as normalizeStringEntriesLower, M as openBoundaryFileSync, O as runExec, Q as buildAgentMainSessionKey, S as ensureAgentWorkspace, T as resolveJohnnessPackageRoot, U as normalizeAtHashSlug, V as normalizeWindowsPathForComparison, W as normalizeHyphenSlug, X as DEFAULT_AGENT_ID, Y as toAgentModelListLike, Z as DEFAULT_MAIN_KEY, _t as isSubagentSessionKey, a as resolveAgentDir, at as resolveAgentIdFromSessionKey, c as resolveAgentWorkspaceDir, ct as scopedHeartbeatWakeOptions, d as resolveRunModelFallbacksOverride, dt as normalizeOptionalAccountId, f as resolveSessionAgentId, ft as isBlockedObjectKey, gt as isCronSessionKey, h as DEFAULT_AGENT_WORKSPACE_DIR, ht as isAcpSessionKey, i as resolveAgentConfig, it as normalizeMainKey, j as openBoundaryFile, k as spawnWithFallback, l as resolveDefaultAgentId, lt as DEFAULT_ACCOUNT_ID, mt as getSubagentDepth, n as hasConfiguredModelFallbacks, nt as isValidAgentId, ot as resolveThreadSessionKeys, p as resolveSessionAgentIds, pt as deriveSessionChatType, q as resolveAgentModelFallbackValues, r as listAgentIds, rt as normalizeAgentId, s as resolveAgentSkillsFilter, st as sanitizeAgentId, t as runTasksWithConcurrency, tt as classifySessionKeyShape, u as resolveEffectiveModelFallbacks, ut as normalizeAccountId$2, v as DEFAULT_IDENTITY_FILENAME, vt as parseAgentSessionKey, w as loadWorkspaceBootstrapFiles, yt as resolveThreadParentSessionKey } from "./run-with-concurrency-XmKq-xNS.js";
3
6
  import { a as getWebAuthAgeMs, d as readWebSelfId, g as resolveAccountEntry, h as webAuthExists, n as resolveWhatsAppAccount, o as logWebSelfId, s as logoutWeb, t as hasAnyWhatsAppAuth } from "./accounts-D-pSCe1E.js";
@@ -96313,6 +96316,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
96313
96316
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
96314
96317
  }
96315
96318
  async function handleCommands(params) {
96319
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
96320
+ if (deviceLoginReply) return deviceLoginReply;
96316
96321
  if (HANDLERS === null) HANDLERS = [
96317
96322
  handlePluginCommand,
96318
96323
  handleBashCommand,
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "../codex-oauth-device.js";
1
4
  // JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59
2
5
  import { A as mediaKindFromMime, C as imageMimeFromFormat, E as kindFromMime, O as MAX_IMAGE_BYTES, T as isGifMedia, a as isMarkdownCapableMessageChannel, b as detectMime, c as resolveGatewayMessageChannel, d as GATEWAY_CLIENT_MODES, f as GATEWAY_CLIENT_NAMES, g as getImageMetadata, i as isInternalMessageChannel, l as resolveMessageChannel, n as isDeliverableMessageChannel, o as listDeliverableMessageChannels, r as isGatewayMessageChannel, s as normalizeMessageChannel, t as INTERNAL_MESSAGE_CHANNEL, u as GATEWAY_CLIENT_IDS, w as isAudioFileName, x as extensionForMime, y as resizeToJpeg } from "./message-channel-DfmYEnql.js";
3
6
  import { B as shouldLogVerbose, C as isPlainObject, D as logWarn, E as logInfo, F as unregisterActiveProgressLine, G as theme, H as warn, I as danger, K as getChildLogger, M as defaultRuntime, N as clearActiveProgressLine, O as createSubsystemLogger, P as registerActiveProgressLine, R as logVerbose, S as truncateUtf16Safe, T as logError, U as colorize, W as isRich, X as resolvePreferredJohnnessTmpDir, Y as normalizeLogLevel, Z as consumeRootOptionToken, _ as shortenHomeInString, a as ensureDir$3, b as sliceUtf16Safe, c as isRecord$1, d as normalizeE164, h as resolveUserPath, j as createNonExitingRuntime, k as sanitizeForLog, o as escapeRegExp, r as clampInt, s as formatTerminalLink, t as CONFIG_DIR, v as shortenHomePath, w as logDebug, y as sleep$1, z as setVerbose } from "./utils-B0IyLNx9.js";
@@ -96313,6 +96316,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
96313
96316
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
96314
96317
  }
96315
96318
  async function handleCommands(params) {
96319
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
96320
+ if (deviceLoginReply) return deviceLoginReply;
96316
96321
  if (HANDLERS === null) HANDLERS = [
96317
96322
  handlePluginCommand,
96318
96323
  handleBashCommand,
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "../codex-oauth-device.js";
1
4
  // JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59
2
5
  import { $ as buildAgentPeerSessionKey, C as filterBootstrapFilesForSession, D as runCommandWithTimeout, G as normalizeStringEntries, J as resolveAgentModelPrimaryValue, K as normalizeStringEntriesLower, M as openBoundaryFileSync, O as runExec, Q as buildAgentMainSessionKey, S as ensureAgentWorkspace, T as resolveJohnnessPackageRoot, U as normalizeAtHashSlug, V as normalizeWindowsPathForComparison, W as normalizeHyphenSlug, X as DEFAULT_AGENT_ID, Y as toAgentModelListLike, Z as DEFAULT_MAIN_KEY, _t as isSubagentSessionKey, a as resolveAgentDir, at as resolveAgentIdFromSessionKey, c as resolveAgentWorkspaceDir, ct as scopedHeartbeatWakeOptions, d as resolveRunModelFallbacksOverride, dt as normalizeOptionalAccountId, f as resolveSessionAgentId, ft as isBlockedObjectKey, gt as isCronSessionKey, h as DEFAULT_AGENT_WORKSPACE_DIR, ht as isAcpSessionKey, i as resolveAgentConfig, it as normalizeMainKey, j as openBoundaryFile, k as spawnWithFallback, l as resolveDefaultAgentId, lt as DEFAULT_ACCOUNT_ID, mt as getSubagentDepth, n as hasConfiguredModelFallbacks, nt as isValidAgentId, ot as resolveThreadSessionKeys, p as resolveSessionAgentIds, pt as deriveSessionChatType, q as resolveAgentModelFallbackValues, r as listAgentIds, rt as normalizeAgentId, s as resolveAgentSkillsFilter, st as sanitizeAgentId, t as runTasksWithConcurrency, tt as classifySessionKeyShape, u as resolveEffectiveModelFallbacks, ut as normalizeAccountId$2, v as DEFAULT_IDENTITY_FILENAME, vt as parseAgentSessionKey, w as loadWorkspaceBootstrapFiles, yt as resolveThreadParentSessionKey } from "./run-with-concurrency-BMh-rgTO.js";
3
6
  import { c as getWebAuthAgeMs, i as resolveWhatsAppAccount, l as logWebSelfId, m as readWebSelfId, t as hasAnyWhatsAppAuth, u as logoutWeb, v as webAuthExists, y as resolveAccountEntry } from "./accounts-B2lcx0zq.js";
@@ -96627,6 +96630,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
96627
96630
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
96628
96631
  }
96629
96632
  async function handleCommands(params) {
96633
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
96634
+ if (deviceLoginReply) return deviceLoginReply;
96630
96635
  if (HANDLERS === null) HANDLERS = [
96631
96636
  handlePluginCommand,
96632
96637
  handleBashCommand,
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "../codex-oauth-device.js";
1
4
  // JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59
2
5
  import { $ as isValidAgentId, C as filterBootstrapFilesForSession, D as runCommandWithTimeout, G as normalizeStringEntries, J as DEFAULT_MAIN_KEY, K as normalizeStringEntriesLower, M as openBoundaryFileSync, O as runExec, Q as classifySessionKeyShape, S as ensureAgentWorkspace, T as resolveJohnnessPackageRoot, U as normalizeAtHashSlug, V as normalizeWindowsPathForComparison, W as normalizeHyphenSlug, X as buildAgentPeerSessionKey, Y as buildAgentMainSessionKey, _t as resolveAgentModelFallbackValues, a as resolveAgentDir, at as scopedHeartbeatWakeOptions, c as resolveAgentWorkspaceDir, ct as normalizeOptionalAccountId, d as resolveRunModelFallbacksOverride, dt as getSubagentDepth, et as normalizeAgentId, f as resolveSessionAgentId, ft as isAcpSessionKey, gt as resolveThreadParentSessionKey, h as DEFAULT_AGENT_WORKSPACE_DIR, ht as parseAgentSessionKey, i as resolveAgentConfig, it as sanitizeAgentId, j as openBoundaryFile, k as spawnWithFallback, l as resolveDefaultAgentId, lt as isBlockedObjectKey, mt as isSubagentSessionKey, n as hasConfiguredModelFallbacks, nt as resolveAgentIdFromSessionKey, ot as DEFAULT_ACCOUNT_ID, p as resolveSessionAgentIds, pt as isCronSessionKey, q as DEFAULT_AGENT_ID, r as listAgentIds, rt as resolveThreadSessionKeys, s as resolveAgentSkillsFilter, st as normalizeAccountId$2, t as runTasksWithConcurrency, tt as normalizeMainKey, u as resolveEffectiveModelFallbacks, ut as deriveSessionChatType, v as DEFAULT_IDENTITY_FILENAME, vt as resolveAgentModelPrimaryValue, w as loadWorkspaceBootstrapFiles, yt as toAgentModelListLike } from "./run-with-concurrency-DCb27DCP.js";
3
6
  import { a as resolveGatewayPort, c as resolveStateDir, d as resolveRequiredHomeDir, l as expandHomePrefix, n as STATE_DIR, r as resolveConfigPath } from "./paths-B9fwHuf0.js";
@@ -96354,6 +96357,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
96354
96357
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
96355
96358
  }
96356
96359
  async function handleCommands(params) {
96360
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
96361
+ if (deviceLoginReply) return deviceLoginReply;
96357
96362
  if (HANDLERS === null) HANDLERS = [
96358
96363
  handlePluginCommand,
96359
96364
  handleBashCommand,
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "../codex-oauth-device.js";
1
4
  // JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59
2
5
  import { $ as stripUndefinedFields, A as unpinMessageDiscord, At as resolveAllowlistProviderRuntimeGroupPolicy, B as listScheduledEventsDiscord, C as editMessageDiscord, Ct as hasAnyGuildPermissionDiscord, D as pinMessageDiscord, Dt as evaluateGroupRouteAccessForPolicy, E as listThreadsDiscord, Et as chunkDiscordTextWithMode, F as fetchMemberInfoDiscord, Ft as buildChannelKeyCandidates, G as uploadStickerDiscord, H as timeoutMemberDiscord, I as fetchRoleInfoDiscord, It as normalizeChannelSlug, J as editChannelDiscord, K as createChannelDiscord, L as fetchVoiceStatusDiscord, Lt as resolveChannelEntryMatchWithFallback, M as banMemberDiscord, Mt as resolveOpenProviderRuntimeGroupPolicy, N as createScheduledEventDiscord, Nt as warnMissingProviderGroupPolicyFallbackOnce, O as readMessagesDiscord, Ot as evaluateMatchedGroupAccessForPolicy, P as fetchChannelInfoDiscord, Pt as applyChannelMatchMeta, Q as sendDiscordText, R as kickMemberDiscord, S as deleteMessageDiscord, St as fetchChannelPermissionsDiscord, T as listPinsDiscord, Tt as createDiscordRestClient, U as listGuildEmojisDiscord, V as removeRoleDiscord, W as uploadEmojiDiscord, X as removeChannelPermissionDiscord, Y as moveChannelDiscord, Z as setChannelPermissionDiscord, _ as sendStickerDiscord, _t as formatDiscordUserTag, a as sendDiscordComponentMessage, at as normalizeDiscordAllowList, b as formatMention, bt as DiscordApiError, c as parseDiscordComponentCustomId, ct as resolveDiscordChannelConfigWithFallback, d as parseDiscordModalCustomIdForCarbon, dt as resolveDiscordOwnerAccess, et as parseDiscordTarget, f as readDiscordComponentSpec, ft as resolveDiscordOwnerAllowFrom, g as sendPollDiscord, gt as formatDiscordReactionEmoji, h as sendMessageDiscord, ht as shouldEmitDiscordReactionNotification, i as removeReactionDiscord, it as isDiscordGroupAllowedByPolicy, j as addRoleDiscord, jt as resolveDefaultGroupPolicy, k as searchMessagesDiscord, kt as GROUP_POLICY_BLOCKED_LABEL, l as parseDiscordComponentCustomIdForCarbon, lt as resolveDiscordGuildEntry, m as resolveDiscordModalEntry, mt as resolveGroupDmAllow, n as reactMessageDiscord, nt as listDiscordDirectoryGroupsLive, o as createDiscordFormModal, ot as normalizeDiscordSlug, p as resolveDiscordComponentEntry, pt as resolveDiscordShouldRequireMention, q as deleteChannelDiscord, r as removeOwnReactionsDiscord, rt as listDiscordDirectoryPeersLive, s as formatDiscordComponentEventText, st as resolveDiscordAllowListMatch, t as fetchReactionsDiscord, tt as resolveDiscordChannelId, u as parseDiscordModalCustomId, ut as resolveDiscordMemberAccessState, v as sendVoiceMessageDiscord, vt as resolveDiscordSystemLocation, w as fetchMessageDiscord, wt as createDiscordClient, x as createThreadDiscord, xt as fetchDiscord, y as sendWebhookMessageDiscord, yt as resolveTimestampMs, z as listGuildChannelsDiscord } from "./send-Dy8sRjb5.js";
3
6
  import { $ as buildAgentPeerSessionKey, C as filterBootstrapFilesForSession, D as runCommandWithTimeout, G as normalizeStringEntries, J as resolveAgentModelPrimaryValue, K as normalizeStringEntriesLower, M as openBoundaryFileSync, O as runExec, Q as buildAgentMainSessionKey, S as ensureAgentWorkspace, T as resolveJohnnessPackageRoot, U as normalizeAtHashSlug, V as normalizeWindowsPathForComparison, W as normalizeHyphenSlug, X as DEFAULT_AGENT_ID, Y as toAgentModelListLike, Z as DEFAULT_MAIN_KEY, _t as isSubagentSessionKey, a as resolveAgentDir, at as resolveAgentIdFromSessionKey, c as resolveAgentWorkspaceDir, ct as scopedHeartbeatWakeOptions, d as resolveRunModelFallbacksOverride, dt as normalizeOptionalAccountId, f as resolveSessionAgentId, ft as isBlockedObjectKey, gt as isCronSessionKey, h as DEFAULT_AGENT_WORKSPACE_DIR, ht as isAcpSessionKey, i as resolveAgentConfig, it as normalizeMainKey, j as openBoundaryFile, k as spawnWithFallback, l as resolveDefaultAgentId, lt as DEFAULT_ACCOUNT_ID, mt as getSubagentDepth, n as hasConfiguredModelFallbacks, nt as isValidAgentId, ot as resolveThreadSessionKeys, p as resolveSessionAgentIds, pt as deriveSessionChatType, q as resolveAgentModelFallbackValues, r as listAgentIds, rt as normalizeAgentId, s as resolveAgentSkillsFilter, st as sanitizeAgentId, t as runTasksWithConcurrency, tt as classifySessionKeyShape, u as resolveEffectiveModelFallbacks, ut as normalizeAccountId$2, v as DEFAULT_IDENTITY_FILENAME, vt as parseAgentSessionKey, w as loadWorkspaceBootstrapFiles, yt as resolveThreadParentSessionKey } from "./run-with-concurrency-D4zr_gVN.js";
@@ -96313,6 +96316,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
96313
96316
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
96314
96317
  }
96315
96318
  async function handleCommands(params) {
96319
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
96320
+ if (deviceLoginReply) return deviceLoginReply;
96316
96321
  if (HANDLERS === null) HANDLERS = [
96317
96322
  handlePluginCommand,
96318
96323
  handleBashCommand,
@@ -1,3 +1,5 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
1
3
  import { $ as findOverlappingPluginHttpRoute, A as parseCanonicalIpAddress, B as resolveMemorySlotDecision, Bt as resolvePreferredJohnnessTmpDir, C as isBlockedSpecialUseIpv6Address, Ct as normalizeStringEntries, D as isLegacyIpv4Literal, Dt as normalizeE164, E as isIpv4Address, Et as isRecord, F as resolveProcessScopedMap, Ft as normalizeOptionalAccountId, G as normalizeChatChannelId, Gt as resolveRequiredHomeDir, H as MANIFEST_KEY, Ht as resolveOAuthDir, I as isPathInsideWithRealpath, It as resolveThreadParentSessionKey, J as isValidFileSecretRefId, K as isTruthyEnvValue, L as loadPluginManifestRegistry, Lt as createSubsystemLogger, M as applyConfigEnvVars, Mt as resolveThreadSessionKeys, Nt as DEFAULT_ACCOUNT_ID, O as isLoopbackIpAddress, Ot as resolveUserPath, P as resolveJohnnessAgentDir, Pt as normalizeAccountId, Q as requireActivePluginRegistry, R as normalizePluginsConfig, Rt as logVerbose, S as isBlockedSpecialUseIpv4Address, St as normalizeHyphenSlug, T as isIpInCidr, Tt as escapeRegExp, U as CHAT_CHANNEL_ORDER, Ut as resolveStateDir, V as LEGACY_MANIFEST_KEYS, Vt as consumeRootOptionToken, W as getChatChannelMeta, Wt as expandHomePrefix, X as getActivePluginRegistry, Y as resolveDefaultSecretProviderAlias, Z as getActivePluginRegistryVersion, _ as DmPolicySchema, _t as isNotFoundPathError, a as resolveModelRefFromString, at as normalizeResolvedSecretInputString, b as requireOpenAllowFrom, bt as normalizeSkillFilter, c as resolveImplicitCopilotProvider, ct as resolveAgentConfig, d as resolveSecretRefString, dt as resolveDefaultAgentId, et as normalizePluginHttpPath, f as encodeJsonPointerToken, ft as resolveJohnnessPackageRootSync, g as BlockStreamingCoalesceSchema, gt as hasNodeErrorCode, h as loadConfig, ht as sameFileIdentity, i as resolveDefaultModelForAgent, it as isValidEnvSecretRefId, j as parseLooseIpAddress, jt as normalizeAgentId, k as normalizeIpAddress, kt as safeParseJson, l as resolveImplicitProviders, lt as resolveAgentSkillsFilter, m as getRuntimeConfigSourceSnapshot, mt as logWarn, n as buildModelAliasIndex, nt as coerceSecretRef, o as normalizeProviders, ot as normalizeSecretInputString, p as getRuntimeConfigSnapshot, pt as runExec, q as parseBooleanValue, r as normalizeProviderId, rt as hasConfiguredSecretInput, s as resolveImplicitBedrockProvider, st as listAgentIds, t as buildAllowedModelSet, tt as formatCliCommand, u as isNonSecretApiKeyMarker, ut as resolveAgentWorkspaceDir, v as GroupPolicySchema, vt as isPathInside, w as isCanonicalDottedDecimalIPv4, wt as CONFIG_DIR, x as extractEmbeddedIpv4FromIpv6, xt as normalizeAtHashSlug, y as MarkdownConfigSchema, yt as isSymlinkOpenError, z as resolveEffectiveEnableState, zt as shouldLogVerbose } from "./model-selection-Dbj4jDYM.js";
2
4
  import path from "node:path";
3
5
  import fs, { constants } from "node:fs";
@@ -3264,6 +3266,13 @@ function buildChatCommands() {
3264
3266
  captureRemaining: true
3265
3267
  }]
3266
3268
  }),
3269
+ defineChatCommand({
3270
+ key: "login",
3271
+ nativeName: "login",
3272
+ description: "Sign in to ChatGPT with a device code (owner only).",
3273
+ textAliases: ["/login", "/relogin"],
3274
+ category: "management"
3275
+ }),
3267
3276
  defineChatCommand({
3268
3277
  key: "status",
3269
3278
  nativeName: "status",
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "../codex-oauth-device.js";
1
4
  // JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59
2
5
  import { $ as buildAgentPeerSessionKey, C as filterBootstrapFilesForSession, D as runCommandWithTimeout, G as toAgentModelListLike, J as normalizeStringEntries, K as normalizeAtHashSlug, M as openBoundaryFileSync, O as runExec, Q as buildAgentMainSessionKey, S as ensureAgentWorkspace, T as resolveJohnnessPackageRoot, U as resolveAgentModelFallbackValues, V as normalizeWindowsPathForComparison, W as resolveAgentModelPrimaryValue, X as DEFAULT_AGENT_ID, Y as normalizeStringEntriesLower, Z as DEFAULT_MAIN_KEY, _t as isSubagentSessionKey, a as resolveAgentDir, at as resolveAgentIdFromSessionKey, c as resolveAgentWorkspaceDir, ct as scopedHeartbeatWakeOptions, d as resolveRunModelFallbacksOverride, dt as normalizeOptionalAccountId, f as resolveSessionAgentId, ft as isBlockedObjectKey, gt as isCronSessionKey, h as DEFAULT_AGENT_WORKSPACE_DIR, ht as isAcpSessionKey, i as resolveAgentConfig, it as normalizeMainKey, j as openBoundaryFile, k as spawnWithFallback, l as resolveDefaultAgentId, lt as DEFAULT_ACCOUNT_ID, mt as getSubagentDepth, n as hasConfiguredModelFallbacks, nt as isValidAgentId, ot as resolveThreadSessionKeys, p as resolveSessionAgentIds, pt as deriveSessionChatType, q as normalizeHyphenSlug, r as listAgentIds, rt as normalizeAgentId, s as resolveAgentSkillsFilter, st as sanitizeAgentId, t as runTasksWithConcurrency, tt as classifySessionKeyShape, u as resolveEffectiveModelFallbacks, ut as normalizeAccountId$2, v as DEFAULT_IDENTITY_FILENAME, vt as parseAgentSessionKey, w as loadWorkspaceBootstrapFiles, yt as resolveThreadParentSessionKey } from "./run-with-concurrency-8rEOAFIb.js";
3
6
  import { $ as resetConfigOverrides, $n as getShellPathFromLoginShell, An as describeUnknownError$1, Ar as listPluginCommands, At as mapStreamingModeToSlackLegacyDraftStreamMode, B as isSupportedLocalAvatarExtension, Bn as resolveJohnnessAgentDir, Bt as buildModelAliasIndex, C as parseDurationMs, Cn as markAuthProfileFailure, Cr as setActivePluginRegistry, Ct as unwrapKnownShellMultiplexerInvocation, Dn as resolveSecretRefString, Dr as clearPluginCommands, Dt as resolveSafeBinProfiles, En as resolveApiKeyForProfile, Er as normalizePluginHttpPath, Et as normalizeSafeBinProfileFixtures, Fn as dedupeProfileIds, Fr as resolveContextEngine, G as safeStatSync, Gn as resolveAuthProfileDisplayLabel, Gt as normalizeProviderId, H as loadPluginManifestRegistry, Ht as isCliProvider, In as listProfilesForProvider, Ir as formatCliCommand, J as applyTestPluginDefaults, Jn as splitTrailingAuthProfile, Jt as resolveModelRefFromString, Kt as resolveConfiguredModelRef, L as isAvatarDataUrl, Ln as markAuthProfileGood, Lr as resolveCliName, Lt as resolveAgentMaxConcurrent, Mn as isRecord$1, Mr as createInternalHookEvent, Mt as resolveSlackNativeStreaming, Nn as parseDotPath, Nr as triggerInternalHook, Nt as resolveSlackStreamingMode, On as resolveSecretRefValue, Or as executePluginCommand, Ot as applyMergePatch, Pr as registerContextEngine, Q as getConfigOverrides, Qn as VERSION, R as isAvatarHttpUrl, Rn as ensureAuthProfileStore, Rt as buildAllowedModelSet, Sn as isProfileInCooldown, Sr as requireActivePluginRegistry, St as unwrapKnownDispatchWrapperInvocation, Tn as resolveProfilesUnavailableReason, Tr as findOverlappingPluginHttpRoute, Tt as SAFE_BIN_PROFILES, U as discoverJohnnessPlugins, Ut as modelKey, V as validateJsonSchemaValue, Vn as withFileLock, Vt as findNormalizedProviderValue, W as isPathInside, Wt as normalizeModelRef$2, X as resolveEffectiveEnableState, Xn as DEFAULT_MODEL, Xt as resolveSubagentSpawnModelSelection, Y as normalizePluginsConfig, Yn as DEFAULT_CONTEXT_TOKENS, Yt as resolveReasoningDefault, Z as resolveMemorySlotDecision, Zn as DEFAULT_PROVIDER, Zt as resolveThinkingDefault, _n as isSecretRefHeaderValueMarker, _r as normalizeChannelId$1, _t as resolveAllowlistCandidatePath, a as readConfigFileSnapshot, an as createConfiguredOllamaStreamFn, at as unsetConfigValueAtPath, b as normalizeScpRemoteHost, bn as clearExpiredCooldowns, br as getActivePluginRegistryKey, bt as isDispatchWrapperExecutable, c as setRuntimeConfigSnapshot, cn as buildStreamErrorAssistantMessage, cr as hasConfiguredSecretInput, ct as listWritableExplicitTrustedSafeBinDirs, dn as getCustomProviderApiKey, dr as resolveSecretInputRef, dt as analyzeShellCommand, er as resolveShellEnvFallbackTimeoutMs, et as setConfigOverride, f as TELEGRAM_COMMAND_NAME_PATTERN, fn as requireApiKey, ft as buildEnforcedShellCommand, g as isInboundPathAllowed, gn as isNonSecretApiKeyMarker, gr as normalizeAnyChannelId, gt as matchAllowlist, hn as resolveModelAuthMode, hr as listChatChannels, ht as DEFAULT_SAFE_BINS, i as loadConfig, in as retryAsync, ir as parseBooleanValue$1, it as setConfigValueAtPath, jn as isNonEmptyString, jr as matchPluginCommand, jt as resolveDiscordPreviewStreamMode, kr as getPluginCommandSpecs, kt as isSafeExecutableValue, l as writeConfigFile, ln as buildUsageWithNoCost, lr as normalizeResolvedSecretInputString, lt as normalizeTrustedSafeBinDirs, m as resolveTelegramCustomCommands, mn as resolveEnvApiKey, mr as getChatChannelMeta, mt as splitCommandChain, nr as resolveOwnerDisplaySetting, nt as getConfigValueAtPath, o as readConfigFileSnapshotForWrite, on as buildAssistantMessage, ot as getTrustedSafeBinDirs, p as normalizeTelegramCommandName, pn as resolveApiKeyForProvider, pr as CHAT_CHANNEL_ORDER, pt as isWindowsPlatform, q as MANIFEST_KEY, qn as normalizeSecretInput, qt as resolveDefaultModelForAgent, rn as resolveRetryConfig, rr as isTruthyEnvValue, rt as parseConfigPath, s as resolveConfigSnapshotHash, sn as buildAssistantMessageWithZeroUsage, sr as coerceSecretRef, st as isTrustedSafeBinPath, tr as isDangerousHostEnvVarName, tt as unsetConfigOverride, u as validateConfigObjectWithPlugins, un as getApiKeyForModel, ur as normalizeSecretInputString, ut as validateSafeBinArgv, v as resolveIMessageAttachmentRoots, vr as normalizeChatChannelId, vt as resolveCommandResolutionFromArgv, wn as markAuthProfileUsed, wr as createPluginRegistry, wt as splitShellArgs, x as parseNonNegativeByteSize, xn as getSoonestCooldownExpiry, xt as isShellWrapperExecutable, y as resolveIMessageRemoteAttachmentRoots, yn as resolveAuthProfileOrder, yr as getActivePluginRegistry, yt as extractShellWrapperInlineCommand, z as isPathWithinRoot, zn as resolveAuthStorePathForDisplay, zt as buildConfiguredAllowlistKeys } from "./config-Di-lyCdh.js";
@@ -97802,6 +97805,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
97802
97805
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
97803
97806
  }
97804
97807
  async function handleCommands(params) {
97808
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
97809
+ if (deviceLoginReply) return deviceLoginReply;
97805
97810
  if (HANDLERS === null) HANDLERS = [
97806
97811
  handlePluginCommand,
97807
97812
  handleBashCommand,
@@ -1,3 +1,6 @@
1
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60
2
+ // JOHNNESS_PATCH_DEVICE_LOGIN_60B
3
+ import { handleDeviceLoginCommand } from "./codex-oauth-device.js";
1
4
  import { g as resolveStateDir, r as STATE_DIR, u as resolveGatewayPort, y as resolveRequiredHomeDir } from "./paths-BJV7vkaX.js";
2
5
  import { E as normalizeLogLevel, R as consumeRootOptionToken, a as logVerbose, c as shouldLogVerbose, k as resolvePreferredJohnnessTmpDir, m as getChildLogger, p as theme, t as danger, u as warn } from "./globals-BM8hKFm0.js";
3
6
  import { C as sleep$1, D as isPlainObject, E as truncateUtf16Safe, S as shortenHomePath, d as isRecord$1, i as clampInt, l as escapeRegExp, m as normalizeE164, t as CONFIG_DIR, w as sliceUtf16Safe, x as shortenHomeInString, y as resolveUserPath } from "./utils-DC4zYvW0.js";
@@ -89523,6 +89526,8 @@ function resolveSessionEntryForHookSessionKey(sessionStore, sessionKey) {
89523
89526
  for (const [candidateKey, candidateEntry] of Object.entries(sessionStore)) if (candidateKey.trim().toLowerCase() === normalizedTarget) return candidateEntry;
89524
89527
  }
89525
89528
  async function handleCommands(params) {
89529
+ const deviceLoginReply = await handleDeviceLoginCommand(params, shouldHandleTextCommands({ cfg: params.cfg, surface: params.command.surface, commandSource: params.ctx.CommandSource }), { routeReply, callGateway });
89530
+ if (deviceLoginReply) return deviceLoginReply;
89526
89531
  if (HANDLERS === null) HANDLERS = [
89527
89532
  handlePluginCommand,
89528
89533
  handleBashCommand,
package/docs/PATCHES.md CHANGED
@@ -17,7 +17,7 @@ they predate ~2026-06-06).
17
17
 
18
18
  ## Index
19
19
 
20
- Family 01 is retired. Families 02 through 41 and 43 through 59 are live on `main`. Family 42 is omitted; native Codex OAuth ownership is Family 58.
20
+ Family 01 is retired. Families 02 through 41 and 43 through 60 are live on `main`. Family 42 is omitted; native Codex OAuth ownership is Family 58.
21
21
 
22
22
  1. **01, announce queue TTL, retired:** Removed the five-minute age filter that could discard delayed sub-agent completion announcements. [Details](#01--announce-ttl-retired-2026-07-30)
23
23
  2. **02, failover rotation:** Returns model selection to the highest-priority profile after its cooldown ends. [Details](#02--failover-rotation-undocumented)
@@ -78,6 +78,7 @@ Family 01 is retired. Families 02 through 41 and 43 through 59 are live on `main
78
78
  57. **57, swarm operating protocol:** the swarm preamble carries the protocol as guidance with escape hatches (plan the shape, one integrator per converging target, one gateway or shared budget per rate-limited host, done means done, verification sized to the blast radius); the board gains service items, durable requests with inbox wake and `wait`, and token-bucket budgets; `swarm_status` shows services and budgets. [Details](#57-swarm-operating-protocol-guidance-preamble-integrator-and-gateway-services-requests-shared-budgets)
79
79
  58. **58, Codex OAuth ownership:** One refresh authority per credential, cross-process locks, durable read-through, compare-and-swap persistence, and metadata-only updates prevent rotating-token races. [Details](#58-codex-oauth-ownership)
80
80
  59. **59, native state migration:** `johnness migrate` backs up and rewrites selected state into the native namespace, with staged publication, verification and rollback; installed plugins win over bundled duplicates. [Details](#59-native-state-migration)
81
+ 60. **60, device-code OAuth login:** Owner/allowlisted `/login` and `/relogin`, headless `models auth login --device`, canonical token handoff, shared polling and six-hour dead-chain recovery notices. [Details](#60-device-code-oauth-login)
81
82
 
82
83
  ---
83
84
 
@@ -2341,3 +2342,23 @@ Confirm its in-flight request has finished before handoff, archive its plist/scr
2341
2342
  **Files and replay.** Shared `dist/migrate-*.js` modules and CLI registration, legacy doctor/path resolvers, fourteen discovery copies, exec-approvals resolvers, native plugin adapter integration, [`scripts/patch-native-state-migration.py`](../scripts/patch-native-state-migration.py), [`patches/59-native-state-migration.patch`](../patches/59-native-state-migration.patch), and `tests/migrate-*.test.mjs` plus runtime regression tests. Source-name detection literals are protected from namespace replay. Marker: `JOHNNESS_PATCH_NATIVE_STATE_MIGRATION_59`.
2342
2343
 
2343
2344
  **Acceptance and rollback.** Synthetic tests cover filesystem boundaries, redaction, plugin precedence, staged-copy/resume/idempotency, service artifacts, offline validation and rollback. The release additionally exercises an isolated packed installation and a private temporary copy of real state; no private fixtures enter the repository or package. `johnness migrate --rollback --to <target>` restores only transaction-owned state after ownership/divergence checks and prints the selected old-service handoff. Runtime rollback and state rollback are separate operations; retain the verified backup and known-good package until cutover is accepted.
2345
+
2346
+ ## 60. Device-code OAuth login
2347
+
2348
+ **Patch:** `patches/60-codex-oauth-device.patch`; `scripts/patch-codex-oauth-device.py` validates all anchors before writing and supports an alternate package root.
2349
+ **Runtime:** `dist/codex-oauth-device.js`, `dist/codex-oauth-nudge.js`, the shared Family 58 owner, ten command dispatch bundles, eleven command registries, two CLI action copies and two CLI registrations (three distinct CLI files). Total: 27 runtime files; one `JOHNNESS_PATCH_DEVICE_LOGIN_60` marker per file.
2350
+
2351
+ **What it does:** `/login` and `/relogin` run before model-backed work, use the existing config-changing-command sender authorization, return a device URL/code in the originating channel, and poll without holding its command lane. Duplicate requests share the public prompt and one poller across bundle copies, inherited main-agent profiles, and CLI/gateway processes. A separate ownership-checked device lock spans the human authorization window. Public prompt/status metadata is kept in the canonical auth store; the device-auth id and PKCE verifier are memory-only. Success says "Signed in. I'm back."; expiry says "That code expired (they last 15 minutes). Send /login and I'll give you a fresh one." Native slash registries advertise `/login`; `/relogin` is a text alias. Natural-language phrases are not added to the slash matcher.
2352
+
2353
+ `johnness models auth login --device` defaults to `openai-codex`, needs no TTY, and prints the prompt followed by a redacted identity. `--provider openai-codex --device` is equivalent. Without `--device`, the browser/callback flow is unchanged.
2354
+
2355
+ **Protocol:** Verified against OpenAI Codex CLI 0.154.0 and `codex-rs/login/src/device_code_auth.rs` at commit `9e22e74e8dcab53f8bf1799c0eed1f9834c32f1a`. This is not the generic RFC 8628 grant: POST `/api/accounts/deviceauth/usercode` with the existing Codex client id only, poll `/api/accounts/deviceauth/token` with `device_auth_id` and `user_code`, then exchange the returned authorization code plus PKCE verifier at `/oauth/token` using `/deviceauth/callback`. Bare 403/404 mean pending; explicit denial/expiry is distinct. Respect server polling interval, Retry-After/slow-down, bounded fetch deadlines, and a 15-minute window. No scope is sent in the device request because that endpoint's contract accepts only the client id.
2356
+
2357
+ **Credential handoff:** Resolve the existing provider profile, including legacy ids and inherited main ownership. Validate account continuity with Family 58; an explicit owner-selected account change is accepted with an identity-redacted notice. Commit through `mutateAuthProfile`, clear the durable dead-chain marker and cooldown/usage failure entry, and publish the owner's snapshots. New profiles use `openai-codex:<email-or-default>`. No secondary token store or Codex CLI helper is involved.
2358
+
2359
+ **Recovery notice:** The shared owner (already reached by all ten refresh adapters) recognizes refresh-token reuse, invalid grant/expired refresh chains, and expired credentials with no refresh token. A fresh-store transaction reserves one notice per profile per six hours in `codexLoginNudges` before sending to a concrete configured owner on an enabled channel. Wildcard/group targets are not used. Unresolved owners or delivery failures get a token-free log notice instead; the reservation prevents storms across process restarts. Existing auth failures nudge when next inspected by the owner, not via an extra periodic job.
2360
+
2361
+ **Tests:** `tests/codex-oauth-device.test.mjs` covers exact fake endpoints, pending/success, expiry/denial, rate limits, sanitized failures, canonical handoff, explicit account changes, inherited ownership, authorization, duplicate prompts, background routing, CLI identity redaction, nudge debounce, all ten real dispatcher adapters, and exact/idempotent/atomic patch replay. Historical Family 51/54 replay tests now replay Family 60 before exact byte comparison; the Family 59 header fixture excludes only the independent Family 60 import/comment. Verifier checks every runtime copy plus exact read-only replay. Live end-to-end account authorization still requires the human to complete OpenAI's device page.
2362
+
2363
+ <!-- JOHNNESS_PATCH_DEVICE_LOGIN_60B -->
2364
+ **Amendment 60B (2026.9.12): first-try instructions.** Additive patch: `patches/60b-device-login-instructions.patch`. Chat and CLI now give four numbered steps: open the device page, sign in first if asked, enter the code on that same page, then tap Approve. Telegram's code is on its own line in inline-code formatting, with a tap-to-copy instruction. A redacted known account (or "the same account as before") helps choose the right sign-in. The prompt explains the one-time chatgpt.com > Settings > Security > "Device code authorization for Codex" setting and asks for a fresh code after enabling it. CLI startup unavailability or an explicit disabled-setting HTTP 403 prints that same fix without echoing the response body. Bare polling 403/404 semantics are unchanged. Expiry, declined-sign-in and dead-chain notices now explain the next action. All 27 runtime surfaces carry the 60B sub-marker and are checked by exact replay; tests cover walkthrough content, copyable Telegram code, sanitized startup errors, and unchanged pending behavior.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "johns-harness",
3
- "version": "2026.9.10",
3
+ "version": "2026.9.12",
4
4
  "description": "John's Harness: a production agent harness that runs autonomous coding agents as one cooperative swarm.",
5
5
  "keywords": [
6
6
  "ai-agents",