@songsid/agend 2.3.0-beta.20 → 2.3.0-beta.22

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.
Files changed (101) hide show
  1. package/dist/agent-endpoint.d.ts +8 -0
  2. package/dist/agent-endpoint.js +11 -0
  3. package/dist/agent-endpoint.js.map +1 -1
  4. package/dist/auth-api.js +2 -1
  5. package/dist/auth-api.js.map +1 -1
  6. package/dist/backend/grok.js +56 -3
  7. package/dist/backend/grok.js.map +1 -1
  8. package/dist/channel/mcp-tools.js +2 -2
  9. package/dist/channel/mcp-tools.js.map +1 -1
  10. package/dist/config-validator.js +8 -1
  11. package/dist/config-validator.js.map +1 -1
  12. package/dist/config.d.ts +2 -2
  13. package/dist/config.js +14 -2
  14. package/dist/config.js.map +1 -1
  15. package/dist/daemon.d.ts +6 -0
  16. package/dist/daemon.js +14 -0
  17. package/dist/daemon.js.map +1 -1
  18. package/dist/fleet-manager.d.ts +28 -2
  19. package/dist/fleet-manager.js +102 -15
  20. package/dist/fleet-manager.js.map +1 -1
  21. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  22. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -1
  23. package/dist/instance-config-impact.js +3 -0
  24. package/dist/instance-config-impact.js.map +1 -1
  25. package/dist/instance-lifecycle.js +2 -2
  26. package/dist/instance-lifecycle.js.map +1 -1
  27. package/dist/instructions.js +2 -1
  28. package/dist/instructions.js.map +1 -1
  29. package/dist/locale.js +19 -6
  30. package/dist/locale.js.map +1 -1
  31. package/dist/outbound-handlers.js +25 -10
  32. package/dist/outbound-handlers.js.map +1 -1
  33. package/dist/provider-secret-registry.d.ts +5 -3
  34. package/dist/provider-secret-registry.js +8 -2
  35. package/dist/provider-secret-registry.js.map +1 -1
  36. package/dist/public-tunnel-config.d.ts +6 -0
  37. package/dist/public-tunnel-config.js +24 -0
  38. package/dist/public-tunnel-config.js.map +1 -0
  39. package/dist/public-web-gateway.d.ts +2 -0
  40. package/dist/public-web-gateway.js +3 -2
  41. package/dist/public-web-gateway.js.map +1 -1
  42. package/dist/public-web-link.d.ts +13 -2
  43. package/dist/public-web-link.js +35 -9
  44. package/dist/public-web-link.js.map +1 -1
  45. package/dist/secret-store.d.ts +2 -0
  46. package/dist/secret-store.js +21 -0
  47. package/dist/secret-store.js.map +1 -1
  48. package/dist/settings-api.d.ts +1 -0
  49. package/dist/settings-api.js +13 -9
  50. package/dist/settings-api.js.map +1 -1
  51. package/dist/settings-change.js +2 -2
  52. package/dist/settings-change.js.map +1 -1
  53. package/dist/settings-effect.js +4 -1
  54. package/dist/settings-effect.js.map +1 -1
  55. package/dist/settings-http-confirmation.js +1 -1
  56. package/dist/settings-http-confirmation.js.map +1 -1
  57. package/dist/tunnel/cloudflared.d.ts +6 -15
  58. package/dist/tunnel/cloudflared.js +20 -9
  59. package/dist/tunnel/cloudflared.js.map +1 -1
  60. package/dist/types.d.ts +3 -0
  61. package/dist/ui/app.html +2 -0
  62. package/dist/ui/chat-render.js +39 -0
  63. package/dist/ui/chat-store.js +8 -0
  64. package/dist/ui/chat-strings.js +2 -2
  65. package/dist/ui/chat-thread.js +1 -1
  66. package/dist/ui/fleet-canvas.js +14 -6
  67. package/dist/ui/fleet-route-scene.js +86 -21
  68. package/dist/ui/fleet-traffic.js +57 -10
  69. package/dist/ui/icons/icon-192.png +0 -0
  70. package/dist/ui/icons/icon-512.png +0 -0
  71. package/dist/ui/panel-settings.js +57 -4
  72. package/dist/ui/settings-appearance.js +126 -1
  73. package/dist/ui/settings-learn.js +1 -0
  74. package/dist/ui/settings-strings.js +78 -0
  75. package/dist/ui/shared/app-stream.js +2 -1
  76. package/dist/ui/shared/app.css +48 -21
  77. package/dist/ui/signin.html +2 -0
  78. package/dist/web-api.d.ts +6 -2
  79. package/dist/web-api.js +114 -4
  80. package/dist/web-api.js.map +1 -1
  81. package/dist/web-app-icon-worker.d.ts +1 -0
  82. package/dist/web-app-icon-worker.js +15 -0
  83. package/dist/web-app-icon-worker.js.map +1 -0
  84. package/dist/web-app-icon.d.ts +107 -0
  85. package/dist/web-app-icon.js +256 -0
  86. package/dist/web-app-icon.js.map +1 -0
  87. package/dist/web-chat-history.d.ts +23 -0
  88. package/dist/web-chat-history.js +32 -2
  89. package/dist/web-chat-history.js.map +1 -1
  90. package/dist/web-guest-access.js +7 -1
  91. package/dist/web-guest-access.js.map +1 -1
  92. package/dist/web-icons.d.ts +33 -1
  93. package/dist/web-icons.js +83 -11
  94. package/dist/web-icons.js.map +1 -1
  95. package/dist/web-png.d.ts +29 -0
  96. package/dist/web-png.js +321 -0
  97. package/dist/web-png.js.map +1 -0
  98. package/dist/web-reactions.d.ts +34 -0
  99. package/dist/web-reactions.js +47 -0
  100. package/dist/web-reactions.js.map +1 -0
  101. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { type HumanOrigin } from "./human-delivery-receipts.js";
2
2
  import { SettingsExecution } from "./settings-transaction.js";
3
+ import { AppIconStore } from "./web-app-icon.js";
3
4
  import type { LoginCodeOwner } from "./web-login.js";
4
5
  import { type ProfileTicket } from "./runtime-cpu-profile.js";
5
6
  import type { CpuProfile } from "./cpu-profile.js";
@@ -57,6 +58,7 @@ export declare function applyTaskListCap<T extends {
57
58
  import { type PublicPreviewForUi } from "./web-api.js";
58
59
  import { type LogAnswer, type LogQuery } from "./log-service.js";
59
60
  import { WebChatHistory } from "./web-chat-history.js";
61
+ import { type WebEchoCopy } from "./web-reactions.js";
60
62
  import { SeenUsers, type MentionNames, type MentionQuery } from "./web-mentions.js";
61
63
  import { WebPrefsStore } from "./web-prefs.js";
62
64
  import { OwnerCssStore } from "./web-owner-css-store.js";
@@ -611,6 +613,9 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
611
613
  tagline?: string;
612
614
  bio?: string;
613
615
  };
616
+ private appIconStore?;
617
+ /** #2308: the person's own app icon (Settings → Web appearance), under the state dir; none set: the bundled one. */
618
+ get appIcon(): AppIconStore;
614
619
  private ownerCssStore?;
615
620
  /** #1629 r4 §3: each person's own CSS; which version is in force is their `css.active` in the web preferences. */
616
621
  get ownerCss(): OwnerCssStore;
@@ -1120,8 +1125,28 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1120
1125
  * #1920 §5.2: a web message's channel copy was sent — it is now what the channel saw. Kept so (and on disk), and sent
1121
1126
  * to the guests' streams only: an admin's page already has it, and nothing about it changed for them.
1122
1127
  */
1123
- webMessageInChannel(instance: string, messageId: string): void;
1124
- sendClassicWebEcho(instance: string, text: string, sender?: string): Promise<number>;
1128
+ webMessageInChannel(instance: string, messageId: string, copies?: readonly WebEchoCopy[]): void;
1129
+ /** #2305: a web message's channel copies (topic, ClassicBot rooms), newest WEB_ECHO_COPIES_MAX, this process only. */
1130
+ private webEchoCopies;
1131
+ private rememberWebEchoCopies;
1132
+ /**
1133
+ * #2305: an instance's reaction to a web chat message (`messageId`, the id it was handed): its minimal reply, as on
1134
+ * the platforms. Kept on the message's row (and on disk), shown live (`reactions` on the stream), and put on the
1135
+ * message's channel copies when this process sent them — in the background, each within WEB_MIRROR_TIMEOUT_MS, so a
1136
+ * slow or rate-limited platform never holds the tool's answer (best effort: a copy's failure or timeout is logged,
1137
+ * never the tool's). `mirrored`: how many copies took it, when that is known.
1138
+ * The tool's errors: an emoji outside the web set or one of the instance's delivery marks (web-reactions.ts), or a
1139
+ * message the chat does not have. An emoji the message already has is a success that changes nothing.
1140
+ */
1141
+ reactToWebMessage(instance: string, messageId: string, raw: unknown): Promise<{
1142
+ ok: true;
1143
+ mirrored: Promise<number>;
1144
+ } | {
1145
+ ok: false;
1146
+ error: string;
1147
+ }>;
1148
+ /** #2305: `copies` collects each room's copy (its message id), for a reaction to the web message. */
1149
+ sendClassicWebEcho(instance: string, text: string, sender?: string, copies?: WebEchoCopy[]): Promise<number>;
1125
1150
  /**
1126
1151
  * #1666: a delivered web chat message starts the same cancel/progress lifecycle in the instance's topic as a platform
1127
1152
  * inbound (afterUserInboundDelivered): the same bubble, ticker and retire rules. Not its ✅ tracking — the web message
@@ -3753,6 +3778,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
3753
3778
  verifyProviderSecret(input: {
3754
3779
  specId: string;
3755
3780
  secret: string;
3781
+ clear?: boolean;
3756
3782
  sessionBinding: string;
3757
3783
  idempotencyKey: string;
3758
3784
  }): Promise<{
@@ -1,3 +1,4 @@
1
+ import { PUBLIC_TUNNEL_TOKEN_ENV, validPublicTunnelToken } from "./public-tunnel-config.js";
1
2
  import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
2
3
  import { SettingsBaselines } from "./settings-baseline.js";
3
4
  import { SettingsConfirmationStore } from "./settings-confirmation.js";
@@ -8,7 +9,8 @@ import { performance } from "node:perf_hooks";
8
9
  import { withSpawnHoldReply } from "./spawn-hold-reply.js";
9
10
  import { gatewayRequestContext } from "./web-request-context.js";
10
11
  import { createPublicWebGateway, PUBLIC_GATEWAY_RECEIVE_MS } from "./public-web-gateway.js";
11
- import { handleWebIconRequest } from "./web-icons.js";
12
+ import { handleWebIconRequest, useAppIconStore } from "./web-icons.js";
13
+ import { AppIconStore } from "./web-app-icon.js";
12
14
  import { trackWebServer } from "./web-server-drain.js";
13
15
  import { renderPublicLinkProgress, ThrottledMessageEditor } from "./public-link-progress.js";
14
16
  import { PublicWebLink, publicLinkSettings } from "./public-web-link.js";
@@ -143,6 +145,7 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
143
145
  import { handleWebRequest, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
144
146
  import { LogService } from "./log-service.js";
145
147
  import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
148
+ import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
146
149
  import { WebChatDiskStore } from "./web-chat-store.js";
147
150
  import { SeenUsers, resolveMentions } from "./web-mentions.js";
148
151
  import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
@@ -250,7 +253,7 @@ import { checkSelfRestartAllowance, recordSelfRestartAttempt } from "./self-rest
250
253
  import { SecretStore } from "./secret-store.js";
251
254
  import { opaqueId, safeSecretError, SECRET_CHALLENGE_TTL_MS, } from "./connection-secrets.js";
252
255
  import { verifyDiscordToken, verifyTelegramToken } from "./provider-probe.js";
253
- import { PROVIDER_SECRET_SPECS, providerSecretSpec, providerRegistryEnvKeys, isReservedProviderEnvKey, verifyProviderSecret, } from "./provider-secret-registry.js";
256
+ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, providerRegistryEnvKeys, isReservedProviderEnvKey, verifyProviderSecret, } from "./provider-secret-registry.js";
254
257
  /** A self-restart is a whole service restart; 120s is the apply budget, not this. */
255
258
  const SELF_RESTART_DEADLINE_MS = 300_000;
256
259
  import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
@@ -1174,6 +1177,11 @@ export class FleetManager {
1174
1177
  const card = resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card);
1175
1178
  return { ...(card.tagline ? { tagline: card.tagline.text } : {}), ...(card.bio ? { bio: card.bio.text } : {}) };
1176
1179
  }
1180
+ appIconStore;
1181
+ /** #2308: the person's own app icon (Settings → Web appearance), under the state dir; none set: the bundled one. */
1182
+ get appIcon() {
1183
+ return (this.appIconStore ??= new AppIconStore(this.dataDir));
1184
+ }
1177
1185
  ownerCssStore;
1178
1186
  /** #1629 r4 §3: each person's own CSS; which version is in force is their `css.active` in the web preferences. */
1179
1187
  get ownerCss() {
@@ -3792,12 +3800,61 @@ export class FleetManager {
3792
3800
  * #1920 §5.2: a web message's channel copy was sent — it is now what the channel saw. Kept so (and on disk), and sent
3793
3801
  * to the guests' streams only: an admin's page already has it, and nothing about it changed for them.
3794
3802
  */
3795
- webMessageInChannel(instance, messageId) {
3803
+ webMessageInChannel(instance, messageId, copies = []) {
3804
+ // #2305: the copies' own message ids, so an instance's reaction to the web message is put on them too.
3805
+ if (copies.length)
3806
+ this.rememberWebEchoCopies(messageId, copies);
3796
3807
  const m = this.webChatHistory.markChannel(instance, messageId);
3797
3808
  if (m)
3798
3809
  broadcastToGuests(this.sseClients, "message", m, (err) => this.logger.debug({ err }, "SSE client write failed; evicting"));
3799
3810
  }
3800
- async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK) {
3811
+ /** #2305: a web message's channel copies (topic, ClassicBot rooms), newest WEB_ECHO_COPIES_MAX, this process only. */
3812
+ webEchoCopies = new Map();
3813
+ rememberWebEchoCopies(messageId, copies) {
3814
+ const list = [...(this.webEchoCopies.get(messageId) ?? []), ...copies.filter(c => c && typeof c.adapter?.react === "function" && c.messageId)];
3815
+ this.webEchoCopies.delete(messageId);
3816
+ this.webEchoCopies.set(messageId, list);
3817
+ while (this.webEchoCopies.size > WEB_ECHO_COPIES_MAX)
3818
+ this.webEchoCopies.delete(this.webEchoCopies.keys().next().value);
3819
+ }
3820
+ /**
3821
+ * #2305: an instance's reaction to a web chat message (`messageId`, the id it was handed): its minimal reply, as on
3822
+ * the platforms. Kept on the message's row (and on disk), shown live (`reactions` on the stream), and put on the
3823
+ * message's channel copies when this process sent them — in the background, each within WEB_MIRROR_TIMEOUT_MS, so a
3824
+ * slow or rate-limited platform never holds the tool's answer (best effort: a copy's failure or timeout is logged,
3825
+ * never the tool's). `mirrored`: how many copies took it, when that is known.
3826
+ * The tool's errors: an emoji outside the web set or one of the instance's delivery marks (web-reactions.ts), or a
3827
+ * message the chat does not have. An emoji the message already has is a success that changes nothing.
3828
+ */
3829
+ async reactToWebMessage(instance, messageId, raw) {
3830
+ const problem = webReactionProblem(raw, this.statusEmojiAvoidList(instance));
3831
+ if (problem)
3832
+ return { ok: false, error: problem };
3833
+ const emoji = webReactionEmoji(raw);
3834
+ const r = this.webChatHistory.addReaction(instance, messageId, emoji);
3835
+ if (!r)
3836
+ return { ok: false, error: `react: ${messageId} is not a message in ${instance}'s web chat now (it left the history, or it is from before AgEnD restarted) — answer with reply instead` };
3837
+ if (!r.added)
3838
+ return { ok: true, mirrored: Promise.resolve(0) };
3839
+ this.emitSseEvent("reactions", { instance, boot: r.message.boot, id: r.message.id, reactions: r.message.reactions ?? [] });
3840
+ const mirrorOne = (copy) => {
3841
+ let timer;
3842
+ const timeout = new Promise((_, reject) => {
3843
+ timer = setTimeout(() => reject(new Error(`no answer in ${WEB_MIRROR_TIMEOUT_MS} ms`)), WEB_MIRROR_TIMEOUT_MS);
3844
+ timer.unref?.();
3845
+ });
3846
+ return Promise.race([Promise.resolve().then(() => copy.adapter.react(copy.chatId, copy.messageId, reactionForm(copy.adapter.type, emoji), copy.threadId)), timeout])
3847
+ .then(() => 1, (err) => {
3848
+ this.logger.warn({ err, instance, adapter: copy.adapter.type }, "Reaction on a web message's channel copy failed (shown on the web)");
3849
+ return 0;
3850
+ })
3851
+ .finally(() => clearTimeout(timer));
3852
+ };
3853
+ const mirrored = Promise.all((this.webEchoCopies.get(messageId) ?? []).map(mirrorOne)).then(n => n.reduce((a, b) => a + b, 0));
3854
+ return { ok: true, mirrored };
3855
+ }
3856
+ /** #2305: `copies` collects each room's copy (its message id), for a reaction to the web message. */
3857
+ async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK, copies) {
3801
3858
  const guard = this.webChannelEchoGuards.get(instance);
3802
3859
  const handled = new Set();
3803
3860
  let posted = 0;
@@ -3827,7 +3884,9 @@ export class FleetManager {
3827
3884
  try {
3828
3885
  if (!adapter?.sendText)
3829
3886
  continue;
3830
- await adapter.sendText(entry.channelId, text, { format: "text", allowedMentions: { parse: [] } });
3887
+ const sent = await adapter.sendText(entry.channelId, text, { format: "text", allowedMentions: { parse: [] } });
3888
+ if (copies && sent && typeof sent.messageId === "string" && sent.messageId)
3889
+ copies.push({ adapter, chatId: entry.channelId, messageId: sent.messageId });
3831
3890
  ClassicChannelManager.logMessage(instance, sender, text, new Date());
3832
3891
  posted++;
3833
3892
  }
@@ -5866,7 +5925,7 @@ export class FleetManager {
5866
5925
  dataDir: this.dataDir, web: () => this.fleetConfig?.web,
5867
5926
  permitted: owner => this.publicOwnerCurrent(owner),
5868
5927
  reserve: id => this.getTunnelLane().reserve("dashboard", id),
5869
- createGateway: (exposureId, current, open, failed) => createPublicWebGateway({ exposureId, isCurrent: current, isOpen: open, onError: failed,
5928
+ createGateway: (exposureId, current, open, failed, named) => createPublicWebGateway({ ...named, exposureId, isCurrent: current, isOpen: open, onError: failed,
5870
5929
  dispatch: (req, res) => this.dispatchWebHttp(req, res, this.fleetConfig?.health_port ?? 19280) }),
5871
5930
  revoke: id => {
5872
5931
  this.webLoginCodes?.revokeAudience(id);
@@ -9649,6 +9708,17 @@ export class FleetManager {
9649
9708
  });
9650
9709
  return;
9651
9710
  }
9711
+ // #2305: a reaction to a web chat message is the web chat's own (any fleet, web-only included): never an adapter's.
9712
+ if (tool === "react" && typeof args.message_id === "string" && isWebMessageId(args.message_id)) {
9713
+ // The socket's own instance, never a session name the caller wrote itself (as checkToolPermission: it was not
9714
+ // identified) — a channel tool's call from a daemon carries none anyway (#2310 review).
9715
+ const r = await this.reactToWebMessage(instanceName, args.message_id, args.emoji);
9716
+ if (r.ok)
9717
+ respond("ok");
9718
+ else
9719
+ respond(null, r.error);
9720
+ return;
9721
+ }
9652
9722
  // A fleet with no chat platform at all is driven from the web dashboard alone: a reply has nowhere
9653
9723
  // else to go, and "retry shortly" would have the agent retry forever. It goes to the web chat.
9654
9724
  const webOnlyReply = tool === "reply" && this.worlds.size === 0 && this.isWebOnlyFleet();
@@ -19285,6 +19355,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19285
19355
  await this.retirePendingNoncePrompts();
19286
19356
  this.topicArchiver.stop();
19287
19357
  this.taskBoardWaker?.stop();
19358
+ this.appIconStore?.close();
19288
19359
  this.scheduler?.shutdown();
19289
19360
  // What it was sent is written first (bounded: search-index.ts close()).
19290
19361
  const search = this.searchIndex;
@@ -20006,12 +20077,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20006
20077
  return this.fleetConfig?.web?.provider_secrets === true;
20007
20078
  }
20008
20079
  listProviderSecrets() {
20009
- return PROVIDER_SECRET_SPECS.map(spec => ({
20080
+ return [...PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC].map(spec => ({
20010
20081
  id: spec.id,
20011
20082
  display_name: spec.displayName,
20012
20083
  kind: spec.kind,
20013
20084
  token_present: !!process.env[spec.envKey],
20014
- verifier: spec.verifier ? "available" : "unsupported",
20085
+ verifier: spec.kind === "tunnel_token" ? "local_format" : spec.verifier ? "available" : "unsupported",
20015
20086
  activation: spec.activation,
20016
20087
  stale_consumers: this.providerSecretStaleConsumers(spec.envKey),
20017
20088
  }));
@@ -20021,7 +20092,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20021
20092
  // a child process's private environment safely, so report the conservative
20022
20093
  // set of already-running children; the UI can then say "restart these"
20023
20094
  // rather than claiming an existing process reloaded.
20024
- if (envKey === "GROQ_API_KEY")
20095
+ if (envKey === "GROQ_API_KEY" || envKey === PUBLIC_TUNNEL_TOKEN_ENV)
20025
20096
  return [];
20026
20097
  return [...this.children.keys()].sort();
20027
20098
  }
@@ -20039,9 +20110,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20039
20110
  if (!spec || !this.providerSecretEnvAllowed(spec.envKey)) {
20040
20111
  return { ok: false, status: "invalid", error: "provider secret is not configured" };
20041
20112
  }
20042
- if (!spec.verifier)
20113
+ const tunnel = spec.kind === "tunnel_token";
20114
+ if (!tunnel && !spec.verifier)
20043
20115
  return { ok: false, status: "unsupported_verifier", error: "this provider has no supported verifier" };
20044
- if (!input.secret || input.secret.length > 4096 || /[\r\n\0]/.test(input.secret) || /[^\x20-\x7e]/.test(input.secret)) {
20116
+ if (input.clear && (!tunnel || input.secret !== "") || tunnel && !(input.clear === true && input.secret === "" || validPublicTunnelToken(input.secret))
20117
+ || !input.clear && (!input.secret || input.secret.length > 4096 || /[\r\n\0]/.test(input.secret) || /[^\x20-\x7e]/.test(input.secret))) {
20045
20118
  return { ok: false, status: "invalid", error: "secret is invalid" };
20046
20119
  }
20047
20120
  const challengeKey = `${input.sessionBinding}:api_key:${spec.id}:${spec.envKey}:${input.idempotencyKey}`;
@@ -20052,7 +20125,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20052
20125
  }
20053
20126
  if (existingId)
20054
20127
  this.providerSecretChallengesByKey.delete(challengeKey);
20055
- const result = await verifyProviderSecret(spec, input.secret, this.providerSecretHttpClient);
20128
+ const result = tunnel ? { ok: true } : await verifyProviderSecret(spec, input.secret, this.providerSecretHttpClient);
20056
20129
  if (!result.ok) {
20057
20130
  // Do not log provider detail: the HTTP verifier already redacted it and
20058
20131
  // this endpoint has no need to disclose whether a key was close to valid.
@@ -20140,6 +20213,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20140
20213
  async runProviderSecretApply(job, secret, execution) {
20141
20214
  const spec = providerSecretSpec(job.specId);
20142
20215
  const allowed = new Set([
20216
+ PUBLIC_TUNNEL_TOKEN_ENV,
20143
20217
  ...PROVIDER_SECRET_SPECS.map(item => item.envKey),
20144
20218
  ...(this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).map(channel => channel.bot_token_env),
20145
20219
  ]);
@@ -20153,7 +20227,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20153
20227
  // Construct inside the transaction: symlink/permission refusal must
20154
20228
  // settle the job as a safe failure, not escape the queued microtask.
20155
20229
  store = new SecretStore(join(this.dataDir, ".env"), allowed, { owner: execution?.owner });
20156
- const write = () => { before = store.write(job.envKey, secret); wrote = true; process.env[job.envKey] = secret; };
20230
+ const write = () => {
20231
+ before = spec.kind === "tunnel_token" && !secret ? store.clear(job.envKey) : store.write(job.envKey, secret);
20232
+ wrote = true;
20233
+ if (secret)
20234
+ process.env[job.envKey] = secret;
20235
+ else
20236
+ delete process.env[job.envKey];
20237
+ };
20157
20238
  if (execution)
20158
20239
  execution.mutate(write);
20159
20240
  else
@@ -20175,12 +20256,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20175
20256
  job.finishedAt = Date.now();
20176
20257
  }
20177
20258
  catch (err) {
20178
- const safe = safeSecretError(err, secret);
20259
+ const safe = spec?.kind === "tunnel_token" ? "connector token change failed" : safeSecretError(err, secret);
20179
20260
  this.logger.warn({ specId: job.specId, reason: safe }, "Provider API-key apply failed");
20180
20261
  try {
20181
20262
  // Memory and disk have separate receipts: a host-edited file must
20182
20263
  // remain, but cannot strand our owned process value or hook snapshot.
20183
- if (wrote && process.env[job.envKey] === secret) {
20264
+ if (wrote && (process.env[job.envKey] ?? "") === secret) {
20184
20265
  if (previousProcessValue === undefined)
20185
20266
  delete process.env[job.envKey];
20186
20267
  else
@@ -20220,6 +20301,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20220
20301
  * hot activation contract explicit and gives tests a failure seam; no generic
20221
20302
  * SIGHUP or caller-provided hook is ever executed. */
20222
20303
  async runProviderSecretReloadHook(hookId, _next, _previous) {
20304
+ if (hookId === "cloudflare.public_link") {
20305
+ await this.publicWebLink?.close("connector token changed");
20306
+ return;
20307
+ }
20223
20308
  if (hookId !== "groq.voice")
20224
20309
  throw new Error("unknown provider secret reload hook");
20225
20310
  const before = this.providerSecretHotSnapshots.get(hookId);
@@ -21498,6 +21583,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21498
21583
  return;
21499
21584
  // Branding is public, after Host admission: automatic favicon requests
21500
21585
  // must not become noisy web-token 401s in the browser console.
21586
+ // #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
21587
+ useAppIconStore(this.appIcon);
21501
21588
  if (handleWebIconRequest(req, res, requestPath))
21502
21589
  return;
21503
21590
  // Public: the health probe, /agent (instance-token auth of its own), the