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

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 (180) hide show
  1. package/dist/agent-cli-instructions.md +1 -1
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +4 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-file-read.js +68 -8
  7. package/dist/agent-file-read.js.map +1 -1
  8. package/dist/agent-whats-new.js +4 -0
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.js +3 -1
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/authority/authority-state.d.ts +38 -0
  13. package/dist/authority/authority-state.js +114 -0
  14. package/dist/authority/authority-state.js.map +1 -0
  15. package/dist/authority/session-authorizations.d.ts +43 -0
  16. package/dist/authority/session-authorizations.js +102 -0
  17. package/dist/authority/session-authorizations.js.map +1 -0
  18. package/dist/authority/web-authority.d.ts +31 -11
  19. package/dist/authority/web-authority.js +39 -12
  20. package/dist/authority/web-authority.js.map +1 -1
  21. package/dist/cache-ledger.d.ts +16 -3
  22. package/dist/cache-ledger.js +33 -13
  23. package/dist/cache-ledger.js.map +1 -1
  24. package/dist/cache-service.d.ts +55 -3
  25. package/dist/cache-service.js +152 -16
  26. package/dist/cache-service.js.map +1 -1
  27. package/dist/channel/mcp-tools.js +12 -6
  28. package/dist/channel/mcp-tools.js.map +1 -1
  29. package/dist/classic-channel-manager.d.ts +2 -2
  30. package/dist/classic-channel-manager.js +2 -6
  31. package/dist/classic-channel-manager.js.map +1 -1
  32. package/dist/config-validator.js +10 -4
  33. package/dist/config-validator.js.map +1 -1
  34. package/dist/daemon.js +2 -0
  35. package/dist/daemon.js.map +1 -1
  36. package/dist/delivery-outbox.d.ts +8 -0
  37. package/dist/delivery-outbox.js +7 -1
  38. package/dist/delivery-outbox.js.map +1 -1
  39. package/dist/fleet-manager.d.ts +109 -2
  40. package/dist/fleet-manager.js +280 -12
  41. package/dist/fleet-manager.js.map +1 -1
  42. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
  43. package/dist/general-knowledge/skills/multi-channel/SKILL.md +2 -1
  44. package/dist/general-knowledge/skills/web-appearance/SKILL.md +81 -25
  45. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +55 -8
  46. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  47. package/dist/human-delivery-receipts.d.ts +14 -0
  48. package/dist/human-delivery-receipts.js +42 -6
  49. package/dist/human-delivery-receipts.js.map +1 -1
  50. package/dist/instance-names.d.ts +4 -0
  51. package/dist/instance-names.js +12 -0
  52. package/dist/instance-names.js.map +1 -0
  53. package/dist/instructions.d.ts +8 -1
  54. package/dist/instructions.js +9 -2
  55. package/dist/instructions.js.map +1 -1
  56. package/dist/locale.js +52 -0
  57. package/dist/locale.js.map +1 -1
  58. package/dist/outbound-handlers.d.ts +6 -0
  59. package/dist/outbound-handlers.js +45 -1
  60. package/dist/outbound-handlers.js.map +1 -1
  61. package/dist/outbound-schemas.d.ts +57 -1
  62. package/dist/outbound-schemas.js +33 -1
  63. package/dist/outbound-schemas.js.map +1 -1
  64. package/dist/public-web-gateway.js +8 -2
  65. package/dist/public-web-gateway.js.map +1 -1
  66. package/dist/rollout-index.d.ts +5 -0
  67. package/dist/rollout-index.js +26 -5
  68. package/dist/rollout-index.js.map +1 -1
  69. package/dist/settings-api.d.ts +6 -0
  70. package/dist/settings-api.js +50 -1
  71. package/dist/settings-api.js.map +1 -1
  72. package/dist/settings-change.js +27 -0
  73. package/dist/settings-change.js.map +1 -1
  74. package/dist/settings-effect.js +8 -1
  75. package/dist/settings-effect.js.map +1 -1
  76. package/dist/tool-permissions.d.ts +0 -7
  77. package/dist/tool-permissions.js +73 -5
  78. package/dist/tool-permissions.js.map +1 -1
  79. package/dist/topic-commands.d.ts +2 -1
  80. package/dist/topic-commands.js +2 -4
  81. package/dist/topic-commands.js.map +1 -1
  82. package/dist/types.d.ts +13 -2
  83. package/dist/ui/app-effects.js +43 -25
  84. package/dist/ui/chat-ambient.js +306 -0
  85. package/dist/ui/chat-effect-posters.js +6 -7
  86. package/dist/ui/chat-effects.js +38 -15
  87. package/dist/ui/chat-strings.js +17 -4
  88. package/dist/ui/panel-chat.js +15 -7
  89. package/dist/ui/panel-settings.js +4 -1
  90. package/dist/ui/settings-appearance.js +8 -0
  91. package/dist/ui/settings-mods.js +19 -6
  92. package/dist/ui/settings-owner-effects.js +3 -3
  93. package/dist/ui/settings-strings.js +28 -0
  94. package/dist/ui/settings-web-authority.js +91 -0
  95. package/dist/ui/shared/app-session.js +2 -0
  96. package/dist/ui/shared/app-shell.js +2 -1
  97. package/dist/ui/shared/app-themes.js +24 -6
  98. package/dist/ui/shared/app.css +28 -0
  99. package/dist/ui/shared/effect-app-runtime.js +67 -18
  100. package/dist/ui/shared/effect-chat-runtime.js +16 -3
  101. package/dist/ui/shared/effect-frame.js +17 -3
  102. package/dist/ui/shared/effect-policy.js +18 -4
  103. package/dist/ui/shared/effect-registry.js +11 -18
  104. package/dist/ui/shared/effect-stock-trust.js +2 -2
  105. package/dist/ui/shared/effect-trial-stage.js +18 -7
  106. package/dist/ui/shared/fx-motion.js +58 -0
  107. package/dist/ui/shared/web-authorize.js +151 -0
  108. package/dist/usage-report.d.ts +147 -0
  109. package/dist/usage-report.js +195 -0
  110. package/dist/usage-report.js.map +1 -0
  111. package/dist/usage-rollup.d.ts +51 -0
  112. package/dist/usage-rollup.js +168 -0
  113. package/dist/usage-rollup.js.map +1 -0
  114. package/dist/web-ambient.d.ts +72 -0
  115. package/dist/web-ambient.js +154 -0
  116. package/dist/web-ambient.js.map +1 -0
  117. package/dist/web-api.d.ts +32 -0
  118. package/dist/web-api.js +143 -18
  119. package/dist/web-api.js.map +1 -1
  120. package/dist/web-appearance-tools.d.ts +3 -0
  121. package/dist/web-appearance-tools.js +18 -3
  122. package/dist/web-appearance-tools.js.map +1 -1
  123. package/dist/web-appearance-view.d.ts +14 -2
  124. package/dist/web-appearance-view.js +32 -5
  125. package/dist/web-appearance-view.js.map +1 -1
  126. package/dist/web-appearance.d.ts +15 -1
  127. package/dist/web-appearance.js +77 -1
  128. package/dist/web-appearance.js.map +1 -1
  129. package/dist/web-asset-parts.d.ts +2 -2
  130. package/dist/web-asset-parts.js +3 -3
  131. package/dist/web-asset-parts.js.map +1 -1
  132. package/dist/web-auth.js +3 -1
  133. package/dist/web-auth.js.map +1 -1
  134. package/dist/web-effect-bootstrap.d.ts +12 -0
  135. package/dist/web-effect-bootstrap.js +49 -0
  136. package/dist/web-effect-bootstrap.js.map +1 -0
  137. package/dist/web-effect-bundle.js +46 -1
  138. package/dist/web-effect-bundle.js.map +1 -1
  139. package/dist/web-effect-playback.d.ts +30 -0
  140. package/dist/web-effect-playback.js +71 -0
  141. package/dist/web-effect-playback.js.map +1 -0
  142. package/dist/web-effect-profiles.d.ts +4 -4
  143. package/dist/web-effect-service.d.ts +11 -4
  144. package/dist/web-effect-service.js +25 -4
  145. package/dist/web-effect-service.js.map +1 -1
  146. package/dist/web-effect-tools.d.ts +1 -0
  147. package/dist/web-effect-tools.js +2 -1
  148. package/dist/web-effect-tools.js.map +1 -1
  149. package/dist/web-effect-worker.d.ts +1 -1
  150. package/dist/web-effect-worker.js +2 -2
  151. package/dist/web-effect-worker.js.map +1 -1
  152. package/dist/web-guest-access.js +3 -1
  153. package/dist/web-guest-access.js.map +1 -1
  154. package/dist/web-mod-agent.js +5 -1
  155. package/dist/web-mod-agent.js.map +1 -1
  156. package/dist/web-mod-bundle.d.ts +9 -1
  157. package/dist/web-mod-bundle.js +62 -3
  158. package/dist/web-mod-bundle.js.map +1 -1
  159. package/dist/web-mod-export.d.ts +9 -0
  160. package/dist/web-mod-export.js +21 -0
  161. package/dist/web-mod-export.js.map +1 -1
  162. package/dist/web-mod-store.d.ts +2 -1
  163. package/dist/web-mod-store.js +13 -6
  164. package/dist/web-mod-store.js.map +1 -1
  165. package/dist/web-session-authorization.d.ts +132 -0
  166. package/dist/web-session-authorization.js +193 -0
  167. package/dist/web-session-authorization.js.map +1 -0
  168. package/dist/web-session.d.ts +5 -0
  169. package/dist/web-session.js +16 -0
  170. package/dist/web-session.js.map +1 -1
  171. package/dist/web-settings-patch.d.ts +24 -2
  172. package/dist/web-settings-patch.js +47 -1
  173. package/dist/web-settings-patch.js.map +1 -1
  174. package/dist/web-settings-tools.d.ts +8 -0
  175. package/dist/web-settings-tools.js +14 -1
  176. package/dist/web-settings-tools.js.map +1 -1
  177. package/dist/web-theme-store.d.ts +39 -0
  178. package/dist/web-theme-store.js +122 -11
  179. package/dist/web-theme-store.js.map +1 -1
  180. package/package.json +1 -1
@@ -227,11 +227,15 @@ import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken } f
227
227
  import { bypassesWebGate, handleAuthRequest, serveSigninPage } from "./auth-api.js";
228
228
  import { isWebPageNavigation } from "./web-shell-routes.js";
229
229
  import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
230
- import { evaluateWebAuthority, webAuthorityPolicy } from "./authority/web-authority.js";
230
+ import { authorityNow, evaluateWebAuthority, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
231
+ import { AuthorityStateStore } from "./authority/authority-state.js";
232
+ import { SessionAuthorizations } from "./authority/session-authorizations.js";
233
+ import { SessionAuthorizationRequests } from "./web-session-authorization.js";
231
234
  import { GuestLoginCodes, WebLoginCodes, LOGIN_CODE_TTL_MS } from "./web-login.js";
232
235
  import { CodeRequestLimiter, ReturningDevices } from "./web-returning-device.js";
233
- import { allowedHostNames, applyWebSecurityHeaders, hostnameOf, isHostAllowed, WEB_HOST_REJECTED_MESSAGE } from "./web-host-guard.js";
236
+ import { allowedHostNames, applyWebSecurityHeaders, hostnameOf, isHostAllowed, LOOPBACK_HOST_NAMES, WEB_HOST_REJECTED_MESSAGE } from "./web-host-guard.js";
234
237
  import { createPreviewListener, normalizeOrigin, previewAvailability, previewSettings } from "./web-preview.js";
238
+ import { createEffectBootstrap } from "./web-effect-bootstrap.js";
235
239
  import { fleetLevelDifferences, fleetLevelSignature, appendedConnections } from "./fleet-level-config.js";
236
240
  import { checkSelfRestartAllowance, recordSelfRestartAttempt } from "./self-restart-limit.js";
237
241
  import { SecretStore } from "./secret-store.js";
@@ -779,6 +783,10 @@ export class FleetManager {
779
783
  collabInstances = new Set();
780
784
  // Health endpoint
781
785
  healthServer = null;
786
+ /** #2118: independent of HTML preview availability/port; fixed bootstrap only, no effect resources. */
787
+ effectBootstrap = createEffectBootstrap();
788
+ /** Fixed trusted document coherence, independent of the HTML preview listener; never a resource grant. */
789
+ get effectBoot() { return this.effectBootstrap.boot; }
782
790
  healthServerDrain = null;
783
791
  /** #1306: the preview listener (health_port + 1 by default), and whether it is listening. */
784
792
  previewListener = null;
@@ -1003,7 +1011,7 @@ export class FleetManager {
1003
1011
  this.emitSseEvent("status", this.getUiStatus());
1004
1012
  }
1005
1013
  catch { /* later status carries placements */ } },
1006
- // A nonmutating own/shared offer is not an appearance write. Only an explicit local-admin click may start it.
1014
+ // A nonmutating own/shared offer is not an appearance write. Only an explicit non-public admin-session click may start it.
1007
1015
  preview: async (instance, id) => {
1008
1016
  const result = this.effectTrials.offer(instance, id);
1009
1017
  if (!result.ok)
@@ -1011,11 +1019,12 @@ export class FleetManager {
1011
1019
  const present = this.webChatHistory.list(instance).some(row => row.effectTrial?.offer === result.offer.offer);
1012
1020
  if (!present && !this.publishEffectTrial(instance, result.offer))
1013
1021
  return { error: "trial_unavailable" };
1014
- return { id, status: "offered", expiresAt: result.offer.expiresAt, message: "A trial card is in your web chat. A signed-in local admin can start it; nothing was applied." };
1022
+ return { id, status: "offered", expiresAt: result.offer.expiresAt, message: "A trial card is in your web chat. A signed-in admin outside the public link can start it; nothing was applied." };
1015
1023
  },
1016
1024
  profiles: id => this.effectProfiles.read(id),
1017
1025
  profileSummary: id => this.effectService.profileSummary(id),
1018
1026
  profileSummaries: ids => this.effectService.profileSummaries(ids),
1027
+ playbackSummary: (name, id) => this.effectService.playbackSummary(name, id),
1019
1028
  }));
1020
1029
  }
1021
1030
  appearanceToolsService;
@@ -1023,7 +1032,12 @@ export class FleetManager {
1023
1032
  get appearanceTools() {
1024
1033
  return (this.appearanceToolsService ??= new AppearanceTools({
1025
1034
  frame: (name) => (this.getUiStatus().instances.find(i => i.name === name) ?? { name }),
1026
- theme: (name) => { const r = this.themeStore.get(name); return { theme: r.theme, locked: r.locked, widgets: r.placements.widgets }; },
1035
+ theme: (name) => {
1036
+ const r = this.themeStore.get(name);
1037
+ return { theme: r.theme, locked: r.locked, widgets: r.placements.widgets, ambient: r.placements.ambient,
1038
+ glyphAsset: (id) => this.themeStore.asset(name, id) };
1039
+ },
1040
+ effects: name => this.themeStore.readable() ? this.themeStore.get(name).placements.effects.map(p => ({ id: p.id, by: p.by, ...(p.params ? { params: p.params } : {}), playback: this.effectService.playbackSummary(name, p.id) })) : [],
1027
1041
  card: (name) => resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card),
1028
1042
  // #1958: the host's identity-colour settings, as the page reads them (an unreadable prefs file: the defaults).
1029
1043
  colorPrefs: () => { try {
@@ -1121,6 +1135,8 @@ export class FleetManager {
1121
1135
  setBannerFocus: (name, focus) => { const w = setPersonBannerFocus(this.dataDir, name, focus); if (w.ok)
1122
1136
  this.identityChanged(); return w; },
1123
1137
  placements: (name) => this.themeStore.get(name).placements.effects.map((e) => (typeof e === "string" ? { id: e } : e)),
1138
+ ambient: (name) => Object.fromEntries(Object.entries(this.themeStore.get(name).placements.ambient).flatMap(([k, a]) => a ? [[k, { spec: a.spec, by: a.by }]] : [])),
1139
+ setAmbient: (name, spec, which) => this.webAppearance.humanSetAmbient(name, spec, which),
1124
1140
  announce: () => { try {
1125
1141
  this.emitSseEvent("status", this.getUiStatus());
1126
1142
  }
@@ -1745,10 +1761,18 @@ export class FleetManager {
1745
1761
  queryDurableDeliveryStatus(callerInstance, selector) {
1746
1762
  this.ensureDeliveryOutbox();
1747
1763
  const page = this.deliveryOutbox.queryStatusForInstance(callerInstance, selector);
1764
+ // #1939 §10: a human receipt's authority as it reads now (switch, epoch, loopback_only).
1765
+ const state = page.items.some(item => item.authority) ? this.webAuthorityState() : null;
1766
+ const human = (item) => {
1767
+ if (!item.authority || !state)
1768
+ return item;
1769
+ const authority = authorityNow(item.authority, state);
1770
+ return { ...item, authority, ...(authority.downgraded ? { source_instance: "human:web:unknown" } : {}) };
1771
+ };
1748
1772
  // #1641: a sender that sees only `queued` cannot tell "next in line" from "the target is stuck": say which.
1749
1773
  return {
1750
1774
  ...page,
1751
- items: page.items.map(item => (item.state === "queued" || item.state === "retry_wait")
1775
+ items: page.items.map(human).map(item => (item.state === "queued" || item.state === "retry_wait")
1752
1776
  ? { ...item, waiting_reason: this.deliveryWaitingReason(item.target_instance) }
1753
1777
  : item),
1754
1778
  };
@@ -3832,24 +3856,31 @@ export class FleetManager {
3832
3856
  }
3833
3857
  /** Evidence-only admission. Bots/system/agent payloads have no trusted origin. */
3834
3858
  humanReceiptPayload(instanceName, payload, origin, originalBody) {
3859
+ // #1939 S3: "via web (verified)" comes only from the receipt written here — never from a caller's meta.
3860
+ const given = payload.meta;
3861
+ if (given && "web_verified" in given) {
3862
+ const { web_verified: _claimed, ...rest } = given;
3863
+ payload = { ...payload, meta: rest };
3864
+ }
3835
3865
  if (!origin || !this.deliveryOutbox)
3836
3866
  return payload;
3837
3867
  const row = this.deliveryOutbox.createHumanReceipt({ targetInstance: instanceName, originalBody,
3838
3868
  origin: { ...origin }, inputKind: String(payload.type), managerBootId: this.managerBootId });
3839
3869
  // #1939 §7: each ordinary verified web message, for the S1 beta to watch. No text, cookie or session id: the handle.
3840
3870
  const authority = origin.authority;
3841
- if (authority?.kind === "web-session" && authority.actor && authority.session) {
3871
+ if (authority?.kind === "web-verified" && authority.actor && authority.session) {
3842
3872
  try {
3843
3873
  this.eventLog?.insert(instanceName, "web_authority", { receipt_id: row.receipt_id, session: authority.session.handle,
3844
3874
  peer: authority.session.peer, surface: "local", actor: `${authority.actor.adapter_id}:${authority.actor.user_id}`,
3845
- input_kind: String(payload.type), outcome: "web-session", authority_epoch: authority.authority_epoch });
3875
+ input_kind: String(payload.type), outcome: "web-verified", authority_epoch: authority.authority_epoch });
3846
3876
  }
3847
3877
  catch (err) {
3848
3878
  this.logger.warn({ err, instanceName }, "Could not record web authority");
3849
3879
  }
3850
3880
  }
3881
+ const verified = origin.platform === "web" && authority?.kind === "web-verified";
3851
3882
  return { ...payload, meta: { ...(payload.meta ?? {}),
3852
- human_receipt_id: row.receipt_id, verification_id: row.message_id } };
3883
+ human_receipt_id: row.receipt_id, verification_id: row.message_id, ...(verified ? { web_verified: "1" } : {}) } };
3853
3884
  }
3854
3885
  /** Bind at the actual IPC send, after all wake/reconnect waits. No human retry lane. */
3855
3886
  bindHumanReceiptPayload(instanceName, payload) {
@@ -6045,7 +6076,9 @@ export class FleetManager {
6045
6076
  // #1570: no browser may ask the sign-in page for a code any more.
6046
6077
  const devices = this.returningDevices.revokeAll();
6047
6078
  const sessions = this.webSessions.revokeAll();
6048
- const result = { count: sessions.count, durable: sessions.durable && devices.durable };
6079
+ // #1939 §10: and every web authority given so far (its receipts read as none).
6080
+ const authority = this.revokeWebAuthority();
6081
+ const result = { count: sessions.count, durable: sessions.durable && devices.durable && authority.durable };
6049
6082
  if (result.durable)
6050
6083
  this.logger.info({ count: result.count, devices: devices.count }, "Web sessions revoked (all)");
6051
6084
  else
@@ -7078,6 +7111,13 @@ export class FleetManager {
7078
7111
  this.pruneEventLog();
7079
7112
  this.eventLogPruneTimer = setInterval(() => this.pruneEventLog(), 24 * 60 * 60_000);
7080
7113
  this.eventLogPruneTimer.unref?.();
7114
+ // #2122 §5.5: usage is collected from the start, not from the first time someone opens a report — the same bounded
7115
+ // background passes the cache report uses (16 MB a pass, then every 10 minutes).
7116
+ {
7117
+ const cache = this.cacheServiceFor();
7118
+ cache.start();
7119
+ void cache.kick();
7120
+ }
7081
7121
  // #1920 §3.2: guests' ClassicBot room membership, read in the background — now, then each key ten minutes after
7082
7122
  // its answer (a minute after a failed read; the tick only finds what is due). A request only reads the answers.
7083
7123
  void this.refreshAllGuestRooms();
@@ -10008,6 +10048,7 @@ export class FleetManager {
10008
10048
  if (type === "fleet_get_web_theme" || type === "fleet_set_web_theme" || type === "fleet_set_web_theme_asset" || type === "fleet_reset_web_theme"
10009
10049
  || type === "fleet_get_web_appearance" || type === "fleet_preview_web_appearance"
10010
10050
  || type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
10051
+ || type === "fleet_set_web_ambient" || type === "fleet_clear_web_ambient"
10011
10052
  || type === "fleet_export_web_mod" || type === "fleet_propose_web_mod") {
10012
10053
  this.handleWebTheme(name, msg);
10013
10054
  return;
@@ -12891,6 +12932,8 @@ export class FleetManager {
12891
12932
  }
12892
12933
  /** Routes a click to the handler that owns its prefix; false when none does. */
12893
12934
  async dispatchAdapterCallback(data, adapterId, adapter) {
12935
+ if (await this.sessionAuthorizationRequests().handleCallback(data, adapterId))
12936
+ return true; // #1939 S2b
12894
12937
  if (data.callbackData.startsWith(REPLY_BUTTON_PREFIX)) { // #1266
12895
12938
  const buttons = this.replyButtons();
12896
12939
  if (!buttons) {
@@ -13028,7 +13071,7 @@ export class FleetManager {
13028
13071
  cacheInstances() {
13029
13072
  const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
13030
13073
  name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
13031
- claudeConfigDir: this.claudeConfigDirOf(name),
13074
+ claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
13032
13075
  }));
13033
13076
  const taken = new Set(fleet.map(i => i.name));
13034
13077
  const classic = (this.classicChannels?.getAll() ?? []).filter(ch => !taken.has(ch.instanceName)).map(ch => ({
@@ -13037,6 +13080,7 @@ export class FleetManager {
13037
13080
  workingDirectory: join(getAgendHome(), "workspaces", ch.instanceName),
13038
13081
  ledgerPath: join(this.getInstanceDir(ch.instanceName), "cache-ledger.json"),
13039
13082
  claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
13083
+ profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
13040
13084
  }));
13041
13085
  return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
13042
13086
  }
@@ -13048,6 +13092,45 @@ export class FleetManager {
13048
13092
  cacheReport(window) {
13049
13093
  return this.cacheServiceFor().report(cacheWindow(window));
13050
13094
  }
13095
+ /**
13096
+ * #2122 §5.1: the profile the usage read now is tagged with — the claude profile whose config dir is read ("default"
13097
+ * without one); codex "unrecorded" (its rollouts don't say which login wrote them).
13098
+ */
13099
+ usageProfileOf(name, backend) {
13100
+ if (backend === "claude-code")
13101
+ return this.credentialProfileOf(name, "claude-code") ?? "default";
13102
+ if (backend === "codex")
13103
+ return "unrecorded";
13104
+ return undefined;
13105
+ }
13106
+ /** #2122: the fleet's day boundary zone — defaults.scheduler.default_timezone, else this host's. */
13107
+ usageZone() {
13108
+ return this.fleetConfig?.defaults?.scheduler?.default_timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
13109
+ }
13110
+ /**
13111
+ * #2122: the usage report (usage-report.ts) — from the ledgers and rollups as they are now and the delivered-message
13112
+ * counts; never a transcript read. `only`: just that instance (an ordinary agent's own row).
13113
+ */
13114
+ async usageReport(params) {
13115
+ const { buildUsageReport } = await import("./usage-report.js");
13116
+ const data = await this.cacheServiceFor().usageData();
13117
+ const team = new Map();
13118
+ for (const [t, cfg] of Object.entries(this.fleetConfig?.teams ?? {}))
13119
+ for (const m of cfg?.members ?? [])
13120
+ if (!team.has(m))
13121
+ team.set(m, t);
13122
+ const now = Date.now();
13123
+ this.ensureDeliveryOutbox?.();
13124
+ // Every hour of the kept history: the report places each on a local date and keeps the range's own.
13125
+ const messages = this.deliveryOutbox?.deliveredMessageHours(now - 401 * 86_400_000, now + 3_600_000) ?? [];
13126
+ const keep = (name) => params.only === undefined || name === params.only;
13127
+ return buildUsageReport({
13128
+ now, zone: this.usageZone(), range: params.range, group: params.group, scanning: data.scanning,
13129
+ instances: data.instances.filter(i => keep(i.inst.name)).map(i => ({ name: i.inst.name, backend: i.inst.backend, team: team.get(i.inst.name) ?? null,
13130
+ status: i.status, ...(i.with ? { with: i.with } : {}), ledger: i.ledger, days: i.days })),
13131
+ messages: messages.filter(m => keep(m.instance)),
13132
+ });
13133
+ }
13051
13134
  /** #2001: get_cache_report's — the same report, as last worked out (reused for a minute, until the next pass). */
13052
13135
  cacheReportPublished(window) {
13053
13136
  return this.cacheServiceFor().published(cacheWindow(window));
@@ -13059,7 +13142,9 @@ export class FleetManager {
13059
13142
  claudeKey: claudeProjectKey,
13060
13143
  codexSessionsDir: () => join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions"),
13061
13144
  listRollouts: root => sharedRolloutIndex(root).list(),
13145
+ rolloutsUnreadable: root => sharedRolloutIndex(root).unreadable,
13062
13146
  metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
13147
+ zone: () => this.usageZone(),
13063
13148
  log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
13064
13149
  });
13065
13150
  }
@@ -13576,13 +13661,192 @@ export class FleetManager {
13576
13661
  * token; `instance` once the body has named it. Observe only: receipts record it, no agent acts on it yet.
13577
13662
  */
13578
13663
  webAuthority(input) {
13664
+ const state = this.webAuthorityState();
13579
13665
  return evaluateWebAuthority({
13580
13666
  session: input.session, gateway: input.gateway, peer: input.peer,
13581
13667
  tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
13582
13668
  ...(input.instance !== undefined ? { target: { adapterId: this.getInstanceAdapterId(input.instance) } } : {}),
13583
13669
  isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
13584
- policy: webAuthorityPolicy(this.fleetConfig?.web), epoch: 0, now: Date.now(),
13670
+ authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
13671
+ policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly }, epoch: state.epoch, now: Date.now(),
13672
+ });
13673
+ }
13674
+ sessionAuthorizations = null;
13675
+ sessionAuthorizationStore() {
13676
+ if (!this.dataDir)
13677
+ return null;
13678
+ this.sessionAuthorizations ??= new SessionAuthorizations(join(this.dataDir, "web-authorizations.json"), this.logger);
13679
+ return this.sessionAuthorizations;
13680
+ }
13681
+ /**
13682
+ * #1939 §0: record that a session's account has authorized it — called only once that account has confirmed on Discord
13683
+ * or Telegram (S2b). The session must qualify now (§3, as a message from it would be judged); then every message,
13684
+ * click, /steer and /btw from it is web-verified until max_session_hours after now, the session ends, or a revocation.
13685
+ * Ones no longer worth keeping (their session gone, an old epoch, run out) are dropped on the way.
13686
+ */
13687
+ authorizeWebSession(session, peer, gateway) {
13688
+ const store = this.sessionAuthorizationStore();
13689
+ const state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
13690
+ if (!store || !session.owner)
13691
+ return { ok: false, refusal: !store ? "disabled" : "no_account" };
13692
+ const check = evaluateWebAuthority({
13693
+ session, gateway, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
13694
+ isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
13695
+ authorization: { handle: session.handle, owner: session.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
13696
+ policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly }, epoch: state.epoch, now,
13697
+ });
13698
+ if (check.refusal)
13699
+ return { ok: false, refusal: check.refusal };
13700
+ const live = new Set(this.webSessions?.list(undefined, () => true).map(r => r.handle) ?? []);
13701
+ store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
13702
+ return { ok: true, ...store.authorize(session.handle, session.owner, now, state.epoch, now + policy.maxSessionMs) };
13703
+ }
13704
+ authorityStateStore = null;
13705
+ /**
13706
+ * #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
13707
+ * it is now (a change since last seen moves the epoch first; authority-state.ts). No data directory: off.
13708
+ */
13709
+ webAuthorityState() {
13710
+ if (!this.dataDir)
13711
+ return { enabled: false, loopbackOnly: true, epoch: 0 };
13712
+ this.authorityStateStore ??= new AuthorityStateStore(join(this.dataDir, "web-authority.json"), this.logger, undefined, undefined, () => { this.ensureDeliveryOutbox?.(); return this.deliveryOutbox?.maxWebAuthorityEpoch() ?? 0; });
13713
+ return this.authorityStateStore.current(this.fleetConfig?.web?.verified_authority);
13714
+ }
13715
+ /**
13716
+ * #1939 S2a: Settings → Web → Web authority — the state in force (the switch counts as off while its state cannot be
13717
+ * written), what fleet.yaml says, the hosts that make the dashboard reachable from elsewhere (the warning while
13718
+ * loopback_only is off), and the sessions authorized now (§0): each one's device, account, and when it runs out.
13719
+ */
13720
+ webAuthorityOverview() {
13721
+ const configured = this.fleetConfig?.web?.verified_authority ?? {};
13722
+ const lanHosts = (this.fleetConfig?.web?.allowed_hosts ?? []).filter(h => { const name = hostnameOf(String(h)); return name !== null && !LOOPBACK_HOST_NAMES.includes(name); });
13723
+ const state = this.webAuthorityState(), maxMs = webAuthorityPolicy(this.fleetConfig?.web).maxSessionMs, now = Date.now();
13724
+ const sessions = new Map((this.webSessions?.list(undefined, () => true) ?? []).map(r => [r.handle, r]));
13725
+ const authorized = state.enabled ? (this.sessionAuthorizationStore()?.list() ?? [])
13726
+ .filter(a => a.epoch === state.epoch && sessions.has(a.handle) && now <= Math.min(a.until, a.authorizedAt + maxMs))
13727
+ .map(a => ({ handle: a.handle, label: sessions.get(a.handle).label, account: `${a.owner.adapterId}:${a.owner.userId}`, authorizedAt: a.authorizedAt,
13728
+ until: Math.min(a.until, a.authorizedAt + maxMs, sessions.get(a.handle).absoluteExpiry) })) : [];
13729
+ return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized };
13730
+ }
13731
+ authorizationRequests = null;
13732
+ /**
13733
+ * #1939 S2b (§0.1): "Authorize this browser" — the Confirm card in the account's DM. Confirm runs §3 again at the tap
13734
+ * (authorizeWebSession on the session read again by its handle); Not me signs the session out.
13735
+ */
13736
+ sessionAuthorizationRequests() {
13737
+ if (this.authorizationRequests)
13738
+ return this.authorizationRequests;
13739
+ const adapterOf = (adapterId) => this.adapters.get(adapterId) ?? this.worlds.get(adapterId)?.adapter;
13740
+ // The host's clock, with its zone: the person may read the DM on a phone in another one.
13741
+ const when = (ms) => new Date(ms).toLocaleString(undefined, { timeZoneName: "short" });
13742
+ const where = (peer) => t(peer === "loopback" ? "web_auth.peer_loopback" : "web_auth.peer_lan");
13743
+ return this.authorizationRequests = new SessionAuthorizationRequests({
13744
+ now: () => performance.now(), wallNow: () => Date.now(),
13745
+ maxSessionMs: () => webAuthorityPolicy(this.fleetConfig?.web).maxSessionMs,
13746
+ sendDirect: async (owner, text, choices) => {
13747
+ const adapter = adapterOf(owner.adapterId);
13748
+ if (!adapter?.sendDirect)
13749
+ return null;
13750
+ const sent = await adapter.sendDirect(owner.userId, text, { choices, allowedMentions: { parse: [] } });
13751
+ return sent?.messageId ? { chatId: sent.chatId, messageId: sent.messageId } : null;
13752
+ },
13753
+ sendGeneralHint: async (owner, text) => {
13754
+ const adapter = adapterOf(owner.adapterId), general = this.findGeneralInstance(owner.adapterId);
13755
+ const topic = general ? this.fleetConfig?.instances[general]?.topic_id : undefined;
13756
+ const group = general ? this.getGroupIdForInstance(general) : undefined;
13757
+ if (adapter && group)
13758
+ await adapter.sendText(String(group), text, { ...(topic != null ? { threadId: String(topic) } : {}), allowedMentions: { parse: [] } });
13759
+ },
13760
+ editCard: async (owner, at, text, choices) => {
13761
+ const adapter = adapterOf(owner.adapterId);
13762
+ if (!adapter)
13763
+ return;
13764
+ if (!choices.length && adapter.editMessageRemoveButtons) {
13765
+ await adapter.editMessageRemoveButtons(at.chatId, at.messageId, text);
13766
+ return;
13767
+ }
13768
+ await adapter.editMessage(at.chatId, at.messageId, text);
13769
+ if (adapter.replaceMessageButtons)
13770
+ await adapter.replaceMessageButtons(at.chatId, at.messageId, choices);
13771
+ },
13772
+ authorize: (handle, peer) => {
13773
+ const record = this.webSessions?.findByHandle(handle);
13774
+ if (!record)
13775
+ return { ok: false, refusal: "session_gone" };
13776
+ const r = this.authorizeWebSession(record, peer, false);
13777
+ // What the card says: the end of the authorization, or the session's own end when that comes first.
13778
+ return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
13779
+ },
13780
+ signOut: (handle) => {
13781
+ this.webSessions?.revokeByHandle(handle);
13782
+ this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
13783
+ },
13784
+ event: (type, payload) => { try {
13785
+ this.eventLog?.insert("web", type, payload);
13786
+ }
13787
+ catch { /* the event log is best effort */ } },
13788
+ text: {
13789
+ ask: (c) => t("web_auth.card_ask", c.label, where(c.peer), when(c.askedAt), when(c.until)),
13790
+ confirmed: (c) => t("web_auth.card_confirmed", c.label, where(c.peer), when(c.until)),
13791
+ denied: (c) => t("web_auth.card_denied", c.label),
13792
+ expired: (c) => t("web_auth.card_expired", c.label),
13793
+ refused: (c) => t("web_auth.card_refused", c.label, t(`web_auth.refusal_${c.refusal}`)),
13794
+ revoked: (c) => t("web_auth.card_revoked", c.label),
13795
+ ended: (c) => t("web_auth.card_ended", c.label),
13796
+ generalHint: () => t("web_auth.general_hint"),
13797
+ confirmLabel: () => t("web_auth.confirm"),
13798
+ notMeLabel: () => t("web_auth.not_me"),
13799
+ notYours: () => t("web_auth.not_yours"),
13800
+ },
13801
+ log: this.logger,
13802
+ });
13803
+ }
13804
+ /** #1939 S2b: the web session asks to be authorized (the request judged by §3 first — as it will be again at the tap). */
13805
+ async requestSessionAuthorization(input) {
13806
+ const s = input.session;
13807
+ const gate = this.webAuthorityQualifies(input);
13808
+ if (gate)
13809
+ return { ok: false, refusal: gate };
13810
+ return this.sessionAuthorizationRequests().ask({ handle: s.handle, owner: s.owner, label: s.label, peer: input.peer, sessionEnd: s.absoluteExpiry });
13811
+ }
13812
+ /**
13813
+ * #1939 §3 without the authorization: would this request's session qualify to be authorized? null: yes; otherwise the
13814
+ * first check it fails (for the app to say why it can't be authorized here).
13815
+ */
13816
+ webAuthorityQualifies(input) {
13817
+ const s = input.session, state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
13818
+ const r = evaluateWebAuthority({
13819
+ session: s, gateway: input.gateway, peer: input.peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
13820
+ isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
13821
+ authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
13822
+ policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly }, epoch: state.epoch, now,
13585
13823
  });
13824
+ return r.refusal;
13825
+ }
13826
+ /** #1939 S2b: what the app shows for this browser — verified (until when), a card pending, or why not. */
13827
+ webSessionAuthorization(input) {
13828
+ const s = input.session;
13829
+ const verdict = this.webAuthority(input);
13830
+ if (!verdict.refusal && verdict.authority.session) {
13831
+ const a = this.sessionAuthorizationStore()?.get(s.handle);
13832
+ const max = webAuthorityPolicy(this.fleetConfig?.web).maxSessionMs;
13833
+ if (a)
13834
+ return { state: "verified", until: Math.min(a.until, a.authorizedAt + max, s.absoluteExpiry) };
13835
+ }
13836
+ const pending = s ? this.sessionAuthorizationRequests().pending(s.handle) : null;
13837
+ if (pending)
13838
+ return { state: "pending", expiresInMs: pending.expiresInMs };
13839
+ return { state: "none", reason: this.webAuthorityQualifies(input) ?? (verdict.refusal ?? "not_authorized") };
13840
+ }
13841
+ /** #1939 §10: revoke every web authority given so far (receipts read as none), without signing anyone out. */
13842
+ revokeWebAuthority() {
13843
+ this.webAuthorityState(); // any switch change first, then this revocation
13844
+ const result = this.authorityStateStore?.revoke() ?? { epoch: 0, durable: false };
13845
+ if (result.durable)
13846
+ this.logger.info({ epoch: result.epoch }, "Web authority revoked");
13847
+ else
13848
+ this.logger.warn({ epoch: result.epoch }, "Web authority revoked in memory only — web authority stays off until its state can be written; a restart before then undoes this revocation");
13849
+ return result;
13586
13850
  }
13587
13851
  /** #1269: an instance's chat command from the web chat (web-commands.ts); `publicLink`: a gateway request. */
13588
13852
  webCommand(input, opts) {
@@ -20711,6 +20975,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20711
20975
  return;
20712
20976
  }
20713
20977
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
20978
+ if (this.effectBootstrap.handle(req, res))
20979
+ return;
20714
20980
  // Branding is public, after Host admission: automatic favicon requests
20715
20981
  // must not become noisy web-token 401s in the browser console.
20716
20982
  if (handleWebIconRequest(req, res, requestPath))
@@ -21338,6 +21604,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21338
21604
  catch {
21339
21605
  return {};
21340
21606
  } })(),
21607
+ // #2118: only this Chat's placement content; guest projection drops global revisions.
21608
+ effect_chat_v: createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32),
21341
21609
  // #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
21342
21610
  color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
21343
21611
  ...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },