@songsid/agend 2.3.0-beta.2 → 2.3.0-beta.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/dist/agent-whats-new.js +2 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +42 -12
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +5 -4
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js +15 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +21 -3
- package/dist/backend-command.js +23 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +7 -0
- package/dist/channel/adapters/discord.js +35 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +8 -4
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +6 -0
- package/dist/classic-channel-manager.d.ts +8 -3
- package/dist/classic-channel-manager.js +10 -3
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +73 -2
- package/dist/config-validator.js.map +1 -1
- package/dist/delivery-outbox.js +5 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -0
- package/dist/fleet-manager.d.ts +238 -5
- package/dist/fleet-manager.js +878 -71
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-traffic.d.ts +38 -0
- package/dist/fleet-traffic.js +96 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +13 -5
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +56 -11
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +398 -0
- package/dist/human-delivery-receipts.js +2 -1
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/locale.js +48 -8
- package/dist/locale.js.map +1 -1
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +40 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +92 -0
- package/dist/outbound-schemas.js +55 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +10 -1
- package/dist/quickstart-api.js +12 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +2 -0
- package/dist/reply-buttons-controller.js +1 -1
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +16 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.js +4 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +5 -0
- package/dist/topic-commands.js +16 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +25 -1
- package/dist/ui/app-effects.js +33 -9
- package/dist/ui/chat-effects.js +62 -4
- package/dist/ui/chat-strings.js +40 -4
- package/dist/ui/chat-thread.js +17 -6
- package/dist/ui/effect-trial-panel.js +19 -16
- package/dist/ui/fleet-canvas-layout.js +111 -0
- package/dist/ui/fleet-canvas.js +489 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-traffic.js +224 -0
- package/dist/ui/panel-chat.js +12 -5
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +32 -11
- package/dist/ui/panel-settings.js +6 -4
- package/dist/ui/preview.js +3 -2
- package/dist/ui/settings-appearance.js +34 -18
- package/dist/ui/settings-catalog.js +89 -0
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +4 -3
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +6 -2
- package/dist/ui/settings-mods.js +4 -3
- package/dist/ui/settings-owner-effects.js +8 -8
- package/dist/ui/settings-strings.js +40 -38
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-prefs.js +1 -1
- package/dist/ui/shared/app-shell.js +23 -1
- package/dist/ui/shared/app-store.js +12 -1
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +186 -11
- package/dist/ui/shared/card-strings.js +4 -0
- package/dist/ui/shared/effect-app-runtime.js +1 -1
- package/dist/ui/shared/effect-frame.js +10 -5
- package/dist/ui/shared/effect-geometry.js +24 -9
- package/dist/ui/shared/effect-policy.js +2 -1
- package/dist/ui/shared/effect-registry.js +71 -17
- package/dist/ui/shared/effect-stock-trust.js +23 -2
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-stage.js +15 -9
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/identity-card.js +75 -4
- package/dist/ui/shared/identity-color.js +3 -3
- package/dist/ui/shared/instance-nav.js +20 -4
- package/dist/ui/shared/panel-view.js +24 -4
- package/dist/ui/shared/view-strings.js +22 -0
- package/dist/view-api.d.ts +32 -0
- package/dist/view-api.js +55 -4
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +19 -4
- package/dist/web-api.js +178 -20
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-policy.d.ts +4 -6
- package/dist/web-appearance-policy.js +5 -12
- package/dist/web-appearance-policy.js.map +1 -1
- package/dist/web-appearance-tools.js +5 -1
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +14 -1
- package/dist/web-appearance-view.js +98 -10
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +2 -2
- package/dist/web-appearance.js +5 -6
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +14 -0
- package/dist/web-card.js +40 -6
- package/dist/web-card.js.map +1 -1
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +40 -0
- package/dist/web-chat-history.js +66 -3
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +1 -0
- package/dist/web-effect-tools.js +2 -3
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.d.ts +2 -2
- package/dist/web-effect-trials.js +3 -3
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-guest-access.d.ts +125 -0
- package/dist/web-guest-access.js +232 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-identity-palette.d.ts +1 -1
- package/dist/web-identity-palette.js +2 -2
- package/dist/web-identity-palette.js.map +1 -1
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +1 -1
- package/dist/web-mod-agent.js +2 -4
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-store.d.ts +5 -0
- package/dist/web-mod-store.js +6 -0
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +8 -0
- package/dist/web-prefs.js +9 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -2
- package/dist/web-preview.js +4 -2
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +39 -0
- package/dist/web-returning-device.js +46 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session.d.ts +58 -10
- package/dist/web-session.js +104 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-tools.d.ts +81 -0
- package/dist/web-settings-tools.js +257 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-theme.d.ts +2 -0
- package/dist/web-theme.js +10 -1
- package/dist/web-theme.js.map +1 -1
- package/package.json +1 -1
package/dist/web-api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { webHumanOrigin } from "./human-delivery-receipts.js";
|
|
2
|
+
import { guestChatRefusal, guestCommandText, guestDeliveries, guestFrameData, guestHistory, guestMessages, guestPollBody, guestPrefs, guestRequestOf, guestSeesMessage, guestStatus } from "./web-guest-access.js";
|
|
2
3
|
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
3
4
|
import { binaryProbe } from "./binary-probe.js";
|
|
4
5
|
import { settingsRequestExecution, settingsWrite, isSettingsReplay } from "./settings-request-capability.js";
|
|
@@ -97,11 +98,41 @@ const safeInstanceSegment = (name) => /^[^\\/\x00]+$/.test(name) && name !== "."
|
|
|
97
98
|
* the eviction the dead entry leaks forever, and without the try/catch a
|
|
98
99
|
* single dead client breaks delivery to every client iterated after it.
|
|
99
100
|
*/
|
|
101
|
+
/** #1920 phase 1b: the open streams that are a guest's, and whose — each event is projected for them (guestFrameData). */
|
|
102
|
+
const streamGuests = new WeakMap();
|
|
103
|
+
/**
|
|
104
|
+
* #1920 §5.2: one event for the guests' streams only, each as projected for it — a message that has just become what
|
|
105
|
+
* its channel saw (an admin's page already has it, unchanged).
|
|
106
|
+
*/
|
|
107
|
+
export function broadcastToGuests(clients, event, data, onError) {
|
|
108
|
+
const guests = [...clients].filter((c) => streamGuests.has(c));
|
|
109
|
+
if (!guests.length)
|
|
110
|
+
return;
|
|
111
|
+
const live = new Set(guests);
|
|
112
|
+
broadcastSseEvent(live, event, data, onError);
|
|
113
|
+
for (const c of guests)
|
|
114
|
+
if (!live.has(c))
|
|
115
|
+
clients.delete(c); // the ones it ended (their session went)
|
|
116
|
+
}
|
|
100
117
|
export function broadcastSseEvent(clients, event, data, onError, id) {
|
|
101
118
|
const payload = sseFrame(event, data, id);
|
|
102
119
|
const dead = [];
|
|
103
120
|
for (const client of clients) {
|
|
104
121
|
try {
|
|
122
|
+
// An admin's stream gets these bytes; a guest's, the event as projected for its scope now, or nothing (no id: it
|
|
123
|
+
// never replays chat, so a cursor of the admin's stream is not its to hold).
|
|
124
|
+
const guest = streamGuests.get(client);
|
|
125
|
+
if (guest) {
|
|
126
|
+
// The exact session that opened it ended (revoked, expired, rotated away): nothing more, and the stream goes.
|
|
127
|
+
if (!guest.current()) {
|
|
128
|
+
dead.push(client);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const shown = guestFrameData(event, data, guest.scope(), guest);
|
|
132
|
+
if (shown !== undefined)
|
|
133
|
+
client.write(sseFrame(event, shown));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
105
136
|
client.write(payload);
|
|
106
137
|
}
|
|
107
138
|
catch (err) {
|
|
@@ -382,7 +413,7 @@ function handleEffectTrials(req, res, url, ctx, path, method) {
|
|
|
382
413
|
css = value;
|
|
383
414
|
}
|
|
384
415
|
catch { /* unavailable is never off */ }
|
|
385
|
-
// CSS
|
|
416
|
+
// CSS is diagnostic for Try; its host-owned shadow shell isolates controls and local paint.
|
|
386
417
|
const actor = Object.freeze({ via: "session", surface: "local", tier: "admin", sessionHash: v.session.idHash, epoch: v.session.handle, person, ownerCss: css });
|
|
387
418
|
return { actor, record: v.session, sessions };
|
|
388
419
|
};
|
|
@@ -946,6 +977,17 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
946
977
|
return true;
|
|
947
978
|
}
|
|
948
979
|
// ── Backend detection ─────────────────────────────────
|
|
980
|
+
// #2072: the model a Settings field offers — the cached catalog of that backend (that instance's account's). Read only.
|
|
981
|
+
if (method === "GET" && path === "/ui/models") {
|
|
982
|
+
const backend = url.searchParams.get("backend") ?? "";
|
|
983
|
+
const instance = url.searchParams.get("instance");
|
|
984
|
+
if (!/^[a-z][a-z0-9-]{0,31}$/.test(backend) || (instance !== null && (instance.length < 1 || instance.length > 256))) {
|
|
985
|
+
json(res, 400, { error: "backend (and optional instance) required" });
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
json(res, 200, { backend, models: ctx.modelCatalogFor?.(backend, instance) ?? [] });
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
949
991
|
if (method === "GET" && path === "/ui/backends") {
|
|
950
992
|
const BACKENDS = [
|
|
951
993
|
{ name: "claude-code", binary: "claude" },
|
|
@@ -956,6 +998,16 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
956
998
|
{ name: "grok", binary: "grok" },
|
|
957
999
|
{ name: "muse", binary: "muse" },
|
|
958
1000
|
];
|
|
1001
|
+
// #2072: the published availability snapshot — installed, signed in, new enough — when the fleet has one.
|
|
1002
|
+
if (ctx.backendAvailabilityRows) {
|
|
1003
|
+
const binaryOf = new Map(BACKENDS.map(b => [b.name, b]));
|
|
1004
|
+
const backends = ctx.backendAvailabilityRows().filter(r => binaryOf.has(r.backend)).map(r => ({
|
|
1005
|
+
name: r.backend, binary: binaryOf.get(r.backend).binary, installed: r.state !== "not_installed" && r.path !== null, path: r.path ?? "",
|
|
1006
|
+
deprecated: binaryOf.get(r.backend).deprecated ?? false, state: r.state, minVersion: r.minVersion, ...(r.state === "unknown" ? { unknown: true } : {}),
|
|
1007
|
+
}));
|
|
1008
|
+
json(res, 200, { backends });
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
959
1011
|
// #1490: seven sequential 2 s `execFileSync("which")` calls used to block the event loop here. The shared probe
|
|
960
1012
|
// runs `which` as bounded async children and reuses an answer for BINARY_PROBE_TTL_MS. `unknown`: no answer in
|
|
961
1013
|
// time (shown as not installed, as the old timeout was).
|
|
@@ -978,9 +1030,24 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
978
1030
|
// that arrived while the stream was silent is not skipped by the cursor this answer hands out (#1251
|
|
979
1031
|
// review). The page keeps one entry per boot+id, so what it already loaded from /ui/history is not doubled.
|
|
980
1032
|
if (method === "GET" && path === "/ui/poll") {
|
|
1033
|
+
// #1920: a guest's poll is projected for its scope (web-guest-access.ts) — the same rule as its stream.
|
|
1034
|
+
const guest = guestRequestOf(req);
|
|
981
1035
|
const history = ctx.webChatHistory;
|
|
982
1036
|
const after = url.searchParams.get("after");
|
|
983
1037
|
const fresh = after === null || after === ""; // a cursor that is not ours in shape still gets nothing
|
|
1038
|
+
if (guest) {
|
|
1039
|
+
// #2085 review: and what became the channel's after its cursor (an older row, its copy sent since), once each.
|
|
1040
|
+
json(res, 200, guestPollBody(ctx.getUiStatus(), guest.scope(), Date.now(), guest, after, () => {
|
|
1041
|
+
if (!history)
|
|
1042
|
+
return [];
|
|
1043
|
+
if (fresh)
|
|
1044
|
+
return history.after(0);
|
|
1045
|
+
const cursor = parseLastEventId(after);
|
|
1046
|
+
const rows = [...history.replayFor(cursor), ...history.seenAfter(cursor)];
|
|
1047
|
+
return rows.filter((m, i) => rows.findIndex((x) => x.boot === m.boot && x.id === m.id) === i);
|
|
1048
|
+
}));
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
984
1051
|
json(res, 200, {
|
|
985
1052
|
status: ctx.getUiStatus(),
|
|
986
1053
|
messages: history ? (fresh ? history.after(0) : history.replayFor(parseLastEventId(after))) : [],
|
|
@@ -1013,30 +1080,49 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1013
1080
|
});
|
|
1014
1081
|
// #1823 (#1830 review): with the status, the server's clock — now and with every heartbeat — that the page counts a
|
|
1015
1082
|
// poll down by (one write: the frames a connect sends are unchanged in number).
|
|
1016
|
-
|
|
1083
|
+
// #1920: a guest's stream: its projected status now and with each heartbeat, its replay and ticks as it may see them
|
|
1084
|
+
// (§5.2), then only what guestFrameData lets through — no prompts or Needs you.
|
|
1085
|
+
const guest = guestRequestOf(req);
|
|
1086
|
+
if (guest) {
|
|
1087
|
+
streamGuests.set(res, guest);
|
|
1088
|
+
guest.streamOpened?.();
|
|
1089
|
+
}
|
|
1090
|
+
const statusFrame = () => `event: status\ndata: ${JSON.stringify(guest ? guestStatus(ctx.getUiStatus(), guest.scope()) : ctx.getUiStatus())}\n\n`;
|
|
1091
|
+
res.write(`${statusFrame()}${sseClock()}`);
|
|
1017
1092
|
// An EventSource that reconnects says what it last saw: send what it missed, before anything new.
|
|
1018
1093
|
const lastSeen = parseLastEventId(req.headers["last-event-id"]);
|
|
1019
1094
|
if (lastSeen !== null && ctx.webChatHistory) {
|
|
1020
1095
|
const history = ctx.webChatHistory;
|
|
1021
|
-
|
|
1022
|
-
|
|
1096
|
+
if (guest) {
|
|
1097
|
+
// Its own projection, with no cursor: an admin's ids are not a guest's to hold (it reloads its history instead).
|
|
1098
|
+
for (const m of guestMessages(history.replayFor(lastSeen), guest, guest.scope()))
|
|
1099
|
+
res.write(sseFrame("message", m));
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
for (const m of history.replayFor(lastSeen))
|
|
1103
|
+
res.write(sseFrame("message", m, history.cursorOf(m)));
|
|
1104
|
+
}
|
|
1023
1105
|
}
|
|
1024
1106
|
// Ticks change messages already sent and carry no cursor: every (re)connect gets where each one is now, so a
|
|
1025
1107
|
// report that fell into a gap is not lost until a reload (#1253 review). No id: it is not a message.
|
|
1026
|
-
if (ctx.webChatHistory)
|
|
1027
|
-
|
|
1108
|
+
if (ctx.webChatHistory) {
|
|
1109
|
+
const ticks = guest ? guestDeliveries(ctx.webChatHistory, guest, guest.scope()) : ctx.webChatHistory.deliveries();
|
|
1110
|
+
res.write(`event: deliveries\ndata: ${JSON.stringify(ticks)}\n\n`);
|
|
1111
|
+
}
|
|
1028
1112
|
// The same for the fleet prompts (C4): one posted, answered or expired during a gap is caught up here.
|
|
1029
|
-
if (ctx.listWebPrompts)
|
|
1113
|
+
if (!guest && ctx.listWebPrompts)
|
|
1030
1114
|
res.write(`event: prompts\ndata: ${JSON.stringify(ctx.listWebPrompts())}\n\n`);
|
|
1031
1115
|
// #1386: and "Needs you" as it is now; changes follow as `needs` events.
|
|
1032
|
-
if (ctx.needsYouItems)
|
|
1116
|
+
if (!guest && ctx.needsYouItems)
|
|
1033
1117
|
res.write(`event: needs\ndata: ${JSON.stringify({ items: ctx.needsYouItems() })}\n\n`);
|
|
1034
1118
|
ctx.sseClients.add(res);
|
|
1035
1119
|
const interval = setInterval(() => {
|
|
1036
1120
|
// A stream authorized once must not outlive the authorization. Re-checked
|
|
1037
1121
|
// without counting as activity, or an open tab would keep an idle session
|
|
1038
1122
|
// alive forever; a revoked, expired or rotated-away session ends here.
|
|
1039
|
-
|
|
1123
|
+
// #2049 review: a guest's stream also needs the exact session that opened it — another cookie on the same request
|
|
1124
|
+
// still authenticating does not keep a revoked device's stream (with its scope) alive.
|
|
1125
|
+
if (!isWebRequestAuthorized(req, url, ctx.webToken, ctx.webSessions, { touch: false }) || (guest && !guest.current())) {
|
|
1040
1126
|
// Close the connection too, not just the response: an ended response leaves the socket idle in
|
|
1041
1127
|
// keep-alive, still held open by whoever's session just ended.
|
|
1042
1128
|
const socket = req.socket;
|
|
@@ -1049,7 +1135,7 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1049
1135
|
}
|
|
1050
1136
|
try {
|
|
1051
1137
|
// Every open dashboard tab, every 10 s: the status of every instance, serialized on the fleet's loop (#1235).
|
|
1052
|
-
res.write(measureSyncWork("web.sseStatus",
|
|
1138
|
+
res.write(measureSyncWork("web.sseStatus", statusFrame) + sseClock());
|
|
1053
1139
|
}
|
|
1054
1140
|
catch {
|
|
1055
1141
|
cleanup();
|
|
@@ -1083,7 +1169,10 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1083
1169
|
json(res, 400, { error: "instance (1-128 chars) and limit (1-500) required" });
|
|
1084
1170
|
return true;
|
|
1085
1171
|
}
|
|
1086
|
-
|
|
1172
|
+
// #1920 §5.2: a guest gets what the channel showed of it, and its own messages (the gate checked the scope).
|
|
1173
|
+
const guest = guestRequestOf(req);
|
|
1174
|
+
const messages = !ctx.webChatHistory ? [] : guest ? guestHistory(ctx.webChatHistory, instance, limit, guest, guest.scope()) : ctx.webChatHistory.list(instance, limit);
|
|
1175
|
+
json(res, 200, { messages, boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0 });
|
|
1087
1176
|
return true;
|
|
1088
1177
|
}
|
|
1089
1178
|
// ── #1218: the Log tab ─────────────────────────────────
|
|
@@ -1138,6 +1227,16 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1138
1227
|
const fileMatch = path.match(/^\/ui\/file\/([^/]+)$/);
|
|
1139
1228
|
if (method === "GET" && fileMatch) {
|
|
1140
1229
|
const id = fileMatch[1];
|
|
1230
|
+
// #1920: a guest gets a file only when its stored instance is in scope — "no such file" otherwise, like an unknown id
|
|
1231
|
+
// (an id from another instance's chat is useless).
|
|
1232
|
+
const guest = guestRequestOf(req);
|
|
1233
|
+
if (guest) {
|
|
1234
|
+
const served = isFileId(id) ? ctx.webFiles?.servedFile?.(id) ?? null : null;
|
|
1235
|
+
if (!served || !guest.scope().has(served.instance)) {
|
|
1236
|
+
json(res, 404, { error: "No such file" });
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1141
1240
|
// #1589: audio and video, played in the page — one byte range at a time (206), by id only, never a whole video in
|
|
1142
1241
|
// memory. Every other type is answered exactly as before.
|
|
1143
1242
|
const media = isFileId(id) ? ctx.webFiles?.mediaOf?.(id) ?? null : null;
|
|
@@ -1457,13 +1556,28 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1457
1556
|
json(res, 400, { error: "instance, id and index required" });
|
|
1458
1557
|
return;
|
|
1459
1558
|
}
|
|
1559
|
+
// #1920 step 2b: a guest clicks only on a reply it may see (§5.2), of an in-scope instance, while guests may chat.
|
|
1560
|
+
const guest = guestRequestOf(req);
|
|
1561
|
+
const refused = guestChatRefusal(guest, instance);
|
|
1562
|
+
if (refused) {
|
|
1563
|
+
json(res, refused.status, { error: refused.error });
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (guest) {
|
|
1567
|
+
const row = guest.history?.()?.findByButtons(instance, id);
|
|
1568
|
+
if (!row || !guestSeesMessage(row, guest, guest.scope())) {
|
|
1569
|
+
json(res, 404, { error: "not found" });
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1460
1573
|
// #1823/#1827: the click is this page's person's — on a poll their vote (one per person, on every device of
|
|
1461
1574
|
// theirs), on one-click buttons their choice — under their web id and name. A request that no longer passes the
|
|
1462
1575
|
// gate (a session revoked meanwhile) clicks nothing as anyone.
|
|
1463
1576
|
const person = webPersonOr401(req, res, url, ctx);
|
|
1464
1577
|
if (!person)
|
|
1465
1578
|
return;
|
|
1466
|
-
|
|
1579
|
+
// #1920 review: a guest's choice reaches the agent marked as a guest's, as its typed messages do.
|
|
1580
|
+
const r = await click(instance, id, index, { key: person.voterKey, userId: person.userId, name: person.name, ...(guest ? { guest: true } : {}) });
|
|
1467
1581
|
json(res, r.status, r.status === 200 ? { answered: true, ...(r.options ? { options: r.options } : {}) } : { error: r.error ?? "Refused", ...(r.status === 409 ? { gone: true } : {}) });
|
|
1468
1582
|
})().catch(err => {
|
|
1469
1583
|
ctx.logger.error({ err: err.message }, "Web reply-button click failed");
|
|
@@ -1705,6 +1819,12 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
1705
1819
|
json(res, 404, { error: `Instance not found: ${name}` });
|
|
1706
1820
|
return true;
|
|
1707
1821
|
}
|
|
1822
|
+
// #1920 step 2b: a guest's Cancel is the platform button's (anyone allowed there), while guests may chat.
|
|
1823
|
+
const cancelRefused = guestChatRefusal(guestRequestOf(req), name);
|
|
1824
|
+
if (cancelRefused) {
|
|
1825
|
+
json(res, cancelRefused.status, { error: cancelRefused.error });
|
|
1826
|
+
return true;
|
|
1827
|
+
}
|
|
1708
1828
|
if (!ctx.cancelInstance(name)) {
|
|
1709
1829
|
json(res, 409, { error: `${name} is not running` });
|
|
1710
1830
|
return true;
|
|
@@ -2131,6 +2251,12 @@ export function handleWebRequest(req, res, url, ctx) {
|
|
|
2131
2251
|
json(res, 200, ctx.orgChart?.() ?? { general: [], teams: [], instances: {} });
|
|
2132
2252
|
return true;
|
|
2133
2253
|
}
|
|
2254
|
+
// #1918: the org canvas's traffic on opening, and the public link's (which has no stream) on its poll cadence. The
|
|
2255
|
+
// fleet works it out at most once every two seconds and caps it, whoever asks how often.
|
|
2256
|
+
if (method === "GET" && path === "/ui/traffic") {
|
|
2257
|
+
json(res, 200, ctx.trafficSnapshot?.() ?? { now: new Date().toISOString(), items: [] });
|
|
2258
|
+
return true;
|
|
2259
|
+
}
|
|
2134
2260
|
if (method === "GET" && path === "/ui/teams") {
|
|
2135
2261
|
const teams = ctx.fleetConfig?.teams ?? {};
|
|
2136
2262
|
json(res, 200, { teams });
|
|
@@ -2342,6 +2468,17 @@ function handleSendMessage(req, res, ctx) {
|
|
|
2342
2468
|
return;
|
|
2343
2469
|
}
|
|
2344
2470
|
const { instance, message: typed, attachments = [] } = parsed.data;
|
|
2471
|
+
// #1920 step 2b: a guest writes only to an in-scope instance, while guests may chat, and never a command.
|
|
2472
|
+
const guest = guestRequestOf(req);
|
|
2473
|
+
const refused = guestChatRefusal(guest, instance);
|
|
2474
|
+
if (refused) {
|
|
2475
|
+
json(res, refused.status, { error: refused.error });
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
if (guest && guestCommandText(typed)) {
|
|
2479
|
+
json(res, 403, { error: "guest_no_commands" });
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2345
2482
|
const ipc = ctx.instanceIpcClients.get(instance);
|
|
2346
2483
|
if (!ipc) {
|
|
2347
2484
|
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
@@ -2389,11 +2526,13 @@ function handleSendMessage(req, res, ctx) {
|
|
|
2389
2526
|
&& !ctx.isClassicInstance?.(instance) && syncAdapter && groupId && topicId != null;
|
|
2390
2527
|
// Add no runtime credentials or local attachment paths to the display copy.
|
|
2391
2528
|
const preview = webEchoPreview(message, files.map(f => f.name));
|
|
2392
|
-
// ClassicBot echo (#1320 part B): same ordering lane,
|
|
2393
|
-
//
|
|
2529
|
+
// ClassicBot echo (#1320 part B): same ordering lane, each room's setting (#2054: on unless it says false, or
|
|
2530
|
+
// the fleet-wide switch is off for a room that is unset) resolved at send time inside sendClassicWebEcho.
|
|
2394
2531
|
const canClassicEcho = ctx.isClassicInstance?.(instance) === true && !!ctx.sendClassicWebEcho;
|
|
2395
2532
|
const settleClassicEcho = canClassicEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
|
|
2396
|
-
await ctx.sendClassicWebEcho(instance, formatWebChannelEcho(person.name, preview, t("web.echo_full_text")), person.name);
|
|
2533
|
+
const posted = await ctx.sendClassicWebEcho(instance, formatWebChannelEcho(person.name, preview, t("web.echo_full_text"), { guest: !!guest }), person.name);
|
|
2534
|
+
if (posted > 0)
|
|
2535
|
+
ctx.webMessageInChannel?.(instance, messageId); // #1920 §5.2: the room saw it
|
|
2397
2536
|
}) : undefined;
|
|
2398
2537
|
const settleEcho = canEcho ? ctx.reserveWebChannelEcho?.(instance, async () => {
|
|
2399
2538
|
// A replacement adapter or edited binding is not the route we reserved.
|
|
@@ -2401,7 +2540,9 @@ function handleSendMessage(req, res, ctx) {
|
|
|
2401
2540
|
|| (ctx.getGroupIdForInstance?.(instance) ?? String(ctx.fleetConfig?.channel?.group_id ?? "")) !== groupId
|
|
2402
2541
|
|| ctx.fleetConfig?.instances[instance]?.topic_id !== topicId)
|
|
2403
2542
|
return;
|
|
2404
|
-
|
|
2543
|
+
const sent = await syncAdapter.sendText(String(groupId), formatWebChannelEcho(person.name, preview, t("web.echo_full_text"), { guest: !!guest }), { threadId: String(topicId), format: "text", allowedMentions: { parse: [] } });
|
|
2544
|
+
ctx.webMessageInChannel?.(instance, messageId); // #1920 §5.2: the topic saw it
|
|
2545
|
+
return sent;
|
|
2405
2546
|
}) : undefined;
|
|
2406
2547
|
let delivered;
|
|
2407
2548
|
try {
|
|
@@ -2419,6 +2560,7 @@ function handleSendMessage(req, res, ctx) {
|
|
|
2419
2560
|
ts,
|
|
2420
2561
|
thread_id: topicId != null ? String(topicId) : "",
|
|
2421
2562
|
source: "web",
|
|
2563
|
+
...(guest ? { web_guest: "1" } : {}), // #1920: "[user:… via web (guest), id:…]" (authority none)
|
|
2422
2564
|
adapter_id: syncAdapter?.id,
|
|
2423
2565
|
...delivery.meta,
|
|
2424
2566
|
},
|
|
@@ -2455,7 +2597,8 @@ function handleSendMessage(req, res, ctx) {
|
|
|
2455
2597
|
ctx.webFiles?.commit(files);
|
|
2456
2598
|
ctx.lastInboundUser.set(instance, person.name);
|
|
2457
2599
|
ctx.eventLog?.logActivity("message", person.name, (message || `[${files.length} file(s)]`).slice(0, 200), instance);
|
|
2458
|
-
|
|
2600
|
+
// #1920: the account that wrote it (a guest sees its own); channel-visible once its copy was sent (above).
|
|
2601
|
+
ctx.emitSseEvent("message", { instance, sender: person.name, role: "user", text: message, ts, attachments: files.map(publicAttachment), messageId, ...(person.bound ? { from: person.voterKey } : {}) });
|
|
2459
2602
|
json(res, 200, { sent: true, messageId });
|
|
2460
2603
|
}
|
|
2461
2604
|
catch {
|
|
@@ -2479,6 +2622,13 @@ function handleUpload(req, res, url, ctx) {
|
|
|
2479
2622
|
json(res, 404, { error: "uploads are not available" });
|
|
2480
2623
|
return;
|
|
2481
2624
|
}
|
|
2625
|
+
// #1920 step 2b: a guest's upload is for an in-scope instance's message, while guests may chat.
|
|
2626
|
+
const guestRefused = guestChatRefusal(guestRequestOf(req), instance);
|
|
2627
|
+
if (guestRefused) {
|
|
2628
|
+
req.resume();
|
|
2629
|
+
json(res, guestRefused.status, { error: guestRefused.error });
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2482
2632
|
if (!instance || instance.length > 128 || !safeInstanceSegment(instance) || !ctx.instanceIpcClients.has(instance)) {
|
|
2483
2633
|
json(res, 404, { error: `Instance not found: ${instance}` });
|
|
2484
2634
|
return;
|
|
@@ -2673,14 +2823,22 @@ function prefsViewer(req, url, ctx) {
|
|
|
2673
2823
|
const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
|
|
2674
2824
|
if (v.kind !== "allow")
|
|
2675
2825
|
return null;
|
|
2826
|
+
// #1920: a guest is a guest on every listener. The public link's page and reads too — before, a guest signed in through
|
|
2827
|
+
// it (#2094) read the host's preferences unfiltered: the sidebar's order and favourites, naming instances out of its
|
|
2828
|
+
// scope.
|
|
2829
|
+
const guest = v.via === "session" && v.session?.tier === "guest" ? { guest: true } : {};
|
|
2676
2830
|
if (!gatewayRequestContext(req) && v.via === "session" && v.session) {
|
|
2677
2831
|
const person = webPersonKey(v.session.owner);
|
|
2678
|
-
return { person, epoch: v.session.handle, scope: person === OPERATOR ? "operator" : "person", canWrite: v.session.tier === "admin", via: "session" };
|
|
2832
|
+
return { person, epoch: v.session.handle, scope: person === OPERATOR ? "operator" : "person", canWrite: v.session.tier === "admin", via: "session", ...guest };
|
|
2679
2833
|
}
|
|
2680
|
-
return { person: null, epoch: null, scope: "none", canWrite: v.via !== "session" || v.session?.tier === "admin", via: v.via };
|
|
2834
|
+
return { person: null, epoch: null, scope: "none", canWrite: v.via !== "session" || v.session?.tier === "admin", via: v.via, ...guest };
|
|
2681
2835
|
}
|
|
2682
2836
|
const prefsScope = (v) => ({ ownerScope: v?.scope ?? "none", ownerEpoch: v?.epoch ?? null });
|
|
2683
|
-
|
|
2837
|
+
// #1920: a guest reads the host's look and its own, never a preference that names an instance (guestPrefs).
|
|
2838
|
+
const prefsView = (prefs, v) => {
|
|
2839
|
+
const view = { ...prefs.readForViewer(v?.person ?? null), ...prefsScope(v) };
|
|
2840
|
+
return v?.guest ? guestPrefs(view) : view;
|
|
2841
|
+
};
|
|
2684
2842
|
function shellHtmlTag(ctx, mode, viewer) {
|
|
2685
2843
|
if (mode !== "full" || !ctx.webPrefs)
|
|
2686
2844
|
return '<html lang="en">';
|