@songsid/agend 2.1.4 → 2.1.5-beta.2
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/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +7 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +5 -0
- package/dist/channel/adapters/telegram.js +11 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +7 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -1
- package/dist/fleet-context.d.ts +3 -0
- package/dist/fleet-manager.d.ts +19 -0
- package/dist/fleet-manager.js +209 -29
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +46 -0
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +127 -0
- package/dist/login-controller.js +450 -0
- package/dist/login-controller.js.map +1 -0
- package/dist/login-flows.d.ts +41 -3
- package/dist/login-flows.js +37 -5
- package/dist/login-flows.js.map +1 -1
- package/dist/login-manager.d.ts +12 -1
- package/dist/login-manager.js +64 -7
- package/dist/login-manager.js.map +1 -1
- package/dist/login-window-lock.d.ts +26 -0
- package/dist/login-window-lock.js +64 -0
- package/dist/login-window-lock.js.map +1 -0
- package/dist/tmux-manager.d.ts +19 -0
- package/dist/tmux-manager.js +79 -12
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-commands.js +1 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/ui/web-terminal/terminal.css +17 -0
- package/dist/ui/web-terminal/terminal.html +43 -0
- package/dist/ui/web-terminal/terminal.js +148 -0
- package/dist/ui/web-terminal/vendor/LICENSE.addon-fit +19 -0
- package/dist/ui/web-terminal/vendor/LICENSE.addon-web-links +19 -0
- package/dist/ui/web-terminal/vendor/LICENSE.xterm +21 -0
- package/dist/ui/web-terminal/vendor/VENDORED.md +22 -0
- package/dist/ui/web-terminal/vendor/addon-fit.js +1 -0
- package/dist/ui/web-terminal/vendor/addon-web-links.js +1 -0
- package/dist/ui/web-terminal/vendor/xterm.css +285 -0
- package/dist/ui/web-terminal/vendor/xterm.js +1 -0
- package/dist/web-terminal-http.d.ts +53 -0
- package/dist/web-terminal-http.js +396 -0
- package/dist/web-terminal-http.js.map +1 -0
- package/dist/web-terminal.d.ts +336 -0
- package/dist/web-terminal.js +888 -0
- package/dist/web-terminal.js.map +1 -0
- package/dist/ws-server.d.ts +69 -0
- package/dist/ws-server.js +392 -0
- package/dist/ws-server.js.map +1 -0
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -976,6 +976,18 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
976
976
|
/** Invalidate work queued before a user cancel and wake idle-gate waiters. */
|
|
977
977
|
private cancelPendingDeliveries;
|
|
978
978
|
private activeLogin;
|
|
979
|
+
/**
|
|
980
|
+
* Web-terminal login (v2.1.5 default, `login.mode: web`). The relay code
|
|
981
|
+
* below (activeLogin/LoginSession) is `login.mode: relay`, kept for one
|
|
982
|
+
* release as the rollback lever and removed in 2.1.6.
|
|
983
|
+
*/
|
|
984
|
+
private loginController;
|
|
985
|
+
/**
|
|
986
|
+
* One login/install window fleet-wide. Claimed synchronously before the
|
|
987
|
+
* first await by web login, relay login and install alike (sol B1).
|
|
988
|
+
*/
|
|
989
|
+
private readonly loginWindow;
|
|
990
|
+
private get webLogin();
|
|
979
991
|
/** Post the backend chooser for a bare `/login`. Caller enforces admin. */
|
|
980
992
|
promptLoginBackends(chat: {
|
|
981
993
|
adapter: ChannelAdapter;
|
|
@@ -1042,11 +1054,16 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1042
1054
|
adapterId: string;
|
|
1043
1055
|
chatId: string;
|
|
1044
1056
|
threadId?: string;
|
|
1057
|
+
userId?: string;
|
|
1045
1058
|
}, opts?: {
|
|
1046
1059
|
skipAuthCheck?: boolean;
|
|
1060
|
+
tokenPresent?: boolean;
|
|
1047
1061
|
}): Promise<string | null>;
|
|
1062
|
+
private startRelayClaimed;
|
|
1048
1063
|
/** Create the login window and session (pre-check already settled). */
|
|
1049
1064
|
private launchLoginSession;
|
|
1065
|
+
/** Fleet shutdown: end any web/relay login or install window and wait for its confirmed teardown. */
|
|
1066
|
+
private shutdownLoginWindows;
|
|
1050
1067
|
/** `/login code <text>` — paste admin-supplied text into the login window. */
|
|
1051
1068
|
loginSubmitInput(text: string): Promise<string>;
|
|
1052
1069
|
/** `/login cancel` — abort the active session and remove its window. */
|
|
@@ -1068,6 +1085,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1068
1085
|
private handleLoginBackendSelect;
|
|
1069
1086
|
/** Re-login confirmation (auth pre-check said credentials still work). */
|
|
1070
1087
|
private handleLoginConfirm;
|
|
1088
|
+
/** "Resend token" button (web login): only the requester may press it; the token never enters the channel. */
|
|
1089
|
+
private handleLoginTokenResend;
|
|
1071
1090
|
/** Kiro provider button → drive the CLI's arrow-key selector. */
|
|
1072
1091
|
private handleLoginMenuSelect;
|
|
1073
1092
|
private activeInstall;
|
package/dist/fleet-manager.js
CHANGED
|
@@ -48,6 +48,8 @@ import { handleViewRequest, isViewPath } from "./view-api.js";
|
|
|
48
48
|
import { handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
49
49
|
import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES, checkAuthStatus } from "./login-flows.js";
|
|
50
50
|
import { LoginSession } from "./login-manager.js";
|
|
51
|
+
import { LoginController, LOGIN_TOKEN_RESEND_PREFIX } from "./login-controller.js";
|
|
52
|
+
import { LoginWindowLock } from "./login-window-lock.js";
|
|
51
53
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
52
54
|
import { setLocale, detectLocale, getLocale, t } from "./locale.js";
|
|
53
55
|
import { handleAgentRequest } from "./agent-endpoint.js";
|
|
@@ -1965,6 +1967,8 @@ export class FleetManager {
|
|
|
1965
1967
|
}
|
|
1966
1968
|
/** Start all instances from fleet config */
|
|
1967
1969
|
async startAll(configPath) {
|
|
1970
|
+
this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
|
|
1971
|
+
this.loginController?.reopen();
|
|
1968
1972
|
const startupStartedAt = Date.now();
|
|
1969
1973
|
FleetManager.signalTarget = this;
|
|
1970
1974
|
this.startupComplete = false;
|
|
@@ -2735,6 +2739,8 @@ export class FleetManager {
|
|
|
2735
2739
|
return;
|
|
2736
2740
|
if (await this.handleLoginConfirm(data, adapterId, this.adapter ?? undefined))
|
|
2737
2741
|
return;
|
|
2742
|
+
if (await this.handleLoginTokenResend(data, adapterId, this.adapter ?? undefined))
|
|
2743
|
+
return;
|
|
2738
2744
|
if (await this.handleInstallLoginConfirm(data, adapterId, this.adapter ?? undefined))
|
|
2739
2745
|
return;
|
|
2740
2746
|
if (await this.handleClearConfirmation(data, adapterId, this.adapter ?? undefined))
|
|
@@ -3135,6 +3141,8 @@ export class FleetManager {
|
|
|
3135
3141
|
return;
|
|
3136
3142
|
if (await this.handleLoginConfirm(data, adapterId, adapter))
|
|
3137
3143
|
return;
|
|
3144
|
+
if (await this.handleLoginTokenResend(data, adapterId, adapter))
|
|
3145
|
+
return;
|
|
3138
3146
|
if (await this.handleInstallLoginConfirm(data, adapterId, adapter))
|
|
3139
3147
|
return;
|
|
3140
3148
|
if (await this.handleClearConfirmation(data, adapterId, adapter))
|
|
@@ -6859,6 +6867,40 @@ export class FleetManager {
|
|
|
6859
6867
|
// repairs every instance of that backend, and instance delivery, pane-state
|
|
6860
6868
|
// detection, tool progress, and mcp_proxy_reply never observe login output.
|
|
6861
6869
|
activeLogin = null;
|
|
6870
|
+
/**
|
|
6871
|
+
* Web-terminal login (v2.1.5 default, `login.mode: web`). The relay code
|
|
6872
|
+
* below (activeLogin/LoginSession) is `login.mode: relay`, kept for one
|
|
6873
|
+
* release as the rollback lever and removed in 2.1.6.
|
|
6874
|
+
*/
|
|
6875
|
+
loginController = null;
|
|
6876
|
+
/**
|
|
6877
|
+
* One login/install window fleet-wide. Claimed synchronously before the
|
|
6878
|
+
* first await by web login, relay login and install alike (sol B1).
|
|
6879
|
+
*/
|
|
6880
|
+
loginWindow = new LoginWindowLock();
|
|
6881
|
+
get webLogin() {
|
|
6882
|
+
if (!this.loginController) {
|
|
6883
|
+
this.loginController = new LoginController({
|
|
6884
|
+
logger: this.logger,
|
|
6885
|
+
fleetConfig: () => this.fleetConfig,
|
|
6886
|
+
isFleetAdmin: (userId, adapterId) => this.isFleetAdmin(userId, adapterId),
|
|
6887
|
+
eventLog: () => this.eventLog,
|
|
6888
|
+
recoverBackendInstances: backend => this.recoverBackendInstances(backend),
|
|
6889
|
+
claimWindow: backend => this.loginWindow.tryClaim("web", backend),
|
|
6890
|
+
releaseWindow: claim => { this.loginWindow.release(claim); },
|
|
6891
|
+
isClaimCurrent: claim => this.loginWindow.isCurrent(claim),
|
|
6892
|
+
windowBusyMessage: () => this.loginWindow.busyMessage(),
|
|
6893
|
+
postButtons: async ({ prefix, instanceName, chat, message, choices, expiredText }) => {
|
|
6894
|
+
await this.postNonceButtonPrompt({
|
|
6895
|
+
prefix, alertType: "login", instanceName,
|
|
6896
|
+
adapter: chat.adapter, adapterId: chat.adapterId, chatId: chat.chatId, threadId: chat.threadId,
|
|
6897
|
+
message, choices, expiredText,
|
|
6898
|
+
});
|
|
6899
|
+
},
|
|
6900
|
+
});
|
|
6901
|
+
}
|
|
6902
|
+
return this.loginController;
|
|
6903
|
+
}
|
|
6862
6904
|
/** Post the backend chooser for a bare `/login`. Caller enforces admin. */
|
|
6863
6905
|
async promptLoginBackends(chat) {
|
|
6864
6906
|
const configured = new Set();
|
|
@@ -7034,21 +7076,40 @@ export class FleetManager {
|
|
|
7034
7076
|
* posted instead (auth still valid — see the pre-check below).
|
|
7035
7077
|
*/
|
|
7036
7078
|
async startLoginSession(backendArg, chat, opts = {}) {
|
|
7079
|
+
if (this.webLogin.mode() === "web")
|
|
7080
|
+
return this.webLogin.start(backendArg, chat, opts);
|
|
7081
|
+
// ── legacy relay mode (login.mode: relay) — removed in 2.1.6 ──
|
|
7037
7082
|
const backend = LOGIN_BACKEND_ALIASES[backendArg.toLowerCase()] ?? backendArg.toLowerCase();
|
|
7038
7083
|
const flow = LOGIN_FLOWS[backend];
|
|
7039
7084
|
if (!flow)
|
|
7040
7085
|
return t("login.unsupported", backendArg);
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7086
|
+
// Declined in every mode — never a silent relay fallback (e.g. Antigravity).
|
|
7087
|
+
if (flow.remoteLogin === "unsupported")
|
|
7088
|
+
return t("login.remote_unsupported_agent_cli", backend, flow.command);
|
|
7089
|
+
// Reserve the window before the pre-check await. The claim is owned by this
|
|
7090
|
+
// region until it is transferred to launchLoginSession; any other exit
|
|
7091
|
+
// (buttons only, throw, shutdown) releases it.
|
|
7092
|
+
const claim = this.loginWindow.tryClaim("relay", backend);
|
|
7093
|
+
if (!claim)
|
|
7094
|
+
return this.loginWindow.busyMessage();
|
|
7095
|
+
let transferred = false;
|
|
7096
|
+
try {
|
|
7097
|
+
return await this.startRelayClaimed(flow, backend, chat, opts, claim, () => { transferred = true; });
|
|
7098
|
+
}
|
|
7099
|
+
finally {
|
|
7100
|
+
if (!transferred)
|
|
7101
|
+
this.loginWindow.release(claim);
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
async startRelayClaimed(flow, backend, chat, opts, claim, markTransferred) {
|
|
7046
7105
|
// Token-free pre-check (5s cap): re-login while auth still works is
|
|
7047
7106
|
// usually a mistake, so it needs a confirmed click. An invalid OR
|
|
7048
7107
|
// uncertain result (timeout, missing binary) proceeds straight to login —
|
|
7049
7108
|
// an unreliable probe must never block the re-login the admin asked for.
|
|
7050
7109
|
if (!opts.skipAuthCheck && flow.authCheck) {
|
|
7051
7110
|
const status = await checkAuthStatus(flow.authCheck);
|
|
7111
|
+
if (!this.loginWindow.isCurrent(claim))
|
|
7112
|
+
return t("login.web_shutting_down"); // fleet shut down while we probed
|
|
7052
7113
|
if (status === "valid") {
|
|
7053
7114
|
await this.postNonceButtonPrompt({
|
|
7054
7115
|
prefix: LOGIN_CONFIRM_CALLBACK_PREFIX,
|
|
@@ -7068,18 +7129,27 @@ export class FleetManager {
|
|
|
7068
7129
|
return null;
|
|
7069
7130
|
}
|
|
7070
7131
|
}
|
|
7071
|
-
|
|
7132
|
+
markTransferred(); // launchLoginSession owns the claim from here
|
|
7133
|
+
return this.launchLoginSession(flow, backend, chat, claim);
|
|
7072
7134
|
}
|
|
7073
7135
|
/** Create the login window and session (pre-check already settled). */
|
|
7074
|
-
async launchLoginSession(flow, backend, chat) {
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7136
|
+
async launchLoginSession(flow, backend, chat, claim) {
|
|
7137
|
+
// Owns `claim`: released on any failure before the session is published,
|
|
7138
|
+
// and by onDone afterwards. A shutdown during ensureSession stops us.
|
|
7139
|
+
let tmux;
|
|
7140
|
+
try {
|
|
7141
|
+
const sessionName = getTmuxSession();
|
|
7142
|
+
await TmuxManager.ensureSession(sessionName);
|
|
7143
|
+
if (!this.loginWindow.isCurrent(claim)) {
|
|
7144
|
+
this.loginWindow.release(claim);
|
|
7145
|
+
return t("login.web_shutting_down");
|
|
7146
|
+
}
|
|
7147
|
+
tmux = new TmuxManager(sessionName, "");
|
|
7148
|
+
}
|
|
7149
|
+
catch (err) {
|
|
7150
|
+
this.loginWindow.release(claim);
|
|
7151
|
+
return t("login.failed", backend, err.message);
|
|
7152
|
+
}
|
|
7083
7153
|
const session = new LoginSession(flow, tmux, {
|
|
7084
7154
|
onMenu: async (options) => {
|
|
7085
7155
|
await this.postNonceButtonPrompt({
|
|
@@ -7101,8 +7171,12 @@ export class FleetManager {
|
|
|
7101
7171
|
onNeedInput: async (promptExcerpt) => {
|
|
7102
7172
|
await chat.adapter.sendText(chat.chatId, t("login.need_input", backend, promptExcerpt), { threadId: chat.threadId }).catch(() => { });
|
|
7103
7173
|
},
|
|
7104
|
-
onDone: async ({ ok, detail }) => {
|
|
7174
|
+
onDone: async ({ ok, detail, cleanupFailed }) => {
|
|
7105
7175
|
this.activeLogin = null;
|
|
7176
|
+
this.loginWindow.release(claim);
|
|
7177
|
+
if (cleanupFailed) {
|
|
7178
|
+
await chat.adapter.sendText(chat.chatId, t("login.web_cleanup_failed", backend), { threadId: chat.threadId }).catch(() => { });
|
|
7179
|
+
}
|
|
7106
7180
|
let text;
|
|
7107
7181
|
if (ok) {
|
|
7108
7182
|
const { woken, restarted } = await this.recoverBackendInstances(backend);
|
|
@@ -7128,12 +7202,68 @@ export class FleetManager {
|
|
|
7128
7202
|
}
|
|
7129
7203
|
catch (err) {
|
|
7130
7204
|
this.activeLogin = null;
|
|
7131
|
-
|
|
7205
|
+
this.loginWindow.release(claim);
|
|
7206
|
+
const text = t("login.failed", backend, err.message);
|
|
7207
|
+
return err.cleanupFailed ? `${text}\n${t("login.web_cleanup_failed", backend)}` : text;
|
|
7208
|
+
}
|
|
7209
|
+
if (session.state === "done") {
|
|
7210
|
+
// Cancelled (user or shutdown) while starting: start() joined the
|
|
7211
|
+
// teardown, so nothing is left. onDone already released the claim.
|
|
7212
|
+
this.activeLogin = null;
|
|
7213
|
+
this.loginWindow.release(claim);
|
|
7214
|
+
return this.loginWindow.isClosed ? t("login.web_shutting_down") : t("login.cancelled", backend);
|
|
7215
|
+
}
|
|
7216
|
+
if (!this.loginWindow.isCurrent(claim)) {
|
|
7217
|
+
await session.cancel("cancelled").catch(() => { });
|
|
7218
|
+
this.activeLogin = null;
|
|
7219
|
+
this.loginWindow.release(claim);
|
|
7220
|
+
return t("login.web_shutting_down");
|
|
7132
7221
|
}
|
|
7133
7222
|
return t("login.started", backend);
|
|
7134
7223
|
}
|
|
7224
|
+
/** Fleet shutdown: end any web/relay login or install window and wait for its confirmed teardown. */
|
|
7225
|
+
async shutdownLoginWindows() {
|
|
7226
|
+
// Close the lock FIRST: in-flight starts parked in a pre-check or
|
|
7227
|
+
// ensureSession observe !isCurrent when they resume and stop; no new
|
|
7228
|
+
// window can be claimed while we stop.
|
|
7229
|
+
this.loginWindow.close();
|
|
7230
|
+
// Completion semantics live INSIDE the sessions: each awaits its own
|
|
7231
|
+
// confirmed cleanup, every tmux op on that path has a hard per-op bound
|
|
7232
|
+
// (web: abort ≤10 s + kill ≤31 s; legacy: create/list/kill ≤10 s each,
|
|
7233
|
+
// duplicates killed in parallel). This outer deadline is only a loud last
|
|
7234
|
+
// resort so `agend stop` cannot hang forever on a wedged tmux; reaching it
|
|
7235
|
+
// is an ERROR, logged and recorded, and it releases nothing re-claimable
|
|
7236
|
+
// (the lock stays closed, the controller keeps its entry). The timer is
|
|
7237
|
+
// cleared on success so a long-lived process never fires it spuriously.
|
|
7238
|
+
const SHUTDOWN_DEADLINE_MS = 120_000;
|
|
7239
|
+
const bounded = (p, what) => {
|
|
7240
|
+
if (!p)
|
|
7241
|
+
return Promise.resolve();
|
|
7242
|
+
let timer;
|
|
7243
|
+
const deadline = new Promise(r => {
|
|
7244
|
+
timer = setTimeout(() => {
|
|
7245
|
+
this.logger.error({ what, deadlineMs: SHUTDOWN_DEADLINE_MS }, "login window teardown still in flight at the shutdown deadline — a dedicated tmux server may survive; check `tmux -L agend-term-* ls`");
|
|
7246
|
+
this.eventLog?.insert("login", "login_window_shutdown_deadline", { what });
|
|
7247
|
+
r();
|
|
7248
|
+
}, SHUTDOWN_DEADLINE_MS);
|
|
7249
|
+
timer.unref?.();
|
|
7250
|
+
});
|
|
7251
|
+
return Promise.race([p.then(() => undefined, () => this.logger.warn({ what }, "login window shutdown failed")), deadline])
|
|
7252
|
+
.finally(() => { if (timer)
|
|
7253
|
+
clearTimeout(timer); });
|
|
7254
|
+
};
|
|
7255
|
+
// "cancelled" is the detail both legacy onDone handlers keep quiet about —
|
|
7256
|
+
// a stopping fleet must not announce "login failed — fleet shutdown".
|
|
7257
|
+
await Promise.all([
|
|
7258
|
+
bounded(this.loginController?.shutdown(), "web-login"),
|
|
7259
|
+
bounded(this.activeLogin?.session.cancel("cancelled"), "relay-login"),
|
|
7260
|
+
bounded(this.activeInstall?.session.cancel("cancelled"), "install"),
|
|
7261
|
+
]);
|
|
7262
|
+
}
|
|
7135
7263
|
/** `/login code <text>` — paste admin-supplied text into the login window. */
|
|
7136
7264
|
async loginSubmitInput(text) {
|
|
7265
|
+
if (this.loginController?.isActive())
|
|
7266
|
+
return t("login.web_code_not_needed");
|
|
7137
7267
|
if (!this.activeLogin)
|
|
7138
7268
|
return t("login.no_session");
|
|
7139
7269
|
const ok = await this.activeLogin.session.submitInput(text);
|
|
@@ -7141,6 +7271,8 @@ export class FleetManager {
|
|
|
7141
7271
|
}
|
|
7142
7272
|
/** `/login cancel` — abort the active session and remove its window. */
|
|
7143
7273
|
async cancelLoginSession() {
|
|
7274
|
+
if (this.loginController?.isActive())
|
|
7275
|
+
return this.loginController.cancel();
|
|
7144
7276
|
if (!this.activeLogin)
|
|
7145
7277
|
return t("login.no_session");
|
|
7146
7278
|
const backend = this.activeLogin.backend;
|
|
@@ -7214,6 +7346,7 @@ export class FleetManager {
|
|
|
7214
7346
|
adapterId: entry.adapterId,
|
|
7215
7347
|
chatId: entry.chatId,
|
|
7216
7348
|
threadId: entry.threadId,
|
|
7349
|
+
userId: data.userId,
|
|
7217
7350
|
});
|
|
7218
7351
|
if (text)
|
|
7219
7352
|
await entry.adapter.sendText(entry.chatId, text, { threadId: entry.threadId }).catch(() => { });
|
|
@@ -7221,7 +7354,7 @@ export class FleetManager {
|
|
|
7221
7354
|
}
|
|
7222
7355
|
/** Re-login confirmation (auth pre-check said credentials still work). */
|
|
7223
7356
|
async handleLoginConfirm(data, callbackAdapterId, receivingAdapter) {
|
|
7224
|
-
const claimed = this.consumeNonceCallback(LOGIN_CONFIRM_CALLBACK_PREFIX, /^login-confirm:([0-9a-f]+):(go|cancel)$/, data, callbackAdapterId, receivingAdapter);
|
|
7357
|
+
const claimed = this.consumeNonceCallback(LOGIN_CONFIRM_CALLBACK_PREFIX, /^login-confirm:([0-9a-f]+):(go|go-relogin|cancel)$/, data, callbackAdapterId, receivingAdapter);
|
|
7225
7358
|
if (claimed === null)
|
|
7226
7359
|
return false;
|
|
7227
7360
|
if (claimed === "consumed")
|
|
@@ -7238,11 +7371,24 @@ export class FleetManager {
|
|
|
7238
7371
|
adapterId: entry.adapterId,
|
|
7239
7372
|
chatId: entry.chatId,
|
|
7240
7373
|
threadId: entry.threadId,
|
|
7241
|
-
|
|
7374
|
+
userId: data.userId,
|
|
7375
|
+
}, { skipAuthCheck: true, tokenPresent: action === "go-relogin" });
|
|
7242
7376
|
if (text)
|
|
7243
7377
|
await entry.adapter.sendText(entry.chatId, text, { threadId: entry.threadId }).catch(() => { });
|
|
7244
7378
|
return true;
|
|
7245
7379
|
}
|
|
7380
|
+
/** "Resend token" button (web login): only the requester may press it; the token never enters the channel. */
|
|
7381
|
+
async handleLoginTokenResend(data, callbackAdapterId, receivingAdapter) {
|
|
7382
|
+
const claimed = this.consumeNonceCallback(LOGIN_TOKEN_RESEND_PREFIX, /^login-token:([0-9a-f]+):(resend)$/, data, callbackAdapterId, receivingAdapter);
|
|
7383
|
+
if (claimed === null)
|
|
7384
|
+
return false;
|
|
7385
|
+
if (claimed === "consumed")
|
|
7386
|
+
return true;
|
|
7387
|
+
const { entry } = claimed;
|
|
7388
|
+
const text = await this.webLogin.resendToken(data.userId);
|
|
7389
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, text);
|
|
7390
|
+
return true;
|
|
7391
|
+
}
|
|
7246
7392
|
/** Kiro provider button → drive the CLI's arrow-key selector. */
|
|
7247
7393
|
async handleLoginMenuSelect(data, callbackAdapterId, receivingAdapter) {
|
|
7248
7394
|
const claimed = this.consumeNonceCallback(LOGIN_MENU_CALLBACK_PREFIX, /^login-menu:([0-9a-f]+):(\d)$/, data, callbackAdapterId, receivingAdapter);
|
|
@@ -7272,13 +7418,25 @@ export class FleetManager {
|
|
|
7272
7418
|
return t("install.unsupported", backendArg);
|
|
7273
7419
|
if (checkBinaryInstalled(info.binary))
|
|
7274
7420
|
return t("install.already", backend, info.binary);
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7421
|
+
// Reserve the fleet-wide window before the first await (shared with web/relay
|
|
7422
|
+
// login). Owned by this method until the session is published.
|
|
7423
|
+
const claim = this.loginWindow.tryClaim("install", backend);
|
|
7424
|
+
if (!claim)
|
|
7425
|
+
return this.loginWindow.busyMessage();
|
|
7426
|
+
let tmux;
|
|
7427
|
+
try {
|
|
7428
|
+
const sessionName = getTmuxSession();
|
|
7429
|
+
await TmuxManager.ensureSession(sessionName);
|
|
7430
|
+
if (!this.loginWindow.isCurrent(claim)) {
|
|
7431
|
+
this.loginWindow.release(claim);
|
|
7432
|
+
return t("login.web_shutting_down");
|
|
7433
|
+
}
|
|
7434
|
+
tmux = new TmuxManager(sessionName, "");
|
|
7435
|
+
}
|
|
7436
|
+
catch (err) {
|
|
7437
|
+
this.loginWindow.release(claim);
|
|
7438
|
+
return t("install.failed", backend, err.message);
|
|
7439
|
+
}
|
|
7282
7440
|
// A synthetic login flow: success is decided by the installer's exit code
|
|
7283
7441
|
// (LoginSession treats a clean exit as success), never by pane text — and
|
|
7284
7442
|
// installer output that happens to contain a URL must not be forwarded as
|
|
@@ -7293,8 +7451,12 @@ export class FleetManager {
|
|
|
7293
7451
|
onMenu: () => { },
|
|
7294
7452
|
onAuthHint: () => { },
|
|
7295
7453
|
onNeedInput: () => { },
|
|
7296
|
-
onDone: async ({ ok, detail }) => {
|
|
7454
|
+
onDone: async ({ ok, detail, cleanupFailed }) => {
|
|
7297
7455
|
this.activeInstall = null;
|
|
7456
|
+
this.loginWindow.release(claim);
|
|
7457
|
+
if (cleanupFailed) {
|
|
7458
|
+
await chat.adapter.sendText(chat.chatId, t("login.web_cleanup_failed", backend), { threadId: chat.threadId }).catch(() => { });
|
|
7459
|
+
}
|
|
7298
7460
|
if (!ok) {
|
|
7299
7461
|
// A cancel is user-initiated — the cancel command's own reply already
|
|
7300
7462
|
// said so; a second message here would be a duplicate.
|
|
@@ -7336,7 +7498,20 @@ export class FleetManager {
|
|
|
7336
7498
|
}
|
|
7337
7499
|
catch (err) {
|
|
7338
7500
|
this.activeInstall = null;
|
|
7339
|
-
|
|
7501
|
+
this.loginWindow.release(claim);
|
|
7502
|
+
const text = t("install.failed", backend, err.message);
|
|
7503
|
+
return err.cleanupFailed ? `${text}\n${t("login.web_cleanup_failed", backend)}` : text;
|
|
7504
|
+
}
|
|
7505
|
+
if (session.state === "done") {
|
|
7506
|
+
this.activeInstall = null;
|
|
7507
|
+
this.loginWindow.release(claim);
|
|
7508
|
+
return this.loginWindow.isClosed ? t("login.web_shutting_down") : t("install.cancelled", backend);
|
|
7509
|
+
}
|
|
7510
|
+
if (!this.loginWindow.isCurrent(claim)) {
|
|
7511
|
+
await session.cancel("cancelled").catch(() => { });
|
|
7512
|
+
this.activeInstall = null;
|
|
7513
|
+
this.loginWindow.release(claim);
|
|
7514
|
+
return t("login.web_shutting_down");
|
|
7340
7515
|
}
|
|
7341
7516
|
return t("install.started", backend);
|
|
7342
7517
|
}
|
|
@@ -7377,6 +7552,7 @@ export class FleetManager {
|
|
|
7377
7552
|
adapterId: entry.adapterId,
|
|
7378
7553
|
chatId: entry.chatId,
|
|
7379
7554
|
threadId: entry.threadId,
|
|
7555
|
+
userId: data.userId,
|
|
7380
7556
|
});
|
|
7381
7557
|
if (text)
|
|
7382
7558
|
await entry.adapter.sendText(entry.chatId, text, { threadId: entry.threadId }).catch(() => { });
|
|
@@ -7388,7 +7564,7 @@ export class FleetManager {
|
|
|
7388
7564
|
await data.respond(t("permission.denied"));
|
|
7389
7565
|
return;
|
|
7390
7566
|
}
|
|
7391
|
-
const chat = { adapter, adapterId, chatId: data.channelId };
|
|
7567
|
+
const chat = { adapter, adapterId, chatId: data.channelId, userId: data.userId };
|
|
7392
7568
|
if (data.options?.cancel === true) {
|
|
7393
7569
|
await data.respond(await this.cancelLoginSession());
|
|
7394
7570
|
return;
|
|
@@ -9255,6 +9431,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
9255
9431
|
clearInterval(this.watchdogTimer);
|
|
9256
9432
|
this.watchdogTimer = null;
|
|
9257
9433
|
}
|
|
9434
|
+
// A login/install window is a dedicated tmux server with its own TTL
|
|
9435
|
+
// timer and HTTP listener living in THIS process: without an explicit
|
|
9436
|
+
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|
|
9437
|
+
await this.shutdownLoginWindows();
|
|
9258
9438
|
// Cancel adapter retry timers
|
|
9259
9439
|
for (const state of this.adapterState.values()) {
|
|
9260
9440
|
if (state.retryTimer) {
|