agl 40.5.0 → 40.6.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/changelog.md +16 -0
- package/dist_serve/bundle.js +598 -598
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +2 -2
- package/dist_ts/classes.aglhome.d.ts +5 -0
- package/dist_ts/classes.aglhome.js +2 -1
- package/dist_ts/classes.authstore.js +5 -1
- package/dist_ts/classes.controller.js +11 -2
- package/dist_ts/classes.inferencegateway.d.ts +35 -1
- package/dist_ts/classes.inferencegateway.js +86 -26
- package/dist_ts/classes.inferenceservice.d.ts +4 -2
- package/dist_ts/classes.inferenceservice.js +5 -4
- package/dist_ts/classes.proxycontrol.d.ts +58 -4
- package/dist_ts/classes.proxycontrol.js +23 -44
- package/dist_ts/classes.proxycontrolclient.d.ts +15 -5
- package/dist_ts/classes.proxycontrolclient.js +67 -17
- package/dist_ts/classes.proxydaemon.d.ts +22 -3
- package/dist_ts/classes.proxydaemon.js +73 -12
- package/dist_ts/classes.proxyleaseholder.d.ts +43 -14
- package/dist_ts/classes.proxyleaseholder.js +66 -22
- package/dist_ts/classes.proxystore.d.ts +9 -0
- package/dist_ts/classes.proxystore.js +15 -7
- package/dist_ts/classes.workercallerbindings.d.ts +66 -0
- package/dist_ts/classes.workercallerbindings.js +211 -0
- package/dist_ts/classes.workercallers.d.ts +63 -2
- package/dist_ts/classes.workercallers.js +430 -65
- package/dist_ts/fleet/classes.workerlaunches.d.ts +12 -5
- package/dist_ts/fleet/classes.workerlaunches.js +25 -19
- package/dist_ts/fleet/classes.workers.d.ts +17 -0
- package/dist_ts/fleet/classes.workers.js +74 -1
- package/dist_ts/functions.inferencecli.d.ts +3 -2
- package/dist_ts/functions.inferencecli.js +67 -5
- package/dist_ts/functions.inferencecliarguments.d.ts +6 -2
- package/dist_ts/functions.inferencecliarguments.js +29 -2
- package/dist_ts/functions.inferencelaunch.d.ts +24 -0
- package/dist_ts/functions.inferencelaunch.js +42 -2
- package/dist_ts/functions.proxycontract.d.ts +11 -0
- package/dist_ts/functions.proxycontract.js +12 -1
- package/dist_ts/functions.proxysocket.d.ts +6 -0
- package/dist_ts/functions.proxysocket.js +46 -0
- package/dist_ts/functions.workerprocessproof.d.ts +6 -0
- package/dist_ts/functions.workerprocessproof.js +28 -4
- package/dist_ts_authswitch/authority-contract.d.ts +30 -5
- package/dist_ts_authswitch/authority-contract.js +7 -1
- package/dist_ts_authswitch/classes.authoritycli.js +6 -2
- package/dist_ts_authswitch/classes.authorityclient.js +15 -5
- package/dist_ts_authswitch/classes.claudenative.d.ts +7 -1
- package/dist_ts_authswitch/classes.claudenative.js +21 -6
- package/dist_ts_authswitch/classes.claudesignin.d.ts +14 -2
- package/dist_ts_authswitch/classes.claudesignin.js +68 -22
- package/dist_ts_authswitch/classes.codexmanaged.d.ts +8 -0
- package/dist_ts_authswitch/classes.codexmanaged.js +33 -18
- package/dist_ts_authswitch/classes.harnessprocesses.d.ts +8 -0
- package/dist_ts_authswitch/classes.harnessprocesses.js +41 -9
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/workercallers.d.ts +43 -4
- package/package.json +1 -1
- package/readme.md +103 -14
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +2 -2
- package/ts/classes.aglhome.ts +6 -0
- package/ts/classes.authstore.ts +4 -0
- package/ts/classes.controller.ts +10 -1
- package/ts/classes.inferencegateway.ts +104 -23
- package/ts/classes.inferenceservice.ts +6 -3
- package/ts/classes.proxycontrol.ts +76 -39
- package/ts/classes.proxycontrolclient.ts +72 -20
- package/ts/classes.proxydaemon.ts +85 -15
- package/ts/classes.proxyleaseholder.ts +84 -28
- package/ts/classes.proxystore.ts +16 -5
- package/ts/classes.workercallerbindings.ts +147 -0
- package/ts/classes.workercallers.ts +410 -59
- package/ts/fleet/classes.workerlaunches.ts +23 -15
- package/ts/fleet/classes.workers.ts +51 -1
- package/ts/functions.inferencecli.ts +61 -7
- package/ts/functions.inferencecliarguments.ts +32 -1
- package/ts/functions.inferencelaunch.ts +49 -2
- package/ts/functions.proxycontract.ts +12 -0
- package/ts/functions.proxysocket.ts +39 -0
- package/ts/functions.workerprocessproof.ts +27 -4
- package/ts_authswitch/authority-contract.ts +34 -5
- package/ts_authswitch/classes.authoritycli.ts +5 -1
- package/ts_authswitch/classes.authorityclient.ts +25 -9
- package/ts_authswitch/classes.claudenative.ts +23 -7
- package/ts_authswitch/classes.claudesignin.ts +70 -23
- package/ts_authswitch/classes.codexmanaged.ts +30 -16
- package/ts_authswitch/classes.harnessprocesses.ts +39 -8
- package/ts_authswitch/native-contracts.md +1 -1
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/workercallers.ts +32 -4
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/accounts.plugins.ts +5 -1
- package/ts_web/classes.accountsreadmodel.ts +4 -1
|
@@ -14,7 +14,8 @@ import type {
|
|
|
14
14
|
IReq_AuthSwitchSetAccountSpendLimit,
|
|
15
15
|
IReq_AuthSwitchRecheckUsage, IReq_AuthSwitchMoveClaudeHomeIn, IReq_AuthSwitchMoveClaudeHomeOut,
|
|
16
16
|
IReq_AuthSwitchRegisterClaudeHome, IReq_AuthSwitchResolveClaudeHome, IReq_AuthSwitchUnregisterClaudeHome,
|
|
17
|
-
IReq_AuthSwitchCancelClaudeSignIn, IReq_AuthSwitchGetClaudeSignIn,
|
|
17
|
+
IAuthSwitchClaudeSignInOperation, IReq_AuthSwitchCancelClaudeSignIn, IReq_AuthSwitchGetClaudeSignIn,
|
|
18
|
+
IReq_AuthSwitchStartClaudeSignIn,
|
|
18
19
|
IReq_AuthSwitchCodexNativeHandoff, IReq_AuthSwitchCodexNativeHandoffs, IReq_AuthSwitchSwitchCodexNative,
|
|
19
20
|
IReq_AuthSwitchReleaseCodexNative,
|
|
20
21
|
} from './authority-contract.js';
|
|
@@ -137,6 +138,17 @@ const assertCurrentSnapshot = (snapshot: IAuthSwitchSnapshot): IAuthSwitchSnapsh
|
|
|
137
138
|
return snapshot;
|
|
138
139
|
};
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A Claude sign-in carries every member of this release's contract; one without `heldBy`, which names the processes
|
|
143
|
+
* holding a sign-in, comes from a daemon older than this client.
|
|
144
|
+
*/
|
|
145
|
+
const assertCurrentClaudeSignIn = (operation: IAuthSwitchClaudeSignInOperation): IAuthSwitchClaudeSignInOperation => {
|
|
146
|
+
if ((operation as Partial<IAuthSwitchClaudeSignInOperation>).heldBy === undefined) {
|
|
147
|
+
throw new AuthSwitchDaemonOutdatedError();
|
|
148
|
+
}
|
|
149
|
+
return operation;
|
|
150
|
+
};
|
|
151
|
+
|
|
140
152
|
/** A snapshot is usable only while state is current; lastVerifiedAt is the last successful daemon proof. */
|
|
141
153
|
export interface IAuthSwitchSubscriptionStatus {
|
|
142
154
|
state: 'current' | 'unavailable' | 'closed';
|
|
@@ -628,13 +640,15 @@ export class AuthSwitchClient {
|
|
|
628
640
|
/** Starts a Claude sign-in; see `IAuthSwitchClaudeSignInOperation`. The same operation id answers the same sign-in. */
|
|
629
641
|
public async startClaudeSignIn(operationId: string,
|
|
630
642
|
signal?: AbortSignal): Promise<IReq_AuthSwitchStartClaudeSignIn['response']['operation']> {
|
|
631
|
-
return (await this.request<IReq_AuthSwitchStartClaudeSignIn>(
|
|
632
|
-
{ operationId }, 35_000, false,
|
|
643
|
+
return assertCurrentClaudeSignIn((await this.request<IReq_AuthSwitchStartClaudeSignIn>(
|
|
644
|
+
'authswitch.authority.claude.signin.start', { operationId }, 35_000, false,
|
|
645
|
+
signal)).operation);
|
|
633
646
|
}
|
|
634
647
|
public async getClaudeSignIn(operationId: string,
|
|
635
648
|
signal?: AbortSignal): Promise<IReq_AuthSwitchGetClaudeSignIn['response']['operation']> {
|
|
636
|
-
return (await this.request<IReq_AuthSwitchGetClaudeSignIn>(
|
|
637
|
-
{ operationId }, 35_000, false,
|
|
649
|
+
return assertCurrentClaudeSignIn((await this.request<IReq_AuthSwitchGetClaudeSignIn>(
|
|
650
|
+
'authswitch.authority.claude.signin.operation', { operationId }, 35_000, false,
|
|
651
|
+
signal)).operation);
|
|
638
652
|
}
|
|
639
653
|
/**
|
|
640
654
|
* Waits up to `waitMs` for the Claude sign-in to change past `afterRevision`, as `watchOperation` does for a device
|
|
@@ -642,13 +656,15 @@ export class AuthSwitchClient {
|
|
|
642
656
|
*/
|
|
643
657
|
public async watchClaudeSignIn(operationId: string, afterRevision: number, waitMs = 30_000,
|
|
644
658
|
signal?: AbortSignal): Promise<IReq_AuthSwitchGetClaudeSignIn['response']['operation']> {
|
|
645
|
-
return (await this.request<IReq_AuthSwitchGetClaudeSignIn>(
|
|
646
|
-
{ operationId, afterRevision, waitMs }, waitMs + 5_000, false,
|
|
659
|
+
return assertCurrentClaudeSignIn((await this.request<IReq_AuthSwitchGetClaudeSignIn>(
|
|
660
|
+
'authswitch.authority.claude.signin.operation', { operationId, afterRevision, waitMs }, waitMs + 5_000, false,
|
|
661
|
+
signal)).operation);
|
|
647
662
|
}
|
|
648
663
|
public async cancelClaudeSignIn(operationId: string,
|
|
649
664
|
signal?: AbortSignal): Promise<IReq_AuthSwitchCancelClaudeSignIn['response']['operation']> {
|
|
650
|
-
return (await this.request<IReq_AuthSwitchCancelClaudeSignIn>(
|
|
651
|
-
{ operationId }, 35_000, false,
|
|
665
|
+
return assertCurrentClaudeSignIn((await this.request<IReq_AuthSwitchCancelClaudeSignIn>(
|
|
666
|
+
'authswitch.authority.claude.signin.cancel', { operationId }, 35_000, false,
|
|
667
|
+
signal)).operation);
|
|
652
668
|
}
|
|
653
669
|
/** Registers a Claude home the caller created for a container; see `IReq_AuthSwitchRegisterClaudeHome`. */
|
|
654
670
|
public async registerClaudeHome(input: Omit<IReq_AuthSwitchRegisterClaudeHome['request'], 'runtime'>,
|
|
@@ -189,6 +189,10 @@ export interface IClaudeNativeRunningContext {
|
|
|
189
189
|
/** The Agent SDK version whose platform package holds its executable, for an Agent SDK binary only. */
|
|
190
190
|
agentSdkVersion?: string;
|
|
191
191
|
env: NodeJS.ProcessEnv | null;
|
|
192
|
+
/**
|
|
193
|
+
* Its working directory as its own root sees it; null when that cannot be resolved -- deleted under it, for one -- so
|
|
194
|
+
* nothing that needs it (a relative home or store, its project settings) can be proven.
|
|
195
|
+
*/
|
|
192
196
|
cwd: string | null;
|
|
193
197
|
hasSettingsOverride: boolean | null;
|
|
194
198
|
/** The selected native home as seen through this process's mount namespace. */
|
|
@@ -708,10 +712,24 @@ class ClaudeNativeVersionProbe implements IClaudeNativeVersionProbe {
|
|
|
708
712
|
}
|
|
709
713
|
}
|
|
710
714
|
|
|
715
|
+
/**
|
|
716
|
+
* The working directory `/proc/<pid>/cwd` names, when that path is the very directory the process works in as its own
|
|
717
|
+
* root sees it; null otherwise, as for a directory deleted under the process, which the link names `<path> (deleted)`.
|
|
718
|
+
*/
|
|
719
|
+
const resolvedProcessCwd = (pid: number, root: string): string | null => {
|
|
720
|
+
try {
|
|
721
|
+
const cwd = plugins.fs.readlinkSync(`/proc/${pid}/cwd`);
|
|
722
|
+
return plugins.path.isAbsolute(cwd) && sameDirectory(directoryIdentity(`/proc/${pid}/cwd`),
|
|
723
|
+
directoryIdentity(processViewPath(cwd, root))) ? cwd : null;
|
|
724
|
+
} catch { return null; }
|
|
725
|
+
};
|
|
726
|
+
|
|
711
727
|
/**
|
|
712
728
|
* Every Claude Code process of this user, the home it selects and the directory of its credential store, resolved
|
|
713
729
|
* through its own mount namespace (`/proc/<pid>/root`), so a process in a container that bind-mounts a home is matched to
|
|
714
|
-
* that home's inode. A live process whose context cannot be read is listed with unknown fields rather than skipped.
|
|
730
|
+
* that home's inode. A live process whose context cannot be read is listed with unknown fields rather than skipped. A
|
|
731
|
+
* working directory that cannot be resolved leaves `cwd` unknown and only what depends on it: a home or store the
|
|
732
|
+
* process names by an absolute path is still resolved under its root, one named by a relative path is not.
|
|
715
733
|
*/
|
|
716
734
|
export const scanClaudeRunningContexts = (): readonly IClaudeNativeRunningContext[] => {
|
|
717
735
|
const contexts: IClaudeNativeRunningContext[] = [];
|
|
@@ -738,20 +756,18 @@ export const scanClaudeRunningContexts = (): readonly IClaudeNativeRunningContex
|
|
|
738
756
|
}
|
|
739
757
|
mountNamespace = plugins.fs.readlinkSync(`/proc/${running.pid}/ns/mnt`);
|
|
740
758
|
env = selectorEnvironment(readProcessContext(`/proc/${running.pid}/environ`));
|
|
741
|
-
cwd = plugins.fs.readlinkSync(`/proc/${running.pid}/cwd`);
|
|
742
759
|
const root = `/proc/${running.pid}/root`;
|
|
743
760
|
processRoot = root;
|
|
744
|
-
|
|
745
|
-
directoryIdentity(`/proc/${running.pid}/cwd`),
|
|
746
|
-
directoryIdentity(processViewPath(cwd, root)))) {
|
|
747
|
-
throw new Error('Claude process working directory cannot be resolved in its mount namespace.');
|
|
748
|
-
}
|
|
761
|
+
cwd = resolvedProcessCwd(running.pid, root);
|
|
749
762
|
// A directory the process names, as it sees it: an absolute path under its own root, a relative one under its cwd.
|
|
750
763
|
const processDirectory = (selected: string | null, name: string): IClaudeNativeDirectoryIdentity => {
|
|
751
764
|
if (!selected) throw new Error(`Claude process ${name} is unknown.`);
|
|
752
765
|
if (plugins.path.isAbsolute(selected) && plugins.path.normalize(selected) !== selected) {
|
|
753
766
|
throw new Error(`Claude process ${name} path is ambiguous.`);
|
|
754
767
|
}
|
|
768
|
+
if (!plugins.path.isAbsolute(selected) && cwd === null) {
|
|
769
|
+
throw new Error(`Claude process ${name} is relative to a working directory that cannot be resolved.`);
|
|
770
|
+
}
|
|
755
771
|
return directoryIdentity(plugins.path.isAbsolute(selected)
|
|
756
772
|
? processViewPath(selected, root)
|
|
757
773
|
// Preserve the proc magic symlink before `..` is resolved by the target's VFS.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import { AuthSwitchRefusal, authSwitchClaudeSignInArguments,
|
|
3
|
-
type IAuthSwitchClaudeSignInOperation } from './authority-contract.js';
|
|
2
|
+
import { AuthSwitchRefusal, authSwitchClaudeSignInArguments, authSwitchClaudeSignInHoldersText,
|
|
3
|
+
type IAuthSwitchClaudeSignInHolder, type IAuthSwitchClaudeSignInOperation } from './authority-contract.js';
|
|
4
4
|
import type { AuthSwitchGrantImport, IAuthSwitchImportClaudeTokens } from './classes.authorityimport.js';
|
|
5
5
|
import type { IAuthSwitchSecretCodec } from './classes.authoritysecrets.js';
|
|
6
6
|
import { canonicalContainerDirectory, ClaudeContainerHome, discardContainerPair, inspectClaudeContainerDirectory,
|
|
@@ -38,6 +38,20 @@ const isUuid = (value: string): boolean => /^[a-f0-9-]{36}$/.test(value);
|
|
|
38
38
|
const shellWord = (value: string): string => `'${value.replaceAll('\'', '\'\\\'\'')}'`;
|
|
39
39
|
const finished = (state: IAuthSwitchClaudeSignInOperation['state']): boolean =>
|
|
40
40
|
state !== 'pending' && state !== 'committing';
|
|
41
|
+
/**
|
|
42
|
+
* How much earlier than a sign-in's home a process must have started, by the process table, to be proven older than
|
|
43
|
+
* it: `ps` names the start to the whole second, cut down, from a boot time the kernel also keeps to the whole second, so
|
|
44
|
+
* a process may have started up to two seconds after the moment the table names. The rest is room for clock slew.
|
|
45
|
+
*/
|
|
46
|
+
const startedBeforeMarginMs = 5_000;
|
|
47
|
+
const startedBefore = (startedAt: string | null, madeAtMs: number): boolean => {
|
|
48
|
+
const started = startedAt === null ? Number.NaN : Date.parse(startedAt);
|
|
49
|
+
return Number.isFinite(started) && started + startedBeforeMarginMs <= madeAtMs;
|
|
50
|
+
};
|
|
51
|
+
const sameHolders = (left: readonly IAuthSwitchClaudeSignInHolder[] | null,
|
|
52
|
+
right: readonly IAuthSwitchClaudeSignInHolder[] | null): boolean =>
|
|
53
|
+
left === null || right === null ? left === right : left.length === right.length && left.every((holder, index) =>
|
|
54
|
+
holder.pid === right[index]!.pid && holder.startedAt === right[index]!.startedAt && holder.reason === right[index]!.reason);
|
|
41
55
|
|
|
42
56
|
interface IClaudeSignIn {
|
|
43
57
|
operation: IAuthSwitchClaudeSignInOperation;
|
|
@@ -51,7 +65,8 @@ interface IClaudeSignIn {
|
|
|
51
65
|
|
|
52
66
|
/**
|
|
53
67
|
* Claude sign-ins: Claude Code signs in to a home the daemon made for that one sign-in, and the daemon imports the
|
|
54
|
-
* login it finds there once no Claude Code process
|
|
68
|
+
* login it finds there once no Claude Code process may still run on the home, then removes the home. Until then the
|
|
69
|
+
* sign-in names the processes that hold it (`heldBy`).
|
|
55
70
|
*
|
|
56
71
|
* The home is the daemon's own, below a directory of its user that no one else can write, and it is touched only by
|
|
57
72
|
* descriptor, as a container home is (`ClaudeContainerHome`): a home replaced by a link cannot redirect a read or the
|
|
@@ -103,8 +118,9 @@ export class ClaudeSignInAuthority {
|
|
|
103
118
|
const directory = plugins.path.join(this.options.directory, operationId);
|
|
104
119
|
const { parent, leaf } = inspectClaudeContainerDirectory(directory, this.owner);
|
|
105
120
|
if (leaf !== null) throw new Error('A Claude sign-in home already exists for this operation.');
|
|
106
|
-
|
|
121
|
+
// Read before the home is made, so no process that started after it can seem to have started before it.
|
|
107
122
|
const now = this.now();
|
|
123
|
+
ClaudeContainerHome.open(directory, parent, { create: true, owner: this.owner }).close();
|
|
108
124
|
const signIn: IClaudeSignIn = {
|
|
109
125
|
operation: { id: operationId, state: 'pending',
|
|
110
126
|
// The credential store is pinned to the home as well: one an inherited CLAUDE_SECURESTORAGE_CONFIG_DIR names
|
|
@@ -112,6 +128,7 @@ export class ClaudeSignInAuthority {
|
|
|
112
128
|
prompt: { flow: 'terminal', directory,
|
|
113
129
|
command: `CLAUDE_CONFIG_DIR=${shellWord(directory)} CLAUDE_SECURESTORAGE_CONFIG_DIR=${shellWord(directory)} `
|
|
114
130
|
+ `claude ${authSwitchClaudeSignInArguments.join(' ')}` },
|
|
131
|
+
heldBy: null,
|
|
115
132
|
result: null, error: null, createdAt: new Date(now).toISOString(), updatedAt: new Date(now).toISOString(),
|
|
116
133
|
expiresAt: new Date(now + signInWindowMs).toISOString(), finishedAt: null, revision: 1 },
|
|
117
134
|
directory, parent, timer: null, commit: null,
|
|
@@ -192,14 +209,13 @@ export class ClaudeSignInAuthority {
|
|
|
192
209
|
}, this.pollIntervalMs);
|
|
193
210
|
}
|
|
194
211
|
|
|
195
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* Looks once for the login: imports it when it is there and nothing holds it (`holders`), else waits on, keeping in
|
|
214
|
+
* `heldBy` what holds it. At `expiresAt` it ends instead and imports nothing, saying whether a login had arrived and
|
|
215
|
+
* what held it.
|
|
216
|
+
*/
|
|
196
217
|
private poll(signIn: IClaudeSignIn): void {
|
|
197
218
|
if (this.closed || signIn.operation.state !== 'pending') return;
|
|
198
|
-
if (this.now() >= Date.parse(signIn.operation.expiresAt)) {
|
|
199
|
-
this.end(signIn, 'failed', 'No Claude login arrived within fifteen minutes, so nothing was added. Start a new '
|
|
200
|
-
+ 'sign-in.');
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
219
|
let read: { pair: IClaudeContainerPair; identity: IClaudeContainerDirectoryIdentity | null } | null = null;
|
|
204
220
|
try {
|
|
205
221
|
const home = ClaudeContainerHome.open(signIn.directory, signIn.parent, { create: false, owner: this.owner });
|
|
@@ -213,29 +229,59 @@ export class ClaudeSignInAuthority {
|
|
|
213
229
|
return;
|
|
214
230
|
}
|
|
215
231
|
const { pair, identity } = read;
|
|
216
|
-
let
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
232
|
+
let holders: IAuthSwitchClaudeSignInHolder[] | null = null;
|
|
233
|
+
if (pair.credentialRaw !== null && identity !== null) {
|
|
234
|
+
try { holders = this.holders(identity, Date.parse(signIn.operation.createdAt)); }
|
|
235
|
+
catch {
|
|
236
|
+
discardContainerPair(pair);
|
|
237
|
+
this.end(signIn, 'failed', 'The authority could not tell whether Claude Code still runs on the sign-in home, '
|
|
238
|
+
+ 'so nothing was added. Start a new sign-in.');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (this.now() >= Date.parse(signIn.operation.expiresAt)) {
|
|
224
243
|
discardContainerPair(pair);
|
|
225
|
-
this.end(signIn, 'failed',
|
|
226
|
-
|
|
244
|
+
this.end(signIn, 'failed', holders === null
|
|
245
|
+
? 'No Claude login arrived within fifteen minutes, so nothing was added. Start a new sign-in.'
|
|
246
|
+
: holders.length > 0
|
|
247
|
+
? 'A Claude login arrived but was not added: until the sign-in ended, fifteen minutes after it started, these '
|
|
248
|
+
+ `Claude Code processes kept it from being imported: ${authSwitchClaudeSignInHoldersText(holders)}. Start a `
|
|
249
|
+
+ 'new sign-in once they have ended.'
|
|
250
|
+
: 'A Claude login arrived only as the sign-in ended, fifteen minutes after it started, so it was not added. '
|
|
251
|
+
+ 'Start a new sign-in.');
|
|
227
252
|
return;
|
|
228
253
|
}
|
|
229
|
-
if (
|
|
254
|
+
if (holders === null || holders.length > 0) {
|
|
230
255
|
discardContainerPair(pair);
|
|
256
|
+
if (!sameHolders(signIn.operation.heldBy, holders)) this.change(signIn, { heldBy: holders });
|
|
231
257
|
this.schedule(signIn);
|
|
232
258
|
return;
|
|
233
259
|
}
|
|
234
260
|
signIn.commit = this.commit(signIn, pair);
|
|
235
261
|
}
|
|
236
262
|
|
|
263
|
+
/**
|
|
264
|
+
* The Claude Code processes that keep a login in the home `identity` from being imported, as the process table lists
|
|
265
|
+
* them: each whose home or credential store is the home, so it may still write the login there, and each whose home
|
|
266
|
+
* or store is unknown, unless it provably started before the home was made at `madeAtMs`. A process's settings are
|
|
267
|
+
* fixed when it starts, and the home's name was random until it was made, so an older process cannot name it.
|
|
268
|
+
*/
|
|
269
|
+
private holders(identity: IClaudeContainerDirectoryIdentity, madeAtMs: number): IAuthSwitchClaudeSignInHolder[] {
|
|
270
|
+
const holders: IAuthSwitchClaudeSignInHolder[] = [];
|
|
271
|
+
for (const context of this.runningContexts()) {
|
|
272
|
+
const reason: IAuthSwitchClaudeSignInHolder['reason'] | null =
|
|
273
|
+
(context.nativeHomeIdentity !== null && sameContainerDirectory(context.nativeHomeIdentity, identity))
|
|
274
|
+
|| (context.credentialStoreIdentity !== null && sameContainerDirectory(context.credentialStoreIdentity, identity))
|
|
275
|
+
? 'on_home'
|
|
276
|
+
: (context.nativeHomeIdentity === null || context.credentialStoreIdentity === null)
|
|
277
|
+
&& !startedBefore(context.startedAt, madeAtMs) ? 'uninspectable' : null;
|
|
278
|
+
if (reason !== null) holders.push({ pid: context.pid, startedAt: context.startedAt, reason });
|
|
279
|
+
}
|
|
280
|
+
return holders;
|
|
281
|
+
}
|
|
282
|
+
|
|
237
283
|
private async commit(signIn: IClaudeSignIn, pair: IClaudeContainerPair): Promise<void> {
|
|
238
|
-
this.change(signIn, { state: 'committing', prompt: null });
|
|
284
|
+
this.change(signIn, { state: 'committing', prompt: null, heldBy: null });
|
|
239
285
|
try {
|
|
240
286
|
const result = await this.grants.importClaudeSignIn({ directory: signIn.directory,
|
|
241
287
|
credentialRaw: pair.credentialRaw!, configRaw: pair.configRaw });
|
|
@@ -262,7 +308,8 @@ export class ClaudeSignInAuthority {
|
|
|
262
308
|
signIn.timer = null;
|
|
263
309
|
try { this.removeHome(signIn.parent, plugins.path.basename(signIn.directory)); }
|
|
264
310
|
catch { /* The sign-in is over whatever the removal did; the next start removes a home that stayed. */ }
|
|
265
|
-
this.change(signIn, { state, prompt: null, error, result,
|
|
311
|
+
this.change(signIn, { state, prompt: null, heldBy: null, error, result,
|
|
312
|
+
finishedAt: new Date(this.now()).toISOString() });
|
|
266
313
|
}
|
|
267
314
|
|
|
268
315
|
private change(signIn: IClaudeSignIn, patch: Partial<IAuthSwitchClaudeSignInOperation>): void {
|
|
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
|
|
|
2
2
|
import { authSwitchRelease } from './packageidentity.js';
|
|
3
3
|
import type { AuthSwitchAuthorityBroker } from './classes.authoritybroker.js';
|
|
4
4
|
import { requireVerifiedCodexServer, verifyManagedCodexContract, type IManagedCodexContract } from './codexcontract.js';
|
|
5
|
+
import { psLstartInstant } from './classes.harnessprocesses.js';
|
|
5
6
|
|
|
6
7
|
export interface IAuthSwitchManagedCodexOptions {
|
|
7
8
|
/** Private runtime root; only the disposable control socket lives here. */
|
|
@@ -71,6 +72,21 @@ const claimsManagedCodexSocket = (argv: readonly Buffer[], socketPath: string):
|
|
|
71
72
|
return false;
|
|
72
73
|
};
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Rows of `ps -o pid=,lstart=` run with `TZ=UTC0`: the start is read by the shared `psLstartInstant`, null when it
|
|
77
|
+
* cannot be read (a claimant with a null start fails the inspection).
|
|
78
|
+
*/
|
|
79
|
+
export const managedCodexProcessRows = (output: string): Array<{ pid: number; startedAt: string | null }> => {
|
|
80
|
+
const rows: Array<{ pid: number; startedAt: string | null }> = [];
|
|
81
|
+
for (const line of output.split('\n')) {
|
|
82
|
+
const parts = line.trim().split(/\s+/);
|
|
83
|
+
if (parts.length !== 6) continue;
|
|
84
|
+
const pid = Number(parts[0]);
|
|
85
|
+
if (Number.isSafeInteger(pid) && pid > 0) rows.push({ pid, startedAt: psLstartInstant(parts.slice(1, 6)) });
|
|
86
|
+
}
|
|
87
|
+
return rows;
|
|
88
|
+
};
|
|
89
|
+
|
|
74
90
|
/** Same-user Linux process inspection for a bounded socket set, using exact /proc argv boundaries. */
|
|
75
91
|
export const observeManagedCodexProcesses = async (
|
|
76
92
|
socketPaths: readonly string[],
|
|
@@ -85,30 +101,28 @@ export const observeManagedCodexProcesses = async (
|
|
|
85
101
|
}
|
|
86
102
|
const uid = process.getuid?.();
|
|
87
103
|
if (uid === undefined) throw new Error('Managed Codex process owner inspection is unavailable.');
|
|
104
|
+
// ps prints lstart in its TZ; under UTC0 the shared reader takes it as UTC (never Date.parse, which reads local time).
|
|
88
105
|
const output = await new Promise<string>((resolve, reject) => {
|
|
89
106
|
plugins.childProcess.execFile('ps', ['-ww', '-u', String(uid), '-o', 'pid=,lstart='],
|
|
90
|
-
{ timeout: 5_000, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C' } },
|
|
107
|
+
{ timeout: 5_000, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C', TZ: 'UTC0' } },
|
|
91
108
|
(error, stdout) => error ? reject(new Error('Managed Codex process inspection failed.')) : resolve(stdout));
|
|
92
109
|
});
|
|
93
|
-
const processes: Array<{ pid: number; startedAt: string; argv: Buffer[] }> = [];
|
|
94
|
-
for (const
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
const pid = Number(parts[0]);
|
|
98
|
-
const date = Date.parse(parts.slice(1, 6).join(' '));
|
|
99
|
-
if (Number.isSafeInteger(pid) && pid > 0 && Number.isFinite(date)) {
|
|
100
|
-
const argv = await linuxArgv(pid);
|
|
101
|
-
if (argv) processes.push({ pid, startedAt: new Date(date).toISOString(), argv });
|
|
102
|
-
}
|
|
110
|
+
const processes: Array<{ pid: number; startedAt: string | null; argv: Buffer[] }> = [];
|
|
111
|
+
for (const row of managedCodexProcessRows(output)) {
|
|
112
|
+
const argv = await linuxArgv(row.pid);
|
|
113
|
+
if (argv) processes.push({ ...row, argv });
|
|
103
114
|
}
|
|
104
115
|
const result = new Map<string, TManagedCodexProcessObservation>();
|
|
105
116
|
for (const socketPath of unique) {
|
|
106
117
|
const matches = processes.filter(process => claimsManagedCodexSocket(process.argv, socketPath));
|
|
107
|
-
|
|
108
|
-
? { state: 'absent' }
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
if (matches.length !== 1) {
|
|
119
|
+
result.set(socketPath, matches.length === 0 ? { state: 'absent' } : { state: 'multiple_claimants' });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const [claimant] = matches;
|
|
123
|
+
// A claimant whose start cannot be read has no identity to compare: the inspection fails, never reads as absent.
|
|
124
|
+
if (claimant.startedAt === null) throw new Error('Managed Codex process inspection failed.');
|
|
125
|
+
result.set(socketPath, { state: 'observed', pid: claimant.pid, startedAt: claimant.startedAt });
|
|
112
126
|
}
|
|
113
127
|
return result;
|
|
114
128
|
};
|
|
@@ -41,12 +41,31 @@ const executablePatterns: Record<THarnessBinary, RegExp[]> = {
|
|
|
41
41
|
const MAX_COMMAND_LINE = 200;
|
|
42
42
|
const POSIX_FIELDS = 'pid=,ppid=,uid=,lstart=,comm=,args=';
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
const LSTART_WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
45
|
+
const LSTART_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The instant of `ps -o lstart` under `LC_ALL=C` and `TZ=UTC0`: the five tokens `weekday month day hh:mm:ss year`,
|
|
49
|
+
* read as UTC with `Date.UTC`. Never `Date.parse`, which reads them in local time: in the repeated hour of a DST
|
|
50
|
+
* fall-back that dates a process an hour early. Anything that is not exactly that shape, or names no real day, is null.
|
|
51
|
+
*/
|
|
52
|
+
export const psLstartInstant = (tokens: readonly string[]): string | null => {
|
|
53
|
+
if (tokens.length !== 5) return null;
|
|
54
|
+
const [weekdayToken, monthToken, dayToken, timeToken, yearToken] = tokens;
|
|
55
|
+
const weekday = LSTART_WEEKDAYS.indexOf(weekdayToken);
|
|
56
|
+
const month = LSTART_MONTHS.indexOf(monthToken);
|
|
57
|
+
const time = /^(\d{2}):(\d{2}):(\d{2})$/.exec(timeToken);
|
|
58
|
+
if (weekday < 0 || month < 0 || !time || !/^\d{1,2}$/.test(dayToken) || !/^\d{4}$/.test(yearToken)) return null;
|
|
59
|
+
const [day, year, hours, minutes, seconds] = [dayToken, yearToken, time[1], time[2], time[3]].map(Number);
|
|
60
|
+
if (hours > 23 || minutes > 59 || seconds > 59) return null;
|
|
61
|
+
const instant = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
|
|
62
|
+
if (instant.getUTCFullYear() !== year || instant.getUTCMonth() !== month || instant.getUTCDate() !== day
|
|
63
|
+
|| instant.getUTCDay() !== weekday) return null;
|
|
64
|
+
return instant.toISOString();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Rows of `ps -o pid=,ppid=,uid=,lstart=,comm=,args=` run with `TZ=UTC0`, for the process owner `uid` only. */
|
|
68
|
+
export const parsePosixProcessTable = (output: string, uid: number): IEnumeratedProcess[] => {
|
|
50
69
|
const processes: IEnumeratedProcess[] = [];
|
|
51
70
|
for (const line of output.split('\n')) {
|
|
52
71
|
// pid ppid uid, then the five fixed tokens of lstart, then comm and the full argument vector.
|
|
@@ -54,17 +73,29 @@ const enumeratePosix = (): IEnumeratedProcess[] => {
|
|
|
54
73
|
if (parts.length < 9) continue;
|
|
55
74
|
const [pid, parentPid, owner] = parts.slice(0, 3).map(Number);
|
|
56
75
|
if (!Number.isInteger(pid) || pid <= 0 || owner !== uid) continue;
|
|
57
|
-
const started = Date.parse(parts.slice(3, 8).join(' '));
|
|
58
76
|
const args = parts.slice(9);
|
|
59
77
|
processes.push({
|
|
60
78
|
pid, parentPid: Number.isInteger(parentPid) ? parentPid : null, command: parts[8],
|
|
61
79
|
executable: args[0] ?? parts[8], commandLine: (args.length ? args.join(' ') : parts[8]).slice(0, MAX_COMMAND_LINE),
|
|
62
|
-
startedAt:
|
|
80
|
+
startedAt: psLstartInstant(parts.slice(3, 8)),
|
|
63
81
|
});
|
|
64
82
|
}
|
|
65
83
|
return processes;
|
|
66
84
|
};
|
|
67
85
|
|
|
86
|
+
/**
|
|
87
|
+
* A bounded read of the current user's process table. Never a pattern-based kill list. `ps` prints lstart in its
|
|
88
|
+
* `TZ`, so it runs with `TZ=UTC0` and the start is read as UTC (`psLstartInstant`).
|
|
89
|
+
*/
|
|
90
|
+
const enumeratePosix = (): IEnumeratedProcess[] => {
|
|
91
|
+
const uid = process.getuid!();
|
|
92
|
+
const output = plugins.childProcess.execFileSync('ps', ['-u', String(uid), '-o', POSIX_FIELDS], {
|
|
93
|
+
encoding: 'utf8', timeout: 5000, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C', TZ: 'UTC0' },
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
});
|
|
96
|
+
return parsePosixProcessTable(output, uid);
|
|
97
|
+
};
|
|
98
|
+
|
|
68
99
|
/** Windows reports an image name and a pid, with no owner, parent or start time in this view. */
|
|
69
100
|
const enumerateWindows = (): IEnumeratedProcess[] => {
|
|
70
101
|
const output = plugins.childProcess.execFileSync('tasklist', ['/FO', 'CSV', '/NH'], {
|
|
@@ -446,7 +446,7 @@ remote agent from `~/.claude/remote/ccd-cli/<version>` (since 2.1.295 a build na
|
|
|
446
446
|
number as the command name while argv[0] carries the identity -- and a forked helper can
|
|
447
447
|
carry the same `comm` while running something else entirely (`ugrep`). `HarnessProcesses`
|
|
448
448
|
therefore matches argv[0] and those install paths (`ps -u <uid> -o pid=,ppid=,uid=,lstart=,comm=,args=`
|
|
449
|
-
under `LC_ALL=C`, filtered on the uid column). It deliberately never matches the rest of the
|
|
449
|
+
under `LC_ALL=C` and `TZ=UTC0`, filtered on the uid column; lstart is read as UTC with `Date.UTC`). It deliberately never matches the rest of the
|
|
450
450
|
command line, which would count an unrelated process that merely mentions `claude`. The
|
|
451
451
|
consequence is a known blind spot: an instance started through a wrapper that execs
|
|
452
452
|
`node <path>/cli.js` has argv[0] `node`, so it is not listed. Listing signals nothing; the
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
TControllerBrowserPaintAvailability,
|
|
6
6
|
TControllerBrowserPaintTermination,
|
|
7
7
|
} from './browserpaint.js';
|
|
8
|
-
export const controllerProtocolVersion =
|
|
8
|
+
export const controllerProtocolVersion = 45 as const;
|
|
9
9
|
export const controllerUpgradeManagementVersion = 2 as const;
|
|
10
10
|
export const controllerPackageName = 'agl' as const;
|
|
11
11
|
/** The systemd user unit `agl service install` writes; a controller it runs reports it in its status. */
|
|
@@ -6,6 +6,8 @@ export const workerCallerLaunchGenerationEnvironmentVariable = 'AGL_MCP_LAUNCH_G
|
|
|
6
6
|
export interface IFleetWorkerCallerLaunchBase {
|
|
7
7
|
launchId: string;
|
|
8
8
|
projectId: string;
|
|
9
|
+
/** The fleet worker the launch identifies, `${projectId}:${name}`: a worker holds one current launch. */
|
|
10
|
+
workerId: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -27,11 +29,15 @@ export interface IFleetClaudeProcess {
|
|
|
27
29
|
startTicks: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
export type TFleetWorkerCallerBinding = IFleetWorkerCallerLaunchBase & {
|
|
32
|
+
export type TFleetWorkerCallerBinding = Omit<IFleetWorkerCallerLaunchBase, 'workerId'> & {
|
|
31
33
|
nativeSessionId: string;
|
|
32
34
|
expectedBindingRevision: number;
|
|
33
35
|
} & (
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* A Claude launch binds to its background job and to the job's current process; a job whose process already exited
|
|
38
|
+
* binds without one, and the next process of the same job proves itself when its bridge enrolls.
|
|
39
|
+
*/
|
|
40
|
+
| { harnessId: 'claude'; claudeJobId: string; claudeProcess?: IFleetClaudeProcess }
|
|
35
41
|
| { harnessId: 'codex' }
|
|
36
42
|
);
|
|
37
43
|
|
|
@@ -53,12 +59,34 @@ export interface IFleetWorkerCallerHooks {
|
|
|
53
59
|
ticketSessionId: string;
|
|
54
60
|
}>;
|
|
55
61
|
getWorkerCallerStatus(launchId: string): IWorkerCallerReadiness;
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Revokes one launch, or the current launch of a worker, also one the controller kept from before a restart. Every
|
|
64
|
+
* reason but `generation_change` revokes it for good: its kept binding is deleted with it.
|
|
65
|
+
*/
|
|
66
|
+
revokeWorkerCaller(request: ({ launchId: string } | { workerId: string }) & {
|
|
58
67
|
reason: 'failure' | 'stop' | 'replacement' | 'generation_change' | 'cancelled';
|
|
59
68
|
}): Promise<void>;
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
/** The background job a Claude worker's launch is bound to, as the controller asks the fleet about it. */
|
|
72
|
+
export interface IWorkerClaudeJobRequest {
|
|
73
|
+
workerId: string;
|
|
74
|
+
projectId: string;
|
|
75
|
+
nativeSessionId: string;
|
|
76
|
+
jobId: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What a fresh Claude inventory shows of that job: `running` in one process, `settled` without a process, `ended` for
|
|
81
|
+
* this worker (the worker is gone or archived, continues in another session, or the session continues as another job),
|
|
82
|
+
* or `unproven` when the registry or the inventory could not be read or did not list the session once.
|
|
83
|
+
*/
|
|
84
|
+
export type TWorkerClaudeJobObservation =
|
|
85
|
+
| { state: 'running'; process: IFleetClaudeProcess }
|
|
86
|
+
| { state: 'settled' }
|
|
87
|
+
| { state: 'ended'; reason: string }
|
|
88
|
+
| { state: 'unproven'; reason: string };
|
|
89
|
+
|
|
62
90
|
export interface IWorkerCallerSubject {
|
|
63
91
|
kind: 'worker_launch';
|
|
64
92
|
launchId: string;
|
|
@@ -13,5 +13,9 @@ import * as authswitchUsageLimit from '../ts_authswitch/authority-usagelimit.js'
|
|
|
13
13
|
import * as authswitchSubscription from '../ts_authswitch/authority-subscription.js';
|
|
14
14
|
// An API key's month: the spend limit rule and dollar amounts, as the command line states them; it imports nothing.
|
|
15
15
|
import * as authswitchSpend from '../ts_authswitch/authority-spend.js';
|
|
16
|
+
// The service's contract, which the controller's interfaces load already: the processes holding a Claude sign-in, in
|
|
17
|
+
// the words the command line uses.
|
|
18
|
+
import * as authswitchAuthority from '../ts_authswitch/authority-contract.js';
|
|
16
19
|
|
|
17
|
-
export { authswitchPreuseText, authswitchSpend, authswitchSubscription, authswitchUsageLimit,
|
|
20
|
+
export { authswitchAuthority, authswitchPreuseText, authswitchSpend, authswitchSubscription, authswitchUsageLimit,
|
|
21
|
+
authswitchUsageWindows };
|
|
@@ -1163,7 +1163,10 @@ const codexReleaseSlot = (
|
|
|
1163
1163
|
const claudeSignInStatusText = (signInArg: interfaces.TControllerClaudeSignIn): string => (
|
|
1164
1164
|
signInArg.state === 'committing'
|
|
1165
1165
|
? 'Adding the account…'
|
|
1166
|
-
:
|
|
1166
|
+
: signInArg.heldBy
|
|
1167
|
+
? 'Claude Code signed in. The account is added once these Claude Code processes have ended: '
|
|
1168
|
+
+ `${plugins.authswitchAuthority.authSwitchClaudeSignInHoldersText(signInArg.heldBy)}.`
|
|
1169
|
+
: 'Waiting for Claude Code to sign in. Run the command in a terminal and follow Claude Code there.'
|
|
1167
1170
|
);
|
|
1168
1171
|
|
|
1169
1172
|
/** One Claude sign-in in progress, with the command that completes it and a terminal to run it in. */
|