@songsid/agend 2.3.0-beta.4 → 2.3.0-beta.6

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 (68) hide show
  1. package/dist/agent-file-read.d.ts +4 -1
  2. package/dist/agent-file-read.js +129 -16
  3. package/dist/agent-file-read.js.map +1 -1
  4. package/dist/authority/web-authority.d.ts +100 -0
  5. package/dist/authority/web-authority.js +130 -0
  6. package/dist/authority/web-authority.js.map +1 -0
  7. package/dist/channel/mcp-tools.js +1 -1
  8. package/dist/channel/mcp-tools.js.map +1 -1
  9. package/dist/config-validator.js +19 -1
  10. package/dist/config-validator.js.map +1 -1
  11. package/dist/daemon.js +5 -5
  12. package/dist/daemon.js.map +1 -1
  13. package/dist/delivery-outbox.d.ts +2 -0
  14. package/dist/delivery-outbox.js.map +1 -1
  15. package/dist/fleet-manager.d.ts +56 -0
  16. package/dist/fleet-manager.js +86 -10
  17. package/dist/fleet-manager.js.map +1 -1
  18. package/dist/general-knowledge/skills/web-appearance/SKILL.md +28 -11
  19. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +7 -3
  20. package/dist/human-delivery-receipts.d.ts +10 -1
  21. package/dist/human-delivery-receipts.js +24 -8
  22. package/dist/human-delivery-receipts.js.map +1 -1
  23. package/dist/outbound-schemas.d.ts +3 -3
  24. package/dist/reply-buttons-controller.d.ts +2 -0
  25. package/dist/reply-buttons-controller.js +2 -2
  26. package/dist/reply-buttons-controller.js.map +1 -1
  27. package/dist/settings-api.d.ts +9 -0
  28. package/dist/settings-api.js +76 -2
  29. package/dist/settings-api.js.map +1 -1
  30. package/dist/settings-change.js +66 -0
  31. package/dist/settings-change.js.map +1 -1
  32. package/dist/settings-effect.js +8 -1
  33. package/dist/settings-effect.js.map +1 -1
  34. package/dist/types.d.ts +12 -0
  35. package/dist/ui/chat-strings.js +4 -2
  36. package/dist/ui/panel-chat.js +15 -6
  37. package/dist/ui/panel-fleet.js +4 -1
  38. package/dist/ui/panel-settings.js +2 -0
  39. package/dist/ui/settings-catalog.js +4 -1
  40. package/dist/ui/settings-guests.js +117 -0
  41. package/dist/ui/settings-strings.js +22 -0
  42. package/dist/ui/shared/app-i18n.js +2 -0
  43. package/dist/ui/shared/app-prefs.js +3 -3
  44. package/dist/ui/shared/app-shell.js +36 -6
  45. package/dist/ui/shared/app-store.js +14 -0
  46. package/dist/ui/shared/app.css +32 -8
  47. package/dist/ui/shared/app.js +21 -14
  48. package/dist/ui/shared/instance-nav.js +8 -5
  49. package/dist/web-api.d.ts +16 -1
  50. package/dist/web-api.js +37 -10
  51. package/dist/web-api.js.map +1 -1
  52. package/dist/web-effect-upload.js +1 -1
  53. package/dist/web-effect-upload.js.map +1 -1
  54. package/dist/web-identity-color.d.ts +1 -1
  55. package/dist/web-identity-color.js +2 -2
  56. package/dist/web-identity-color.js.map +1 -1
  57. package/dist/web-prefs.js +1 -1
  58. package/dist/web-prefs.js.map +1 -1
  59. package/dist/web-returning-device.d.ts +2 -0
  60. package/dist/web-returning-device.js +11 -0
  61. package/dist/web-returning-device.js.map +1 -1
  62. package/dist/web-session.d.ts +7 -0
  63. package/dist/web-session.js +17 -0
  64. package/dist/web-session.js.map +1 -1
  65. package/dist/web-settings-patch.d.ts +25 -2
  66. package/dist/web-settings-patch.js +96 -1
  67. package/dist/web-settings-patch.js.map +1 -1
  68. package/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  // Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
10
10
  import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
11
11
  import { t } from "./app-i18n.js";
12
- import { appStore, createStore, useStore } from "./app-store.js";
12
+ import { appStore, createStore, guestPage, useStore } from "./app-store.js";
13
13
  import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
14
14
  import { Icon } from "./ui-icons.js";
15
15
  import { Skeleton } from "./ui-states.js";
@@ -391,10 +391,13 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
391
391
  const favs = nav.favorites.filter((n) => known.has(n));
392
392
  const favSet = new Set(favs);
393
393
  const canStar = !viewOnly && prefsWritable();
394
+ // #1920: a guest's list is to pick from — no reordering and no filters (one this device kept would hide rows with no
395
+ // way to clear it).
396
+ const guest = guestPage();
394
397
  const shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
395
398
  const shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
396
399
  if (favs.length) shownInsts.set(FAV_GROUP, favs);
397
- const f = nav.filter;
400
+ const f = guest ? { q: "", status: [], cli: [] } : nav.filter;
398
401
  const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
399
402
  // #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
400
403
  // it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
@@ -408,7 +411,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
408
411
  const flip = (key, value) => { const cur = new Set(navStore.get().filter[key]); if (cur.has(value)) cur.delete(value); else cur.add(value); setNavFilter({ [key]: [...cur] }); };
409
412
  // Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
410
413
  // their group.
411
- const dragProps = (item) => shown.active ? {} : {
414
+ const dragProps = (item) => shown.active || guest ? {} : {
412
415
  draggable: true,
413
416
  onDragStart: (e) => { drag.current = item; e.currentTarget.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data */ } },
414
417
  onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
@@ -504,7 +507,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
504
507
  })}
505
508
  ${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
506
509
  </div>
507
- <div class="v-filter">
510
+ ${guest ? null : html`<div class="v-filter">
508
511
  <label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
509
512
  <input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
510
513
  onInput=${(e) => setNavFilter({ q: e.target.value })}
@@ -520,7 +523,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
520
523
  </div>
521
524
  <p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
522
525
  </div>
523
- </div>
526
+ </div>`}
524
527
  </div>`;
525
528
  }
526
529
 
package/dist/web-api.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type HumanOrigin } from "./human-delivery-receipts.js";
2
+ import { type HumanAuthority, type WebAuthorityResult, type WebPeer } from "./authority/web-authority.js";
2
3
  import type { CachedImage } from "./web-images.js";
3
4
  import type { SettingsExecution } from "./settings-transaction.js";
4
5
  import type { PaneContextSource } from "./pane-context-cache.js";
@@ -13,7 +14,7 @@ import type { LifecycleCreateArgs } from "./instance-lifecycle.js";
13
14
  import { type PreviewAvailability } from "./web-preview.js";
14
15
  import { type WebChatHistory } from "./web-chat-history.js";
15
16
  import { type WebFileLedger } from "./web-upload.js";
16
- import type { WebSessionStore } from "./web-session.js";
17
+ import type { WebSessionStore, SessionRecord } from "./web-session.js";
17
18
  import { type WebPrefsStore } from "./web-prefs.js";
18
19
  import { type WebAccount } from "./web-profile.js";
19
20
  import { type MentionNames, type MentionQuery } from "./web-mentions.js";
@@ -204,10 +205,18 @@ export interface WebApiContext {
204
205
  }, opts: {
205
206
  publicLink: boolean;
206
207
  person?: WebPerson;
208
+ authority?: HumanAuthority;
207
209
  }): Promise<{
208
210
  status: number;
209
211
  body: unknown;
210
212
  }>;
213
+ /** #1939 S1: the design's §3 for one request (`instance` once the body names it); absent: web carries no authority. */
214
+ webAuthority?(input: {
215
+ session: SessionRecord | null;
216
+ gateway: boolean;
217
+ peer: WebPeer;
218
+ instance?: string;
219
+ }): WebAuthorityResult;
211
220
  /**
212
221
  * #1827: a web person — for the account a session is bound to (none: the operator) — as agents and chats see them.
213
222
  * Absent: everyone is "web-user", as before.
@@ -238,6 +247,7 @@ export interface WebApiContext {
238
247
  userId: string;
239
248
  name: string;
240
249
  guest?: true;
250
+ authority?: HumanAuthority;
241
251
  }): Promise<{
242
252
  status: number;
243
253
  error?: string;
@@ -359,6 +369,11 @@ export interface AppShellContext {
359
369
  readonly ownerCss?: OwnerCssStore;
360
370
  readonly webToken?: string | null;
361
371
  readonly webSessions?: WebApiContext["webSessions"];
372
+ /** #1920 phase 3: a guest's page says who it is signed in as (its web name and platform), for its "Guest" chip. */
373
+ guestBadge?(owner: WebAccount): {
374
+ name: string;
375
+ platform: string;
376
+ };
362
377
  }
363
378
  export type AppShellMode = "full" | "view-only";
364
379
  /** The app shell page for one entry, under the panels' CSP; it may frame exactly <preview origin>/frame, and only
package/dist/web-api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { webHumanOrigin } from "./human-delivery-receipts.js";
2
+ import { classifyWebPeer, confirmAfterBody, noAuthority } from "./authority/web-authority.js";
2
3
  import { guestChatRefusal, guestCommandText, guestDeliveries, guestFrameData, guestHistory, guestMessages, guestPollBody, guestPrefs, guestRequestOf, guestSeesMessage, guestStatus } from "./web-guest-access.js";
3
4
  import { withSpawnHoldReply } from "./spawn-hold-reply.js";
4
5
  import { binaryProbe } from "./binary-probe.js";
@@ -902,6 +903,17 @@ function webPersonOf(req, url, ctx) {
902
903
  const who = webAccountOf(req, url, ctx);
903
904
  return who ? ctx.webPerson(who.owner) : null;
904
905
  }
906
+ /**
907
+ * #1939 S1: whether this request carries `web-session` authority, asked as it stands now — on arrival without an instance,
908
+ * after the body with the one it names (then confirmAfterBody joins the two). Never touches the session.
909
+ */
910
+ function webAuthorityOf(req, url, ctx, instance) {
911
+ if (!ctx.webAuthority)
912
+ return { authority: noAuthority(0), refusal: "not_session" };
913
+ const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
914
+ return ctx.webAuthority({ session: v.kind === "allow" && v.via === "session" ? v.session : null, gateway: !!gatewayRequestContext(req),
915
+ peer: classifyWebPeer(req), ...(instance !== undefined ? { instance } : {}) });
916
+ }
905
917
  /** #1827: the person, or the request is answered 401 here (it is no longer authorized) and null is returned. */
906
918
  function webPersonOr401(req, res, url, ctx) {
907
919
  const p = webPersonOf(req, url, ctx);
@@ -989,6 +1001,8 @@ export function handleWebRequest(req, res, url, ctx) {
989
1001
  return true;
990
1002
  }
991
1003
  if (method === "GET" && path === "/ui/backends") {
1004
+ // #2072 (#2098 review P3-3): the backend an agent created with "fleet default" runs, so its model list can be shown.
1005
+ const defaultBackend = ctx.fleetConfig?.defaults?.backend ?? "claude-code";
992
1006
  const BACKENDS = [
993
1007
  { name: "claude-code", binary: "claude" },
994
1008
  { name: "codex", binary: "codex" },
@@ -1005,7 +1019,7 @@ export function handleWebRequest(req, res, url, ctx) {
1005
1019
  name: r.backend, binary: binaryOf.get(r.backend).binary, installed: r.state !== "not_installed" && r.path !== null, path: r.path ?? "",
1006
1020
  deprecated: binaryOf.get(r.backend).deprecated ?? false, state: r.state, minVersion: r.minVersion, ...(r.state === "unknown" ? { unknown: true } : {}),
1007
1021
  }));
1008
- json(res, 200, { backends });
1022
+ json(res, 200, { backends, defaultBackend });
1009
1023
  return true;
1010
1024
  }
1011
1025
  // #1490: seven sequential 2 s `execFileSync("which")` calls used to block the event loop here. The shared probe
@@ -1017,7 +1031,7 @@ export function handleWebRequest(req, res, url, ctx) {
1017
1031
  return { name: b.name, binary: b.binary, installed: path !== null, path: path ?? "", deprecated: b.deprecated ?? false,
1018
1032
  ...(found.known ? {} : { unknown: true }) };
1019
1033
  })).then((backends) => { if (!res.headersSent)
1020
- json(res, 200, { backends }); }, () => { if (!res.headersSent)
1034
+ json(res, 200, { backends, defaultBackend }); }, () => { if (!res.headersSent)
1021
1035
  json(res, 500, { error: "backend probe failed" }); });
1022
1036
  return true;
1023
1037
  }
@@ -1540,6 +1554,7 @@ export function handleWebRequest(req, res, url, ctx) {
1540
1554
  return true;
1541
1555
  }
1542
1556
  const click = ctx.clickWebReplyButton.bind(ctx);
1557
+ const arrival = webAuthorityOf(req, url, ctx); // #1939 §3: on arrival, and again after the body
1543
1558
  (async () => {
1544
1559
  let body;
1545
1560
  try {
@@ -1577,7 +1592,8 @@ export function handleWebRequest(req, res, url, ctx) {
1577
1592
  if (!person)
1578
1593
  return;
1579
1594
  // #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 } : {}) });
1595
+ const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, url, ctx, instance));
1596
+ const r = await click(instance, id, index, { key: person.voterKey, userId: person.userId, name: person.name, ...(guest ? { guest: true } : {}), authority });
1581
1597
  json(res, r.status, r.status === 200 ? { answered: true, ...(r.options ? { options: r.options } : {}) } : { error: r.error ?? "Refused", ...(r.status === 409 ? { gone: true } : {}) });
1582
1598
  })().catch(err => {
1583
1599
  ctx.logger.error({ err: err.message }, "Web reply-button click failed");
@@ -1761,6 +1777,7 @@ export function handleWebRequest(req, res, url, ctx) {
1761
1777
  return true;
1762
1778
  }
1763
1779
  const run = ctx.webCommand.bind(ctx);
1780
+ const arrival = webAuthorityOf(req, url, ctx); // #1939 §3: on arrival, and again after the body
1764
1781
  (async () => {
1765
1782
  let body;
1766
1783
  try {
@@ -1789,7 +1806,8 @@ export function handleWebRequest(req, res, url, ctx) {
1789
1806
  const person = webPersonOr401(req, res, url, ctx); // #1827: asked again, now the body is in
1790
1807
  if (!person)
1791
1808
  return;
1792
- const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req), person });
1809
+ const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, url, ctx, instance));
1810
+ const r = await run({ instance, command, args: str(body.args, MAX_COMMAND_ARGS), confirm: body.confirm }, { publicLink: !!gatewayRequestContext(req), person, authority });
1793
1811
  json(res, r.status, r.body);
1794
1812
  })().catch(err => {
1795
1813
  ctx.logger.error({ err: err.message }, "Web command failed");
@@ -2445,6 +2463,7 @@ function webEchoPreview(message, names) {
2445
2463
  }
2446
2464
  /** Handle POST /ui/send — extracted for readability. */
2447
2465
  function handleSendMessage(req, res, ctx) {
2466
+ const arrival = webAuthorityOf(req, new URL(req.url ?? "/", "http://localhost"), ctx); // #1939 §3: again after the body
2448
2467
  let body = "";
2449
2468
  let size = 0;
2450
2469
  req.on("data", (chunk) => {
@@ -2512,6 +2531,8 @@ function handleSendMessage(req, res, ctx) {
2512
2531
  json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
2513
2532
  return;
2514
2533
  }
2534
+ // #1939 S1: what the receipt records about who authorized it (observe only: agents still read web as recognition).
2535
+ const { authority } = confirmAfterBody(arrival, webAuthorityOf(req, new URL(req.url ?? "/", "http://localhost"), ctx, instance));
2515
2536
  const ts = new Date().toISOString();
2516
2537
  // The id the agent is given, and the one its delivery reports come back under: the page's ticks.
2517
2538
  const messageId = newWebMessageId();
@@ -2547,7 +2568,7 @@ function handleSendMessage(req, res, ctx) {
2547
2568
  let delivered;
2548
2569
  try {
2549
2570
  const deliver = (name, payload) => ctx.deliverHumanToInstance
2550
- ? ctx.deliverHumanToInstance(name, payload, webHumanOrigin(), typed)
2571
+ ? ctx.deliverHumanToInstance(name, payload, webHumanOrigin(false, authority), typed)
2551
2572
  : ctx.deliverToInstance(name, payload);
2552
2573
  delivered = await deliver(instance, {
2553
2574
  type: "fleet_inbound",
@@ -2825,8 +2846,8 @@ function prefsViewer(req, url, ctx) {
2825
2846
  return null;
2826
2847
  // #1920: a guest is a guest on every listener. The public link's page and reads too — before, a guest signed in through
2827
2848
  // 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 } : {};
2849
+ // scope. Its account (phase 3) names it on its page.
2850
+ const guest = v.via === "session" && v.session?.tier === "guest" ? { guest: true, ...(v.session.owner ? { owner: v.session.owner } : {}) } : {};
2830
2851
  if (!gatewayRequestContext(req) && v.via === "session" && v.session) {
2831
2852
  const person = webPersonKey(v.session.owner);
2832
2853
  return { person, epoch: v.session.handle, scope: person === OPERATOR ? "operator" : "person", canWrite: v.session.tier === "admin", via: "session", ...guest };
@@ -2854,6 +2875,10 @@ function shellHtmlTag(ctx, mode, viewer) {
2854
2875
  function shellOwnerCss(ctx, mode, viewer) {
2855
2876
  return mode === "full" && viewer?.person !== null && viewer?.person !== undefined && ctx.ownerCss ? ctx.ownerCss.active(viewer.person) : null;
2856
2877
  }
2878
+ function guestBodyAttrs(ctx, owner, attr) {
2879
+ const badge = ctx.guestBadge?.(owner);
2880
+ return ` data-tier="guest"${badge ? ` data-guest-name="${attr(badge.name)}" data-guest-platform="${attr(badge.platform)}"` : ""}`;
2881
+ }
2857
2882
  function shellBodyTag(req, ctx, mode, viewer) {
2858
2883
  const p = shellPreview(req, ctx, mode);
2859
2884
  const ownerCss = shellOwnerCss(ctx, mode, viewer);
@@ -2870,8 +2895,8 @@ function shellBodyTag(req, ctx, mode, viewer) {
2870
2895
  + ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
2871
2896
  // #1723: why the public link's preview tunnel failed (a fixed reason the page words), when it did.
2872
2897
  + `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
2873
- // #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔.
2874
- + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
2898
+ // #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔ — never to a guest (#1920).
2899
+ + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false || viewer?.guest ? "0" : "1"}"`
2875
2900
  // #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
2876
2901
  + ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
2877
2902
  // #1937: the upload limits in force for this page (the config's; the public link's own cap on it) — what the chat
@@ -2882,7 +2907,9 @@ function shellBodyTag(req, ctx, mode, viewer) {
2882
2907
  // #1629 r4 §3: the person's owner CSS in force (a sha), linked by the page unless this device or this load says not.
2883
2908
  + ` data-owner-css-state="${ownerCssState}"${ownerCss ? ` data-owner-css="${attr(ownerCss)}"` : ""}`
2884
2909
  // #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.
2885
- + `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}>`;
2910
+ + `${gatewayRequestContext(req) ? ' data-public-link="1"' : ""}`
2911
+ // #1920 phase 3: a guest's page — the shell shows only what a guest may use, and who it is signed in as.
2912
+ + `${mode === "full" && viewer?.guest && viewer.owner ? guestBodyAttrs(ctx, viewer.owner, attr) : ""}>`;
2886
2913
  }
2887
2914
  /** The app shell page for one entry, under the panels' CSP; it may frame exactly <preview origin>/frame, and only
2888
2915
  * when this load chose one. */