@songsid/agend 2.3.0-beta.6 → 2.3.0-beta.8
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-cli-instructions.md +1 -1
- package/dist/agent-cli.js +21 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.js +68 -8
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.js +4 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.js +3 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +31 -11
- package/dist/authority/web-authority.js +39 -12
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +55 -3
- package/dist/cache-service.js +152 -16
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/mcp-tools.js +13 -7
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +2 -2
- package/dist/classic-channel-manager.js +2 -6
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/config-validator.js +10 -4
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +31 -9
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -0
- package/dist/delivery-outbox.js +7 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +109 -2
- package/dist/fleet-manager.js +280 -12
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +2 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +89 -30
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +62 -8
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +14 -0
- package/dist/human-delivery-receipts.js +42 -6
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +8 -1
- package/dist/instructions.js +9 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +52 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +6 -0
- package/dist/outbound-handlers.js +49 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +57 -1
- package/dist/outbound-schemas.js +34 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +8 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/settings-api.d.ts +6 -0
- package/dist/settings-api.js +54 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +31 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +10 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/tool-permissions.d.ts +0 -7
- package/dist/tool-permissions.js +73 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -1
- package/dist/topic-commands.js +2 -4
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +13 -2
- package/dist/ui/app-effects.js +46 -28
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +8 -8
- package/dist/ui/chat-effects.js +51 -22
- package/dist/ui/chat-strings.js +21 -6
- package/dist/ui/chat-thread.js +1 -1
- package/dist/ui/fleet-usage.js +401 -0
- package/dist/ui/panel-chat.js +22 -9
- package/dist/ui/panel-fleet.js +6 -5
- package/dist/ui/panel-settings.js +10 -2
- package/dist/ui/settings-appearance.js +8 -0
- package/dist/ui/settings-mods.js +19 -6
- package/dist/ui/settings-owner-effects.js +3 -3
- package/dist/ui/settings-strings.js +30 -0
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-session.js +2 -0
- package/dist/ui/shared/app-shell.js +2 -1
- package/dist/ui/shared/app-themes.js +24 -6
- package/dist/ui/shared/app.css +95 -0
- package/dist/ui/shared/effect-app-runtime.js +68 -19
- package/dist/ui/shared/effect-chat-runtime.js +16 -3
- package/dist/ui/shared/effect-frame.js +17 -3
- package/dist/ui/shared/effect-policy.js +18 -5
- package/dist/ui/shared/effect-profiler.js +31 -9
- package/dist/ui/shared/effect-registry.js +17 -41
- package/dist/ui/shared/effect-stock-trust.js +4 -4
- package/dist/ui/shared/effect-trial-stage.js +18 -7
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/usage-dialog.js +8 -0
- package/dist/ui/shared/view-strings.js +4 -0
- package/dist/ui/shared/web-authorize.js +151 -0
- package/dist/usage-report.d.ts +158 -0
- package/dist/usage-report.js +226 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +154 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +33 -0
- package/dist/web-api.js +148 -18
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +3 -0
- package/dist/web-appearance-tools.js +18 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +14 -2
- package/dist/web-appearance-view.js +32 -5
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +15 -1
- package/dist/web-appearance.js +77 -1
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +2 -2
- package/dist/web-asset-parts.js +3 -3
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +3 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.js +46 -1
- package/dist/web-effect-bundle.js.map +1 -1
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +4 -4
- package/dist/web-effect-service.d.ts +11 -4
- package/dist/web-effect-service.js +25 -4
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.d.ts +1 -0
- package/dist/web-effect-tools.js +2 -1
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-worker.d.ts +1 -1
- package/dist/web-effect-worker.js +2 -2
- package/dist/web-effect-worker.js.map +1 -1
- package/dist/web-guest-access.js +3 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-agent.js +5 -1
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +9 -1
- package/dist/web-mod-bundle.js +62 -3
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-export.d.ts +9 -0
- package/dist/web-mod-export.js +21 -0
- package/dist/web-mod-export.js.map +1 -1
- package/dist/web-mod-store.d.ts +2 -1
- package/dist/web-mod-store.js +13 -6
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +5 -0
- package/dist/web-session.js +16 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +26 -2
- package/dist/web-settings-patch.js +50 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +8 -0
- package/dist/web-settings-tools.js +14 -1
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +39 -0
- package/dist/web-theme-store.js +122 -11
- package/dist/web-theme-store.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -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
|
|
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
|
|
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) => {
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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) },
|