@foxden-app/foxclaw 0.5.19 → 0.5.21
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/.env.example +4 -0
- package/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/dist/auth/cross_node_sync.d.ts +55 -0
- package/dist/auth/cross_node_sync.js +199 -2
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/controller/controller.d.ts +7 -1
- package/dist/controller/controller.js +277 -82
- package/dist/i18n.d.ts +28 -0
- package/dist/i18n.js +28 -0
- package/dist/main.js +141 -7
- package/dist/store/database.d.ts +9 -0
- package/dist/store/database.js +87 -0
- package/docs/cross-node-auth-sync.md +5 -1
- package/docs/user-manual.md +4 -1
- package/docs/zh/cross-node-auth-sync.md +5 -1
- package/docs/zh/user-manual.md +4 -1
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
|
|
|
19
19
|
import { applySessionLog, bootstrapSessionLog, splitJsonlChunk, } from './session_observer.js';
|
|
20
20
|
import { renderActiveTurnStatus } from './status.js';
|
|
21
21
|
import { writeRuntimeStatus } from '../runtime.js';
|
|
22
|
+
const AUTH_DELETE_REASON_NEEDS_REPAIR = 'needs_repair';
|
|
22
23
|
class UserFacingError extends Error {
|
|
23
24
|
}
|
|
24
25
|
const OBSERVED_THREAD_POLL_MS = 1500;
|
|
@@ -119,6 +120,8 @@ export class BridgeSessionCore {
|
|
|
119
120
|
pendingAuthRotation = null;
|
|
120
121
|
authRotationInProgress = false;
|
|
121
122
|
authRefreshAllInProgress = false;
|
|
123
|
+
externalAuthValidationInProgress = false;
|
|
124
|
+
turnStartInProgress = 0;
|
|
122
125
|
authRotationFailedTargets = new Set();
|
|
123
126
|
localUsageCache = null;
|
|
124
127
|
localUsageCacheLoaded = false;
|
|
@@ -245,6 +248,8 @@ export class BridgeSessionCore {
|
|
|
245
248
|
this.commandUsageSequence = 0;
|
|
246
249
|
this.pendingAuthRotation = null;
|
|
247
250
|
this.authRefreshAllInProgress = false;
|
|
251
|
+
this.externalAuthValidationInProgress = false;
|
|
252
|
+
this.turnStartInProgress = 0;
|
|
248
253
|
this.clearObservedThreadWatchers();
|
|
249
254
|
this.releaseActiveTurnsForBridgeShutdown();
|
|
250
255
|
this.bot.stop();
|
|
@@ -311,6 +316,10 @@ export class BridgeSessionCore {
|
|
|
311
316
|
await this.handleActiveTurnInboundMessage(event, locale, event.text.trim());
|
|
312
317
|
return;
|
|
313
318
|
}
|
|
319
|
+
if (this.externalAuthValidationInProgress) {
|
|
320
|
+
await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
314
323
|
await this.startBoundTurnFromEvent(event, locale, event.text.trim());
|
|
315
324
|
return;
|
|
316
325
|
}
|
|
@@ -365,6 +374,10 @@ export class BridgeSessionCore {
|
|
|
365
374
|
await this.handleActiveTurnInboundMessage(event, locale, decision.text);
|
|
366
375
|
return;
|
|
367
376
|
}
|
|
377
|
+
if (this.externalAuthValidationInProgress) {
|
|
378
|
+
await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
368
381
|
await this.startBoundTurnFromEvent(event, locale, decision.text);
|
|
369
382
|
}
|
|
370
383
|
async handleCommand(event, locale, name, args) {
|
|
@@ -420,6 +433,7 @@ export class BridgeSessionCore {
|
|
|
420
433
|
t(locale, 'status_pending_user_inputs', { value: this.store.countPendingUserInputs() }),
|
|
421
434
|
t(locale, 'status_queued_turns', { value: this.store.countQueuedTurnInputs(scopeId) }),
|
|
422
435
|
t(locale, 'status_active_turns', { value: this.activeTurns.size }),
|
|
436
|
+
formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()),
|
|
423
437
|
];
|
|
424
438
|
if (serviceStatus) {
|
|
425
439
|
lines.push('', t(locale, 'status_runtime_overview'));
|
|
@@ -743,7 +757,7 @@ export class BridgeSessionCore {
|
|
|
743
757
|
return;
|
|
744
758
|
}
|
|
745
759
|
case 'config': {
|
|
746
|
-
await this.handleConfigCommand(scopeId, locale);
|
|
760
|
+
await this.handleConfigCommand(scopeId, locale, args);
|
|
747
761
|
return;
|
|
748
762
|
}
|
|
749
763
|
case 'requirements': {
|
|
@@ -1041,6 +1055,11 @@ export class BridgeSessionCore {
|
|
|
1041
1055
|
await this.handleSetupCallback(event, setupMatch[1], setupMatch[2], locale);
|
|
1042
1056
|
return;
|
|
1043
1057
|
}
|
|
1058
|
+
const configMatch = /^config:auth_auto_delete:(on|off)$/.exec(event.data);
|
|
1059
|
+
if (configMatch) {
|
|
1060
|
+
await this.handleConfigToggleCallback(event, configMatch[1] === 'on', locale);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1044
1063
|
const settingsMatch = /^settings:(model|effort|access):(.+)$/.exec(event.data);
|
|
1045
1064
|
if (settingsMatch) {
|
|
1046
1065
|
await this.handleSettingsCallback(event, settingsMatch[1], settingsMatch[2], locale);
|
|
@@ -2919,53 +2938,75 @@ export class BridgeSessionCore {
|
|
|
2919
2938
|
async getCurrentAuthLabel() {
|
|
2920
2939
|
return (await this.listCodexAuthState()).currentLabel;
|
|
2921
2940
|
}
|
|
2941
|
+
async handleExternalCodexAuthCandidateDeleted(candidateName, reason = null) {
|
|
2942
|
+
this.store.deleteCodexAuthCandidate(candidateName);
|
|
2943
|
+
if (isInvalidCodexAuthDeleteReason(reason)) {
|
|
2944
|
+
this.store.recordCodexAuthCandidateInvalidDelete(candidateName, reason);
|
|
2945
|
+
}
|
|
2946
|
+
else {
|
|
2947
|
+
this.store.recordCodexAuthCandidateRemoved(candidateName, reason);
|
|
2948
|
+
}
|
|
2949
|
+
this.authRotationFailedTargets.delete(path.join(this.resolveAuthDir(), candidateName));
|
|
2950
|
+
this.pendingTurnErrors.clear();
|
|
2951
|
+
this.attachedThreads.clear();
|
|
2952
|
+
await this.app.restart();
|
|
2953
|
+
}
|
|
2922
2954
|
async validateExternalCodexAuthCandidate(candidateName, rawAuth, expectedAccountId) {
|
|
2923
|
-
if (
|
|
2955
|
+
if (this.externalAuthValidationInProgress) {
|
|
2924
2956
|
return { ok: false, reason: 'runtime is not idle' };
|
|
2925
2957
|
}
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
return { ok: false, reason: 'remote auth account id mismatch' };
|
|
2929
|
-
}
|
|
2930
|
-
const state = await this.listCodexAuthState();
|
|
2931
|
-
const existing = state.candidates.find(candidate => candidate.name === candidateName) ?? null;
|
|
2932
|
-
if (existing) {
|
|
2933
|
-
const existingMetadata = await readChatGptAuthMetadata(existing.path);
|
|
2934
|
-
if (existingMetadata && existingMetadata.accountId !== expectedAccountId) {
|
|
2935
|
-
return { ok: false, reason: 'same candidate belongs to a different account' };
|
|
2936
|
-
}
|
|
2958
|
+
if (!this.isIdleForServiceUpdate()) {
|
|
2959
|
+
return { ok: false, reason: 'runtime is not idle' };
|
|
2937
2960
|
}
|
|
2938
|
-
|
|
2939
|
-
const originalRegularAuth = authStat?.isFile()
|
|
2940
|
-
? await fs.readFile(state.authPath, 'utf8').catch(() => null)
|
|
2941
|
-
: null;
|
|
2942
|
-
const tempPath = path.join(state.authDir, `.auth-sync-validate-${process.pid}-${Date.now()}.json`);
|
|
2961
|
+
this.externalAuthValidationInProgress = true;
|
|
2943
2962
|
try {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
await this.
|
|
2949
|
-
const
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2963
|
+
const metadata = parseChatGptAuthMetadata(rawAuth);
|
|
2964
|
+
if (!metadata || metadata.accountId !== expectedAccountId) {
|
|
2965
|
+
return { ok: false, reason: 'remote auth account id mismatch' };
|
|
2966
|
+
}
|
|
2967
|
+
const state = await this.listCodexAuthState();
|
|
2968
|
+
const existing = state.candidates.find(candidate => candidate.name === candidateName) ?? null;
|
|
2969
|
+
if (existing) {
|
|
2970
|
+
const existingMetadata = await readChatGptAuthMetadata(existing.path);
|
|
2971
|
+
if (existingMetadata && existingMetadata.accountId !== expectedAccountId) {
|
|
2972
|
+
return { ok: false, reason: 'same candidate belongs to a different account' };
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
const authStat = await fs.lstat(state.authPath).catch(() => null);
|
|
2976
|
+
const originalRegularAuth = authStat?.isFile()
|
|
2977
|
+
? await fs.readFile(state.authPath, 'utf8').catch(() => null)
|
|
2978
|
+
: null;
|
|
2979
|
+
const tempPath = path.join(state.authDir, `.auth-sync-validate-${process.pid}-${Date.now()}.json`);
|
|
2980
|
+
try {
|
|
2981
|
+
await fs.writeFile(tempPath, rawAuth, { encoding: 'utf8', mode: 0o600 });
|
|
2982
|
+
await pointCodexAuthAtTarget(state.authDir, state.authPath, tempPath);
|
|
2983
|
+
this.pendingTurnErrors.clear();
|
|
2984
|
+
this.attachedThreads.clear();
|
|
2985
|
+
await this.app.restart();
|
|
2986
|
+
const account = await this.app.readAccount(false);
|
|
2987
|
+
const rateLimits = await this.app.readAccountRateLimits();
|
|
2988
|
+
if (!account || !rateLimits || !selectCodexRateLimitSnapshot(rateLimits)) {
|
|
2989
|
+
return { ok: false, reason: 'Codex did not validate ChatGPT usage for remote auth' };
|
|
2990
|
+
}
|
|
2991
|
+
return { ok: true };
|
|
2992
|
+
}
|
|
2993
|
+
catch (error) {
|
|
2994
|
+
return { ok: false, reason: formatUserError(error) };
|
|
2995
|
+
}
|
|
2996
|
+
finally {
|
|
2997
|
+
await restoreCodexAuthTarget(state.authDir, state.authPath, state.currentTargetPath, originalRegularAuth).catch((error) => {
|
|
2998
|
+
this.logger.warn('codex.auth_sync_restore_failed', { error: toErrorMeta(error) });
|
|
2999
|
+
});
|
|
3000
|
+
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
3001
|
+
this.pendingTurnErrors.clear();
|
|
3002
|
+
this.attachedThreads.clear();
|
|
3003
|
+
await this.app.restart().catch((error) => {
|
|
3004
|
+
this.logger.warn('codex.auth_sync_restart_restore_failed', { error: toErrorMeta(error) });
|
|
3005
|
+
});
|
|
2953
3006
|
}
|
|
2954
|
-
return { ok: true };
|
|
2955
|
-
}
|
|
2956
|
-
catch (error) {
|
|
2957
|
-
return { ok: false, reason: formatUserError(error) };
|
|
2958
3007
|
}
|
|
2959
3008
|
finally {
|
|
2960
|
-
|
|
2961
|
-
this.logger.warn('codex.auth_sync_restore_failed', { error: toErrorMeta(error) });
|
|
2962
|
-
});
|
|
2963
|
-
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
2964
|
-
this.pendingTurnErrors.clear();
|
|
2965
|
-
this.attachedThreads.clear();
|
|
2966
|
-
await this.app.restart().catch((error) => {
|
|
2967
|
-
this.logger.warn('codex.auth_sync_restart_restore_failed', { error: toErrorMeta(error) });
|
|
2968
|
-
});
|
|
3009
|
+
this.externalAuthValidationInProgress = false;
|
|
2969
3010
|
}
|
|
2970
3011
|
}
|
|
2971
3012
|
isIdleForServiceUpdate() {
|
|
@@ -2975,7 +3016,9 @@ export class BridgeSessionCore {
|
|
|
2975
3016
|
&& this.pendingMcpElicitations.size === 0
|
|
2976
3017
|
&& this.pendingLoginsByScope.size === 0
|
|
2977
3018
|
&& !this.authRotationInProgress
|
|
2978
|
-
&& !this.authRefreshAllInProgress
|
|
3019
|
+
&& !this.authRefreshAllInProgress
|
|
3020
|
+
&& !this.externalAuthValidationInProgress
|
|
3021
|
+
&& this.turnStartInProgress === 0;
|
|
2979
3022
|
}
|
|
2980
3023
|
hasLocalBlockingActivity() {
|
|
2981
3024
|
return !this.isIdleForServiceUpdate();
|
|
@@ -4738,13 +4781,13 @@ export class BridgeSessionCore {
|
|
|
4738
4781
|
throw error;
|
|
4739
4782
|
}
|
|
4740
4783
|
}
|
|
4741
|
-
async deleteCodexAuthCandidate(candidate) {
|
|
4784
|
+
async deleteCodexAuthCandidate(candidate, reason = null) {
|
|
4742
4785
|
const wasCurrent = candidate.isCurrent;
|
|
4743
4786
|
const authDir = this.resolveAuthDir();
|
|
4744
4787
|
const authPath = path.join(authDir, 'auth.json');
|
|
4745
4788
|
let deletedByCoordinator = false;
|
|
4746
4789
|
try {
|
|
4747
|
-
await this.coordinator?.authCandidateDeleted?.(this.authRuntimeId(), candidate.name);
|
|
4790
|
+
await this.coordinator?.authCandidateDeleted?.(this.authRuntimeId(), candidate.name, reason);
|
|
4748
4791
|
deletedByCoordinator = Boolean(this.coordinator?.authCandidateDeleted);
|
|
4749
4792
|
}
|
|
4750
4793
|
catch (error) {
|
|
@@ -4761,6 +4804,12 @@ export class BridgeSessionCore {
|
|
|
4761
4804
|
}
|
|
4762
4805
|
}
|
|
4763
4806
|
this.store.deleteCodexAuthCandidate(candidate.name);
|
|
4807
|
+
if (isInvalidCodexAuthDeleteReason(reason)) {
|
|
4808
|
+
this.store.recordCodexAuthCandidateInvalidDelete(candidate.name, reason);
|
|
4809
|
+
}
|
|
4810
|
+
else {
|
|
4811
|
+
this.store.recordCodexAuthCandidateRemoved(candidate.name, reason);
|
|
4812
|
+
}
|
|
4764
4813
|
this.authRotationFailedTargets.delete(candidate.path);
|
|
4765
4814
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
4766
4815
|
if (Object.prototype.hasOwnProperty.call(snapshots, candidate.name)) {
|
|
@@ -5069,10 +5118,58 @@ export class BridgeSessionCore {
|
|
|
5069
5118
|
const features = await this.app.listExperimentalFeatures();
|
|
5070
5119
|
await this.sendMessage(scopeId, formatFeaturesMessage(locale, features));
|
|
5071
5120
|
}
|
|
5072
|
-
async handleConfigCommand(scopeId, locale) {
|
|
5121
|
+
async handleConfigCommand(scopeId, locale, args = []) {
|
|
5122
|
+
const action = args[0]?.toLowerCase() ?? '';
|
|
5123
|
+
if (['auth_auto_delete', 'auth-auto-delete', 'auto_delete_needs_repair', 'auto-delete-needs-repair'].includes(action)) {
|
|
5124
|
+
const enabled = parseConfigBooleanArg(args[1]);
|
|
5125
|
+
if (enabled === null) {
|
|
5126
|
+
await this.sendMessage(scopeId, t(locale, 'config_auth_auto_delete_usage'));
|
|
5127
|
+
return;
|
|
5128
|
+
}
|
|
5129
|
+
const update = await this.setAuthAutoDeleteNeedsRepair(enabled);
|
|
5130
|
+
const binding = this.store.getBinding(scopeId);
|
|
5131
|
+
const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
|
|
5132
|
+
await this.sendMessage(scopeId, `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`, configKeyboard(locale, this.config));
|
|
5133
|
+
return;
|
|
5134
|
+
}
|
|
5073
5135
|
const binding = this.store.getBinding(scopeId);
|
|
5074
5136
|
const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
|
|
5075
|
-
await this.sendMessage(scopeId, formatConfigMessage(locale, result));
|
|
5137
|
+
await this.sendMessage(scopeId, formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats()), configKeyboard(locale, this.config));
|
|
5138
|
+
}
|
|
5139
|
+
async handleConfigToggleCallback(event, enabled, locale) {
|
|
5140
|
+
const update = await this.setAuthAutoDeleteNeedsRepair(enabled);
|
|
5141
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
5142
|
+
const binding = this.store.getBinding(event.scopeId);
|
|
5143
|
+
const result = await this.app.readConfig(binding?.cwd ?? this.config.defaultCwd, true);
|
|
5144
|
+
const message = `${this.formatConfigToggleUpdate(locale, update)}\n\n${formatConfigMessage(locale, result, this.config, this.store.getCodexAuthPoolStats())}`;
|
|
5145
|
+
if (event.messageId !== null) {
|
|
5146
|
+
await this.editMessage(event.scopeId, event.messageId, message, configKeyboard(locale, this.config));
|
|
5147
|
+
}
|
|
5148
|
+
else {
|
|
5149
|
+
await this.sendMessage(event.scopeId, message, configKeyboard(locale, this.config));
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
async setAuthAutoDeleteNeedsRepair(enabled) {
|
|
5153
|
+
this.config.authAutoDeleteNeedsRepair = enabled;
|
|
5154
|
+
const envPath = this.config.envPath;
|
|
5155
|
+
if (!envPath) {
|
|
5156
|
+
return { enabled, envPath: null, envUpdated: false, envError: null };
|
|
5157
|
+
}
|
|
5158
|
+
try {
|
|
5159
|
+
await writeEnvBoolean(envPath, 'AUTH_AUTO_DELETE_NEEDS_REPAIR', enabled);
|
|
5160
|
+
return { enabled, envPath, envUpdated: true, envError: null };
|
|
5161
|
+
}
|
|
5162
|
+
catch (error) {
|
|
5163
|
+
this.logger.warn('config.env_update_failed', { key: 'AUTH_AUTO_DELETE_NEEDS_REPAIR', envPath, error: toErrorMeta(error) });
|
|
5164
|
+
return { enabled, envPath, envUpdated: false, envError: formatUserError(error) };
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
formatConfigToggleUpdate(locale, update) {
|
|
5168
|
+
const lines = [t(locale, 'config_auth_auto_delete_updated', { value: t(locale, update.enabled ? 'yes' : 'no') })];
|
|
5169
|
+
if (update.envError) {
|
|
5170
|
+
lines.push(t(locale, 'config_env_update_failed', { value: update.envPath ?? t(locale, 'unknown'), error: update.envError }));
|
|
5171
|
+
}
|
|
5172
|
+
return lines.join('\n');
|
|
5076
5173
|
}
|
|
5077
5174
|
async handleRequirementsCommand(scopeId, locale) {
|
|
5078
5175
|
const requirements = await this.app.readConfigRequirements();
|
|
@@ -5424,7 +5521,10 @@ export class BridgeSessionCore {
|
|
|
5424
5521
|
}
|
|
5425
5522
|
return false;
|
|
5426
5523
|
}
|
|
5427
|
-
this.markCodexAuthCandidateNeedsRepair(current.name);
|
|
5524
|
+
const disposition = await this.markCodexAuthCandidateNeedsRepair(current.name);
|
|
5525
|
+
if (disposition.deleted) {
|
|
5526
|
+
await this.sendMessage(rotation.scopeId, formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()));
|
|
5527
|
+
}
|
|
5428
5528
|
}
|
|
5429
5529
|
const selection = await this.selectNextCodexAuthCandidate(failedTargets);
|
|
5430
5530
|
if (!selection) {
|
|
@@ -5434,10 +5534,12 @@ export class BridgeSessionCore {
|
|
|
5434
5534
|
return false;
|
|
5435
5535
|
}
|
|
5436
5536
|
const { candidate, fromLabel, toLabel } = selection;
|
|
5437
|
-
await this.sendMessage(rotation.scopeId,
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5537
|
+
await this.sendMessage(rotation.scopeId, this.config.authAutoDeleteNeedsRepair
|
|
5538
|
+
? t(locale, 'auth_auto_switching_quiet', { error: formatShortStatusError(rotation.reason) })
|
|
5539
|
+
: t(locale, 'auth_auto_switching', {
|
|
5540
|
+
...this.codexAuthSwitchParams(locale, fromLabel, toLabel),
|
|
5541
|
+
error: formatShortStatusError(rotation.reason),
|
|
5542
|
+
}));
|
|
5441
5543
|
const outcome = await this.switchCodexAuthAndRestart(rotation.scopeId, locale, candidate, true);
|
|
5442
5544
|
if (!outcome.ok) {
|
|
5443
5545
|
return false;
|
|
@@ -5525,6 +5627,7 @@ export class BridgeSessionCore {
|
|
|
5525
5627
|
}
|
|
5526
5628
|
async listCodexAuthState() {
|
|
5527
5629
|
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames(this.authRuntimeId()), this.store.listCodexAuthCandidateStates(this.authRuntimeId()), this.resolveAuthDir());
|
|
5630
|
+
this.store.recordCodexAuthPoolInventory(state.candidates.map(candidate => candidate.name));
|
|
5528
5631
|
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
5529
5632
|
const candidateQuotaIdentities = await this.readCodexAuthCandidateQuotaIdentities(state.candidates);
|
|
5530
5633
|
state.candidates.forEach((candidate) => {
|
|
@@ -5571,7 +5674,6 @@ export class BridgeSessionCore {
|
|
|
5571
5674
|
await this.app.restart();
|
|
5572
5675
|
const validation = await this.validateCurrentCodexAuthCandidate(candidate);
|
|
5573
5676
|
if (!validation.ok) {
|
|
5574
|
-
this.markCodexAuthCandidateNeedsRepair(candidate.name);
|
|
5575
5677
|
let restoredPrevious = false;
|
|
5576
5678
|
try {
|
|
5577
5679
|
await restoreCodexAuthTarget(initialState.authDir, initialState.authPath, initialState.currentTargetPath, originalRegularAuth);
|
|
@@ -5586,6 +5688,7 @@ export class BridgeSessionCore {
|
|
|
5586
5688
|
error: toErrorMeta(error),
|
|
5587
5689
|
});
|
|
5588
5690
|
}
|
|
5691
|
+
const repairDisposition = await this.markCodexAuthCandidateNeedsRepair(candidate.name);
|
|
5589
5692
|
const outcome = {
|
|
5590
5693
|
...result,
|
|
5591
5694
|
ok: false,
|
|
@@ -5593,6 +5696,8 @@ export class BridgeSessionCore {
|
|
|
5593
5696
|
recovered,
|
|
5594
5697
|
error: validation.error,
|
|
5595
5698
|
restoredPrevious,
|
|
5699
|
+
autoDeleted: repairDisposition.deleted,
|
|
5700
|
+
deleteRestarted: repairDisposition.restarted,
|
|
5596
5701
|
};
|
|
5597
5702
|
if (sendResult) {
|
|
5598
5703
|
const lines = this.formatAuthSwitchValidationLines(locale, outcome);
|
|
@@ -5609,11 +5714,15 @@ export class BridgeSessionCore {
|
|
|
5609
5714
|
recovered,
|
|
5610
5715
|
error: null,
|
|
5611
5716
|
restoredPrevious: false,
|
|
5717
|
+
autoDeleted: false,
|
|
5718
|
+
deleteRestarted: false,
|
|
5612
5719
|
};
|
|
5613
5720
|
if (!sendResult) {
|
|
5614
5721
|
return outcome;
|
|
5615
5722
|
}
|
|
5616
|
-
const lines = [
|
|
5723
|
+
const lines = [automatic && this.config.authAutoDeleteNeedsRepair
|
|
5724
|
+
? t(locale, 'auth_auto_done_quiet')
|
|
5725
|
+
: t(locale, automatic ? 'auth_auto_done' : 'auth_switch_done', this.codexAuthSwitchParams(locale, result.fromLabel, result.toLabel))];
|
|
5617
5726
|
if (recovered) {
|
|
5618
5727
|
lines.push(t(locale, 'auth_recovered_newer_candidate', { value: candidate.name }));
|
|
5619
5728
|
}
|
|
@@ -5662,12 +5771,19 @@ export class BridgeSessionCore {
|
|
|
5662
5771
|
if (outcome.ok) {
|
|
5663
5772
|
return [];
|
|
5664
5773
|
}
|
|
5774
|
+
const validationKey = outcome.autoDeleted && this.config.authAutoDeleteNeedsRepair
|
|
5775
|
+
? 'auth_switch_validation_auto_deleted_quiet'
|
|
5776
|
+
: outcome.autoDeleted
|
|
5777
|
+
? 'auth_switch_validation_auto_deleted'
|
|
5778
|
+
: 'auth_switch_validation_failed';
|
|
5665
5779
|
return [
|
|
5666
|
-
t(locale,
|
|
5780
|
+
t(locale, validationKey, {
|
|
5667
5781
|
value: outcome.candidateName,
|
|
5668
5782
|
error: outcome.error ?? t(locale, 'unknown'),
|
|
5669
5783
|
}),
|
|
5670
5784
|
outcome.restoredPrevious ? t(locale, 'auth_switch_validation_reverted') : '',
|
|
5785
|
+
outcome.deleteRestarted ? t(locale, 'auth_delete_current_restarted') : '',
|
|
5786
|
+
outcome.autoDeleted ? formatCodexAuthPoolSummary(locale, this.store.getCodexAuthPoolStats()) : '',
|
|
5671
5787
|
].filter(Boolean);
|
|
5672
5788
|
}
|
|
5673
5789
|
async recoverCodexAuthCandidate(candidateName, options = { crossNode: true }) {
|
|
@@ -5686,8 +5802,23 @@ export class BridgeSessionCore {
|
|
|
5686
5802
|
return false;
|
|
5687
5803
|
}
|
|
5688
5804
|
}
|
|
5689
|
-
markCodexAuthCandidateNeedsRepair(candidateName) {
|
|
5805
|
+
async markCodexAuthCandidateNeedsRepair(candidateName) {
|
|
5806
|
+
if (this.config.authAutoDeleteNeedsRepair) {
|
|
5807
|
+
const candidate = (await this.listCodexAuthState()).candidates.find(entry => entry.name === candidateName) ?? null;
|
|
5808
|
+
if (!candidate) {
|
|
5809
|
+
this.store.deleteCodexAuthCandidate(candidateName);
|
|
5810
|
+
this.store.recordCodexAuthCandidateInvalidDelete(candidateName, AUTH_DELETE_REASON_NEEDS_REPAIR);
|
|
5811
|
+
return { deleted: true, restarted: false };
|
|
5812
|
+
}
|
|
5813
|
+
const restarted = await this.deleteCodexAuthCandidate(candidate, AUTH_DELETE_REASON_NEEDS_REPAIR);
|
|
5814
|
+
this.logger.warn('codex.auth_candidate_auto_deleted', {
|
|
5815
|
+
candidate: candidateName,
|
|
5816
|
+
runtimeId: this.authRuntimeId(),
|
|
5817
|
+
});
|
|
5818
|
+
return { deleted: true, restarted };
|
|
5819
|
+
}
|
|
5690
5820
|
this.store.setCodexAuthCandidateState(candidateName, 'needs_repair');
|
|
5821
|
+
return { deleted: false, restarted: false };
|
|
5691
5822
|
}
|
|
5692
5823
|
markCodexAuthCandidateActive(candidateName) {
|
|
5693
5824
|
this.store.setCodexAuthCandidateState(candidateName, 'active');
|
|
@@ -6913,36 +7044,46 @@ export class BridgeSessionCore {
|
|
|
6913
7044
|
}
|
|
6914
7045
|
async startBoundTurnFromEvent(event, locale, text) {
|
|
6915
7046
|
const scopeId = event.scopeId;
|
|
6916
|
-
this.
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
: await this.createBinding(scopeId, null);
|
|
6922
|
-
await this.sendTyping(scopeId);
|
|
6923
|
-
const previewMessageId = 0;
|
|
7047
|
+
if (this.externalAuthValidationInProgress) {
|
|
7048
|
+
await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
|
|
7049
|
+
return;
|
|
7050
|
+
}
|
|
7051
|
+
this.turnStartInProgress += 1;
|
|
6924
7052
|
try {
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
7053
|
+
this.clearPlanImplementationPromptsForScope(scopeId);
|
|
7054
|
+
await this.stopWatchingScopeThread(scopeId);
|
|
7055
|
+
const existingBinding = this.store.getBinding(scopeId);
|
|
7056
|
+
const binding = existingBinding
|
|
7057
|
+
? await this.ensureThreadReady(scopeId, existingBinding)
|
|
7058
|
+
: await this.createBinding(scopeId, null);
|
|
7059
|
+
await this.sendTyping(scopeId);
|
|
7060
|
+
const previewMessageId = 0;
|
|
7061
|
+
try {
|
|
7062
|
+
const input = await this.buildTurnInput(binding, { ...event, text }, locale);
|
|
7063
|
+
const turnState = await this.startTurnWithRecovery(scopeId, binding, input);
|
|
7064
|
+
if (turnState.collaborationMode === 'plan') {
|
|
7065
|
+
this.store.setChatCollaborationMode(scopeId, DEFAULT_COLLABORATION_MODE);
|
|
7066
|
+
}
|
|
7067
|
+
await this.registerActiveTurn(scopeId, event.chatId, event.chatType, event.topicId, turnState.threadId, turnState.turnId, previewMessageId, {
|
|
7068
|
+
input,
|
|
7069
|
+
threadId: turnState.threadId,
|
|
7070
|
+
cwd: this.store.getBinding(scopeId)?.cwd ?? binding.cwd ?? this.config.defaultCwd,
|
|
7071
|
+
chatId: event.chatId,
|
|
7072
|
+
chatType: event.chatType,
|
|
7073
|
+
topicId: event.topicId,
|
|
7074
|
+
collaborationMode: turnState.collaborationMode,
|
|
7075
|
+
failedAuthTargets: new Set(),
|
|
7076
|
+
}, turnState.collaborationMode);
|
|
6929
7077
|
}
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
chatType: event.chatType,
|
|
6936
|
-
topicId: event.topicId,
|
|
6937
|
-
collaborationMode: turnState.collaborationMode,
|
|
6938
|
-
failedAuthTargets: new Set(),
|
|
6939
|
-
}, turnState.collaborationMode);
|
|
6940
|
-
}
|
|
6941
|
-
catch (error) {
|
|
6942
|
-
if (previewMessageId > 0) {
|
|
6943
|
-
await this.cleanupTransientPreview(scopeId, previewMessageId);
|
|
7078
|
+
catch (error) {
|
|
7079
|
+
if (previewMessageId > 0) {
|
|
7080
|
+
await this.cleanupTransientPreview(scopeId, previewMessageId);
|
|
7081
|
+
}
|
|
7082
|
+
throw error;
|
|
6944
7083
|
}
|
|
6945
|
-
|
|
7084
|
+
}
|
|
7085
|
+
finally {
|
|
7086
|
+
this.turnStartInProgress -= 1;
|
|
6946
7087
|
}
|
|
6947
7088
|
}
|
|
6948
7089
|
async requestInterrupt(active) {
|
|
@@ -8045,7 +8186,7 @@ function formatFeaturesMessage(locale, features) {
|
|
|
8045
8186
|
}
|
|
8046
8187
|
return lines.join('\n');
|
|
8047
8188
|
}
|
|
8048
|
-
function formatConfigMessage(locale, result) {
|
|
8189
|
+
function formatConfigMessage(locale, result, appConfig, authPoolStats) {
|
|
8049
8190
|
const config = result.config && typeof result.config === 'object' ? result.config : {};
|
|
8050
8191
|
const layers = Array.isArray(result.layers) ? result.layers : [];
|
|
8051
8192
|
const keys = ['model', 'model_provider', 'approval_policy', 'sandbox_mode', 'web_search', 'service_tier', 'profile', 'review_model'];
|
|
@@ -8055,8 +8196,62 @@ function formatConfigMessage(locale, result) {
|
|
|
8055
8196
|
lines.push(`${key}: ${value === null || value === undefined ? '-' : formatConfigValue(value)}`);
|
|
8056
8197
|
}
|
|
8057
8198
|
lines.push(t(locale, 'config_layers', { count: layers.length }));
|
|
8199
|
+
lines.push('');
|
|
8200
|
+
lines.push(t(locale, 'config_foxclaw_title'));
|
|
8201
|
+
lines.push(t(locale, 'config_auth_auto_delete_needs_repair', {
|
|
8202
|
+
value: t(locale, appConfig.authAutoDeleteNeedsRepair ? 'yes' : 'no'),
|
|
8203
|
+
}));
|
|
8204
|
+
lines.push(`AUTH_AUTO_DELETE_NEEDS_REPAIR=${appConfig.authAutoDeleteNeedsRepair ? 'true' : 'false'}`);
|
|
8205
|
+
lines.push(formatCodexAuthPoolSummary(locale, authPoolStats));
|
|
8058
8206
|
return lines.join('\n');
|
|
8059
8207
|
}
|
|
8208
|
+
function formatCodexAuthPoolSummary(locale, stats) {
|
|
8209
|
+
return t(locale, 'auth_pool_summary', {
|
|
8210
|
+
total: stats.totalSeen,
|
|
8211
|
+
alive: stats.alive,
|
|
8212
|
+
deleted: stats.deletedInvalid,
|
|
8213
|
+
});
|
|
8214
|
+
}
|
|
8215
|
+
function isInvalidCodexAuthDeleteReason(reason) {
|
|
8216
|
+
return reason === AUTH_DELETE_REASON_NEEDS_REPAIR;
|
|
8217
|
+
}
|
|
8218
|
+
function configKeyboard(locale, appConfig) {
|
|
8219
|
+
const enabled = appConfig.authAutoDeleteNeedsRepair;
|
|
8220
|
+
return [[{
|
|
8221
|
+
text: t(locale, enabled ? 'button_config_auth_auto_delete_off' : 'button_config_auth_auto_delete_on'),
|
|
8222
|
+
callback_data: `config:auth_auto_delete:${enabled ? 'off' : 'on'}`,
|
|
8223
|
+
}]];
|
|
8224
|
+
}
|
|
8225
|
+
function parseConfigBooleanArg(value) {
|
|
8226
|
+
const normalized = value?.trim().toLowerCase();
|
|
8227
|
+
if (!normalized)
|
|
8228
|
+
return null;
|
|
8229
|
+
if (['1', 'true', 'yes', 'on', 'enable', 'enabled'].includes(normalized))
|
|
8230
|
+
return true;
|
|
8231
|
+
if (['0', 'false', 'no', 'off', 'disable', 'disabled'].includes(normalized))
|
|
8232
|
+
return false;
|
|
8233
|
+
return null;
|
|
8234
|
+
}
|
|
8235
|
+
async function writeEnvBoolean(envPath, key, enabled) {
|
|
8236
|
+
await fs.mkdir(path.dirname(envPath), { recursive: true });
|
|
8237
|
+
const nextLine = `${key}=${enabled ? 'true' : 'false'}`;
|
|
8238
|
+
let contents = '';
|
|
8239
|
+
try {
|
|
8240
|
+
contents = await fs.readFile(envPath, 'utf8');
|
|
8241
|
+
}
|
|
8242
|
+
catch {
|
|
8243
|
+
await fs.writeFile(envPath, `${nextLine}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
8244
|
+
return;
|
|
8245
|
+
}
|
|
8246
|
+
const pattern = new RegExp(`(^|\\n)[ \\t#]*${escapeRegExp(key)}\\s*=.*(?=\\r?\\n|$)`);
|
|
8247
|
+
const nextContents = pattern.test(contents)
|
|
8248
|
+
? contents.replace(pattern, (_match, prefix) => `${prefix}${nextLine}`)
|
|
8249
|
+
: `${contents}${contents.endsWith('\n') || contents.length === 0 ? '' : '\n'}${nextLine}\n`;
|
|
8250
|
+
await fs.writeFile(envPath, nextContents, { encoding: 'utf8', mode: 0o600 });
|
|
8251
|
+
}
|
|
8252
|
+
function escapeRegExp(value) {
|
|
8253
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8254
|
+
}
|
|
8060
8255
|
function formatRequirementsMessage(locale, requirements) {
|
|
8061
8256
|
const lines = [t(locale, 'requirements_title')];
|
|
8062
8257
|
if (!requirements) {
|