@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.4
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/README.md +4 -4
- package/README.zh-TW.md +4 -4
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +14 -2
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/muse.d.ts +7 -0
- package/dist/backend/muse.js +7 -0
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- package/dist/cache-service.d.ts +17 -1
- package/dist/cache-service.js +39 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/cli.js +133 -38
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +12 -0
- package/dist/daemon.js +166 -26
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-general.d.ts +15 -0
- package/dist/discord-general.js +24 -0
- package/dist/discord-general.js.map +1 -0
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-level-config.d.ts +8 -1
- package/dist/fleet-level-config.js +31 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +76 -7
- package/dist/fleet-manager.js +610 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +88 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +6 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- package/dist/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +75 -0
- package/dist/public-link-progress.js +204 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +4 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -2
- package/dist/public-web-link.js +89 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +198 -63
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +100 -29
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +1 -0
- package/dist/restart-guard.js +8 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/service-activation.d.ts +15 -2
- package/dist/service-activation.js +81 -4
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.js +19 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +53 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +4 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +13 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +26 -19
- package/dist/setup-wizard.js.map +1 -1
- package/dist/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +82 -17
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +48 -84
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +42 -0
- package/dist/tunnel/cloudflared-install.js +194 -9
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/deb-extract.d.ts +9 -0
- package/dist/tunnel/deb-extract.js +94 -0
- package/dist/tunnel/deb-extract.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/app.html +3 -0
- package/dist/ui/chat-render.js +32 -6
- package/dist/ui/chat-store.js +21 -1
- package/dist/ui/chat-strings.js +45 -0
- package/dist/ui/chat-thread.js +109 -52
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/icons/apple-touch-icon.png +0 -0
- package/dist/ui/icons/favicon.ico +0 -0
- package/dist/ui/icons/favicon.svg +8 -0
- package/dist/ui/panel-chat.js +16 -7
- package/dist/ui/panel-fleet.js +5 -5
- package/dist/ui/panel-settings.js +18 -8
- package/dist/ui/preview-panel.js +47 -7
- package/dist/ui/preview.js +36 -13
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +46 -6
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-shell.js +19 -33
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +34 -2
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -0
- package/dist/ui/signin.html +3 -0
- package/dist/update-dispatch.d.ts +1 -1
- package/dist/update-dispatch.js +1 -1
- package/dist/update-install.d.ts +15 -3
- package/dist/update-install.js +41 -13
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +8 -0
- package/dist/web-api.js +53 -17
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-icons.d.ts +21 -0
- package/dist/web-icons.js +38 -0
- package/dist/web-icons.js.map +1 -0
- package/dist/web-preview.d.ts +9 -0
- package/dist/web-preview.js +15 -9
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.js.map +1 -1
- package/launcher/postinstall.cjs +8 -4
- package/package.json +1 -1
- package/templates/systemd.service.ejs +3 -3
package/dist/quickstart-api.d.ts
CHANGED
|
@@ -53,10 +53,35 @@ export interface WizardEnvironment {
|
|
|
53
53
|
allowed_users?: string[];
|
|
54
54
|
}>;
|
|
55
55
|
has_fleet: boolean;
|
|
56
|
+
/** #1519 P7: the agents already configured — the wizard offers to connect one instead of overwriting it. */
|
|
57
|
+
agents?: Array<{
|
|
58
|
+
name: string;
|
|
59
|
+
working_directory: string | null;
|
|
60
|
+
backend: string | null;
|
|
61
|
+
}>;
|
|
56
62
|
}
|
|
57
63
|
export interface WizardPlanInput {
|
|
58
64
|
platform: "telegram" | "discord";
|
|
65
|
+
/**
|
|
66
|
+
* The env var for the token. A plan without one gets a generated name (#1519 P1: `AGEND_<PLATFORM>_<ID>_TOKEN`,
|
|
67
|
+
* unique); the commit always carries it — the plan's — so what an admin confirms is what is written.
|
|
68
|
+
*/
|
|
59
69
|
token_env: string;
|
|
70
|
+
/** #1519 P1: "New connection" — add the connection only; no agent is written. */
|
|
71
|
+
connection_only?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* #1519 P7: `instance_name` is an agent that already exists — bind it to the new connection and leave its directory
|
|
74
|
+
* and backend as they are. Without it, an existing name is refused: the wizard never overwrites an agent.
|
|
75
|
+
*/
|
|
76
|
+
existing_agent?: boolean;
|
|
77
|
+
/** The new connection's id; free ids only (a new connection never takes over one). Default: the platform's, else numbered. */
|
|
78
|
+
channel_id?: string;
|
|
79
|
+
/**
|
|
80
|
+
* The token env came from a plan (#1529 review): the commit then re-checks, under its write lease, that the name is
|
|
81
|
+
* still free of everything the plan avoided — a generated name never overwrites a value. Without it, the name is the
|
|
82
|
+
* caller's own (the pre-fleet setup form, the CLI) and only another connection's or a reserved name is refused.
|
|
83
|
+
*/
|
|
84
|
+
token_env_generated?: boolean;
|
|
60
85
|
backend: string;
|
|
61
86
|
working_directory: string;
|
|
62
87
|
instance_name: string;
|
|
@@ -69,28 +94,71 @@ export interface WizardPlanInput {
|
|
|
69
94
|
export interface WizardPlan {
|
|
70
95
|
/** The channel entry that will be merged into fleet.yaml. */
|
|
71
96
|
channel: Record<string, unknown>;
|
|
97
|
+
/**
|
|
98
|
+
* `topic`: an existing agent whose topic belongs to another chat (#1549 review) — `from` its old id, `to` the id it
|
|
99
|
+
* gets here (the General topic of the new chat, for a General agent) or null (none: the old one is not carried).
|
|
100
|
+
*/
|
|
72
101
|
instance: {
|
|
73
102
|
name: string;
|
|
74
103
|
working_directory: string;
|
|
75
104
|
backend: string;
|
|
76
|
-
|
|
105
|
+
channel_id: string;
|
|
106
|
+
existing?: boolean;
|
|
107
|
+
topic?: {
|
|
108
|
+
from: string;
|
|
109
|
+
to: string | null;
|
|
110
|
+
};
|
|
111
|
+
} | null;
|
|
112
|
+
/** The new connection's id and the env var its token goes to. */
|
|
113
|
+
channel_id: string;
|
|
114
|
+
token_env: string;
|
|
77
115
|
/** Env var names only — never the value. */
|
|
78
116
|
env_keys: string[];
|
|
79
117
|
warnings: string[];
|
|
80
118
|
}
|
|
81
119
|
/**
|
|
82
|
-
* A channel id
|
|
83
|
-
*
|
|
84
|
-
* The platform name is the obvious id and the first bot gets it. A second bot on
|
|
85
|
-
* the same platform cannot have it — `duplicate channel id` — so it is
|
|
86
|
-
* qualified by the variable that holds its token. Without this the wizard can
|
|
87
|
-
* never add a second Telegram bot at all.
|
|
120
|
+
* A free channel id for a NEW connection (#1519 P1, S1: the wizard never takes over an existing connection). The
|
|
121
|
+
* platform name is the obvious id and the first bot gets it; the next one on that platform is `<platform>-2`, and so on.
|
|
88
122
|
*/
|
|
89
|
-
export declare function nextChannelId(platform: string,
|
|
123
|
+
export declare function nextChannelId(platform: string, existing: ReadonlyArray<{
|
|
90
124
|
id: string;
|
|
91
|
-
token_env: string | null;
|
|
92
125
|
}>): string;
|
|
93
|
-
|
|
126
|
+
/**
|
|
127
|
+
* #1519 P7: why the agent part of this input cannot be written, or null. A new agent's name must be free (the wizard
|
|
128
|
+
* never overwrites one); an existing agent must exist. A connection-only input has no agent part.
|
|
129
|
+
*/
|
|
130
|
+
export declare function agentConflict(input: Pick<WizardPlanInput, "instance_name" | "existing_agent" | "connection_only">, cfg: FleetConfig | null): string | null;
|
|
131
|
+
/**
|
|
132
|
+
* #1519 P7: an existing agent's input as the plan shows it — with the agent's own directory and backend (the wizard does
|
|
133
|
+
* not ask for them, and they are not changed). Any other input as it is.
|
|
134
|
+
*/
|
|
135
|
+
export declare function withExistingAgent<T extends Pick<WizardPlanInput, "instance_name" | "existing_agent" | "working_directory" | "backend">>(input: T, cfg: FleetConfig | null): T;
|
|
136
|
+
/**
|
|
137
|
+
* #1549 review: a topic id means something only in its chat (a Telegram group's forum topic, a Discord server's
|
|
138
|
+
* channel). An existing agent moved to a connection in another chat would keep an id that, there, is some other topic —
|
|
139
|
+
* or none yet — and inbound messages from that topic would route to it. So its topic binding goes with the move unless
|
|
140
|
+
* the new connection is in the same chat (same platform and group/server). Unknown (the old connection cannot be
|
|
141
|
+
* found) is not "the same": the binding goes.
|
|
142
|
+
*/
|
|
143
|
+
export declare function releasesTopic(cfg: FleetConfig, instanceName: string, newChannel: {
|
|
144
|
+
type?: unknown;
|
|
145
|
+
group_id?: unknown;
|
|
146
|
+
}): boolean;
|
|
147
|
+
/**
|
|
148
|
+
* The topic an existing agent has after moving to `newChannel`: its own when the chat is the same; else, for the
|
|
149
|
+
* General agent, the new chat's General (Telegram's thread 1, the Discord connection's general channel); else none.
|
|
150
|
+
*/
|
|
151
|
+
export declare function movedTopic(cfg: FleetConfig, instanceName: string, newChannel: Record<string, unknown>): {
|
|
152
|
+
from: string;
|
|
153
|
+
to: string | number | null;
|
|
154
|
+
} | null;
|
|
155
|
+
/** The plan as the wizard shows it: an existing agent's topic that the move changes is said so. */
|
|
156
|
+
export declare function withTopicRelease(plan: WizardPlan, cfg: FleetConfig, input: Pick<WizardPlanInput, "instance_name" | "existing_agent">): WizardPlan;
|
|
157
|
+
/** Why this input cannot be a new connection, or null: a taken id or token env (it would replace a connection). */
|
|
158
|
+
export declare function newConnectionConflict(input: Pick<WizardPlanInput, "token_env" | "channel_id">, env: Pick<WizardEnvironment, "channels">): string | null;
|
|
159
|
+
export declare function planQuickstart(input: WizardPlanInput, env: WizardEnvironment & {
|
|
160
|
+
taken_env?: ReadonlySet<string>;
|
|
161
|
+
}): WizardPlan;
|
|
94
162
|
/** Upsert one `KEY=value` line, preserving everything else in the file. */
|
|
95
163
|
export declare function upsertEnvLine(existing: string, key: string, value: string): string;
|
|
96
164
|
export declare function writeQuickstartSecret(dataDir: string, key: string, value: string): SecretWriteResult;
|
|
@@ -101,6 +169,10 @@ export type ProbeRequest = {
|
|
|
101
169
|
} | {
|
|
102
170
|
action: "guilds";
|
|
103
171
|
token: string;
|
|
172
|
+
} | {
|
|
173
|
+
action: "channels";
|
|
174
|
+
token: string;
|
|
175
|
+
guild_id: string;
|
|
104
176
|
} | {
|
|
105
177
|
action: "await-telegram-start";
|
|
106
178
|
token: string;
|
|
@@ -109,6 +181,14 @@ export type ProbeRequest = {
|
|
|
109
181
|
export type ProbeResult = {
|
|
110
182
|
ok: true;
|
|
111
183
|
identity: BotIdentity;
|
|
184
|
+
invite_url?: string;
|
|
185
|
+
portal_url?: string;
|
|
186
|
+
} | {
|
|
187
|
+
ok: true;
|
|
188
|
+
channels: Array<{
|
|
189
|
+
id: string;
|
|
190
|
+
name: string;
|
|
191
|
+
}>;
|
|
112
192
|
} | {
|
|
113
193
|
ok: true;
|
|
114
194
|
guilds: DiscordGuild[];
|
|
@@ -137,15 +217,6 @@ export declare function runProviderProbe(request: ProbeRequest, opts?: {
|
|
|
137
217
|
deadlineMs?: number;
|
|
138
218
|
}): Promise<ProbeResult>;
|
|
139
219
|
export declare function detectWizardBackends(): string[];
|
|
140
|
-
/**
|
|
141
|
-
* What the wizard's routes need from their host.
|
|
142
|
-
*
|
|
143
|
-
* Five fields, and none of them is a fleet: this is the whole "ConfigVerbs +
|
|
144
|
-
* ProviderProbeVerbs" surface. A running fleet supplies it from FleetManager; a
|
|
145
|
-
* pre-fleet setup host supplies it from a file. `tests/prefleet-host.test.ts`
|
|
146
|
-
* asserts this module's import graph reaches neither fleet-manager, daemon nor
|
|
147
|
-
* instance-lifecycle, so the host cannot grow a path to them by accident.
|
|
148
|
-
*/
|
|
149
220
|
/** One pure draft shared by the writer and the authoritative confirmation diff. */
|
|
150
221
|
export declare function draftQuickstart(cfg: FleetConfig, body: WizardPlanInput, plan: WizardPlan): FleetConfig;
|
|
151
222
|
export interface QuickstartApiContext {
|
|
@@ -166,6 +237,8 @@ export interface QuickstartApiContext {
|
|
|
166
237
|
/** The channels as the wizard sees them, from either config shape. */
|
|
167
238
|
export declare function wizardChannels(cfg: FleetConfig | null): WizardEnvironment["channels"];
|
|
168
239
|
/** Reject anything that would land in fleet.yaml or a shell env file unchecked. */
|
|
169
|
-
export declare function validateWizardInput(input: Partial<WizardPlanInput
|
|
240
|
+
export declare function validateWizardInput(input: Partial<WizardPlanInput>, opts?: {
|
|
241
|
+
tokenEnvOptional?: boolean;
|
|
242
|
+
}): string | null;
|
|
170
243
|
/** Handle one wizard route. Returns false when the path is not one of ours. */
|
|
171
244
|
export declare function handleQuickstartRequest(req: IncomingMessage, res: ServerResponse, url: URL, ctx: QuickstartApiContext): boolean;
|
package/dist/quickstart-api.js
CHANGED
|
@@ -2,10 +2,13 @@ import { settingsRequestExecution, settingsWrite } from "./settings-request-capa
|
|
|
2
2
|
import { SettingsExecutionError, settingsFileResource, trySettingsLease } from "./settings-transaction.js";
|
|
3
3
|
import { readBoundedWebBody } from "./web-body.js";
|
|
4
4
|
import { permitWebContinuation } from "./web-continuation.js";
|
|
5
|
+
import { gatewayRequestContext } from "./web-request-context.js";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { SecretStore } from "./secret-store.js";
|
|
8
|
+
import { discordBotPortalUrl, discordInviteUrl } from "./discord-permissions.js";
|
|
9
|
+
import { EnvFileUnreadableError, envFileKeys, generateTokenEnvName, generatedTokenEnvStale, newTokenEnvConflict, takenTokenEnvNames, TOKEN_ENV_PATTERN } from "./token-env-name.js";
|
|
7
10
|
import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
|
|
8
|
-
import { awaitTelegramGroupStart, detectInstalledBackends, listDiscordGuilds, TelegramPollConflictError, verifyDiscordToken, verifyTelegramToken, } from "./provider-probe.js";
|
|
11
|
+
import { awaitTelegramGroupStart, detectInstalledBackends, listDiscordGuilds, listDiscordTextChannels, TelegramPollConflictError, verifyDiscordToken, verifyTelegramToken, } from "./provider-probe.js";
|
|
9
12
|
/** One slice of Telegram long polling, so the browser gets an answer. */
|
|
10
13
|
export const TELEGRAM_WAIT_SLICE_MS = 25_000;
|
|
11
14
|
export const WIZARD_BACKENDS = [
|
|
@@ -18,34 +21,98 @@ export const WIZARD_BACKENDS = [
|
|
|
18
21
|
{ name: "muse", binary: "muse" },
|
|
19
22
|
];
|
|
20
23
|
/**
|
|
21
|
-
* A channel id
|
|
22
|
-
*
|
|
23
|
-
* The platform name is the obvious id and the first bot gets it. A second bot on
|
|
24
|
-
* the same platform cannot have it — `duplicate channel id` — so it is
|
|
25
|
-
* qualified by the variable that holds its token. Without this the wizard can
|
|
26
|
-
* never add a second Telegram bot at all.
|
|
24
|
+
* A free channel id for a NEW connection (#1519 P1, S1: the wizard never takes over an existing connection). The
|
|
25
|
+
* platform name is the obvious id and the first bot gets it; the next one on that platform is `<platform>-2`, and so on.
|
|
27
26
|
*/
|
|
28
|
-
export function nextChannelId(platform,
|
|
29
|
-
const owner = existing.find(channel => channel.token_env === tokenEnv);
|
|
30
|
-
if (owner)
|
|
31
|
-
return owner.id;
|
|
27
|
+
export function nextChannelId(platform, existing) {
|
|
32
28
|
const taken = new Set(existing.map(channel => channel.id));
|
|
33
29
|
if (!taken.has(platform))
|
|
34
30
|
return platform;
|
|
35
|
-
const qualified = `${platform}-${tokenEnv.toLowerCase()}`;
|
|
36
|
-
if (!taken.has(qualified))
|
|
37
|
-
return qualified;
|
|
38
31
|
for (let n = 2;; n++) {
|
|
39
|
-
const candidate = `${
|
|
32
|
+
const candidate = `${platform}-${n}`;
|
|
40
33
|
if (!taken.has(candidate))
|
|
41
34
|
return candidate;
|
|
42
35
|
}
|
|
43
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* #1519 P7: why the agent part of this input cannot be written, or null. A new agent's name must be free (the wizard
|
|
39
|
+
* never overwrites one); an existing agent must exist. A connection-only input has no agent part.
|
|
40
|
+
*/
|
|
41
|
+
export function agentConflict(input, cfg) {
|
|
42
|
+
if (input.connection_only)
|
|
43
|
+
return null;
|
|
44
|
+
const exists = !!cfg && Object.hasOwn(cfg.instances ?? {}, input.instance_name);
|
|
45
|
+
if (input.existing_agent)
|
|
46
|
+
return exists ? null : `there is no agent called "${input.instance_name}"`;
|
|
47
|
+
return exists ? `an agent called "${input.instance_name}" already exists — connect it as an existing agent, or choose another name` : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* #1519 P7: an existing agent's input as the plan shows it — with the agent's own directory and backend (the wizard does
|
|
51
|
+
* not ask for them, and they are not changed). Any other input as it is.
|
|
52
|
+
*/
|
|
53
|
+
export function withExistingAgent(input, cfg) {
|
|
54
|
+
if (!input.existing_agent || !cfg || !Object.hasOwn(cfg.instances ?? {}, input.instance_name))
|
|
55
|
+
return input;
|
|
56
|
+
const inst = cfg.instances[input.instance_name];
|
|
57
|
+
return { ...input, working_directory: inst.working_directory ?? "", backend: inst.backend ?? cfg.defaults?.backend ?? "claude-code" };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* #1549 review: a topic id means something only in its chat (a Telegram group's forum topic, a Discord server's
|
|
61
|
+
* channel). An existing agent moved to a connection in another chat would keep an id that, there, is some other topic —
|
|
62
|
+
* or none yet — and inbound messages from that topic would route to it. So its topic binding goes with the move unless
|
|
63
|
+
* the new connection is in the same chat (same platform and group/server). Unknown (the old connection cannot be
|
|
64
|
+
* found) is not "the same": the binding goes.
|
|
65
|
+
*/
|
|
66
|
+
export function releasesTopic(cfg, instanceName, newChannel) {
|
|
67
|
+
if (!Object.hasOwn(cfg.instances ?? {}, instanceName))
|
|
68
|
+
return false;
|
|
69
|
+
const inst = cfg.instances[instanceName];
|
|
70
|
+
if (inst.topic_id === undefined || inst.topic_id === null || inst.topic_id === "")
|
|
71
|
+
return false;
|
|
72
|
+
const channels = wizardChannels(cfg);
|
|
73
|
+
const old = inst.channel_id != null ? channels.find(c => c.id === String(inst.channel_id)) : channels[0];
|
|
74
|
+
if (!old || old.group_id == null || newChannel.group_id == null)
|
|
75
|
+
return true;
|
|
76
|
+
return !(old.type === String(newChannel.type ?? "") && old.group_id === String(newChannel.group_id));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The topic an existing agent has after moving to `newChannel`: its own when the chat is the same; else, for the
|
|
80
|
+
* General agent, the new chat's General (Telegram's thread 1, the Discord connection's general channel); else none.
|
|
81
|
+
*/
|
|
82
|
+
export function movedTopic(cfg, instanceName, newChannel) {
|
|
83
|
+
if (!releasesTopic(cfg, instanceName, newChannel))
|
|
84
|
+
return null;
|
|
85
|
+
const inst = cfg.instances[instanceName];
|
|
86
|
+
const from = String(inst.topic_id);
|
|
87
|
+
if (inst.general_topic !== true)
|
|
88
|
+
return { from, to: null };
|
|
89
|
+
if (newChannel.type === "telegram")
|
|
90
|
+
return { from, to: 1 };
|
|
91
|
+
const general = newChannel.general_channel_id;
|
|
92
|
+
return { from, to: general != null && /^\d{17,}$/.test(String(general)) ? String(general) : null };
|
|
93
|
+
}
|
|
94
|
+
/** The plan as the wizard shows it: an existing agent's topic that the move changes is said so. */
|
|
95
|
+
export function withTopicRelease(plan, cfg, input) {
|
|
96
|
+
const moved = input.existing_agent && plan.instance ? movedTopic(cfg, input.instance_name, plan.channel) : null;
|
|
97
|
+
if (!moved || !plan.instance)
|
|
98
|
+
return plan;
|
|
99
|
+
return { ...plan, instance: { ...plan.instance, topic: { from: moved.from, to: moved.to == null ? null : String(moved.to) } } };
|
|
100
|
+
}
|
|
101
|
+
/** Why this input cannot be a new connection, or null: a taken id or token env (it would replace a connection). */
|
|
102
|
+
export function newConnectionConflict(input, env) {
|
|
103
|
+
if (input.channel_id && env.channels.some(channel => channel.id === input.channel_id)) {
|
|
104
|
+
return `a connection called "${input.channel_id}" already exists — a new connection never replaces one`;
|
|
105
|
+
}
|
|
106
|
+
return input.token_env ? newTokenEnvConflict(input.token_env, env.channels) : null;
|
|
107
|
+
}
|
|
44
108
|
export function planQuickstart(input, env) {
|
|
45
109
|
const warnings = [];
|
|
110
|
+
const channelId = input.channel_id || nextChannelId(input.platform, env.channels);
|
|
111
|
+
const tokenEnv = input.token_env || generateTokenEnvName(input.platform, channelId, env.taken_env ?? takenTokenEnvNames({ channelEnvs: env.channels.map(channel => channel.token_env) }));
|
|
46
112
|
const channel = {
|
|
113
|
+
id: channelId,
|
|
47
114
|
type: input.platform,
|
|
48
|
-
bot_token_env:
|
|
115
|
+
bot_token_env: tokenEnv,
|
|
49
116
|
mode: "topic",
|
|
50
117
|
access: { mode: "locked", allowed_users: input.admin_user_id ? [input.admin_user_id] : [] },
|
|
51
118
|
};
|
|
@@ -53,8 +120,11 @@ export function planQuickstart(input, env) {
|
|
|
53
120
|
if (input.platform === "discord") {
|
|
54
121
|
if (input.guild_id)
|
|
55
122
|
channel.group_id = input.guild_id;
|
|
123
|
+
// Write general_channel_id under options (the runtime reads it from there).
|
|
124
|
+
// Top-level general_channel_id was a bug introduced in c25045b0; old files
|
|
125
|
+
// that still have it at the top level are handled by discordGeneralChannelId().
|
|
56
126
|
if (input.general_channel_id)
|
|
57
|
-
channel.
|
|
127
|
+
channel.options = { ...(channel.options ?? {}), general_channel_id: input.general_channel_id };
|
|
58
128
|
}
|
|
59
129
|
else if (input.group_id) {
|
|
60
130
|
channel.group_id = input.group_id;
|
|
@@ -62,25 +132,17 @@ export function planQuickstart(input, env) {
|
|
|
62
132
|
if (!input.admin_user_id) {
|
|
63
133
|
warnings.push("No admin user id: access stays locked with an empty allow list, so nobody can drive the bot until you add one.");
|
|
64
134
|
}
|
|
65
|
-
|
|
66
|
-
if (clash) {
|
|
67
|
-
warnings.push(`${input.token_env} is already used by the "${clash.id}" connection — its token will be overwritten.`);
|
|
68
|
-
// Re-running the wizard rewrites that connection's access block, so anyone
|
|
69
|
-
// else on its allow list stops being able to drive the bot. That is a
|
|
70
|
-
// lockout, and it must not happen without the user reading it first.
|
|
71
|
-
const previous = clash.allowed_users ?? [];
|
|
72
|
-
const dropped = previous.filter(user => user !== input.admin_user_id);
|
|
73
|
-
if (dropped.length) {
|
|
74
|
-
warnings.push(`Its allow list is replaced: ${dropped.join(", ")} will no longer be able to use the bot.`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (!env.backends.includes(input.backend)) {
|
|
135
|
+
if (!input.connection_only && !input.existing_agent && !env.backends.includes(input.backend)) {
|
|
78
136
|
warnings.push(`${input.backend} was not found on this host; the agent will fail to start until it is installed.`);
|
|
79
137
|
}
|
|
80
138
|
return {
|
|
81
139
|
channel,
|
|
82
|
-
|
|
83
|
-
|
|
140
|
+
// The agent is the new connection's (#1529 review): bound to it, not to whichever connection is first.
|
|
141
|
+
instance: input.connection_only ? null : { name: input.instance_name, working_directory: input.working_directory, backend: input.backend, channel_id: channelId,
|
|
142
|
+
...(input.existing_agent ? { existing: true } : {}) },
|
|
143
|
+
channel_id: channelId,
|
|
144
|
+
token_env: tokenEnv,
|
|
145
|
+
env_keys: [tokenEnv],
|
|
84
146
|
warnings,
|
|
85
147
|
};
|
|
86
148
|
}
|
|
@@ -120,11 +182,19 @@ export async function runProviderProbe(request, opts = {}) {
|
|
|
120
182
|
const identity = request.platform === "discord"
|
|
121
183
|
? await verifyDiscordToken(request.token)
|
|
122
184
|
: await verifyTelegramToken(request.token);
|
|
185
|
+
// #1519 P3: a verified Discord bot comes with its invite (the one permission set) and its portal page.
|
|
186
|
+
if (request.platform === "discord" && identity.valid && identity.id) {
|
|
187
|
+
return { ok: true, identity, invite_url: discordInviteUrl(identity.id), portal_url: discordBotPortalUrl(identity.id) };
|
|
188
|
+
}
|
|
123
189
|
return { ok: true, identity };
|
|
124
190
|
}
|
|
125
191
|
if (request.action === "guilds") {
|
|
126
192
|
return { ok: true, guilds: await listDiscordGuilds(request.token) };
|
|
127
193
|
}
|
|
194
|
+
if (request.action === "channels") {
|
|
195
|
+
const listed = await listDiscordTextChannels(request.token, request.guild_id);
|
|
196
|
+
return listed.ok ? { ok: true, channels: listed.channels } : { ok: false, error: listed.error };
|
|
197
|
+
}
|
|
128
198
|
if (opts.isTokenInUse?.(request.token)) {
|
|
129
199
|
return {
|
|
130
200
|
ok: false,
|
|
@@ -159,22 +229,31 @@ export function detectWizardBackends() {
|
|
|
159
229
|
* asserts this module's import graph reaches neither fleet-manager, daemon nor
|
|
160
230
|
* instance-lifecycle, so the host cannot grow a path to them by accident.
|
|
161
231
|
*/
|
|
232
|
+
function withMovedTopic(inst, moved) {
|
|
233
|
+
if (!moved)
|
|
234
|
+
return inst;
|
|
235
|
+
const { topic_id: _old, ...rest } = inst;
|
|
236
|
+
return (moved.to == null ? rest : { ...rest, topic_id: moved.to });
|
|
237
|
+
}
|
|
162
238
|
/** One pure draft shared by the writer and the authoritative confirmation diff. */
|
|
163
239
|
export function draftQuickstart(cfg, body, plan) {
|
|
164
240
|
const draft = structuredClone(cfg), summary = wizardChannels(cfg);
|
|
165
241
|
const channels = draft.channels ?? (draft.channel ? [draft.channel] : []);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
else
|
|
171
|
-
channels.push(entry);
|
|
242
|
+
// Always a new connection (#1519 P1, S1): never matched to one by token env, platform or group. The commit refuses a
|
|
243
|
+
// taken id or env before it gets here (newConnectionConflict).
|
|
244
|
+
void summary;
|
|
245
|
+
channels.push({ ...plan.channel });
|
|
172
246
|
draft.channels = channels;
|
|
173
247
|
delete draft.channel;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
248
|
+
if (body.connection_only)
|
|
249
|
+
return draft;
|
|
250
|
+
const existing = Object.hasOwn(draft.instances, body.instance_name) ? draft.instances[body.instance_name] : undefined;
|
|
251
|
+
draft.instances = { ...draft.instances, [body.instance_name]: body.existing_agent && existing
|
|
252
|
+
// #1519 P7: an existing agent keeps everything it has; only its connection becomes the new one — and a topic of
|
|
253
|
+
// another chat is not carried into this one (#1549 review: releasesTopic).
|
|
254
|
+
? withMovedTopic({ ...existing, channel_id: plan.channel_id }, movedTopic(cfg, body.instance_name, plan.channel))
|
|
255
|
+
: { ...(existing ?? {}), working_directory: body.working_directory, backend: body.backend, channel_id: plan.channel_id },
|
|
256
|
+
};
|
|
178
257
|
return draft;
|
|
179
258
|
}
|
|
180
259
|
function json(res, status, data) {
|
|
@@ -197,17 +276,30 @@ export function wizardChannels(cfg) {
|
|
|
197
276
|
}));
|
|
198
277
|
}
|
|
199
278
|
/** Reject anything that would land in fleet.yaml or a shell env file unchecked. */
|
|
200
|
-
export function validateWizardInput(input) {
|
|
279
|
+
export function validateWizardInput(input, opts = {}) {
|
|
201
280
|
if (input.platform !== "telegram" && input.platform !== "discord")
|
|
202
281
|
return "platform must be telegram or discord";
|
|
203
|
-
if (
|
|
282
|
+
if (input.connection_only !== undefined && typeof input.connection_only !== "boolean")
|
|
283
|
+
return "connection_only must be a boolean";
|
|
284
|
+
if (input.existing_agent !== undefined && typeof input.existing_agent !== "boolean")
|
|
285
|
+
return "existing_agent must be a boolean";
|
|
286
|
+
if (input.token_env_generated !== undefined && typeof input.token_env_generated !== "boolean")
|
|
287
|
+
return "token_env_generated must be a boolean";
|
|
288
|
+
if (!(opts.tokenEnvOptional && input.token_env === undefined) && (!input.token_env || !TOKEN_ENV_PATTERN.test(input.token_env)))
|
|
204
289
|
return "token_env must be an UPPER_SNAKE env var name";
|
|
205
|
-
if (
|
|
206
|
-
return "
|
|
207
|
-
if (!input.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
290
|
+
if (input.channel_id !== undefined && (typeof input.channel_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(input.channel_id)))
|
|
291
|
+
return "channel_id must be [A-Za-z0-9_-], up to 32 characters";
|
|
292
|
+
if (!input.connection_only) {
|
|
293
|
+
if (!input.instance_name || !/^[A-Za-z0-9._-]{1,128}$/.test(input.instance_name))
|
|
294
|
+
return "instance_name must be [A-Za-z0-9._-]";
|
|
295
|
+
// An existing agent keeps its own directory and backend: neither is asked for.
|
|
296
|
+
if (!input.existing_agent) {
|
|
297
|
+
if (!input.backend || !KNOWN_BACKENDS.includes(input.backend))
|
|
298
|
+
return "backend must be a known backend";
|
|
299
|
+
if (!input.working_directory || !input.working_directory.startsWith("/"))
|
|
300
|
+
return "working_directory must be an absolute path";
|
|
301
|
+
}
|
|
302
|
+
}
|
|
211
303
|
for (const [field, value] of Object.entries({
|
|
212
304
|
group_id: input.group_id, guild_id: input.guild_id,
|
|
213
305
|
general_channel_id: input.general_channel_id, admin_user_id: input.admin_user_id,
|
|
@@ -229,10 +321,17 @@ export function handleQuickstartRequest(req, res, url, ctx) {
|
|
|
229
321
|
backends: detectWizardBackends(),
|
|
230
322
|
has_fleet: !!cfg && Object.keys(cfg.instances ?? {}).length > 0,
|
|
231
323
|
channels: wizardChannels(cfg),
|
|
324
|
+
agents: Object.entries(cfg?.instances ?? {}).map(([name, inst]) => ({
|
|
325
|
+
name, working_directory: inst.working_directory ?? null, backend: inst.backend ?? null
|
|
326
|
+
})),
|
|
232
327
|
});
|
|
233
328
|
return true;
|
|
234
329
|
}
|
|
235
330
|
if (method === "POST" && path === "/api/settings/quickstart/probe") {
|
|
331
|
+
if (gatewayRequestContext(req)) {
|
|
332
|
+
json(res, 403, { error: "Token verification is not permitted over a public link" });
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
236
335
|
readBody(req, 16 * 1024).then(async (buf) => {
|
|
237
336
|
if (!permitWebContinuation(req, res, ctx))
|
|
238
337
|
return;
|
|
@@ -247,14 +346,14 @@ export function handleQuickstartRequest(req, res, url, ctx) {
|
|
|
247
346
|
if (!token)
|
|
248
347
|
return json(res, 400, { error: "token required" });
|
|
249
348
|
const action = String(body.action ?? "");
|
|
250
|
-
if (action !== "verify" && action !== "guilds" && action !== "await-telegram-start") {
|
|
349
|
+
if (action !== "verify" && action !== "guilds" && action !== "channels" && action !== "await-telegram-start") {
|
|
251
350
|
return json(res, 400, { error: "unknown probe" });
|
|
252
351
|
}
|
|
253
352
|
const platform = body.platform === "discord" ? "discord" : "telegram";
|
|
254
353
|
// The token is read, used for one outbound call, and dropped. It is never
|
|
255
354
|
// logged and never echoed back in the response.
|
|
256
355
|
ctx.logger.info({ action, platform }, "settings: quickstart probe");
|
|
257
|
-
const result = await runProviderProbe({ action, platform, token, offset: typeof body.offset === "number" ? body.offset : 0 }, { isTokenInUse: candidate => ctx.isBotTokenInUse?.(candidate) ?? false });
|
|
356
|
+
const result = await runProviderProbe({ action, platform, token, offset: typeof body.offset === "number" ? body.offset : 0, guild_id: typeof body.guild_id === "string" ? body.guild_id : "" }, { isTokenInUse: candidate => ctx.isBotTokenInUse?.(candidate) ?? false });
|
|
258
357
|
json(res, result.ok ? 200 : 409, result);
|
|
259
358
|
}).catch(() => json(res, 400, { error: "bad request" }));
|
|
260
359
|
return true;
|
|
@@ -274,14 +373,30 @@ export function handleQuickstartRequest(req, res, url, ctx) {
|
|
|
274
373
|
catch {
|
|
275
374
|
return json(res, 400, { error: "invalid JSON" });
|
|
276
375
|
}
|
|
277
|
-
const invalid = validateWizardInput(body);
|
|
376
|
+
const invalid = validateWizardInput(body, { tokenEnvOptional: true });
|
|
278
377
|
if (invalid)
|
|
279
378
|
return json(res, 400, { error: invalid });
|
|
280
|
-
|
|
281
|
-
|
|
379
|
+
const channels = wizardChannels(cfg);
|
|
380
|
+
const conflict = newConnectionConflict(body, { channels }) ?? agentConflict(body, cfg);
|
|
381
|
+
if (conflict)
|
|
382
|
+
return json(res, 409, { error: conflict });
|
|
383
|
+
// A generated name avoids every name already held: connections', providers', this data dir's .env, this process's.
|
|
384
|
+
let envFile;
|
|
385
|
+
try {
|
|
386
|
+
envFile = envFileKeys(ctx.dataDir);
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
if (err instanceof EnvFileUnreadableError)
|
|
390
|
+
return json(res, 503, { error: err.message });
|
|
391
|
+
throw err;
|
|
392
|
+
}
|
|
393
|
+
const taken = takenTokenEnvNames({ channelEnvs: channels.map(c => c.token_env), envFile, processEnv: Object.keys(process.env) });
|
|
394
|
+
json(res, 200, withTopicRelease(planQuickstart(withExistingAgent(body, cfg), {
|
|
395
|
+
backends: body.connection_only ? [] : detectWizardBackends(),
|
|
282
396
|
has_fleet: Object.keys(cfg.instances ?? {}).length > 0,
|
|
283
|
-
channels
|
|
284
|
-
|
|
397
|
+
channels,
|
|
398
|
+
taken_env: taken,
|
|
399
|
+
}), cfg, body));
|
|
285
400
|
}).catch(() => json(res, 400, { error: "bad request" }));
|
|
286
401
|
return true;
|
|
287
402
|
}
|
|
@@ -306,7 +421,11 @@ export function handleQuickstartRequest(req, res, url, ctx) {
|
|
|
306
421
|
if (!body.token)
|
|
307
422
|
return json(res, 400, { error: "token required" });
|
|
308
423
|
const summary = wizardChannels(cfg);
|
|
309
|
-
|
|
424
|
+
// Never a replacement (#1519 P1, S1): an id or token env another connection holds is refused before anything moves.
|
|
425
|
+
const conflict = newConnectionConflict(body, { channels: summary }) ?? agentConflict(body, cfg);
|
|
426
|
+
if (conflict)
|
|
427
|
+
return json(res, 409, { error: conflict });
|
|
428
|
+
const plan = planQuickstart(withExistingAgent(body, cfg), {
|
|
310
429
|
backends: detectWizardBackends(),
|
|
311
430
|
has_fleet: Object.keys(cfg.instances ?? {}).length > 0,
|
|
312
431
|
channels: summary,
|
|
@@ -325,19 +444,35 @@ export function handleQuickstartRequest(req, res, url, ctx) {
|
|
|
325
444
|
if (!lease)
|
|
326
445
|
return json(res, 409, { error: "another secret operation is still running" });
|
|
327
446
|
let stored = false, secretBefore;
|
|
328
|
-
const
|
|
329
|
-
|
|
447
|
+
const agent = !body.connection_only;
|
|
448
|
+
const before = { channels: cfg.channels, channel: cfg.channel, instance: agent ? cfg.instances[body.instance_name] : undefined,
|
|
449
|
+
hadInstance: agent && Object.hasOwn(cfg.instances, body.instance_name) };
|
|
330
450
|
let store;
|
|
331
451
|
const rollback = {};
|
|
332
452
|
try {
|
|
333
453
|
execution?.assert();
|
|
454
|
+
// A generated name is re-checked here, under the .env lease, against what the write would overwrite (#1529 review).
|
|
455
|
+
if (body.token_env_generated) {
|
|
456
|
+
let stale;
|
|
457
|
+
try {
|
|
458
|
+
stale = generatedTokenEnvStale(body.token_env, { dataDir: ctx.dataDir, channelEnvs: summary.map(c => c.token_env) });
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
if (err instanceof EnvFileUnreadableError)
|
|
462
|
+
return json(res, 503, { error: err.message });
|
|
463
|
+
throw err;
|
|
464
|
+
}
|
|
465
|
+
if (stale)
|
|
466
|
+
return json(res, 409, { error: stale }); // nothing written yet; the lease goes in finally
|
|
467
|
+
}
|
|
334
468
|
store = new SecretStore(envPath, new Set([body.token_env]), { owner: lease.owner });
|
|
335
469
|
settingsWrite(req, () => {
|
|
336
470
|
secretBefore = store.write(body.token_env, body.token);
|
|
337
471
|
stored = true;
|
|
338
472
|
cfg.channels = draft.channels;
|
|
339
473
|
delete cfg.channel;
|
|
340
|
-
|
|
474
|
+
if (agent)
|
|
475
|
+
Object.defineProperty(cfg.instances, body.instance_name, { value: draft.instances[body.instance_name], enumerable: true, writable: true, configurable: true });
|
|
341
476
|
const undo = ctx.saveFleetConfig();
|
|
342
477
|
if (typeof undo === "function")
|
|
343
478
|
rollback.config = undo;
|
|
@@ -352,7 +487,7 @@ export function handleQuickstartRequest(req, res, url, ctx) {
|
|
|
352
487
|
delete cfg.channel;
|
|
353
488
|
if (before.hadInstance)
|
|
354
489
|
cfg.instances[body.instance_name] = before.instance;
|
|
355
|
-
else
|
|
490
|
+
else if (agent)
|
|
356
491
|
delete cfg.instances[body.instance_name];
|
|
357
492
|
let cleanupFailed = false;
|
|
358
493
|
try {
|
|
@@ -376,7 +511,7 @@ export function handleQuickstartRequest(req, res, url, ctx) {
|
|
|
376
511
|
finally {
|
|
377
512
|
lease.release();
|
|
378
513
|
}
|
|
379
|
-
ctx.logger.info({ instance: body.instance_name, platform: body.platform }, "settings: quickstart committed");
|
|
514
|
+
ctx.logger.info({ instance: agent ? body.instance_name : null, connection: plan.channel_id, platform: body.platform }, "settings: quickstart committed");
|
|
380
515
|
// No apply here: the page starts the ordinary job so the wizard's last
|
|
381
516
|
// step has the same progress, deadline and restart recovery as everything
|
|
382
517
|
// else the panel applies.
|