@modelprofile.com/authswitch 6.5.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/authority-contract.d.ts +302 -6
- package/dist_ts/authority-contract.js +1 -1
- package/dist_ts/authority-runtime-contract.d.ts +13 -0
- package/dist_ts/authorityrehearsal.child.d.ts +1 -0
- package/dist_ts/authorityrehearsal.child.js +395 -0
- package/dist_ts/classes.accountlist.js +4 -3
- package/dist_ts/classes.authoritybackup.d.ts +134 -0
- package/dist_ts/classes.authoritybackup.js +769 -0
- package/dist_ts/classes.authoritybroker.d.ts +25 -5
- package/dist_ts/classes.authoritybroker.js +392 -123
- package/dist_ts/classes.authoritycli.d.ts +24 -0
- package/dist_ts/classes.authoritycli.js +675 -0
- package/dist_ts/classes.authorityclient.d.ts +19 -7
- package/dist_ts/classes.authorityclient.js +51 -14
- package/dist_ts/classes.authoritydaemon.d.ts +28 -1
- package/dist_ts/classes.authoritydaemon.js +482 -36
- package/dist_ts/classes.authoritydatabase.d.ts +175 -10
- package/dist_ts/classes.authoritydatabase.js +1100 -52
- package/dist_ts/classes.authoritymodels.d.ts +223 -1
- package/dist_ts/classes.authoritymodels.js +798 -9
- package/dist_ts/classes.authoritypreuse.d.ts +47 -0
- package/dist_ts/classes.authoritypreuse.js +278 -0
- package/dist_ts/classes.authorityregistry.d.ts +24 -0
- package/dist_ts/classes.authorityregistry.js +88 -0
- package/dist_ts/classes.authorityrehearsal.d.ts +127 -0
- package/dist_ts/classes.authorityrehearsal.js +632 -0
- package/dist_ts/classes.authorityusage.d.ts +160 -0
- package/dist_ts/classes.authorityusage.js +429 -0
- package/dist_ts/classes.claudeauthority.d.ts +66 -0
- package/dist_ts/classes.claudeauthority.js +532 -0
- package/dist_ts/classes.claudecodeharness.js +3 -2
- package/dist_ts/classes.claudecodelocks.d.ts +13 -0
- package/dist_ts/classes.claudecodelocks.js +75 -3
- package/dist_ts/classes.claudenative.d.ts +137 -0
- package/dist_ts/classes.claudenative.js +731 -0
- package/dist_ts/classes.claudestatus.d.ts +9 -0
- package/dist_ts/classes.claudestatus.js +26 -9
- package/dist_ts/classes.claudetokenrefresh.d.ts +15 -1
- package/dist_ts/classes.claudetokenrefresh.js +74 -21
- package/dist_ts/classes.codexharness.js +4 -2
- package/dist_ts/classes.codexmanaged.d.ts +11 -0
- package/dist_ts/classes.codexmanaged.js +75 -14
- package/dist_ts/classes.codexpreuse.d.ts +8 -0
- package/dist_ts/classes.codexpreuse.js +24 -6
- package/dist_ts/classes.fileharness.d.ts +2 -0
- package/dist_ts/classes.fileharness.js +6 -4
- package/dist_ts/classes.limits.js +12 -7
- package/dist_ts/classes.opencodeharness.js +2 -2
- package/dist_ts/claudehttp.d.ts +10 -0
- package/dist_ts/claudehttp.js +29 -3
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +7 -1
- package/dist_ts/interfaces.harness.d.ts +6 -0
- package/dist_ts/interfaces.list.d.ts +9 -3
- package/dist_ts/plugins.d.ts +5 -2
- package/dist_ts/plugins.js +7 -3
- package/dist_ts/ts_migration/0001_authority_meta.d.ts +3 -0
- package/dist_ts/ts_migration/0001_authority_meta.js +32 -0
- package/dist_ts/ts_migration/index.d.ts +3 -0
- package/dist_ts/ts_migration/index.js +6 -0
- package/package.json +4 -1
- package/readme.md +181 -14
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/authority-contract.ts +264 -9
- package/ts/authority-runtime-contract.ts +14 -0
- package/ts/authorityrehearsal.child.ts +330 -0
- package/ts/classes.accountlist.ts +3 -2
- package/ts/classes.authoritybackup.ts +780 -0
- package/ts/classes.authoritybroker.ts +389 -121
- package/ts/classes.authoritycli.ts +719 -0
- package/ts/classes.authorityclient.ts +75 -18
- package/ts/classes.authoritydaemon.ts +449 -36
- package/ts/classes.authoritydatabase.ts +1147 -55
- package/ts/classes.authoritymodels.ts +541 -6
- package/ts/classes.authoritypreuse.ts +297 -0
- package/ts/classes.authorityregistry.ts +118 -0
- package/ts/classes.authorityrehearsal.ts +625 -0
- package/ts/classes.authorityusage.ts +527 -0
- package/ts/classes.claudeauthority.ts +541 -0
- package/ts/classes.claudecodeharness.ts +2 -1
- package/ts/classes.claudecodelocks.ts +70 -1
- package/ts/classes.claudenative.ts +816 -0
- package/ts/classes.claudestatus.ts +38 -9
- package/ts/classes.claudetokenrefresh.ts +82 -19
- package/ts/classes.codexharness.ts +3 -1
- package/ts/classes.codexmanaged.ts +75 -12
- package/ts/classes.codexpreuse.ts +33 -5
- package/ts/classes.fileharness.ts +7 -3
- package/ts/classes.limits.ts +11 -6
- package/ts/classes.opencodeharness.ts +1 -1
- package/ts/claudehttp.ts +31 -2
- package/ts/index.ts +6 -0
- package/ts/interfaces.harness.ts +6 -0
- package/ts/interfaces.list.ts +9 -3
- package/ts/plugins.ts +7 -2
- package/ts/ts_migration/0001_authority_meta.ts +33 -0
- package/ts/ts_migration/index.ts +7 -0
|
@@ -1,32 +1,58 @@
|
|
|
1
1
|
import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
|
|
2
2
|
import * as plugins from './plugins.js';
|
|
3
3
|
import type {
|
|
4
|
+
IAuthSwitchDoctorPage, IReq_AuthSwitchDoctor,
|
|
4
5
|
IReq_AuthSwitchBeginAdd, IReq_AuthSwitchBeginReauth, IReq_AuthSwitchCancelOperation,
|
|
5
|
-
|
|
6
|
+
IReq_AuthSwitchClaudeNativeHandoff, IReq_AuthSwitchClaudeNativeHandoffs, IReq_AuthSwitchSwitchClaudeNative,
|
|
7
|
+
IReq_AuthSwitchEvents, IReq_AuthSwitchGetOperation, IReq_AuthSwitchListOperations, IReq_AuthSwitchRemoveAccount,
|
|
6
8
|
IReq_AuthSwitchRenameAccount, IReq_AuthSwitchSnapshot,
|
|
9
|
+
IReq_AuthSwitchCancelPreuse, IReq_AuthSwitchGetPreuse, IReq_AuthSwitchStartPreuse, IReq_AuthSwitchUsage,
|
|
7
10
|
} from './authority-contract.js';
|
|
8
|
-
import type { IReq_AuthSwitchBindAccount,
|
|
9
|
-
|
|
11
|
+
import type { IReq_AuthSwitchBindAccount, IReq_AuthSwitchReleaseBinding,
|
|
12
|
+
IReq_AuthSwitchResolveAccess } from './authority-runtime-contract.js';
|
|
13
|
+
import { authSwitchBindingCapabilityHash, AuthSwitchAuthorityBroker,
|
|
14
|
+
type IAuthSwitchAuthorityBrokerOptions } from './classes.authoritybroker.js';
|
|
10
15
|
import { AuthSwitchAuthorityDatabase, type IAuthSwitchAuthorityDatabaseOptions } from './classes.authoritydatabase.js';
|
|
16
|
+
import { ClaudeNativeAuthority, type IClaudeNativeAuthorityOptions } from './classes.claudeauthority.js';
|
|
17
|
+
import { ClaudeAccountStatus } from './classes.claudestatus.js';
|
|
18
|
+
import { AuthSwitchAuthorityUsage, type IAuthSwitchAuthorityUsageOptions } from './classes.authorityusage.js';
|
|
19
|
+
import { AuthSwitchAuthorityPreuse, type IAuthSwitchAuthorityPreuseOptions } from './classes.authoritypreuse.js';
|
|
11
20
|
import { AuthSwitchAuthorityFrameReader, authSwitchAuthorityFrameBytes,
|
|
12
21
|
maxAuthSwitchAuthorityFrameBytes } from './classes.authorityframing.js';
|
|
13
|
-
import { AuthSwitchManagedCodex, findManagedCodexProcess,
|
|
22
|
+
import { AuthSwitchManagedCodex, findManagedCodexProcess, observeManagedCodexProcesses,
|
|
23
|
+
type IAuthSwitchManagedCodexOptions } from './classes.codexmanaged.js';
|
|
14
24
|
|
|
15
25
|
export interface IAuthSwitchAuthorityDaemonOptions extends IAuthSwitchAuthorityDatabaseOptions {
|
|
16
26
|
authoritySocketPath: string;
|
|
17
27
|
runtimeSocketPath: string;
|
|
18
28
|
broker?: IAuthSwitchAuthorityBrokerOptions;
|
|
29
|
+
usage?: Partial<Pick<IAuthSwitchAuthorityUsageOptions, 'now' | 'currentMaxAgeMs' | 'maxStaleAgeMs'
|
|
30
|
+
| 'minAttemptIntervalMs' | 'retryBaseMs' | 'retryMaxMs' | 'providerTimeoutMs' | 'minAccessValidityMs'>>;
|
|
31
|
+
preuse?: IAuthSwitchAuthorityPreuseOptions;
|
|
32
|
+
claudeStatus?: ClaudeAccountStatus;
|
|
33
|
+
/** Activated only after a verified backend source receipt has registered the native home. */
|
|
34
|
+
claudeNative?: IClaudeNativeAuthorityOptions;
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
const hasKeys = (value: unknown, keys: readonly string[]): value is Record<string, unknown> =>
|
|
22
38
|
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
23
39
|
&& Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
24
40
|
const invalid = (): never => { throw new plugins.typedrequest.TypedResponseError('Invalid authswitch authority request.'); };
|
|
41
|
+
const validBindingId = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
42
|
+
const validBindingCapability = (value: unknown): value is string => typeof value === 'string'
|
|
43
|
+
&& /^[A-Za-z0-9_-]{43}$/.test(value);
|
|
44
|
+
|
|
45
|
+
interface IPreuseAccountReservation {
|
|
46
|
+
operationId: string;
|
|
47
|
+
}
|
|
25
48
|
|
|
26
49
|
/** TypedRequest handlers over a private one-request-per-connection Unix socket. */
|
|
27
50
|
export class AuthSwitchAuthorityDaemon {
|
|
28
51
|
public readonly database: AuthSwitchAuthorityDatabase;
|
|
29
52
|
public readonly broker: AuthSwitchAuthorityBroker;
|
|
53
|
+
public readonly usage: AuthSwitchAuthorityUsage;
|
|
54
|
+
public readonly preuse: AuthSwitchAuthorityPreuse;
|
|
55
|
+
public readonly claudeNative?: ClaudeNativeAuthority;
|
|
30
56
|
private readonly managementRouter = new plugins.typedrequest.TypedRouter();
|
|
31
57
|
private readonly runtimeRouter = new plugins.typedrequest.TypedRouter();
|
|
32
58
|
private managementServer?: plugins.net.Server;
|
|
@@ -36,25 +62,195 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
36
62
|
capability: string; homeId: string; runId: string }>>();
|
|
37
63
|
private readonly managedCodexHomes = new Set<string>();
|
|
38
64
|
private readonly managedCodexAccountIds = new Map<string, string>();
|
|
65
|
+
private readonly managedCodexStates = new Map<string, IAuthSwitchDoctorPage['managedCodex'][number]['daemon']['state']>();
|
|
39
66
|
private readonly drainingAccounts = new Set<string>();
|
|
67
|
+
private readonly deviceDrainOwners = new Map<string, string>();
|
|
68
|
+
private readonly preuseAccounts = new Map<string, IPreuseAccountReservation>();
|
|
40
69
|
private readonly managedCodexStops = new Map<string, Promise<void>>();
|
|
41
70
|
private readonly managedCodexFailures: unknown[] = [];
|
|
71
|
+
private readonly runtimeBindingActivity = new Map<string, { active: number; waiters: Set<() => void> }>();
|
|
72
|
+
private readonly runtimeBindingReleaseFences = new Set<string>();
|
|
73
|
+
private readonly runtimeBindingReleases = new Map<string, Promise<IReq_AuthSwitchReleaseBinding['response']>>();
|
|
42
74
|
private closing = false;
|
|
43
75
|
|
|
44
76
|
constructor(private readonly options: IAuthSwitchAuthorityDaemonOptions) {
|
|
45
77
|
this.database = new AuthSwitchAuthorityDatabase(options);
|
|
46
|
-
|
|
78
|
+
const provider = options.broker?.provider ?? new plugins.flexAccounts.OpenAiProviderAdapter();
|
|
79
|
+
this.broker = new AuthSwitchAuthorityBroker(this.database, { ...options.broker, provider });
|
|
80
|
+
this.usage = new AuthSwitchAuthorityUsage({
|
|
81
|
+
...options.usage,
|
|
82
|
+
readContext: async (accountId, loginId) => {
|
|
83
|
+
await this.database.assertOpenAdmission();
|
|
84
|
+
return this.database.readUsageContext(accountId, loginId);
|
|
85
|
+
},
|
|
86
|
+
resolveAccess: (context, rejectedGrantGeneration, minValidityMs) =>
|
|
87
|
+
this.broker.resolveUsageAccess(context, rejectedGrantGeneration, minValidityMs),
|
|
88
|
+
resolveClaudeLogin: (context, rejectedGrantGeneration, minValidityMs) => {
|
|
89
|
+
if (!this.claudeNative) throw new Error('Claude status authority is unavailable.');
|
|
90
|
+
return this.claudeNative.resolveInactiveStatusLogin(context, rejectedGrantGeneration, minValidityMs);
|
|
91
|
+
},
|
|
92
|
+
withNativeSource: (context, action) => {
|
|
93
|
+
if (!this.claudeNative) throw new Error('Claude native status authority is unavailable.');
|
|
94
|
+
return this.claudeNative.withNativeUsageSource(context, action);
|
|
95
|
+
},
|
|
96
|
+
claudeStatus: options.claudeStatus ?? new ClaudeAccountStatus(),
|
|
97
|
+
store: {
|
|
98
|
+
read: (accountId, loginId) => this.database.readUsage(accountId, loginId),
|
|
99
|
+
publishIfCurrent: async (context, previousUpdateId, next) => {
|
|
100
|
+
const published = await this.database.publishUsageIfCurrent(context, previousUpdateId, next);
|
|
101
|
+
if (published) this.broker.notifyChanged();
|
|
102
|
+
return published;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
provider,
|
|
106
|
+
});
|
|
107
|
+
this.preuse = new AuthSwitchAuthorityPreuse(this.database, this.broker, options.preuse);
|
|
108
|
+
if (options.claudeNative) {
|
|
109
|
+
this.claudeNative = new ClaudeNativeAuthority(this.database, {
|
|
110
|
+
...options.claudeNative, onChanged: () => this.broker.notifyChanged(),
|
|
111
|
+
});
|
|
112
|
+
this.broker.setClaudeRefresher(grantId => this.claudeNative!.refreshInactiveGrant(grantId));
|
|
113
|
+
}
|
|
47
114
|
this.register();
|
|
48
115
|
}
|
|
49
116
|
|
|
117
|
+
private runtimeBindingKey(bindingId: string, capability: string): string {
|
|
118
|
+
return `${bindingId}:${authSwitchBindingCapabilityHash(capability)}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private beginRuntimeBindingAccess(key: string): () => void {
|
|
122
|
+
if (this.runtimeBindingReleaseFences.has(key)) throw new Error('Runtime binding release is in progress.');
|
|
123
|
+
const activity = this.runtimeBindingActivity.get(key) ?? { active: 0, waiters: new Set<() => void>() };
|
|
124
|
+
activity.active++;
|
|
125
|
+
this.runtimeBindingActivity.set(key, activity);
|
|
126
|
+
let finished = false;
|
|
127
|
+
return () => {
|
|
128
|
+
if (finished) return;
|
|
129
|
+
finished = true;
|
|
130
|
+
activity.active--;
|
|
131
|
+
if (activity.active !== 0) return;
|
|
132
|
+
this.runtimeBindingActivity.delete(key);
|
|
133
|
+
for (const resolve of activity.waiters) resolve();
|
|
134
|
+
activity.waiters.clear();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private waitForRuntimeBindingAccess(key: string): Promise<void> {
|
|
139
|
+
const activity = this.runtimeBindingActivity.get(key);
|
|
140
|
+
if (!activity || activity.active === 0) return Promise.resolve();
|
|
141
|
+
return new Promise(resolve => activity.waiters.add(resolve));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private releaseRuntimeBinding(bindingId: string,
|
|
145
|
+
capability: string): Promise<IReq_AuthSwitchReleaseBinding['response']> {
|
|
146
|
+
const key = this.runtimeBindingKey(bindingId, capability);
|
|
147
|
+
const existing = this.runtimeBindingReleases.get(key);
|
|
148
|
+
if (existing) return existing;
|
|
149
|
+
// Capabilities are freshly random for every bind, including a successor that reuses the
|
|
150
|
+
// same caller label. A stale release therefore fences only its own unusable capability.
|
|
151
|
+
this.runtimeBindingReleaseFences.add(key);
|
|
152
|
+
const task = (async (): Promise<IReq_AuthSwitchReleaseBinding['response']> => {
|
|
153
|
+
await this.waitForRuntimeBindingAccess(key);
|
|
154
|
+
const state = await this.broker.releaseExternalBinding(bindingId, capability);
|
|
155
|
+
this.runtimeBindingReleaseFences.delete(key);
|
|
156
|
+
return { state };
|
|
157
|
+
})();
|
|
158
|
+
this.runtimeBindingReleases.set(key, task);
|
|
159
|
+
void task.finally(() => {
|
|
160
|
+
if (this.runtimeBindingReleases.get(key) === task) this.runtimeBindingReleases.delete(key);
|
|
161
|
+
// A rejected release keeps only this exact key fenced until a retry reconciles database truth.
|
|
162
|
+
}).catch(() => {});
|
|
163
|
+
return task;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private releaseDeviceDrain(accountId: string, operationId: string): void {
|
|
167
|
+
if (this.deviceDrainOwners.get(accountId) !== operationId) return;
|
|
168
|
+
this.deviceDrainOwners.delete(accountId);
|
|
169
|
+
this.drainingAccounts.delete(accountId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private beginReauthOpenAi(request: IReq_AuthSwitchBeginReauth['request']): Promise<IReq_AuthSwitchBeginReauth['response']['operation']> {
|
|
173
|
+
let ownsReservation = false;
|
|
174
|
+
const prepare = async (): Promise<void> => {
|
|
175
|
+
if (this.closing || this.drainingAccounts.has(request.accountId)
|
|
176
|
+
|| this.preuseAccounts.has(request.accountId)) {
|
|
177
|
+
throw new Error('Account runtime drain is already in progress.');
|
|
178
|
+
}
|
|
179
|
+
this.drainingAccounts.add(request.accountId);
|
|
180
|
+
this.deviceDrainOwners.set(request.accountId, request.operationId);
|
|
181
|
+
ownsReservation = true;
|
|
182
|
+
try { await this.drainManagedCodexAccount(request.accountId); }
|
|
183
|
+
catch (error) {
|
|
184
|
+
this.releaseDeviceDrain(request.accountId, request.operationId);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const started = this.broker.beginReauthOpenAi(request.operationId, request.accountId,
|
|
189
|
+
request.loginId, request.purpose, prepare);
|
|
190
|
+
return started.then(operation => {
|
|
191
|
+
if (!ownsReservation) return operation;
|
|
192
|
+
if (['starting', 'pending', 'committing'].includes(operation.state)) {
|
|
193
|
+
void this.broker.waitOperation(operation.id).finally(() => {
|
|
194
|
+
this.releaseDeviceDrain(request.accountId, request.operationId);
|
|
195
|
+
}).catch(() => {});
|
|
196
|
+
} else this.releaseDeviceDrain(request.accountId, request.operationId);
|
|
197
|
+
return operation;
|
|
198
|
+
}, error => {
|
|
199
|
+
if (ownsReservation) this.releaseDeviceDrain(request.accountId, request.operationId);
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
50
204
|
private register(): void {
|
|
205
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchUsage>(
|
|
206
|
+
'authswitch.authority.usage', async request => {
|
|
207
|
+
if (request === null || typeof request !== 'object' || Array.isArray(request)
|
|
208
|
+
|| !Object.hasOwn(request, 'accountId') || !Object.hasOwn(request, 'loginId')
|
|
209
|
+
|| Object.keys(request).some(key => !['accountId', 'loginId', 'force'].includes(key))
|
|
210
|
+
|| typeof request.accountId !== 'string' || typeof request.loginId !== 'string'
|
|
211
|
+
|| (request.force !== undefined && typeof request.force !== 'boolean')) invalid();
|
|
212
|
+
if (this.closing) throw new Error('Authswitch authority is closing.');
|
|
213
|
+
return { usage: await this.usage.get(request.accountId, request.loginId, request.force ?? false) };
|
|
214
|
+
}));
|
|
215
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchSwitchClaudeNative>(
|
|
216
|
+
'authswitch.authority.claude.switch', async request => {
|
|
217
|
+
if (!hasKeys(request, ['accountId', 'loginId', 'purpose'])
|
|
218
|
+
|| typeof request.accountId !== 'string' || typeof request.loginId !== 'string'
|
|
219
|
+
|| request.purpose !== 'claude_host_native') invalid();
|
|
220
|
+
if (this.closing || !this.claudeNative) throw new Error('Claude native handoff is unavailable.');
|
|
221
|
+
return { handoff: await this.claudeNative.switchTo(request.accountId, request.loginId) };
|
|
222
|
+
}));
|
|
223
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchClaudeNativeHandoff>(
|
|
224
|
+
'authswitch.authority.claude.handoff', async request => {
|
|
225
|
+
if (!hasKeys(request, ['operationId']) || typeof request.operationId !== 'string') invalid();
|
|
226
|
+
if (!this.claudeNative) throw new Error('Claude native handoff is unavailable.');
|
|
227
|
+
return { handoff: await this.claudeNative.getHandoff(request.operationId) };
|
|
228
|
+
}));
|
|
229
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchClaudeNativeHandoffs>(
|
|
230
|
+
'authswitch.authority.claude.handoffs', async request => {
|
|
231
|
+
if (request === null || typeof request !== 'object' || Array.isArray(request)
|
|
232
|
+
|| Object.keys(request).some(key => !['after', 'limit'].includes(key))) invalid();
|
|
233
|
+
if (!this.claudeNative) throw new Error('Claude native handoff is unavailable.');
|
|
234
|
+
return this.claudeNative.listHandoffs(request.after ?? null, request.limit ?? 128);
|
|
235
|
+
}));
|
|
51
236
|
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchSnapshot>(
|
|
52
237
|
'authswitch.authority.snapshot', async request => {
|
|
53
238
|
if (request === null || typeof request !== 'object' || Array.isArray(request)) invalid();
|
|
54
239
|
const keys = Object.keys(request);
|
|
55
|
-
if (keys.some(key => !['accountAfter', 'bindingAfter', 'limit'].includes(key))) invalid();
|
|
240
|
+
if (keys.some(key => !['accountAfter', 'loginAfter', 'bindingAfter', 'limit'].includes(key))) invalid();
|
|
56
241
|
return { snapshot: await this.broker.snapshot(request) };
|
|
57
242
|
}));
|
|
243
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchDoctor>(
|
|
244
|
+
'authswitch.authority.doctor', async request => {
|
|
245
|
+
if (request === null || typeof request !== 'object' || Array.isArray(request)) invalid();
|
|
246
|
+
const cursorKeys = ['accountAfter', 'loginAfter', 'codexHomeAfter', 'claudeHomeAfter',
|
|
247
|
+
'claudeHandoffAfter', 'nativeHandoffAfter', 'migrationAfter'] as const;
|
|
248
|
+
if (Object.keys(request).some(key => ![...cursorKeys, 'limit'].includes(key as typeof cursorKeys[number] | 'limit'))
|
|
249
|
+
|| cursorKeys.some(key => request[key] !== undefined && typeof request[key] !== 'string')
|
|
250
|
+
|| (request.limit !== undefined && (!Number.isSafeInteger(request.limit)
|
|
251
|
+
|| request.limit < 1 || request.limit > 128))) invalid();
|
|
252
|
+
return { page: await this.doctorPage(request) };
|
|
253
|
+
}));
|
|
58
254
|
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchEvents>(
|
|
59
255
|
'authswitch.authority.events', async (request, tools) => {
|
|
60
256
|
if (!hasKeys(request, ['epoch', 'afterRevision', 'waitMs'])) invalid();
|
|
@@ -62,31 +258,82 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
62
258
|
}));
|
|
63
259
|
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchBeginAdd>(
|
|
64
260
|
'authswitch.authority.add', async request => {
|
|
65
|
-
if (!hasKeys(request, ['providerId', 'flow']) ||
|
|
66
|
-
|
|
261
|
+
if (!hasKeys(request, ['operationId', 'providerId', 'flow']) || typeof request.operationId !== 'string'
|
|
262
|
+
|| request.providerId !== 'openai' || request.flow !== 'device') invalid();
|
|
263
|
+
if (this.closing) throw new Error('Authswitch authority is closing.');
|
|
264
|
+
return { operation: await this.broker.beginAddOpenAi(request.operationId) };
|
|
67
265
|
}));
|
|
68
266
|
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchBeginReauth>(
|
|
69
267
|
'authswitch.authority.reauth', async request => {
|
|
70
|
-
if (!hasKeys(request, ['accountId', 'flow']) || request.flow !== 'device'
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
void this.broker.waitOperation(operation.id).finally(() => this.drainingAccounts.delete(request.accountId)).catch(() => {});
|
|
77
|
-
return { operation };
|
|
78
|
-
} catch (error) { this.drainingAccounts.delete(request.accountId); throw error; }
|
|
268
|
+
if (!hasKeys(request, ['operationId', 'accountId', 'loginId', 'purpose', 'flow']) || request.flow !== 'device'
|
|
269
|
+
|| typeof request.operationId !== 'string'
|
|
270
|
+
|| typeof request.accountId !== 'string' || typeof request.loginId !== 'string'
|
|
271
|
+
|| request.purpose !== 'openai_managed') invalid();
|
|
272
|
+
if (this.closing) throw new Error('Authswitch authority is closing.');
|
|
273
|
+
return { operation: await this.beginReauthOpenAi(request) };
|
|
79
274
|
}));
|
|
80
275
|
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchGetOperation>(
|
|
81
276
|
'authswitch.authority.operation', async request => {
|
|
82
277
|
if (!hasKeys(request, ['operationId']) || typeof request.operationId !== 'string') invalid();
|
|
83
|
-
return { operation: this.broker.getOperation(request.operationId) };
|
|
278
|
+
return { operation: await this.broker.getOperation(request.operationId) };
|
|
279
|
+
}));
|
|
280
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchListOperations>(
|
|
281
|
+
'authswitch.authority.operations', async request => {
|
|
282
|
+
if (request === null || typeof request !== 'object' || Array.isArray(request)
|
|
283
|
+
|| Object.keys(request).some(key => !['after', 'limit'].includes(key))) invalid();
|
|
284
|
+
return this.broker.listOperations(request.after ?? null, request.limit ?? 128);
|
|
84
285
|
}));
|
|
85
286
|
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchCancelOperation>(
|
|
86
287
|
'authswitch.authority.cancel', async request => {
|
|
87
288
|
if (!hasKeys(request, ['operationId']) || typeof request.operationId !== 'string') invalid();
|
|
88
289
|
return { operation: await this.broker.cancelOperation(request.operationId) };
|
|
89
290
|
}));
|
|
291
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchStartPreuse>(
|
|
292
|
+
'authswitch.authority.preuse.start', async request => {
|
|
293
|
+
if (request === null || typeof request !== 'object' || Array.isArray(request)
|
|
294
|
+
|| !['operationId', 'accountId', 'loginId', 'purpose', 'prompt'].every(key => Object.hasOwn(request, key))
|
|
295
|
+
|| Object.keys(request).some(key => !['operationId', 'accountId', 'loginId', 'purpose', 'prompt', 'model'].includes(key))
|
|
296
|
+
|| typeof request.operationId !== 'string' || typeof request.accountId !== 'string'
|
|
297
|
+
|| typeof request.loginId !== 'string' || request.purpose !== 'openai_managed'
|
|
298
|
+
|| typeof request.prompt !== 'string'
|
|
299
|
+
|| (request.model !== undefined && typeof request.model !== 'string')) invalid();
|
|
300
|
+
if (this.closing) throw new Error('Authswitch authority is closing.');
|
|
301
|
+
const reservation: IPreuseAccountReservation = { operationId: request.operationId };
|
|
302
|
+
let ownsReservation = false;
|
|
303
|
+
const release = (): void => {
|
|
304
|
+
if (this.preuseAccounts.get(request.accountId) !== reservation) return;
|
|
305
|
+
this.preuseAccounts.delete(request.accountId);
|
|
306
|
+
};
|
|
307
|
+
try {
|
|
308
|
+
const operation = await this.preuse.begin(request, () => {
|
|
309
|
+
const owner = this.preuseAccounts.get(request.accountId);
|
|
310
|
+
if (this.drainingAccounts.has(request.accountId)
|
|
311
|
+
|| (owner && owner.operationId !== request.operationId)) return 'account_busy';
|
|
312
|
+
if (!owner) {
|
|
313
|
+
this.preuseAccounts.set(request.accountId, reservation);
|
|
314
|
+
ownsReservation = true;
|
|
315
|
+
}
|
|
316
|
+
return 'admitted';
|
|
317
|
+
});
|
|
318
|
+
if (ownsReservation && this.preuse.hasActive(request.operationId)) {
|
|
319
|
+
void this.preuse.wait(request.operationId).finally(release).catch(() => {});
|
|
320
|
+
} else if (ownsReservation) release();
|
|
321
|
+
return { operation };
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (ownsReservation) release();
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
}));
|
|
327
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchGetPreuse>(
|
|
328
|
+
'authswitch.authority.preuse.operation', async request => {
|
|
329
|
+
if (!hasKeys(request, ['operationId']) || typeof request.operationId !== 'string') invalid();
|
|
330
|
+
return { operation: await this.preuse.get(request.operationId) };
|
|
331
|
+
}));
|
|
332
|
+
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchCancelPreuse>(
|
|
333
|
+
'authswitch.authority.preuse.cancel', async request => {
|
|
334
|
+
if (!hasKeys(request, ['operationId']) || typeof request.operationId !== 'string') invalid();
|
|
335
|
+
return { operation: await this.preuse.cancel(request.operationId) };
|
|
336
|
+
}));
|
|
90
337
|
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchRenameAccount>(
|
|
91
338
|
'authswitch.authority.rename', async request => {
|
|
92
339
|
if (!hasKeys(request, ['accountId', 'expectedRevision', 'label']) || typeof request.accountId !== 'string'
|
|
@@ -97,7 +344,9 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
97
344
|
'authswitch.authority.remove', async request => {
|
|
98
345
|
if (!hasKeys(request, ['accountId', 'expectedRevision']) || typeof request.accountId !== 'string'
|
|
99
346
|
|| typeof request.expectedRevision !== 'number') invalid();
|
|
100
|
-
if (this.drainingAccounts.has(request.accountId)
|
|
347
|
+
if (this.drainingAccounts.has(request.accountId) || this.preuseAccounts.has(request.accountId)) {
|
|
348
|
+
throw new Error('Account runtime drain is already in progress.');
|
|
349
|
+
}
|
|
101
350
|
this.drainingAccounts.add(request.accountId);
|
|
102
351
|
try {
|
|
103
352
|
await this.drainManagedCodexAccount(request.accountId);
|
|
@@ -106,8 +355,9 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
106
355
|
}));
|
|
107
356
|
this.managementRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchBindAccount>(
|
|
108
357
|
'authswitch.authority.bind', async request => {
|
|
109
|
-
if (!hasKeys(request, ['accountId', 'runtime', 'scopeId', 'incarnationId'])
|
|
110
|
-
|| typeof request.accountId !== 'string' || typeof request.
|
|
358
|
+
if (!hasKeys(request, ['accountId', 'loginId', 'purpose', 'runtime', 'scopeId', 'incarnationId'])
|
|
359
|
+
|| typeof request.accountId !== 'string' || typeof request.loginId !== 'string'
|
|
360
|
+
|| request.purpose !== 'openai_managed' || typeof request.scopeId !== 'string'
|
|
111
361
|
|| typeof request.incarnationId !== 'string') invalid();
|
|
112
362
|
return this.broker.bindAccount(request);
|
|
113
363
|
}));
|
|
@@ -117,19 +367,134 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
117
367
|
|| !['bindingId', 'capability', 'minValidityMs'].every(key => Object.hasOwn(request, key))
|
|
118
368
|
|| Object.keys(request).some(key => !['bindingId', 'capability', 'minValidityMs',
|
|
119
369
|
'rejectedGrantGeneration'].includes(key))
|
|
120
|
-
||
|
|
370
|
+
|| !validBindingId(request.bindingId) || !validBindingCapability(request.capability)
|
|
121
371
|
|| typeof request.minValidityMs !== 'number'
|
|
122
372
|
|| (request.rejectedGrantGeneration !== undefined && typeof request.rejectedGrantGeneration !== 'number')) invalid();
|
|
123
|
-
|
|
124
|
-
|
|
373
|
+
if (this.closing) throw new Error('Authswitch authority is closing.');
|
|
374
|
+
const finish = this.beginRuntimeBindingAccess(this.runtimeBindingKey(request.bindingId, request.capability));
|
|
375
|
+
try {
|
|
376
|
+
return await this.broker.resolveAccess(request.bindingId, request.capability, request.minValidityMs,
|
|
377
|
+
request.rejectedGrantGeneration);
|
|
378
|
+
} finally { finish(); }
|
|
379
|
+
}));
|
|
380
|
+
this.runtimeRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_AuthSwitchReleaseBinding>(
|
|
381
|
+
'authswitch.authority.release', async request => {
|
|
382
|
+
if (!hasKeys(request, ['bindingId', 'capability'])
|
|
383
|
+
|| !validBindingId(request.bindingId) || !validBindingCapability(request.capability)) invalid();
|
|
384
|
+
if (this.closing) throw new Error('Authswitch authority is closing.');
|
|
385
|
+
return this.releaseRuntimeBinding(request.bindingId, request.capability);
|
|
125
386
|
}));
|
|
126
387
|
}
|
|
127
388
|
|
|
389
|
+
private async doctorPage(request: IReq_AuthSwitchDoctor['request']): Promise<IAuthSwitchDoctorPage> {
|
|
390
|
+
const stored = await this.database.doctorPage({
|
|
391
|
+
accountAfter: request.accountAfter ?? null,
|
|
392
|
+
loginAfter: request.loginAfter ?? null,
|
|
393
|
+
codexHomeAfter: request.codexHomeAfter ?? null,
|
|
394
|
+
claudeHomeAfter: request.claudeHomeAfter ?? null,
|
|
395
|
+
claudeHandoffAfter: request.claudeHandoffAfter ?? null,
|
|
396
|
+
nativeHandoffAfter: request.nativeHandoffAfter ?? null,
|
|
397
|
+
migrationAfter: request.migrationAfter ?? null,
|
|
398
|
+
}, request.limit ?? 128);
|
|
399
|
+
const socketPaths = stored.codexHomes.flatMap(home => home.activeRun ? [home.activeRun.socketPath] : []);
|
|
400
|
+
let processInspection: Awaited<ReturnType<typeof observeManagedCodexProcesses>> | null = null;
|
|
401
|
+
try { processInspection = await observeManagedCodexProcesses(socketPaths); }
|
|
402
|
+
catch { /* A fixed diagnostic category is safer than exposing a host-process error. */ }
|
|
403
|
+
const processObservedAt = new Date().toISOString();
|
|
404
|
+
const managedCodex = stored.codexHomes.map(home => {
|
|
405
|
+
const activeRun = home.activeRun;
|
|
406
|
+
const durable: IAuthSwitchDoctorPage['managedCodex'][number]['durable'] = activeRun === null
|
|
407
|
+
? { state: 'idle', runId: null, pid: null, startedAt: null }
|
|
408
|
+
: { state: activeRun.pid === null || activeRun.startedAt === null ? 'launch_prepared' : 'registered',
|
|
409
|
+
runId: activeRun.id, pid: activeRun.pid, startedAt: activeRun.startedAt };
|
|
410
|
+
const daemonState = this.managedCodexStates.get(home.scopeId) ?? 'untracked';
|
|
411
|
+
const assignment = daemonState === 'untracked'
|
|
412
|
+
? 'untracked' as const
|
|
413
|
+
: this.managedCodexAccountIds.get(home.scopeId) === home.accountId && this.managedCodexHomes.has(home.id)
|
|
414
|
+
? 'matches' as const : 'mismatch' as const;
|
|
415
|
+
let process: IAuthSwitchDoctorPage['managedCodex'][number]['process'];
|
|
416
|
+
if (!activeRun) {
|
|
417
|
+
process = { state: 'not_expected', identity: 'not_applicable', pid: null, startedAt: null,
|
|
418
|
+
observedAt: processObservedAt, problem: 'none' };
|
|
419
|
+
} else if (!processInspection) {
|
|
420
|
+
process = { state: 'unknown', identity: 'unknown', pid: null, startedAt: null,
|
|
421
|
+
observedAt: processObservedAt, problem: 'inspection_failed' };
|
|
422
|
+
} else {
|
|
423
|
+
const observed = processInspection.get(activeRun.socketPath);
|
|
424
|
+
if (!observed || observed.state === 'absent') {
|
|
425
|
+
process = { state: 'absent', identity: 'not_applicable', pid: null, startedAt: null,
|
|
426
|
+
observedAt: processObservedAt, problem: 'none' };
|
|
427
|
+
} else if (observed.state === 'multiple_claimants') {
|
|
428
|
+
process = { state: 'unknown', identity: 'unknown', pid: null, startedAt: null,
|
|
429
|
+
observedAt: processObservedAt, problem: 'multiple_claimants' };
|
|
430
|
+
} else {
|
|
431
|
+
const identity = activeRun.pid === null || activeRun.startedAt === null
|
|
432
|
+
? 'unregistered' as const
|
|
433
|
+
: activeRun.pid === observed.pid && activeRun.startedAt === observed.startedAt
|
|
434
|
+
? 'matches' as const : 'mismatch' as const;
|
|
435
|
+
process = { state: 'observed', identity, pid: observed.pid, startedAt: observed.startedAt,
|
|
436
|
+
observedAt: processObservedAt, problem: 'none' };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const health = process.state === 'unknown'
|
|
440
|
+
? 'unknown' as const
|
|
441
|
+
: daemonState === 'degraded' || assignment === 'mismatch'
|
|
442
|
+
|| (activeRun !== null && (daemonState === 'untracked' || process.state === 'absent'
|
|
443
|
+
|| process.identity === 'mismatch'))
|
|
444
|
+
? 'degraded' as const : 'healthy' as const;
|
|
445
|
+
return { homeId: home.id, accountId: home.accountId, loginId: home.grantId, scopeId: home.scopeId,
|
|
446
|
+
health, durable, daemon: { state: daemonState, assignment }, process };
|
|
447
|
+
});
|
|
448
|
+
const observedAt = new Date().toISOString();
|
|
449
|
+
return {
|
|
450
|
+
observedAt,
|
|
451
|
+
daemon: { state: this.closing ? 'closing' : 'ready',
|
|
452
|
+
claudeNativeAuthority: this.claudeNative ? 'configured' : 'not_configured', observedAt },
|
|
453
|
+
database: { state: 'available', observedAt: stored.observedAt, schemaVersion: stored.meta.schemaVersion,
|
|
454
|
+
admission: stored.meta.admission, epoch: stored.meta.epoch, revision: stored.meta.revision },
|
|
455
|
+
accounts: stored.accounts.map(account => ({ id: account.id, providerId: account.providerId,
|
|
456
|
+
removed: account.removed, statusObservedAt: account.statusObservedAt,
|
|
457
|
+
evidence: 'stored', liveCheck: 'not_checked' })),
|
|
458
|
+
logins: stored.logins.map(login => ({ id: login.id, accountId: login.accountId,
|
|
459
|
+
providerId: login.providerId, purpose: login.purpose, owner: login.owner, state: login.state,
|
|
460
|
+
problem: login.problem, grantGeneration: login.grantGeneration,
|
|
461
|
+
authorizationGeneration: login.authorizationGeneration, accessExpiresAt: login.accessExpiresAt,
|
|
462
|
+
retryAt: login.retryAt, statusObservedAt: login.statusObservedAt,
|
|
463
|
+
evidence: 'stored', liveCheck: 'not_checked' })),
|
|
464
|
+
managedCodex,
|
|
465
|
+
claudeHomes: stored.claudeHomes.map(home => ({ id: home.id, activeAccountId: home.activeAccountId,
|
|
466
|
+
activeLoginId: home.activeGrantId, pendingOperationId: home.pendingOperationId, status: home.status,
|
|
467
|
+
revision: home.revision, createdAt: home.createdAt })),
|
|
468
|
+
claudeHandoffs: stored.claudeHandoffs.map(handoff => ({ id: handoff.id, homeId: handoff.homeId,
|
|
469
|
+
outgoingAccountId: handoff.outgoingAccountId, outgoingLoginId: handoff.outgoingGrantId,
|
|
470
|
+
incomingAccountId: handoff.incomingAccountId, incomingLoginId: handoff.incomingGrantId,
|
|
471
|
+
phase: handoff.phase, problem: handoff.problem, runningEffectiveAuth: handoff.runningEffectiveAuth,
|
|
472
|
+
startedAt: handoff.startedAt, updatedAt: handoff.updatedAt, revision: handoff.revision })),
|
|
473
|
+
nativeHandoffs: stored.nativeHandoffs.map(handoff => ({ id: handoff.id, accountId: handoff.accountId,
|
|
474
|
+
loginId: handoff.grantId, nativeHomeId: handoff.nativeHomeId, direction: handoff.direction,
|
|
475
|
+
phase: handoff.phase, operationId: handoff.operationId, revision: handoff.revision,
|
|
476
|
+
statusObservedAt: handoff.statusObservedAt })),
|
|
477
|
+
migrations: stored.migrations.map(migration => ({ id: migration.id, sourceKind: migration.sourceKind,
|
|
478
|
+
status: migration.status, accountId: migration.accountId, loginId: migration.grantId,
|
|
479
|
+
revision: migration.revision, statusObservedAt: migration.statusObservedAt })),
|
|
480
|
+
nextAccountCursor: stored.nextAccountCursor,
|
|
481
|
+
nextLoginCursor: stored.nextLoginCursor,
|
|
482
|
+
nextCodexHomeCursor: stored.nextCodexHomeCursor,
|
|
483
|
+
nextClaudeHomeCursor: stored.nextClaudeHomeCursor,
|
|
484
|
+
nextClaudeHandoffCursor: stored.nextClaudeHandoffCursor,
|
|
485
|
+
nextNativeHandoffCursor: stored.nextNativeHandoffCursor,
|
|
486
|
+
nextMigrationCursor: stored.nextMigrationCursor,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
128
490
|
public async start(): Promise<void> {
|
|
129
491
|
if (this.managementServer || this.runtimeServer) throw new Error('Authswitch authority daemon is already started.');
|
|
130
492
|
await this.database.start();
|
|
131
493
|
try {
|
|
494
|
+
await this.database.assertOpenAdmission();
|
|
132
495
|
await this.broker.start();
|
|
496
|
+
await this.preuse.start();
|
|
497
|
+
if (this.claudeNative) await this.claudeNative.start();
|
|
133
498
|
const managementDirectory = plugins.path.dirname(this.options.authoritySocketPath);
|
|
134
499
|
const runtimeDirectory = plugins.path.dirname(this.options.runtimeSocketPath);
|
|
135
500
|
const databaseDirectory = plugins.path.dirname(this.options.socketPath);
|
|
@@ -148,16 +513,37 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
148
513
|
}
|
|
149
514
|
}
|
|
150
515
|
|
|
151
|
-
/**
|
|
516
|
+
/** The account fence excludes new admissions while durable detached runs are checked. */
|
|
152
517
|
private async drainManagedCodexAccount(accountId: string): Promise<void> {
|
|
153
518
|
const scopes = [...this.managedCodexAccountIds]
|
|
154
519
|
.filter(([, assignedAccountId]) => assignedAccountId === accountId).map(([scopeId]) => scopeId);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
520
|
+
if (scopes.length) throw new Error('Managed Codex may still have active work; stop it explicitly after work finishes before changing this account.');
|
|
521
|
+
let after: string | null = null;
|
|
522
|
+
while (true) {
|
|
523
|
+
const homes = await this.database.codexHomesForAccount(accountId, after);
|
|
524
|
+
for (const home of homes) {
|
|
525
|
+
const run = home.activeRun;
|
|
526
|
+
if (!run) continue;
|
|
527
|
+
const survivor = await findManagedCodexProcess(run.socketPath);
|
|
528
|
+
if (survivor) throw new Error('A managed Codex app-server still owns this account; stop its work before changing the account.');
|
|
529
|
+
if (run.pid === null || run.startedAt === null) {
|
|
530
|
+
throw new Error('A managed Codex launch has uncertain process ownership; reconcile it before changing the account.');
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
process.kill(run.pid, 0);
|
|
534
|
+
throw new Error('A managed Codex process identity is still present; reconcile it before changing the account.');
|
|
535
|
+
} catch (error) {
|
|
536
|
+
if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH')) throw error;
|
|
537
|
+
}
|
|
538
|
+
await this.database.changeCodexRun(home.id, run.id, null, plugins.crypto.randomUUID());
|
|
539
|
+
}
|
|
540
|
+
if (homes.length < 128) break;
|
|
541
|
+
after = homes.at(-1)!.id;
|
|
542
|
+
}
|
|
158
543
|
}
|
|
159
544
|
|
|
160
|
-
public async startManagedCodex(input: { accountId: string;
|
|
545
|
+
public async startManagedCodex(input: { accountId: string; loginId: string; purpose: 'openai_managed';
|
|
546
|
+
scopeId: string; workspaceDirectory: string;
|
|
161
547
|
codexHomeDirectory: string;
|
|
162
548
|
executable?: string; onNotification?: IAuthSwitchManagedCodexOptions['onNotification'];
|
|
163
549
|
onServerRequest?: IAuthSwitchManagedCodexOptions['onServerRequest'] }): Promise<AuthSwitchManagedCodex> {
|
|
@@ -173,12 +559,14 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
173
559
|
}
|
|
174
560
|
this.managedCodexHomes.add(homeId);
|
|
175
561
|
this.managedCodexAccountIds.set(input.scopeId, input.accountId);
|
|
562
|
+
this.managedCodexStates.set(input.scopeId, 'starting');
|
|
176
563
|
const runId = plugins.crypto.randomUUID();
|
|
177
564
|
const task = (async () => {
|
|
178
565
|
let bound: Awaited<ReturnType<typeof this.broker.bindAccount>> | undefined;
|
|
179
566
|
let runtime: AuthSwitchManagedCodex | undefined;
|
|
180
567
|
let claimed = false;
|
|
181
568
|
try {
|
|
569
|
+
await this.database.assertOpenAdmission();
|
|
182
570
|
const home = input.codexHomeDirectory;
|
|
183
571
|
const parent = plugins.path.dirname(home);
|
|
184
572
|
const parentStat = await plugins.fs.promises.lstat(parent);
|
|
@@ -193,7 +581,8 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
193
581
|
throw new Error('Managed Codex cannot adopt an active native Codex home.');
|
|
194
582
|
}
|
|
195
583
|
const existing = await this.database.readCodexHome(homeId);
|
|
196
|
-
if (existing && (existing.accountId !== input.accountId || existing.
|
|
584
|
+
if (existing && (existing.accountId !== input.accountId || existing.grantId !== input.loginId
|
|
585
|
+
|| existing.scopeId !== input.scopeId)) {
|
|
197
586
|
throw new Error('Managed Codex home already belongs to another account or scope.');
|
|
198
587
|
}
|
|
199
588
|
let homeExists = false;
|
|
@@ -210,9 +599,10 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
210
599
|
if (!existing && homeExists && (await plugins.fs.promises.readdir(home)).length) {
|
|
211
600
|
throw new Error('An existing Codex home needs explicit native-owner handoff before enrollment.');
|
|
212
601
|
}
|
|
213
|
-
await this.database.reserveCodexHome(input.accountId, input.scopeId, homeId, plugins.crypto.randomUUID());
|
|
602
|
+
await this.database.reserveCodexHome(input.accountId, input.loginId, input.scopeId, homeId, plugins.crypto.randomUUID());
|
|
214
603
|
if (!homeExists) await plugins.fs.promises.mkdir(home, { mode: 0o700 });
|
|
215
|
-
bound = await this.broker.bindAccount({ accountId: input.accountId,
|
|
604
|
+
bound = await this.broker.bindAccount({ accountId: input.accountId, loginId: input.loginId,
|
|
605
|
+
purpose: input.purpose, runtime: 'codex',
|
|
216
606
|
scopeId: input.scopeId, incarnationId: plugins.crypto.randomUUID() });
|
|
217
607
|
runtime = new AuthSwitchManagedCodex({
|
|
218
608
|
runtimeDirectory: plugins.path.join(plugins.path.dirname(plugins.path.dirname(this.options.authoritySocketPath)), 'managed-codex'),
|
|
@@ -221,7 +611,8 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
221
611
|
broker: this.broker, bindingId: bound.binding.id, capability: bound.capability,
|
|
222
612
|
onPreparedRun: async socketPath => {
|
|
223
613
|
const assigned = await this.database.readCodexHome(homeId);
|
|
224
|
-
if (!assigned || assigned.accountId !== input.accountId || assigned.
|
|
614
|
+
if (!assigned || assigned.accountId !== input.accountId || assigned.grantId !== input.loginId
|
|
615
|
+
|| assigned.scopeId !== input.scopeId) {
|
|
225
616
|
throw new Error('Managed Codex home assignment changed.');
|
|
226
617
|
}
|
|
227
618
|
if (assigned.activeRun) {
|
|
@@ -250,6 +641,9 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
250
641
|
});
|
|
251
642
|
await runtime.start();
|
|
252
643
|
if (this.closing) throw new Error('Managed Codex admission closed during startup.');
|
|
644
|
+
if (this.managedCodexStates.get(input.scopeId) !== 'stopping') {
|
|
645
|
+
this.managedCodexStates.set(input.scopeId, 'ready');
|
|
646
|
+
}
|
|
253
647
|
return { runtime, bindingId: bound.binding.id, capability: bound.capability, homeId, runId };
|
|
254
648
|
} catch (error) {
|
|
255
649
|
const cleanupErrors: unknown[] = [];
|
|
@@ -262,8 +656,12 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
262
656
|
if (cleanupErrors.length === 0) {
|
|
263
657
|
this.managedCodexHomes.delete(homeId);
|
|
264
658
|
this.managedCodexAccountIds.delete(input.scopeId);
|
|
659
|
+
this.managedCodexStates.delete(input.scopeId);
|
|
660
|
+
}
|
|
661
|
+
if (cleanupErrors.length) {
|
|
662
|
+
this.managedCodexStates.set(input.scopeId, 'degraded');
|
|
663
|
+
throw new AggregateError([error, ...cleanupErrors], 'Managed Codex admission cleanup failed.');
|
|
265
664
|
}
|
|
266
|
-
if (cleanupErrors.length) throw new AggregateError([error, ...cleanupErrors], 'Managed Codex admission cleanup failed.');
|
|
267
665
|
throw error;
|
|
268
666
|
}
|
|
269
667
|
})();
|
|
@@ -278,10 +676,15 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
278
676
|
const task = this.managedCodex.get(scopeId);
|
|
279
677
|
if (!task) return Promise.resolve();
|
|
280
678
|
this.managedCodex.delete(scopeId);
|
|
679
|
+
this.managedCodexStates.set(scopeId, 'stopping');
|
|
281
680
|
const stopping = (async () => {
|
|
282
681
|
let entry: Awaited<typeof task>;
|
|
283
682
|
try { entry = await task; }
|
|
284
|
-
catch {
|
|
683
|
+
catch {
|
|
684
|
+
/* Failed admission already closed its process and revoked its binding. */
|
|
685
|
+
if (this.managedCodexStates.get(scopeId) !== 'degraded') this.managedCodexStates.delete(scopeId);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
285
688
|
const { runtime, bindingId, capability, homeId, runId } = entry;
|
|
286
689
|
const errors: unknown[] = [];
|
|
287
690
|
try { await runtime.close(); } catch (error) { errors.push(error); }
|
|
@@ -289,9 +692,13 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
289
692
|
await this.database.changeCodexRun(homeId, runId, null, plugins.crypto.randomUUID());
|
|
290
693
|
this.managedCodexHomes.delete(homeId);
|
|
291
694
|
this.managedCodexAccountIds.delete(scopeId);
|
|
695
|
+
this.managedCodexStates.delete(scopeId);
|
|
292
696
|
} catch (error) { errors.push(error); }
|
|
293
697
|
try { await this.broker.revokeBinding(bindingId, capability); } catch (error) { errors.push(error); }
|
|
294
|
-
if (errors.length)
|
|
698
|
+
if (errors.length) {
|
|
699
|
+
this.managedCodexStates.set(scopeId, 'degraded');
|
|
700
|
+
throw new AggregateError(errors, 'Managed Codex shutdown was incomplete.');
|
|
701
|
+
}
|
|
295
702
|
})();
|
|
296
703
|
this.managedCodexStops.set(scopeId, stopping);
|
|
297
704
|
void stopping.finally(() => { if (this.managedCodexStops.get(scopeId) === stopping) this.managedCodexStops.delete(scopeId); }).catch(() => {});
|
|
@@ -379,14 +786,20 @@ export class AuthSwitchAuthorityDaemon {
|
|
|
379
786
|
public async close(): Promise<void> {
|
|
380
787
|
this.closing = true;
|
|
381
788
|
const errors: unknown[] = [];
|
|
789
|
+
try { await this.preuse.close(); } catch (error) { errors.push(error); }
|
|
382
790
|
for (const scopeId of [...this.managedCodex.keys()]) {
|
|
383
791
|
try { await this.stopManagedCodex(scopeId); } catch (error) { errors.push(error); }
|
|
384
792
|
}
|
|
385
793
|
for (const stopping of [...this.managedCodexStops.values()]) {
|
|
386
794
|
try { await stopping; } catch (error) { errors.push(error); }
|
|
387
795
|
}
|
|
796
|
+
for (const release of [...this.runtimeBindingReleases.values()]) {
|
|
797
|
+
try { await release; } catch (error) { errors.push(error); }
|
|
798
|
+
}
|
|
388
799
|
errors.push(...this.managedCodexFailures);
|
|
800
|
+
try { await this.usage.close(); } catch (error) { errors.push(error); }
|
|
389
801
|
try { await this.broker.close(); } catch (error) { errors.push(error); }
|
|
802
|
+
if (this.claudeNative) try { await this.claudeNative.close(); } catch (error) { errors.push(error); }
|
|
390
803
|
const management = this.managementServer;
|
|
391
804
|
const runtime = this.runtimeServer;
|
|
392
805
|
this.managementServer = undefined;
|