@songsid/agend 2.2.0-beta.3 → 2.2.0-beta.5

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 (139) hide show
  1. package/dist/agent-cli.js +6 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/backend/claude-code.d.ts +46 -2
  4. package/dist/backend/claude-code.js +125 -31
  5. package/dist/backend/claude-code.js.map +1 -1
  6. package/dist/backend/credential-profile.d.ts +33 -3
  7. package/dist/backend/credential-profile.js +107 -5
  8. package/dist/backend/credential-profile.js.map +1 -1
  9. package/dist/backend/types.d.ts +1 -1
  10. package/dist/cache-service.d.ts +2 -0
  11. package/dist/cache-service.js +2 -1
  12. package/dist/cache-service.js.map +1 -1
  13. package/dist/channel/adapters/discord.d.ts +13 -0
  14. package/dist/channel/adapters/discord.js +42 -0
  15. package/dist/channel/adapters/discord.js.map +1 -1
  16. package/dist/channel/adapters/telegram.d.ts +6 -1
  17. package/dist/channel/adapters/telegram.js +27 -2
  18. package/dist/channel/adapters/telegram.js.map +1 -1
  19. package/dist/channel/types.d.ts +22 -1
  20. package/dist/cli.js +35 -0
  21. package/dist/cli.js.map +1 -1
  22. package/dist/config-validator.js +7 -0
  23. package/dist/config-validator.js.map +1 -1
  24. package/dist/daemon.d.ts +96 -0
  25. package/dist/daemon.js +339 -16
  26. package/dist/daemon.js.map +1 -1
  27. package/dist/delivery-outbox.d.ts +18 -5
  28. package/dist/delivery-outbox.js +121 -14
  29. package/dist/delivery-outbox.js.map +1 -1
  30. package/dist/fleet-context.d.ts +1 -0
  31. package/dist/fleet-manager.d.ts +89 -3
  32. package/dist/fleet-manager.js +541 -86
  33. package/dist/fleet-manager.js.map +1 -1
  34. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +43 -10
  35. package/dist/host-memory.d.ts +10 -0
  36. package/dist/host-memory.js +33 -1
  37. package/dist/host-memory.js.map +1 -1
  38. package/dist/instance-lifecycle.d.ts +25 -6
  39. package/dist/instance-lifecycle.js +73 -26
  40. package/dist/instance-lifecycle.js.map +1 -1
  41. package/dist/locale.js +48 -18
  42. package/dist/locale.js.map +1 -1
  43. package/dist/login-controller.d.ts +20 -3
  44. package/dist/login-controller.js +71 -19
  45. package/dist/login-controller.js.map +1 -1
  46. package/dist/login-flows.d.ts +5 -2
  47. package/dist/login-flows.js +5 -4
  48. package/dist/login-flows.js.map +1 -1
  49. package/dist/memory-pressure.d.ts +13 -1
  50. package/dist/memory-pressure.js +50 -10
  51. package/dist/memory-pressure.js.map +1 -1
  52. package/dist/outbound-handlers.d.ts +61 -0
  53. package/dist/outbound-handlers.js +100 -35
  54. package/dist/outbound-handlers.js.map +1 -1
  55. package/dist/outbound-schemas.d.ts +1 -0
  56. package/dist/outbound-schemas.js +2 -0
  57. package/dist/outbound-schemas.js.map +1 -1
  58. package/dist/public-preview.d.ts +100 -0
  59. package/dist/public-preview.js +233 -0
  60. package/dist/public-preview.js.map +1 -0
  61. package/dist/public-web-gateway.js +6 -4
  62. package/dist/public-web-gateway.js.map +1 -1
  63. package/dist/public-web-link.d.ts +20 -0
  64. package/dist/public-web-link.js +26 -1
  65. package/dist/public-web-link.js.map +1 -1
  66. package/dist/restart-progress.d.ts +29 -0
  67. package/dist/restart-progress.js +23 -0
  68. package/dist/restart-progress.js.map +1 -1
  69. package/dist/settings-api.js +10 -4
  70. package/dist/settings-api.js.map +1 -1
  71. package/dist/spawn-gate.js +5 -1
  72. package/dist/spawn-gate.js.map +1 -1
  73. package/dist/spawn-hold-reply.d.ts +12 -0
  74. package/dist/spawn-hold-reply.js +39 -0
  75. package/dist/spawn-hold-reply.js.map +1 -0
  76. package/dist/topic-commands.js +9 -2
  77. package/dist/topic-commands.js.map +1 -1
  78. package/dist/tunnel/purpose-lane.d.ts +5 -1
  79. package/dist/tunnel/purpose-lane.js +14 -7
  80. package/dist/tunnel/purpose-lane.js.map +1 -1
  81. package/dist/turn-resume.d.ts +2 -0
  82. package/dist/turn-resume.js +1 -1
  83. package/dist/turn-resume.js.map +1 -1
  84. package/dist/types.d.ts +8 -0
  85. package/dist/ui/chat-mentions.js +63 -0
  86. package/dist/ui/chat-render.js +25 -0
  87. package/dist/ui/chat-strings.js +14 -2
  88. package/dist/ui/chat-thread.js +28 -10
  89. package/dist/ui/panel-chat.js +26 -11
  90. package/dist/ui/panel-details.js +2 -0
  91. package/dist/ui/panel-fleet.js +199 -57
  92. package/dist/ui/panel-needs.js +4 -3
  93. package/dist/ui/panel-settings.js +4 -2
  94. package/dist/ui/preview-panel.js +48 -14
  95. package/dist/ui/preview-status.js +28 -0
  96. package/dist/ui/preview.js +40 -3
  97. package/dist/ui/settings-token.js +1 -1
  98. package/dist/ui/shared/app-i18n.js +4 -0
  99. package/dist/ui/shared/app-prefs.js +1 -0
  100. package/dist/ui/shared/app-shell.js +146 -2
  101. package/dist/ui/shared/app-store.js +3 -1
  102. package/dist/ui/shared/app.css +47 -20
  103. package/dist/ui/shared/instance-nav.js +79 -23
  104. package/dist/ui/shared/view-strings.js +10 -0
  105. package/dist/unrecognised-prompt.d.ts +23 -0
  106. package/dist/unrecognised-prompt.js +74 -0
  107. package/dist/unrecognised-prompt.js.map +1 -0
  108. package/dist/update-install.d.ts +8 -0
  109. package/dist/update-install.js +9 -3
  110. package/dist/update-install.js.map +1 -1
  111. package/dist/update-marker.d.ts +40 -0
  112. package/dist/update-marker.js +49 -0
  113. package/dist/update-marker.js.map +1 -1
  114. package/dist/usage/i18n-keys.d.ts +1 -1
  115. package/dist/usage/i18n-keys.js +1 -1
  116. package/dist/usage/i18n-keys.js.map +1 -1
  117. package/dist/usage/providers.d.ts +34 -5
  118. package/dist/usage/providers.js +160 -27
  119. package/dist/usage/providers.js.map +1 -1
  120. package/dist/usage/statusline-usage.d.ts +6 -2
  121. package/dist/usage/statusline-usage.js +10 -4
  122. package/dist/usage/statusline-usage.js.map +1 -1
  123. package/dist/web-api.d.ts +20 -0
  124. package/dist/web-api.js +216 -30
  125. package/dist/web-api.js.map +1 -1
  126. package/dist/web-auth.js +3 -2
  127. package/dist/web-auth.js.map +1 -1
  128. package/dist/web-mentions.d.ts +44 -0
  129. package/dist/web-mentions.js +75 -0
  130. package/dist/web-mentions.js.map +1 -0
  131. package/dist/web-prefs.js +2 -0
  132. package/dist/web-prefs.js.map +1 -1
  133. package/dist/web-preview.d.ts +14 -1
  134. package/dist/web-preview.js +35 -13
  135. package/dist/web-preview.js.map +1 -1
  136. package/dist/web-schedules.d.ts +12 -0
  137. package/dist/web-schedules.js +34 -0
  138. package/dist/web-schedules.js.map +1 -0
  139. package/package.json +1 -1
@@ -12,11 +12,15 @@ export interface StatuslineRateLimits {
12
12
  instance: string;
13
13
  /** When that instance last wrote the file. */
14
14
  observedAtMs: number;
15
+ /** Where claude said the conversation is (#1699: under a credential profile's own config dir for a profile). */
16
+ transcriptPath: string | null;
15
17
  }
16
18
  /** A statusline window's `resets_at` as epoch ms, or null when it has no usable reset time. */
17
19
  export declare function statuslineResetsAtMs(value: unknown): number | null;
18
20
  /**
19
21
  * The most recent rate-limit reading any claude-code instance has written, or
20
- * null when no instance has one worth using.
22
+ * null when no instance has one worth using. `from` keeps only the readings of
23
+ * one subscription (#1699): it is asked where each reading's conversation is —
24
+ * a credential profile's instances write under that profile's own config dir.
21
25
  */
22
- export declare function readStatuslineRateLimits(agendHome?: string, now?: number): StatuslineRateLimits | null;
26
+ export declare function readStatuslineRateLimits(agendHome?: string, now?: number, from?: (transcriptPath: string | null) => boolean): StatuslineRateLimits | null;
@@ -31,7 +31,8 @@
31
31
  * window is currently binding, or extra-usage credits; only the API has those.
32
32
  *
33
33
  * Merge rule across instances: **the freshest file wins.** Every instance on
34
- * this machine shares one account, so they are all observing the same counter;
34
+ * one subscription shares one account (a credential profile is a subscription
35
+ * of its own, #1699 — the caller says which), so they observe the same counter;
35
36
  * when two readings disagree the newer one is simply the more recent
36
37
  * observation. Taking the maximum instead would be wrong precisely when it
37
38
  * matters — a pre-reset 95% from an hour ago would drown out a fresh 4%.
@@ -92,13 +93,16 @@ function readOne(dir, name, now) {
92
93
  const sevenDay = parseWindow(rl.seven_day, now);
93
94
  if (!fiveHour && !sevenDay)
94
95
  return null;
95
- return { fiveHour, sevenDay, instance: name, observedAtMs };
96
+ const transcriptPath = typeof parsed.transcript_path === "string" && parsed.transcript_path ? parsed.transcript_path : null;
97
+ return { fiveHour, sevenDay, instance: name, observedAtMs, transcriptPath };
96
98
  }
97
99
  /**
98
100
  * The most recent rate-limit reading any claude-code instance has written, or
99
- * null when no instance has one worth using.
101
+ * null when no instance has one worth using. `from` keeps only the readings of
102
+ * one subscription (#1699): it is asked where each reading's conversation is —
103
+ * a credential profile's instances write under that profile's own config dir.
100
104
  */
101
- export function readStatuslineRateLimits(agendHome = getAgendHome(), now = Date.now()) {
105
+ export function readStatuslineRateLimits(agendHome = getAgendHome(), now = Date.now(), from) {
102
106
  const instancesDir = join(agendHome, "instances");
103
107
  let names;
104
108
  try {
@@ -116,6 +120,8 @@ export function readStatuslineRateLimits(agendHome = getAgendHome(), now = Date.
116
120
  // the failure that makes people stop believing the panel.
117
121
  if (now - reading.observedAtMs > STATUSLINE_MAX_AGE_MS)
118
122
  continue;
123
+ if (from && !from(reading.transcriptPath))
124
+ continue;
119
125
  if (!best || reading.observedAtMs > best.observedAtMs)
120
126
  best = reading;
121
127
  }
@@ -1 +1 @@
1
- {"version":3,"file":"statusline-usage.js","sourceRoot":"","sources":["../../src/usage/statusline-usage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"statusline-usage.js","sourceRoot":"","sources":["../../src/usage/statusline-usage.ts"],"names":[],"mappings":"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"}
package/dist/web-api.d.ts CHANGED
@@ -9,6 +9,7 @@ import { type WebChatHistory } from "./web-chat-history.js";
9
9
  import { type WebFileLedger } from "./web-upload.js";
10
10
  import type { WebSessionStore } from "./web-session.js";
11
11
  import { type WebPrefsStore } from "./web-prefs.js";
12
+ import { type MentionNames, type MentionQuery } from "./web-mentions.js";
12
13
  import type { ExplicitInstanceRemoval } from "./instance-removal.js";
13
14
  /**
14
15
  * Push a single SSE frame to every client. If a client throws (closed socket
@@ -112,6 +113,8 @@ export interface WebApiContext {
112
113
  previewForUi?(hostHeader: string | undefined, secure: boolean): PreviewAvailability & {
113
114
  boot: string | null;
114
115
  };
116
+ /** #1723: the same for a page of the public link (exposure `exposureId`, at `dashboardOrigin`); absent: none there. */
117
+ publicPreviewForUi?(exposureId: string, dashboardOrigin: string): PublicPreviewForUi;
115
118
  emitSseEvent(event: string, data: unknown): void;
116
119
  /** The web chat's recent messages (history + SSE replay); absent in contexts that have no chat. */
117
120
  readonly webChatHistory?: WebChatHistory;
@@ -121,6 +124,8 @@ export interface WebApiContext {
121
124
  webImage?(kind: "bot" | "avatar", key: string): CachedImage | null;
122
125
  /** #1630: one Discord server emoji of the fleet's bots, from the host cache (fetched once); null for any other id. */
123
126
  webEmoji?(id: string): Promise<CachedImage | null>;
127
+ /** #1682: names for mention ids (only those asked; from memory, never a request); absent: no names. */
128
+ webMentionNames?(query: MentionQuery): MentionNames;
124
129
  /** #1630: the custom emoji of the servers the fleet's Discord bots are in. */
125
130
  webEmojiList?(): Promise<Array<{
126
131
  id: string;
@@ -214,6 +219,10 @@ export interface WebApiContext {
214
219
  };
215
220
  list(target?: string): unknown[];
216
221
  create(params: unknown): unknown;
222
+ /** #1724: the update MCP update_schedule makes (cron and at clear each other). */
223
+ update?(id: string, params: Record<string, unknown>): unknown;
224
+ /** #1426: a pending retry of a schedule's occurrence, for the row's "retrying". */
225
+ getRetry?(id: string): unknown;
217
226
  delete(id: string): void;
218
227
  } | null;
219
228
  }
@@ -221,10 +230,21 @@ export interface WebApiContext {
221
230
  * Handle a Web UI request. Returns true if handled, false to pass through.
222
231
  */
223
232
  export declare function handleWebRequest(req: IncomingMessage, res: ServerResponse, url: URL, ctx: WebApiContext): boolean;
233
+ /**
234
+ * #1723: a public-link page's preview: where it may frame from (`frameSrc` — the quick-tunnel wildcard while its own
235
+ * preview tunnel is starting or up, the exact origin for a configured web.preview_origin, null for none) and, once
236
+ * the preview tunnel failed, why (`detail`, a fixed reason the page words).
237
+ */
238
+ export type PublicPreviewForUi = PreviewAvailability & {
239
+ boot: string | null;
240
+ frameSrc: string | null;
241
+ detail?: string;
242
+ };
224
243
  export interface AppShellContext {
225
244
  previewForUi?(hostHeader: string | undefined, secure: boolean): (PreviewAvailability & {
226
245
  boot: string | null;
227
246
  }) | null;
247
+ publicPreviewForUi?(exposureId: string, dashboardOrigin: string): PublicPreviewForUi;
228
248
  /** #1523 N3: `web.usage_panel` — the header's ◔ is offered unless it is false (the page needs no probe read for it). */
229
249
  readonly fleetConfig?: {
230
250
  web?: {
package/dist/web-api.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { withSpawnHoldReply } from "./spawn-hold-reply.js";
1
2
  import { binaryProbe } from "./binary-probe.js";
2
3
  import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
3
4
  import { readBoundedWebBody } from "./web-body.js";
@@ -16,7 +17,9 @@ import { shellRoute } from "./web-shell-routes.js";
16
17
  import { readFileSync } from "node:fs";
17
18
  import { join, dirname } from "node:path";
18
19
  import { fileURLToPath } from "node:url";
19
- import { CreateInstanceArgs, validateArgs } from "./outbound-schemas.js";
20
+ import { CreateInstanceArgs, CreateScheduleArgs, UpdateScheduleArgs, validateArgs } from "./outbound-schemas.js";
21
+ import { scheduleView } from "./web-schedules.js";
22
+ import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
20
23
  import { readStatuslineModel, resolveInstanceContext } from "./topic-commands.js";
21
24
  import { z } from "zod";
22
25
  import { evaluateWebRequest, isPassiveWebRead, isSecureRequest, isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
@@ -25,6 +28,8 @@ import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
25
28
  import { attachmentDelivery, displayName, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment, sniffUpload, UPLOAD_LIMITS, wellFormed } from "./web-upload.js";
26
29
  import { getAgendHome } from "./paths.js";
27
30
  import { OPERATOR } from "./web-prefs.js";
31
+ import { commitTeamCreate, commitTeamUpdate } from "./outbound-handlers.js";
32
+ import { parseMentionQuery } from "./web-mentions.js";
28
33
  import { authorizeExplicitInstanceRemoval } from "./instance-removal.js";
29
34
  import { measureSyncWork } from "./sync-work-attribution.js";
30
35
  // ── Strict public-facing schemas ────────────────────────────────────────────
@@ -46,18 +51,17 @@ const TaskUpdateSchema = z.object({
46
51
  description: z.string().max(MAX_TEXT).optional(),
47
52
  priority: z.string().max(64).optional(),
48
53
  }).strict();
49
- const ScheduleCreateSchema = z.object({
50
- cron: z.string().min(1).max(256),
51
- message: z.string().min(1).max(MAX_TEXT),
52
- target: z.string().min(1).max(256),
53
- label: z.string().max(256).optional(),
54
- timezone: z.string().max(128).optional(),
55
- }).strict();
56
54
  const TeamCreateSchema = z.object({
57
55
  name: z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/),
58
56
  members: z.array(z.string().min(1).max(256)).min(1).max(256),
59
57
  description: z.string().max(MAX_TEXT).optional(),
60
58
  }).strict();
59
+ // #1689: an edit of a team — its members as they should be, and its description ("" clears it). No name: a team is
60
+ // not renamed.
61
+ const TeamEditSchema = z.object({
62
+ members: z.array(z.string().min(1).max(256)).min(1).max(256),
63
+ description: z.string().max(MAX_TEXT).optional(),
64
+ }).strict();
61
65
  const ConfigUpdateSchema = z.object({
62
66
  channel: z.object({
63
67
  group_id: z.union([z.number(), z.string()]).optional(),
@@ -420,6 +424,24 @@ export function handleWebRequest(req, res, url, ctx) {
420
424
  (ctx.webEmojiList ? ctx.webEmojiList() : Promise.resolve([])).then(answer, () => answer([]));
421
425
  return true;
422
426
  }
427
+ // #1682: the names of the mention ids a page found in what it rendered (at most MAX_MENTION_IDS, digits only) — only
428
+ // those, so no page (the public link included) ever gets a directory; from memory, never a request.
429
+ if (method === "GET" && path === "/ui/mentions") {
430
+ const parsed = parseMentionQuery(url.searchParams);
431
+ if (!parsed.ok) {
432
+ json(res, 400, { error: parsed.error });
433
+ return true;
434
+ }
435
+ let names = { u: {}, r: {}, c: {} };
436
+ try {
437
+ names = ctx.webMentionNames?.(parsed.query) ?? names;
438
+ }
439
+ catch (err) {
440
+ ctx.logger.debug({ err: err.message }, "mention names unavailable");
441
+ }
442
+ json(res, 200, names);
443
+ return true;
444
+ }
423
445
  // ── Fleet prompts (C4): the hang / exit / interactive-prompt buttons, answerable here too ──
424
446
  if (method === "GET" && path === "/ui/prompts") {
425
447
  json(res, 200, { prompts: ctx.listWebPrompts?.() ?? [] });
@@ -499,6 +521,18 @@ export function handleWebRequest(req, res, url, ctx) {
499
521
  });
500
522
  return true;
501
523
  }
524
+ // #1723: the public link page's preview, asked while it says "Preview is starting…" — its state and, once up, its
525
+ // origin and boot. Public link only: a local page's preview is decided by its own load.
526
+ if (path === "/ui/preview-status" && method === "GET") {
527
+ const gateway = gatewayRequestContext(req);
528
+ if (!gateway || !ctx.publicPreviewForUi) {
529
+ json(res, 404, { error: "not offered here" });
530
+ return true;
531
+ }
532
+ const p = ctx.publicPreviewForUi(gateway.exposureId, gateway.expectedOrigin);
533
+ json(res, 200, { code: p.code, previewOrigin: p.previewOrigin, boot: p.previewOrigin ? p.boot : null, detail: p.detail ?? null });
534
+ return true;
535
+ }
502
536
  // #1665: the web preferences that follow the person between devices. Read by any page of the panel, the public link
503
537
  // included (it reads what is in force); written only by a signed-in admin session or the CLI's header token — never
504
538
  // over the public link (absent from its route manifest, and refused here too), never by a read-only session.
@@ -720,13 +754,18 @@ export function handleWebRequest(req, res, url, ctx) {
720
754
  }
721
755
  const topicMode = ctx.fleetConfig?.channel?.mode === "topic";
722
756
  (async () => {
757
+ let held = false;
723
758
  try {
724
- await ctx.startInstance(name, config, topicMode ?? false);
759
+ await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.startInstance(name, config, topicMode ?? false));
725
760
  ctx.emitSseEvent("status", ctx.getUiStatus());
726
- json(res, 200, { started: name });
761
+ if (!held)
762
+ json(res, 200, { started: name });
727
763
  }
728
764
  catch (err) {
729
- json(res, 500, { error: err.message });
765
+ if (!held)
766
+ json(res, 500, { error: err.message });
767
+ else
768
+ ctx.logger.error({ err, name }, "Held instance action failed");
730
769
  }
731
770
  })();
732
771
  return true;
@@ -839,14 +878,19 @@ export function handleWebRequest(req, res, url, ctx) {
839
878
  if (method === "POST" && restartMatch) {
840
879
  const name = decodeURIComponent(restartMatch[1]);
841
880
  (async () => {
881
+ let held = false;
842
882
  try {
843
- await ctx.restartSingleInstance(name, { explicit: true });
883
+ await withSpawnHoldReply(async (message) => { held = true; json(res, 202, { held: true, message, instance: name }); }, () => ctx.restartSingleInstance(name, { explicit: true }));
844
884
  ctx.emitSseEvent("status", ctx.getUiStatus());
845
- json(res, 200, { restarted: name });
885
+ if (!held)
886
+ json(res, 200, { restarted: name });
846
887
  }
847
888
  catch (err) {
848
889
  const status = err.message.includes("not found") ? 404 : 500;
849
- json(res, status, { error: err.message });
890
+ if (!held)
891
+ json(res, status, { error: err.message });
892
+ else
893
+ ctx.logger.error({ err, name }, "Held instance action failed");
850
894
  }
851
895
  })();
852
896
  return true;
@@ -964,10 +1008,28 @@ export function handleWebRequest(req, res, url, ctx) {
964
1008
  json(res, 200, { schedules: [] });
965
1009
  return true;
966
1010
  }
967
- json(res, 200, { schedules: ctx.scheduler.list() });
1011
+ // #1724: each row with its next run and one-shot time in the fleet's zone — worked out here, not on the page.
1012
+ const scheduler = ctx.scheduler;
1013
+ const now = Date.now();
1014
+ json(res, 200, {
1015
+ schedules: scheduler.list().map((s) => scheduleView(s, scheduler.getRetry?.(s.id) ?? null, now)),
1016
+ // The zone a new schedule (and a "Once" date and time typed without one) is read in.
1017
+ default_timezone: ctx.fleetConfig?.defaults?.scheduler?.default_timezone
1018
+ ?? DEFAULT_SCHEDULER_CONFIG.default_timezone,
1019
+ });
968
1020
  return true;
969
1021
  }
1022
+ // #1724: who may write a schedule, asked again once the body is in (a session revoked meanwhile, a non-admin tier).
1023
+ const scheduleWriterGone = () => {
1024
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1025
+ return verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin");
1026
+ };
970
1027
  if (method === "POST" && path === "/ui/schedules") {
1028
+ // #1724: schedules are not created, edited or deleted over the public link (absent from its manifest too, #1689).
1029
+ if (gatewayRequestContext(req)) {
1030
+ json(res, 403, { error: "schedule_not_over_public_link" });
1031
+ return true;
1032
+ }
971
1033
  if (!ctx.scheduler) {
972
1034
  json(res, 500, { error: "Scheduler not initialized" });
973
1035
  return true;
@@ -977,10 +1039,23 @@ export function handleWebRequest(req, res, url, ctx) {
977
1039
  const body = await parseBody(req);
978
1040
  if (!permitWebContinuation(req, res, ctx))
979
1041
  return;
980
- const parsed = parseOrReject(ScheduleCreateSchema, body, res);
1042
+ if (scheduleWriterGone()) {
1043
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1044
+ return;
1045
+ }
1046
+ // #1724: the create_schedule tool's own schema — cron or at (exactly one) — plus a target, which the web names.
1047
+ const parsed = parseOrReject(CreateScheduleArgs, body, res);
981
1048
  if (!parsed)
982
1049
  return;
983
- const schedule = ctx.scheduler.create(parsed);
1050
+ if (typeof parsed.target !== "string" || !parsed.target) {
1051
+ json(res, 400, { error: "target: an instance is required" });
1052
+ return;
1053
+ }
1054
+ const schedule = ctx.scheduler.create({
1055
+ cron: parsed.cron, at: parsed.at, message: parsed.message, target: parsed.target, label: parsed.label, timezone: parsed.timezone,
1056
+ ...(parsed.silent !== undefined ? { silent: parsed.silent } : {}),
1057
+ source: "web", reply_chat_id: "", reply_thread_id: null,
1058
+ });
984
1059
  json(res, 200, schedule);
985
1060
  }
986
1061
  catch (err) {
@@ -990,7 +1065,48 @@ export function handleWebRequest(req, res, url, ctx) {
990
1065
  return true;
991
1066
  }
992
1067
  const schedDelMatch = path.match(/^\/ui\/schedules\/(.+)$/);
1068
+ // #1724: Edit — the update_schedule tool's validation (UpdateScheduleArgs) and its call (scheduler.update): cron and at
1069
+ // clear each other there; silent is not touched.
1070
+ if (method === "PATCH" && schedDelMatch) {
1071
+ if (gatewayRequestContext(req)) {
1072
+ json(res, 403, { error: "schedule_not_over_public_link" });
1073
+ return true;
1074
+ }
1075
+ if (!ctx.scheduler || typeof ctx.scheduler.update !== "function") {
1076
+ json(res, 500, { error: "Scheduler not initialized" });
1077
+ return true;
1078
+ }
1079
+ const id = decodeURIComponent(schedDelMatch[1]);
1080
+ (async () => {
1081
+ try {
1082
+ const body = await parseBody(req);
1083
+ if (!permitWebContinuation(req, res, ctx))
1084
+ return;
1085
+ if (scheduleWriterGone()) {
1086
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1087
+ return;
1088
+ }
1089
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.hasOwn(body, "id")) {
1090
+ json(res, 400, { error: "expected the fields to change (the id is the path's)" });
1091
+ return;
1092
+ }
1093
+ const parsed = parseOrReject(UpdateScheduleArgs, { ...body, id }, res);
1094
+ if (!parsed)
1095
+ return;
1096
+ const { id: _id, ...changes } = parsed;
1097
+ json(res, 200, ctx.scheduler.update(id, changes));
1098
+ }
1099
+ catch (err) {
1100
+ json(res, 400, { error: err.message });
1101
+ }
1102
+ })();
1103
+ return true;
1104
+ }
993
1105
  if (method === "DELETE" && schedDelMatch) {
1106
+ if (gatewayRequestContext(req)) {
1107
+ json(res, 403, { error: "schedule_not_over_public_link" });
1108
+ return true;
1109
+ }
994
1110
  if (!ctx.scheduler) {
995
1111
  json(res, 500, { error: "Scheduler not initialized" });
996
1112
  return true;
@@ -1027,26 +1143,82 @@ export function handleWebRequest(req, res, url, ctx) {
1027
1143
  return true;
1028
1144
  }
1029
1145
  if (method === "POST" && path === "/ui/teams") {
1146
+ // #1689: not over the public link (absent from its manifest too), like editing and deleting a team.
1147
+ if (gatewayRequestContext(req)) {
1148
+ json(res, 403, { error: "teams_not_over_public_link" });
1149
+ return true;
1150
+ }
1030
1151
  (async () => {
1031
1152
  try {
1032
1153
  const body = await parseBody(req);
1033
1154
  if (!permitWebContinuation(req, res, ctx))
1034
1155
  return;
1156
+ // #1692: who may write, asked again once the body is in (as the edit does).
1157
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1158
+ if (verdict.kind !== "allow" || (verdict.via === "session" && verdict.session?.tier !== "admin")) {
1159
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1160
+ return;
1161
+ }
1035
1162
  const parsed = parseOrReject(TeamCreateSchema, body, res);
1036
1163
  if (!parsed)
1037
1164
  return;
1038
- if (!ctx.fleetConfig) {
1039
- json(res, 500, { error: "No fleet config" });
1165
+ // #1692: the create_team path — members checked, an existing team never overwritten, a failed save undone.
1166
+ const r = commitTeamCreate(ctx.fleetConfig, parsed, () => ctx.saveFleetConfig());
1167
+ if (!r.ok) {
1168
+ json(res, r.status, { error: r.error });
1040
1169
  return;
1041
1170
  }
1042
- if (!ctx.fleetConfig.teams)
1043
- ctx.fleetConfig.teams = {};
1044
- ctx.fleetConfig.teams[parsed.name] = {
1045
- members: parsed.members,
1046
- description: parsed.description,
1047
- };
1048
- ctx.saveFleetConfig();
1049
- json(res, 200, { created: parsed.name });
1171
+ json(res, 200, r.result);
1172
+ }
1173
+ catch (err) {
1174
+ json(res, 400, { error: err.message });
1175
+ }
1176
+ })();
1177
+ return true;
1178
+ }
1179
+ // #1689: edit a team — the same update as the update_team tool (applyTeamUpdate), from the members the page sends:
1180
+ // what to add and remove is worked out against the team as it is now. Never over the public link.
1181
+ const teamEditMatch = path.match(/^\/ui\/teams\/(.+)$/);
1182
+ if ((method === "PATCH" || method === "PUT") && teamEditMatch) {
1183
+ if (gatewayRequestContext(req)) {
1184
+ json(res, 403, { error: "teams_not_over_public_link" });
1185
+ return true;
1186
+ }
1187
+ const name = decodeURIComponent(teamEditMatch[1]);
1188
+ // Who may write, asked again once the body is in (#1693 review): a session revoked or a token rotated meanwhile
1189
+ // writes nothing.
1190
+ const writerNow = () => {
1191
+ const verdict = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1192
+ return verdict.kind === "allow" && !(verdict.via === "session" && verdict.session?.tier !== "admin");
1193
+ };
1194
+ (async () => {
1195
+ try {
1196
+ const body = await parseBody(req);
1197
+ if (!permitWebContinuation(req, res, ctx))
1198
+ return;
1199
+ if (!writerNow()) {
1200
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
1201
+ return;
1202
+ }
1203
+ const parsed = parseOrReject(TeamEditSchema, body, res);
1204
+ if (!parsed)
1205
+ return;
1206
+ const teams = ctx.fleetConfig?.teams;
1207
+ const current = teams && Object.hasOwn(teams, name) ? teams[name].members : null;
1208
+ if (!current) {
1209
+ json(res, 404, { error: `Team not found: ${name}` });
1210
+ return;
1211
+ }
1212
+ const want = [...new Set(parsed.members)];
1213
+ // The update and its save as one: a failed save leaves the team as it was (500).
1214
+ const r = commitTeamUpdate(ctx.fleetConfig, {
1215
+ name, add: want.filter(m => !current.includes(m)), remove: current.filter(m => !want.includes(m)), description: parsed.description,
1216
+ }, () => ctx.saveFleetConfig());
1217
+ if (!r.ok) {
1218
+ json(res, r.status, { error: r.error });
1219
+ return;
1220
+ }
1221
+ json(res, 200, { updated: r.result });
1050
1222
  }
1051
1223
  catch (err) {
1052
1224
  json(res, 400, { error: err.message });
@@ -1056,8 +1228,13 @@ export function handleWebRequest(req, res, url, ctx) {
1056
1228
  }
1057
1229
  const teamDelMatch = path.match(/^\/ui\/teams\/(.+)$/);
1058
1230
  if (method === "DELETE" && teamDelMatch) {
1231
+ if (gatewayRequestContext(req)) {
1232
+ json(res, 403, { error: "teams_not_over_public_link" });
1233
+ return true;
1234
+ }
1059
1235
  const name = decodeURIComponent(teamDelMatch[1]);
1060
- if (!ctx.fleetConfig?.teams?.[name]) {
1236
+ // Own keys (#1689): a team is one fleet.yaml defines — "__proto__" or "constructor" is not found.
1237
+ if (!ctx.fleetConfig?.teams || !Object.hasOwn(ctx.fleetConfig.teams, name)) {
1061
1238
  json(res, 404, { error: `Team not found: ${name}` });
1062
1239
  return true;
1063
1240
  }
@@ -1350,8 +1527,12 @@ function handleUpload(req, res, url, ctx) {
1350
1527
  });
1351
1528
  }
1352
1529
  function shellPreview(req, ctx, mode) {
1353
- if (mode !== "full" || gatewayRequestContext(req))
1530
+ if (mode !== "full")
1354
1531
  return null;
1532
+ // #1723: a page of the public link asks its own exposure's preview — never the local listener's Host-based choice.
1533
+ const gateway = gatewayRequestContext(req);
1534
+ if (gateway)
1535
+ return ctx.publicPreviewForUi?.(gateway.exposureId, gateway.expectedOrigin) ?? null;
1355
1536
  return ctx.previewForUi?.(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) ?? null;
1356
1537
  }
1357
1538
  /**
@@ -1422,6 +1603,8 @@ function shellBodyTag(req, ctx, mode) {
1422
1603
  + ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}"`
1423
1604
  // #1554: no availability asked (the public link, or not the full app) still says why, in the page's language.
1424
1605
  + ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
1606
+ // #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
1607
+ + `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
1425
1608
  // #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔.
1426
1609
  + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
1427
1610
  // #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
@@ -1438,7 +1621,10 @@ export function serveAppShell(req, res, ctx, mode) {
1438
1621
  const own = mode === "full" && !gatewayRequestContext(req)
1439
1622
  ? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req)) : null;
1440
1623
  const mediaSrc = own ? `${own}/ui/file/` : "'none'";
1441
- sendPanelHtml(res, html.replace('<html lang="en">', shellHtmlTag(ctx, mode)).replace("<body>", shellBodyTag(req, ctx, mode)), 200, {}, { ...(p?.previewOrigin ? { frameSrc: `${p.previewOrigin}/frame` } : {}), mediaSrc });
1624
+ // A public-link page frames what its exposure's preview names (the quick-tunnel wildcard: its host is not known when
1625
+ // the page may load); every other page frames exactly the origin this load chose (#1723).
1626
+ const frameSrc = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin ? `${p.previewOrigin}/frame` : null;
1627
+ sendPanelHtml(res, html.replace('<html lang="en">', shellHtmlTag(ctx, mode)).replace("<body>", shellBodyTag(req, ctx, mode)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
1442
1628
  }
1443
1629
  catch {
1444
1630
  json(res, 500, { error: "app.html not found" });