@bridge4dev/runner 0.65.0 → 0.66.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/account-commands.d.ts +44 -0
- package/dist/account-commands.js +99 -0
- package/dist/adapters/claude-usage.d.ts +79 -15
- package/dist/adapters/claude-usage.js +143 -67
- package/dist/adapters/claude.d.ts +30 -1
- package/dist/adapters/claude.js +171 -11
- package/dist/adapters/codex.js +25 -2
- package/dist/adapters/types.d.ts +22 -0
- package/dist/agent-auth.d.ts +87 -0
- package/dist/agent-auth.js +249 -15
- package/dist/auth-relay.d.ts +110 -26
- package/dist/auth-relay.js +339 -232
- package/dist/claude-homes.d.ts +436 -0
- package/dist/claude-homes.js +1567 -0
- package/dist/commit-message.js +4 -1
- package/dist/config.d.ts +14 -14
- package/dist/daemon-exit.d.ts +40 -0
- package/dist/daemon-exit.js +29 -0
- package/dist/index.js +68 -18
- package/dist/login-marks.d.ts +31 -0
- package/dist/login-marks.js +47 -0
- package/dist/protocol.d.ts +38 -38
- package/dist/protocol.js +3 -1
- package/dist/session-cage.d.ts +49 -0
- package/dist/session-cage.js +238 -12
- package/dist/session-stall.d.ts +25 -0
- package/dist/session-stall.js +84 -0
- package/dist/supervisor.d.ts +66 -2
- package/dist/supervisor.js +181 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/ws-client.js +14 -0
- package/package.json +1 -1
package/dist/commit-message.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import { assertClaudeInstalled, claudeExecutableOption } from './agent-binary.js';
|
|
3
3
|
import { scrubbedEnv } from './adapters/claude.js';
|
|
4
|
+
import { activeAccountHome, withAccountHome } from './claude-homes.js';
|
|
4
5
|
import { gitBranchDiff } from './gitops.js';
|
|
5
6
|
import { isSecretPath, maskString } from './policy.js';
|
|
6
7
|
/**
|
|
@@ -160,7 +161,9 @@ export async function proposeCommitMessage(input, queryFn = query) {
|
|
|
160
161
|
prompt: buildPrompt(input, diff),
|
|
161
162
|
options: {
|
|
162
163
|
cwd: input.worktreePath,
|
|
163
|
-
|
|
164
|
+
// The machine's active account (#422), with the same rule as a session:
|
|
165
|
+
// its home, and no operator token above it.
|
|
166
|
+
env: withAccountHome(scrubbedEnv(), activeAccountHome()),
|
|
164
167
|
// The second place Claude `Options` are built. It must follow the same
|
|
165
168
|
// constant, or after C3 commit messages would still be written by the
|
|
166
169
|
// bundled binary while sessions had moved to the system one.
|
package/dist/config.d.ts
CHANGED
|
@@ -15,12 +15,12 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
15
15
|
name: z.ZodString;
|
|
16
16
|
token: z.ZodString;
|
|
17
17
|
}, "strip", z.ZodTypeAny, {
|
|
18
|
-
name: string;
|
|
19
18
|
id: string;
|
|
19
|
+
name: string;
|
|
20
20
|
token: string;
|
|
21
21
|
}, {
|
|
22
|
-
name: string;
|
|
23
22
|
id: string;
|
|
23
|
+
name: string;
|
|
24
24
|
token: string;
|
|
25
25
|
}>;
|
|
26
26
|
mcp: z.ZodOptional<z.ZodObject<{
|
|
@@ -179,10 +179,13 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
179
179
|
ws_url: string;
|
|
180
180
|
};
|
|
181
181
|
server: {
|
|
182
|
-
name: string;
|
|
183
182
|
id: string;
|
|
183
|
+
name: string;
|
|
184
184
|
token: string;
|
|
185
185
|
};
|
|
186
|
+
codex?: {
|
|
187
|
+
auth: "link" | "own";
|
|
188
|
+
} | undefined;
|
|
186
189
|
checkpoints?: {
|
|
187
190
|
enabled: boolean;
|
|
188
191
|
} | undefined;
|
|
@@ -190,8 +193,8 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
190
193
|
url: string;
|
|
191
194
|
token: string;
|
|
192
195
|
} | undefined;
|
|
193
|
-
|
|
194
|
-
|
|
196
|
+
agents?: {
|
|
197
|
+
install_enabled: boolean;
|
|
195
198
|
} | undefined;
|
|
196
199
|
limits?: {
|
|
197
200
|
max_sessions?: number | undefined;
|
|
@@ -204,19 +207,19 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
204
207
|
verify?: {
|
|
205
208
|
enabled: boolean;
|
|
206
209
|
} | undefined;
|
|
207
|
-
agents?: {
|
|
208
|
-
install_enabled: boolean;
|
|
209
|
-
} | undefined;
|
|
210
210
|
}, {
|
|
211
211
|
api: {
|
|
212
212
|
url: string;
|
|
213
213
|
ws_url: string;
|
|
214
214
|
};
|
|
215
215
|
server: {
|
|
216
|
-
name: string;
|
|
217
216
|
id: string;
|
|
217
|
+
name: string;
|
|
218
218
|
token: string;
|
|
219
219
|
};
|
|
220
|
+
codex?: {
|
|
221
|
+
auth?: "link" | "own" | undefined;
|
|
222
|
+
} | undefined;
|
|
220
223
|
checkpoints?: {
|
|
221
224
|
enabled?: boolean | undefined;
|
|
222
225
|
} | undefined;
|
|
@@ -224,8 +227,8 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
224
227
|
url: string;
|
|
225
228
|
token: string;
|
|
226
229
|
} | undefined;
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
agents?: {
|
|
231
|
+
install_enabled?: boolean | undefined;
|
|
229
232
|
} | undefined;
|
|
230
233
|
limits?: {
|
|
231
234
|
max_sessions?: number | undefined;
|
|
@@ -238,9 +241,6 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
238
241
|
verify?: {
|
|
239
242
|
enabled?: boolean | undefined;
|
|
240
243
|
} | undefined;
|
|
241
|
-
agents?: {
|
|
242
|
-
install_enabled?: boolean | undefined;
|
|
243
|
-
} | undefined;
|
|
244
244
|
}>;
|
|
245
245
|
export type RunnerConfig = z.infer<typeof ConfigSchema>;
|
|
246
246
|
export declare function loadConfig(): RunnerConfig | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one way out of the daemon process (#431).
|
|
3
|
+
*
|
|
4
|
+
* Its own module, and not four lines inside `cmdDaemon`, for one reason: the
|
|
5
|
+
* defect this ticket is about WAS those four lines. `supervisor.shutdown();
|
|
6
|
+
* ws.stop(); process.exit(0);` in a single tick killed one process per session
|
|
7
|
+
* and left everything that process had started running inside a cgroup systemd
|
|
8
|
+
* would then refuse to reuse by name. Nothing in the suite could see that,
|
|
9
|
+
* because no test in this package imports `index.ts` — found by the independent
|
|
10
|
+
* review of #431, which also pointed out that the next edit to that file would
|
|
11
|
+
* be just as invisible.
|
|
12
|
+
*
|
|
13
|
+
* So the order is code with a test on it rather than a comment:
|
|
14
|
+
*
|
|
15
|
+
* 1. **`shutdown()`** — its synchronous half runs before it returns: the cards
|
|
16
|
+
* a person was still being asked are withdrawn, the line saying what this
|
|
17
|
+
* restart costs goes into the journal, the agents are killed.
|
|
18
|
+
* 2. **the socket is closed** — after the events above needed it open, and
|
|
19
|
+
* before the wait below, which must not be a window in which an `event_ack`
|
|
20
|
+
* or a `session_start` reaches a supervisor that has let go of everything.
|
|
21
|
+
* 3. **the promise is awaited** — the cages, and nothing else. What does not
|
|
22
|
+
* finish inside the supervisor's own budget continues on systemd's side:
|
|
23
|
+
* the stop has been accepted by then.
|
|
24
|
+
* 4. **`exit`**, always — including when the wait ends in a rejection. A
|
|
25
|
+
* daemon that would not die because a bus call failed is worse than one
|
|
26
|
+
* that leaves a scope behind.
|
|
27
|
+
*/
|
|
28
|
+
export interface DaemonExitDeps {
|
|
29
|
+
/** The supervisor's shutdown. The synchronous half must run before it returns. */
|
|
30
|
+
shutdown: () => Promise<void>;
|
|
31
|
+
/** Close the socket, both directions. */
|
|
32
|
+
stopWs: () => void;
|
|
33
|
+
/** Write the status file, so `doctor` sees a daemon that is going down. */
|
|
34
|
+
noteStatus?: () => void;
|
|
35
|
+
/** End the process. Injected so the order above can be proved. */
|
|
36
|
+
exit: (code: number) => void;
|
|
37
|
+
}
|
|
38
|
+
/** Build the exit door. The returned function is safe to call more than once. */
|
|
39
|
+
export declare function daemonExit(deps: DaemonExitDeps): (code: number) => void;
|
|
40
|
+
//# sourceMappingURL=daemon-exit.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { log } from './log.js';
|
|
2
|
+
/** Build the exit door. The returned function is safe to call more than once. */
|
|
3
|
+
export function daemonExit(deps) {
|
|
4
|
+
let leaving = false;
|
|
5
|
+
return (code) => {
|
|
6
|
+
/**
|
|
7
|
+
* A second signal does not shorten the first departure.
|
|
8
|
+
*
|
|
9
|
+
* Two `SIGTERM`s, or «Update runner» racing a restart command, used to run
|
|
10
|
+
* the whole thing twice: a second `verify.shutdown()` signalling a group
|
|
11
|
+
* that is already dying, and an `exit` from the second call cutting the
|
|
12
|
+
* first call's wait for its cages — the very wait this door exists for.
|
|
13
|
+
*/
|
|
14
|
+
if (leaving) {
|
|
15
|
+
log.warn('daemon: already leaving, this second request changes nothing', { code });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
leaving = true;
|
|
19
|
+
const cages = deps.shutdown();
|
|
20
|
+
deps.stopWs();
|
|
21
|
+
deps.noteStatus?.();
|
|
22
|
+
void cages
|
|
23
|
+
.catch((error) => {
|
|
24
|
+
log.warn('daemon: the cages did not all stop cleanly', { error: String(error) });
|
|
25
|
+
})
|
|
26
|
+
.finally(() => deps.exit(code));
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=daemon-exit.js.map
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import { loadConfig, mergeIntoPairedConfig, requireConfig, saveConfig, } from '.
|
|
|
15
15
|
import { log } from './log.js';
|
|
16
16
|
import { installIsWritable, installPrefixFor, isSupervisedProcess, restartCapability, manualUpdateCommand, resolveInstalledPackageDir, } from './self-update.js';
|
|
17
17
|
import { applyStoredClaudeToken } from './agent-auth.js';
|
|
18
|
+
import { discardAbandonedStagingHomes, recoverInterruptedSwaps, refreshAccountIdentity, } from './claude-homes.js';
|
|
19
|
+
import { ACCOUNT_AGENTS, ACCOUNT_COMMANDS } from './account-commands.js';
|
|
20
|
+
import { MACHINE_ACCOUNT_ID } from './login-marks.js';
|
|
18
21
|
import { Supervisor } from './supervisor.js';
|
|
19
22
|
import { readStatusFile, isPidAlive, writeStatusFile, STATUS_FRESH_MS } from './status-file.js';
|
|
20
23
|
import { RunnerWsClient } from './ws-client.js';
|
|
@@ -25,6 +28,7 @@ import { buildUnit, cpuQuotaPercent, devbridgeSliceOverridePath, limitsOverrideI
|
|
|
25
28
|
import { defaultCageProbe, initSessionCage, readSliceLimits, listSessionScopes, sessionCage, sweepOrphanSessionScopes, SESSION_TASKS_MAX, } from './session-cage.js';
|
|
26
29
|
import { SEARCH_GUARD_ENABLED, claudeSettingsPath, installSearchGuard, removeSearchGuard, searchGuardCommand, searchGuardHome, searchGuardHookPath, searchGuardStatus, } from './claude-settings.js';
|
|
27
30
|
import { readOomKills, recordCrash, takeLastExit } from './crash-note.js';
|
|
31
|
+
import { daemonExit } from './daemon-exit.js';
|
|
28
32
|
import { agentAuthStatuses } from './auth-relay.js';
|
|
29
33
|
import { addSafeDirectory, agentConfigContour, dockerCheck, ensureAgentPath, firstUnreachableAncestor, hasSafeDirectory, inspectPath, knownWorkspacePaths, lingerEnabled, nodeCheck, otherHomeWithAgents, runnerIdentity, safeDirectoryCommand, systemctlHint, systemdUserBusReachable, } from './environment.js';
|
|
30
34
|
import { mcpConfigDir } from './paths.js';
|
|
@@ -514,6 +518,13 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
514
518
|
* `kind: 'oom'` is inferred from the cgroup counter — see `crash-note.ts`.
|
|
515
519
|
*/
|
|
516
520
|
...(LAST_EXIT ? { lastExit: LAST_EXIT } : {}),
|
|
521
|
+
/**
|
|
522
|
+
* #422 (R11): the agents this runner keeps several logins for. Static per
|
|
523
|
+
* version – the LIST of accounts is always asked with `agent_accounts`,
|
|
524
|
+
* never read from here (a snapshot taken at start, #395). The dashboard
|
|
525
|
+
* shows the account window only to an agent named here.
|
|
526
|
+
*/
|
|
527
|
+
accounts: [...ACCOUNT_AGENTS],
|
|
517
528
|
/** Commands beyond the stage-A/B set. */
|
|
518
529
|
commands: [
|
|
519
530
|
'purge_session',
|
|
@@ -577,6 +588,10 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
577
588
|
// ANSWERS the command, with a sentence saying why — which is a better
|
|
578
589
|
// outcome than a frame nobody replies to.
|
|
579
590
|
'runner_restart',
|
|
591
|
+
// #422: list, sign in to, switch and forget agent logins (plan §8).
|
|
592
|
+
// Unconditional: they read and write only the runner's own state, and an
|
|
593
|
+
// agent they do not serve yet is answered with a sentence.
|
|
594
|
+
...ACCOUNT_COMMANDS,
|
|
580
595
|
],
|
|
581
596
|
};
|
|
582
597
|
}
|
|
@@ -954,6 +969,31 @@ async function cmdDaemon() {
|
|
|
954
969
|
if (applyStoredClaudeToken()) {
|
|
955
970
|
log.info('daemon: using the Claude token stored on this server');
|
|
956
971
|
}
|
|
972
|
+
/**
|
|
973
|
+
* #422: a swap of Claude homes the previous daemon died inside is finished or
|
|
974
|
+
* undone before anything can run under an account; and who the machine login
|
|
975
|
+
* is gets asked once, in the background (R14 c) – the verdict and the list read
|
|
976
|
+
* the answer, the minute poll never asks.
|
|
977
|
+
*/
|
|
978
|
+
try {
|
|
979
|
+
recoverInterruptedSwaps();
|
|
980
|
+
}
|
|
981
|
+
catch (error) {
|
|
982
|
+
log.warn('daemon: could not finish an interrupted swap of Claude homes', {
|
|
983
|
+
error: String(error),
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
// A sign-in the previous daemon accepted but never adopted (S2): nothing can
|
|
988
|
+
// be adopting before the supervisor exists.
|
|
989
|
+
discardAbandonedStagingHomes();
|
|
990
|
+
}
|
|
991
|
+
catch (error) {
|
|
992
|
+
log.warn('daemon: could not remove sign-in homes a previous daemon left', {
|
|
993
|
+
error: String(error),
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
void refreshAccountIdentity(MACHINE_ACCOUNT_ID).catch(() => undefined);
|
|
957
997
|
const agents = installedAgents();
|
|
958
998
|
const ws = new RunnerWsClient(config.api.ws_url, config.server.token, runnerCapabilities());
|
|
959
999
|
const codex = agents.includes('codex') ? bootstrapCodex(config) : null;
|
|
@@ -1019,11 +1059,7 @@ async function cmdDaemon() {
|
|
|
1019
1059
|
from: outcome.fromVersion,
|
|
1020
1060
|
to: outcome.toVersion ?? 'unknown',
|
|
1021
1061
|
});
|
|
1022
|
-
const timer = setTimeout(() =>
|
|
1023
|
-
supervisor.shutdown();
|
|
1024
|
-
ws.stop();
|
|
1025
|
-
process.exit(0);
|
|
1026
|
-
}, RESTART_DELAY_MS);
|
|
1062
|
+
const timer = setTimeout(() => leave(0), RESTART_DELAY_MS);
|
|
1027
1063
|
timer.unref();
|
|
1028
1064
|
},
|
|
1029
1065
|
// #396: the same exit, asked for directly rather than as the tail of an
|
|
@@ -1035,11 +1071,7 @@ async function cmdDaemon() {
|
|
|
1035
1071
|
sessions: supervisor.activeSessionIds.length,
|
|
1036
1072
|
told: Boolean(note),
|
|
1037
1073
|
});
|
|
1038
|
-
const timer = setTimeout(() =>
|
|
1039
|
-
supervisor.shutdown();
|
|
1040
|
-
ws.stop();
|
|
1041
|
-
process.exit(0);
|
|
1042
|
-
}, RESTART_DELAY_MS);
|
|
1074
|
+
const timer = setTimeout(() => leave(0), RESTART_DELAY_MS);
|
|
1043
1075
|
timer.unref();
|
|
1044
1076
|
},
|
|
1045
1077
|
});
|
|
@@ -1052,13 +1084,21 @@ async function cmdDaemon() {
|
|
|
1052
1084
|
activeSessionIds: supervisor.activeSessionIds,
|
|
1053
1085
|
updatedAt: new Date().toISOString(),
|
|
1054
1086
|
});
|
|
1087
|
+
/**
|
|
1088
|
+
* The one way out of this process — the order lives in `daemon-exit.ts`,
|
|
1089
|
+
* where a test can hold it (#431).
|
|
1090
|
+
*/
|
|
1091
|
+
const leave = daemonExit({
|
|
1092
|
+
shutdown: () => supervisor.shutdown(),
|
|
1093
|
+
stopWs: () => ws.stop(),
|
|
1094
|
+
noteStatus: updateStatus,
|
|
1095
|
+
exit: (code) => process.exit(code),
|
|
1096
|
+
});
|
|
1055
1097
|
ws.on('open', updateStatus);
|
|
1056
1098
|
ws.on('close', updateStatus);
|
|
1057
1099
|
ws.on('revoked', (reason) => {
|
|
1058
1100
|
log.error(`daemon: access revoked (${reason}) — exiting`);
|
|
1059
|
-
|
|
1060
|
-
updateStatus();
|
|
1061
|
-
process.exit(3);
|
|
1101
|
+
leave(3);
|
|
1062
1102
|
});
|
|
1063
1103
|
const statusTimer = setInterval(updateStatus, 30_000);
|
|
1064
1104
|
statusTimer.unref();
|
|
@@ -1072,10 +1112,7 @@ async function cmdDaemon() {
|
|
|
1072
1112
|
limitsTimer.unref();
|
|
1073
1113
|
const shutdown = (signal) => {
|
|
1074
1114
|
log.info(`daemon: ${signal} received, shutting down`);
|
|
1075
|
-
|
|
1076
|
-
ws.stop();
|
|
1077
|
-
updateStatus();
|
|
1078
|
-
process.exit(0);
|
|
1115
|
+
leave(0);
|
|
1079
1116
|
};
|
|
1080
1117
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1081
1118
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
@@ -1381,6 +1418,15 @@ async function runnerChecks() {
|
|
|
1381
1418
|
});
|
|
1382
1419
|
return checks;
|
|
1383
1420
|
}
|
|
1421
|
+
/** « · account <email> (saved)» – nothing for the machine login nobody named. */
|
|
1422
|
+
function accountLine(account) {
|
|
1423
|
+
if (!account)
|
|
1424
|
+
return '';
|
|
1425
|
+
if (account.id === MACHINE_ACCOUNT_ID) {
|
|
1426
|
+
return account.email ? ` · machine login ${account.email}` : '';
|
|
1427
|
+
}
|
|
1428
|
+
return ` · saved account ${account.email ?? account.id}`;
|
|
1429
|
+
}
|
|
1384
1430
|
async function agentChecks() {
|
|
1385
1431
|
const me = runnerIdentity();
|
|
1386
1432
|
// The probes log a verdict line each, which belongs in the journal, not in
|
|
@@ -1442,8 +1488,12 @@ async function agentChecks() {
|
|
|
1442
1488
|
checks.push({
|
|
1443
1489
|
ok: signedIn,
|
|
1444
1490
|
name: `${agent} login`,
|
|
1491
|
+
// Which account the verdict is about (#422): the same rule as the panel,
|
|
1492
|
+
// read from the same file, so doctor and the dashboard cannot answer about
|
|
1493
|
+
// two different logins on one machine.
|
|
1445
1494
|
detail: (info.detail ?? info.status) +
|
|
1446
|
-
(info.expiresAt ? ` · until ${formatLocalDay(info.expiresAt)}` : '')
|
|
1495
|
+
(info.expiresAt ? ` · until ${formatLocalDay(info.expiresAt)}` : '') +
|
|
1496
|
+
accountLine(info.activeAccount),
|
|
1447
1497
|
...(signedIn
|
|
1448
1498
|
? {}
|
|
1449
1499
|
: {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RelayAgentName } from './agent-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* What the agent actually experienced with a sign-in — per agent AND per account.
|
|
4
|
+
*
|
|
5
|
+
* The credentials file cannot tell us the provider revoked a login: the refresh
|
|
6
|
+
* token still sits there, dated. The one authority on that is a real refusal
|
|
7
|
+
* from a real run — so a session that failed to authenticate demotes the file's
|
|
8
|
+
* verdict for a while, and any successful turn clears it again.
|
|
9
|
+
*
|
|
10
|
+
* Moved out of `auth-relay.ts` for #422 and keyed by account as well as agent.
|
|
11
|
+
* With several Claude logins on one machine «Claude was refused» stopped being
|
|
12
|
+
* a statement about anything: the session that was refused ran under ONE
|
|
13
|
+
* account, and the machine may have been switched to another since. The mark
|
|
14
|
+
* belongs to the account the session started under (R14) – which is why both
|
|
15
|
+
* the account list (`claude-homes.ts`) and the panel verdict (`auth-relay.ts`)
|
|
16
|
+
* read it from here rather than each keeping a copy.
|
|
17
|
+
*
|
|
18
|
+
* Process memory on purpose: no protocol, no disk. The consequence is worth
|
|
19
|
+
* naming — restarting the daemon forgets the refusal and the panel goes back
|
|
20
|
+
* to trusting the file until the next session tries. (A saved account also
|
|
21
|
+
* carries a mark on disk, D2 – see `markLoginExpired` in `claude-homes.ts`.)
|
|
22
|
+
*/
|
|
23
|
+
/** The machine's own login – the one row every agent has. */
|
|
24
|
+
export declare const MACHINE_ACCOUNT_ID = "machine";
|
|
25
|
+
/** A session under this agent and account was just refused. */
|
|
26
|
+
export declare function noteRefusal(agent: RelayAgentName, accountId?: string): void;
|
|
27
|
+
/** The sign-in worked again. Answers whether a refusal was being held. */
|
|
28
|
+
export declare function clearRefusal(agent: RelayAgentName, accountId?: string): boolean;
|
|
29
|
+
/** Is a refusal still being held against this agent and account? */
|
|
30
|
+
export declare function refusalActive(agent: RelayAgentName, accountId?: string): boolean;
|
|
31
|
+
//# sourceMappingURL=login-marks.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the agent actually experienced with a sign-in — per agent AND per account.
|
|
3
|
+
*
|
|
4
|
+
* The credentials file cannot tell us the provider revoked a login: the refresh
|
|
5
|
+
* token still sits there, dated. The one authority on that is a real refusal
|
|
6
|
+
* from a real run — so a session that failed to authenticate demotes the file's
|
|
7
|
+
* verdict for a while, and any successful turn clears it again.
|
|
8
|
+
*
|
|
9
|
+
* Moved out of `auth-relay.ts` for #422 and keyed by account as well as agent.
|
|
10
|
+
* With several Claude logins on one machine «Claude was refused» stopped being
|
|
11
|
+
* a statement about anything: the session that was refused ran under ONE
|
|
12
|
+
* account, and the machine may have been switched to another since. The mark
|
|
13
|
+
* belongs to the account the session started under (R14) – which is why both
|
|
14
|
+
* the account list (`claude-homes.ts`) and the panel verdict (`auth-relay.ts`)
|
|
15
|
+
* read it from here rather than each keeping a copy.
|
|
16
|
+
*
|
|
17
|
+
* Process memory on purpose: no protocol, no disk. The consequence is worth
|
|
18
|
+
* naming — restarting the daemon forgets the refusal and the panel goes back
|
|
19
|
+
* to trusting the file until the next session tries. (A saved account also
|
|
20
|
+
* carries a mark on disk, D2 – see `markLoginExpired` in `claude-homes.ts`.)
|
|
21
|
+
*/
|
|
22
|
+
/** The machine's own login – the one row every agent has. */
|
|
23
|
+
export const MACHINE_ACCOUNT_ID = 'machine';
|
|
24
|
+
const AUTH_FAILURE_TTL_MS = 15 * 60_000;
|
|
25
|
+
const refusals = new Map();
|
|
26
|
+
const keyOf = (agent, accountId) => `${agent}:${accountId}`;
|
|
27
|
+
/** A session under this agent and account was just refused. */
|
|
28
|
+
export function noteRefusal(agent, accountId = MACHINE_ACCOUNT_ID) {
|
|
29
|
+
refusals.set(keyOf(agent, accountId), Date.now());
|
|
30
|
+
}
|
|
31
|
+
/** The sign-in worked again. Answers whether a refusal was being held. */
|
|
32
|
+
export function clearRefusal(agent, accountId = MACHINE_ACCOUNT_ID) {
|
|
33
|
+
return refusals.delete(keyOf(agent, accountId));
|
|
34
|
+
}
|
|
35
|
+
/** Is a refusal still being held against this agent and account? */
|
|
36
|
+
export function refusalActive(agent, accountId = MACHINE_ACCOUNT_ID) {
|
|
37
|
+
const key = keyOf(agent, accountId);
|
|
38
|
+
const at = refusals.get(key);
|
|
39
|
+
if (at === undefined)
|
|
40
|
+
return false;
|
|
41
|
+
if (Date.now() - at > AUTH_FAILURE_TTL_MS) {
|
|
42
|
+
refusals.delete(key);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=login-marks.js.map
|