@songsid/agend 2.2.0 → 2.3.0-beta.1

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 (271) hide show
  1. package/dist/agent-cli.js +248 -2
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +2 -0
  4. package/dist/agent-endpoint.js +25 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-file-read.d.ts +30 -0
  7. package/dist/agent-file-read.js +129 -0
  8. package/dist/agent-file-read.js.map +1 -0
  9. package/dist/agent-whats-new.js +13 -0
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/atomic-write.js +9 -1
  12. package/dist/atomic-write.js.map +1 -1
  13. package/dist/auth-api.js +17 -0
  14. package/dist/auth-api.js.map +1 -1
  15. package/dist/backend/antigravity.js +3 -3
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/launch-effort.js +1 -1
  18. package/dist/backend/launch-effort.js.map +1 -1
  19. package/dist/backend/muse.js +10 -2
  20. package/dist/backend/muse.js.map +1 -1
  21. package/dist/channel/adapters/discord.d.ts +2 -0
  22. package/dist/channel/adapters/discord.js +17 -2
  23. package/dist/channel/adapters/discord.js.map +1 -1
  24. package/dist/channel/adapters/telegram.d.ts +8 -1
  25. package/dist/channel/adapters/telegram.js +74 -4
  26. package/dist/channel/adapters/telegram.js.map +1 -1
  27. package/dist/channel/mcp-tools.js +34 -0
  28. package/dist/channel/mcp-tools.js.map +1 -1
  29. package/dist/channel/types.d.ts +15 -0
  30. package/dist/config-validator.js +17 -1
  31. package/dist/config-validator.js.map +1 -1
  32. package/dist/daemon.js +244 -20
  33. package/dist/daemon.js.map +1 -1
  34. package/dist/fleet-context.d.ts +5 -0
  35. package/dist/fleet-manager.d.ts +89 -0
  36. package/dist/fleet-manager.js +401 -25
  37. package/dist/fleet-manager.js.map +1 -1
  38. package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
  39. package/dist/instance-avatar.js +3 -0
  40. package/dist/instance-avatar.js.map +1 -1
  41. package/dist/instance-config-impact.js +3 -0
  42. package/dist/instance-config-impact.js.map +1 -1
  43. package/dist/instance-lifecycle.js +9 -4
  44. package/dist/instance-lifecycle.js.map +1 -1
  45. package/dist/needs-you-hub.d.ts +4 -0
  46. package/dist/needs-you-hub.js +31 -6
  47. package/dist/needs-you-hub.js.map +1 -1
  48. package/dist/needs-you.d.ts +1 -13
  49. package/dist/needs-you.js +3 -37
  50. package/dist/needs-you.js.map +1 -1
  51. package/dist/outbound-handlers.d.ts +5 -0
  52. package/dist/outbound-handlers.js +30 -9
  53. package/dist/outbound-handlers.js.map +1 -1
  54. package/dist/outbound-schemas.d.ts +64 -0
  55. package/dist/outbound-schemas.js +50 -0
  56. package/dist/outbound-schemas.js.map +1 -1
  57. package/dist/public-web-gateway.js +14 -0
  58. package/dist/public-web-gateway.js.map +1 -1
  59. package/dist/repo-worktree-guard.d.ts +36 -0
  60. package/dist/repo-worktree-guard.js +143 -0
  61. package/dist/repo-worktree-guard.js.map +1 -0
  62. package/dist/scheduler/scheduler.d.ts +58 -1
  63. package/dist/scheduler/scheduler.js +176 -10
  64. package/dist/scheduler/scheduler.js.map +1 -1
  65. package/dist/settings-api.js +74 -1
  66. package/dist/settings-api.js.map +1 -1
  67. package/dist/settings-change.js +8 -0
  68. package/dist/settings-change.js.map +1 -1
  69. package/dist/settings-effect.js +16 -2
  70. package/dist/settings-effect.js.map +1 -1
  71. package/dist/settings-http-confirmation.js +5 -0
  72. package/dist/settings-http-confirmation.js.map +1 -1
  73. package/dist/tool-permissions.js +50 -0
  74. package/dist/tool-permissions.js.map +1 -1
  75. package/dist/topic-commands.d.ts +10 -6
  76. package/dist/topic-commands.js +21 -17
  77. package/dist/topic-commands.js.map +1 -1
  78. package/dist/topic-link.d.ts +128 -0
  79. package/dist/topic-link.js +269 -0
  80. package/dist/topic-link.js.map +1 -0
  81. package/dist/types.d.ts +13 -0
  82. package/dist/ui/app-effects.js +187 -0
  83. package/dist/ui/chat-effect-posters.js +42 -0
  84. package/dist/ui/chat-effects.js +72 -0
  85. package/dist/ui/chat-live-replies.js +58 -0
  86. package/dist/ui/chat-render.js +4 -4
  87. package/dist/ui/chat-store.js +9 -3
  88. package/dist/ui/chat-strings.js +15 -0
  89. package/dist/ui/chat-thread.js +47 -21
  90. package/dist/ui/chat-tour.js +1 -1
  91. package/dist/ui/effect-trial-controller.js +119 -0
  92. package/dist/ui/effect-trial-offer.js +31 -0
  93. package/dist/ui/effect-trial-panel.js +37 -0
  94. package/dist/ui/image-lightbox.js +1 -1
  95. package/dist/ui/panel-chat.js +85 -17
  96. package/dist/ui/panel-details.js +18 -5
  97. package/dist/ui/panel-fleet.js +3 -3
  98. package/dist/ui/panel-log.js +6 -6
  99. package/dist/ui/panel-needs.js +3 -3
  100. package/dist/ui/panel-settings.js +8 -5
  101. package/dist/ui/preview-panel.js +25 -9
  102. package/dist/ui/settings-appearance.js +325 -0
  103. package/dist/ui/settings-effects.js +201 -0
  104. package/dist/ui/settings-mods.js +257 -0
  105. package/dist/ui/settings-owner-effects.js +80 -0
  106. package/dist/ui/settings-strings.js +245 -0
  107. package/dist/ui/shared/agend-auth.js +27 -3
  108. package/dist/ui/shared/agent-widget.js +248 -0
  109. package/dist/ui/shared/app-i18n.js +2 -0
  110. package/dist/ui/shared/app-prefs.js +30 -10
  111. package/dist/ui/shared/app-route.js +23 -1
  112. package/dist/ui/shared/app-session.js +1 -1
  113. package/dist/ui/shared/app-shell.js +29 -16
  114. package/dist/ui/shared/app-store.js +2 -0
  115. package/dist/ui/shared/app-stream.js +12 -7
  116. package/dist/ui/shared/app-themes.js +188 -0
  117. package/dist/ui/shared/app.css +214 -1
  118. package/dist/ui/shared/app.js +6 -0
  119. package/dist/ui/shared/card-strings.js +28 -0
  120. package/dist/ui/shared/effect-app-runtime.js +248 -0
  121. package/dist/ui/shared/effect-baseline.js +64 -0
  122. package/dist/ui/shared/effect-chat-runtime.js +154 -0
  123. package/dist/ui/shared/effect-cursor.js +125 -0
  124. package/dist/ui/shared/effect-frame.js +127 -0
  125. package/dist/ui/shared/effect-geometry.js +122 -0
  126. package/dist/ui/shared/effect-host-style.js +10 -0
  127. package/dist/ui/shared/effect-owner-regions.js +74 -0
  128. package/dist/ui/shared/effect-owner-source.js +115 -0
  129. package/dist/ui/shared/effect-policy.js +137 -0
  130. package/dist/ui/shared/effect-poster-reader.js +98 -0
  131. package/dist/ui/shared/effect-posters.js +117 -0
  132. package/dist/ui/shared/effect-profiler.js +162 -0
  133. package/dist/ui/shared/effect-profiles.js +65 -0
  134. package/dist/ui/shared/effect-registry.js +496 -0
  135. package/dist/ui/shared/effect-runtime-loader.js +234 -0
  136. package/dist/ui/shared/effect-safety.js +153 -0
  137. package/dist/ui/shared/effect-stock-snapshot.js +34 -0
  138. package/dist/ui/shared/effect-stock-trust.js +165 -0
  139. package/dist/ui/shared/effect-trial-source.js +103 -0
  140. package/dist/ui/shared/effect-trial-stage.js +147 -0
  141. package/dist/ui/shared/header-tools.js +1 -1
  142. package/dist/ui/shared/identity-card.js +202 -0
  143. package/dist/ui/shared/identity-color.js +261 -0
  144. package/dist/ui/shared/instance-nav.js +22 -14
  145. package/dist/ui/shared/owner-css.js +221 -0
  146. package/dist/ui/shared/owner-effect-prefs.js +117 -0
  147. package/dist/ui/shared/panel-view.js +81 -8
  148. package/dist/ui/shared/signin.js +2 -0
  149. package/dist/ui/shared/ui-dialog.js +1 -1
  150. package/dist/ui/shared/ui-icons.js +2 -0
  151. package/dist/ui/shared/ui-menu.js +1 -1
  152. package/dist/ui/shared/ui-toast.js +1 -1
  153. package/dist/ui/shared/view-strings.js +30 -0
  154. package/dist/ui/signin.html +1 -0
  155. package/dist/view-api.d.ts +29 -0
  156. package/dist/view-api.js +188 -8
  157. package/dist/view-api.js.map +1 -1
  158. package/dist/web-api.d.ts +42 -0
  159. package/dist/web-api.js +896 -36
  160. package/dist/web-api.js.map +1 -1
  161. package/dist/web-appearance-limits.d.ts +82 -0
  162. package/dist/web-appearance-limits.js +83 -0
  163. package/dist/web-appearance-limits.js.map +1 -0
  164. package/dist/web-appearance-policy.d.ts +66 -0
  165. package/dist/web-appearance-policy.js +94 -0
  166. package/dist/web-appearance-policy.js.map +1 -0
  167. package/dist/web-appearance-tools.d.ts +50 -0
  168. package/dist/web-appearance-tools.js +105 -0
  169. package/dist/web-appearance-tools.js.map +1 -0
  170. package/dist/web-appearance-view.d.ts +99 -0
  171. package/dist/web-appearance-view.js +163 -0
  172. package/dist/web-appearance-view.js.map +1 -0
  173. package/dist/web-appearance.d.ts +79 -0
  174. package/dist/web-appearance.js +298 -0
  175. package/dist/web-appearance.js.map +1 -0
  176. package/dist/web-asset-parts.d.ts +98 -0
  177. package/dist/web-asset-parts.js +133 -0
  178. package/dist/web-asset-parts.js.map +1 -0
  179. package/dist/web-auth.js +17 -1
  180. package/dist/web-auth.js.map +1 -1
  181. package/dist/web-card.d.ts +96 -0
  182. package/dist/web-card.js +232 -0
  183. package/dist/web-card.js.map +1 -0
  184. package/dist/web-chat-history.d.ts +13 -0
  185. package/dist/web-chat-history.js +23 -2
  186. package/dist/web-chat-history.js.map +1 -1
  187. package/dist/web-effect-bundle.d.ts +28 -0
  188. package/dist/web-effect-bundle.js +126 -0
  189. package/dist/web-effect-bundle.js.map +1 -0
  190. package/dist/web-effect-canonical.d.ts +8 -0
  191. package/dist/web-effect-canonical.js +27 -0
  192. package/dist/web-effect-canonical.js.map +1 -0
  193. package/dist/web-effect-library.d.ts +114 -0
  194. package/dist/web-effect-library.js +530 -0
  195. package/dist/web-effect-library.js.map +1 -0
  196. package/dist/web-effect-poster.d.ts +8 -0
  197. package/dist/web-effect-poster.js +52 -0
  198. package/dist/web-effect-poster.js.map +1 -0
  199. package/dist/web-effect-profiles.d.ts +118 -0
  200. package/dist/web-effect-profiles.js +180 -0
  201. package/dist/web-effect-profiles.js.map +1 -0
  202. package/dist/web-effect-service.d.ts +86 -0
  203. package/dist/web-effect-service.js +312 -0
  204. package/dist/web-effect-service.js.map +1 -0
  205. package/dist/web-effect-tools.d.ts +47 -0
  206. package/dist/web-effect-tools.js +126 -0
  207. package/dist/web-effect-tools.js.map +1 -0
  208. package/dist/web-effect-trials.d.ts +86 -0
  209. package/dist/web-effect-trials.js +204 -0
  210. package/dist/web-effect-trials.js.map +1 -0
  211. package/dist/web-effect-upload.d.ts +29 -0
  212. package/dist/web-effect-upload.js +190 -0
  213. package/dist/web-effect-upload.js.map +1 -0
  214. package/dist/web-effect-worker.d.ts +8 -0
  215. package/dist/web-effect-worker.js +170 -0
  216. package/dist/web-effect-worker.js.map +1 -0
  217. package/dist/web-effect.d.ts +89 -0
  218. package/dist/web-effect.js +132 -0
  219. package/dist/web-effect.js.map +1 -0
  220. package/dist/web-identity-color.d.ts +87 -0
  221. package/dist/web-identity-color.js +209 -0
  222. package/dist/web-identity-color.js.map +1 -0
  223. package/dist/web-identity-palette.d.ts +113 -0
  224. package/dist/web-identity-palette.js +84 -0
  225. package/dist/web-identity-palette.js.map +1 -0
  226. package/dist/web-image-meta.d.ts +25 -0
  227. package/dist/web-image-meta.js +200 -0
  228. package/dist/web-image-meta.js.map +1 -0
  229. package/dist/web-mod-agent.d.ts +50 -0
  230. package/dist/web-mod-agent.js +203 -0
  231. package/dist/web-mod-agent.js.map +1 -0
  232. package/dist/web-mod-bundle.d.ts +144 -0
  233. package/dist/web-mod-bundle.js +464 -0
  234. package/dist/web-mod-bundle.js.map +1 -0
  235. package/dist/web-mod-export.d.ts +59 -0
  236. package/dist/web-mod-export.js +79 -0
  237. package/dist/web-mod-export.js.map +1 -0
  238. package/dist/web-mod-store.d.ts +283 -0
  239. package/dist/web-mod-store.js +624 -0
  240. package/dist/web-mod-store.js.map +1 -0
  241. package/dist/web-org.d.ts +3 -3
  242. package/dist/web-org.js +11 -17
  243. package/dist/web-org.js.map +1 -1
  244. package/dist/web-owner-css-store.d.ts +180 -0
  245. package/dist/web-owner-css-store.js +384 -0
  246. package/dist/web-owner-css-store.js.map +1 -0
  247. package/dist/web-owner-css.d.ts +58 -0
  248. package/dist/web-owner-css.js +552 -0
  249. package/dist/web-owner-css.js.map +1 -0
  250. package/dist/web-prefs.d.ts +48 -0
  251. package/dist/web-prefs.js +209 -33
  252. package/dist/web-prefs.js.map +1 -1
  253. package/dist/web-preview.d.ts +9 -1
  254. package/dist/web-preview.js +24 -7
  255. package/dist/web-preview.js.map +1 -1
  256. package/dist/web-settings-patch.d.ts +18 -0
  257. package/dist/web-settings-patch.js +51 -0
  258. package/dist/web-settings-patch.js.map +1 -1
  259. package/dist/web-shell-routes.d.ts +1 -1
  260. package/dist/web-shell-routes.js +1 -1
  261. package/dist/web-shell-routes.js.map +1 -1
  262. package/dist/web-theme-store.d.ts +218 -0
  263. package/dist/web-theme-store.js +516 -0
  264. package/dist/web-theme-store.js.map +1 -0
  265. package/dist/web-theme.d.ts +121 -0
  266. package/dist/web-theme.js +407 -0
  267. package/dist/web-theme.js.map +1 -0
  268. package/dist/web-widget.d.ts +72 -0
  269. package/dist/web-widget.js +245 -0
  270. package/dist/web-widget.js.map +1 -0
  271. package/package.json +1 -1
@@ -0,0 +1,154 @@
1
+ /** #1629: one committed local Chat lease joins the reviewed host-only effect primitives. */
2
+ import { createEffectRuntimeLoader } from "./effect-runtime-loader.js";
3
+ import { createEffectRegistry } from "./effect-registry.js";
4
+ import { createEffectSafety } from "./effect-safety.js";
5
+ import { stockEffectTrust } from "./effect-stock-trust.js";
6
+ import { createEffectProfileCollector } from "./effect-profiles.js";
7
+
8
+ const writers = new WeakMap();
9
+ /** Physical write reservations outlive a canceled caller. Responses never supply runtime authority. */
10
+ export function createEffectWriter(host, current) {
11
+ let dead = false;
12
+ const own = new Set();
13
+ if (!writers.has(host)) writers.set(host, new Set());
14
+ const all = writers.get(host);
15
+ let previous = null;
16
+ function stop() { if (dead) return; dead = true; for (const op of own) op.cancel(); }
17
+ function time() {
18
+ let now; try { now = host.performance.now(); } catch { stop(); return null; }
19
+ if (!Number.isFinite(now) || now < 0 || previous !== null && now < previous) { stop(); return null; }
20
+ previous = now; return now;
21
+ }
22
+ return {
23
+ async post(path, body) {
24
+ if (dead || !current() || all.size >= 3) return false;
25
+ const began = time(); if (began === null) return false;
26
+ const text = JSON.stringify(body); if (new TextEncoder().encode(text).length > 16384) return false;
27
+ const controller = new host.AbortController(), op = { controller }; own.add(op); all.add(op);
28
+ let timer, cancel;
29
+ const aborted = new Promise(resolve => { cancel = () => { controller.abort(); resolve(false); }; }); op.cancel = cancel;
30
+ const work = Promise.resolve().then(async () => {
31
+ if (dead || !current() || controller.signal.aborted) return false;
32
+ const r = await host.fetch(path, { method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json" }, body: text, signal: controller.signal });
33
+ // Settle the body before returning the physical reservation, even if the caller already timed out.
34
+ try { await r.body?.cancel(); } catch { /* no data is consumed */ }
35
+ const now = time();
36
+ return !dead && current() && !controller.signal.aborted && Number.isFinite(began) && began >= 0 && now !== null && now >= began && now - began < 10000 && r.ok && !r.redirected;
37
+ }).catch(() => false).finally(() => { host.clearTimeout(timer); controller.abort(); own.delete(op); all.delete(op); });
38
+ timer = host.setTimeout(cancel, 10000);
39
+ return Promise.race([work, aborted]);
40
+ },
41
+ dispose: stop,
42
+ };
43
+ }
44
+
45
+ /** Frozen, server-rendered session identity only. A header-token/public/View load has no identity. */
46
+ export function chatEffectIdentity(host) {
47
+ try {
48
+ const b = host.document.body.dataset, x = JSON.parse(host.document.documentElement.getAttribute("data-prefs") || "null");
49
+ if (b.mode !== "full" || b.publicLink === "1" || b.webTransport === "poll" || !["person", "operator"].includes(x?.ownerScope) || x.unreadable === true || typeof x.ownerEpoch !== "string" || !/^[a-f0-9]{16}$/.test(x.ownerEpoch)) return null;
50
+ return Object.freeze({ epoch: x.ownerEpoch, scope: x.ownerScope });
51
+ } catch { return null; }
52
+ }
53
+ export function chatEffectState(state, name) {
54
+ const inst = state.instances?.find(i => i.name === name);
55
+ if (!inst) return null;
56
+ if (["stopped", "paused"].includes(inst.status)) return "stopped";
57
+ if (Object.hasOwn(state.awaiting ?? {}, name)) return "needs_you";
58
+ return ["working", "stuck"].includes(state.exec?.[name]) ? "working" : state.exec?.[name] === "idle" ? "idle" : null;
59
+ }
60
+
61
+ /** `observe` supplies closed host gates, never event/message content. No timers poll server state. */
62
+ export function createChatEffectRuntime({ window: host, name, element, current, observe, onChange = () => {}, factories = {} }) {
63
+ let dead = false, error = null, loader = null, registry = null, writer = null, active = null, queued = false, generation = 0, metadata = null, ctx = null, lastContext = null;
64
+ const paused = new Map(), owners = new Map();
65
+ const placementIdentity = p => JSON.stringify([p.by, p.manifest.layer, p.params]);
66
+ const identity = chatEffectIdentity(host), auth = host.AgendAuth;
67
+ const alive = () => !dead && current() === true && element.isConnected === true && !auth.sessionEnded();
68
+ const tell = () => { if (!dead && current()) onChange({ error, rows: registry?.snapshot() ?? [], effects: metadata?.effects ?? [], loading: metadata === null && !error }); };
69
+ const fail = reason => { if (dead) return; error = reason; generation++; metadata = null; active?.controller.abort(); registry?.dispose?.(); registry = null; loader?.dispose?.(); loader = null; writer?.dispose(); tell(); };
70
+ function dispose() {
71
+ if (dead) return; dead = true; generation++; active?.controller.abort(); registry?.dispose?.(); loader?.dispose?.(); writer?.dispose(); offAuth?.();
72
+ for (const value of owners.values()) value.collector.dispose(); owners.clear();
73
+ }
74
+ let offAuth = null;
75
+ if (!identity || typeof auth?.onSessionEnd !== "function" || typeof auth?.sessionEnded !== "function" || typeof name !== "string" || name.length < 1 || name.length > 128 || !current()) return { ok: false, dispose };
76
+ offAuth = auth.onSessionEnd(dispose); // May synchronously replay an already-ended episode.
77
+ if (dead || !alive()) { dispose(); return { ok: false, dispose }; }
78
+ const clock = () => host.performance.now();
79
+ try {
80
+ const make = (key, fn) => factories[key] ?? fn;
81
+ writer = createEffectWriter(host, alive);
82
+ loader = make("loader", createEffectRuntimeLoader)({ window: host, ownerEpoch: identity.epoch, current: alive });
83
+ if (!loader.ok) throw Error("runtime_unavailable");
84
+ registry = make("registry", createEffectRegistry)({ window: host, origin: host.document.body.dataset.previewOrigin, boot: host.document.body.dataset.previewBoot,
85
+ load: async (d, signal) => {
86
+ const mine = generation, value = await loader.resource({ id: d.id, instance: name }, signal);
87
+ if (!alive() || mine !== generation || signal.aborted) return null;
88
+ if (!value) { fail("resource_unavailable"); return null; }
89
+ return value;
90
+ },
91
+ safety: make("safety", createEffectSafety)({ storage: host.localStorage, crypto: host.crypto }),
92
+ stockSnapshot: ({ frameRoot }) => make("stock", stockEffectTrust)(host, { frameRoot, ownerCss: observe().ownerCss }),
93
+ onChange() { tell(); emit(); },
94
+ onMount(owner) {
95
+ if (!alive() || !owner.current()) return;
96
+ const collector = make("collector", createEffectProfileCollector)({ key: owner.key, id: owner.id, device: owner.device, clock });
97
+ if (collector.ok) owners.set(owner, { collector, began: clock(), attempted: null });
98
+ },
99
+ onRetire(owner) { const row = owners.get(owner); row?.collector.dispose(); owners.delete(owner); },
100
+ onProfile(raw, owner) {
101
+ const row = owners.get(owner); if (!row || !alive() || !owner.current()) return;
102
+ row.collector.add(raw);
103
+ const now = clock();
104
+ if (now - row.began < 10000 && raw.verdict !== "paused" || row.attempted !== null && now - row.attempted < 10000) return;
105
+ const report = row.collector.report(); if (!report || !owner.current()) return;
106
+ row.attempted = now;
107
+ void writer.post(`/ui/effects/profile/${owner.id}?instance=${encodeURIComponent(name)}&ownerEpoch=${identity.epoch}`, report.profile);
108
+ }, clock,
109
+ });
110
+ if (!registry.ok) throw Error("runtime_unavailable");
111
+ } catch { fail("runtime_unavailable"); }
112
+ function descriptors() {
113
+ return (metadata?.effects ?? []).filter(p => p.manifest.regions.includes("chat")).map((p, order) => ({ key: `chat:${p.id}`, id: p.id, by: p.by, region: "chat", layer: p.manifest.layer,
114
+ order, paused: paused.get(p.id) === placementIdentity(p), visible: true, instance: name, params: p.params, element }));
115
+ }
116
+ function emit() {
117
+ if (!alive() || error || !registry?.ok) return;
118
+ const value = observe();
119
+ if (["idle", "working", "needs_you", "stopped"].includes(value.state)) registry.publish("state", { state: value.state }, name);
120
+ if (["light", "dark"].includes(value.mode)) registry.publish("theme", { mode: value.mode });
121
+ }
122
+ function sync() {
123
+ if (!alive()) { dispose(); return; } if (error) return;
124
+ const value = observe(); ctx = value.context;
125
+ const stamp = JSON.stringify(ctx);
126
+ if (stamp !== lastContext) { lastContext = stamp; registry.update(descriptors(), ctx); }
127
+ emit();
128
+ if (!metadata && !active && ctx.enabled && ctx.show && ctx.previewReady && ctx.optIn && !ctx.safe && !ctx.publicLink && !ctx.reducedMotion && !ctx.saveData && !ctx.hidden) void read();
129
+ }
130
+ async function read() {
131
+ if (!alive() || error || active) return;
132
+ const op = { controller: new host.AbortController(), generation }; active = op;
133
+ try {
134
+ const answer = await loader.placements(name, op.controller.signal);
135
+ if (!alive() || active !== op) return;
136
+ if (op.generation !== generation) return;
137
+ if (!answer) { fail("resource_unavailable"); return; }
138
+ metadata = answer;
139
+ for (const [id, stamp] of paused) if (!answer.effects.some(p => p.id === id && placementIdentity(p) === stamp)) paused.delete(id);
140
+ lastContext = null; sync(); tell();
141
+ } finally {
142
+ if (active === op) active = null;
143
+ if (queued && alive() && !error) { queued = false; sync(); }
144
+ }
145
+ }
146
+ return { ok: !error && !dead, sync,
147
+ changed() { if (!alive() || error) return; generation++; metadata = null; registry.update([], observe().context); lastContext = null; if (active) queued = true; else sync(); tell(); },
148
+ reply(event) { if (alive() && !error && event.instance === name) registry.publish("reply", {}, name); },
149
+ pause(id) { if (!alive() || !metadata?.effects.some(p => p.id === id)) return false; paused.set(id, placementIdentity(metadata.effects.find(p => p.id === id))); lastContext = null; sync(); return true; },
150
+ resume(id) { if (!alive() || error || !metadata?.effects.some(p => p.id === id)) return false; paused.delete(id); lastContext = null; sync(); return registry.resume(`chat:${id}`); },
151
+ async remove(id) { if (!alive() || error || !metadata?.effects.some(p => p.id === id)) return false; const mine = generation; const ok = await writer.post("/ui/effects/remove", { instance: name, id }); if (!alive() || mine !== generation) return false; if (ok) this.changed(); return ok; },
152
+ snapshot: () => ({ error, rows: registry?.snapshot() ?? [], effects: metadata?.effects ?? [], loading: metadata === null && !error }), dispose,
153
+ };
154
+ }
@@ -0,0 +1,125 @@
1
+ /** #1629: host-owned static cursor controller; no product startup, metadata reader, Worker or frame. */
2
+ import { effectHostStyle } from "./effect-host-style.js";
3
+ import { checkStaticEffectCursor } from "./effect-runtime-loader.js";
4
+ const owners = new WeakMap(), ATTRIBUTE = "data-agend-effect-cursor";
5
+ let serial = 0;
6
+ const finite = n => typeof n === "number" && Number.isFinite(n) && n >= 0;
7
+ function selection(value) {
8
+ try {
9
+ if (!value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return null;
10
+ const fields = Object.getOwnPropertyDescriptors(value);
11
+ if (Reflect.ownKeys(fields).length !== 3 || !["id", "generation", "hotspot"].every(k => fields[k] && Object.hasOwn(fields[k], "value"))) return null;
12
+ const { id, generation, hotspot } = value;
13
+ if (typeof id !== "string" || !/^[a-f0-9]{32}$/.test(id) || !Number.isSafeInteger(generation) || generation < 0 || !Array.isArray(hotspot) || Object.getPrototypeOf(hotspot) !== Array.prototype) return null;
14
+ const entries = Object.getOwnPropertyDescriptors(hotspot);
15
+ if (Reflect.ownKeys(entries).length !== 3 || entries.length?.value !== 2 || ![0, 1].every(k => entries[k] && Object.hasOwn(entries[k], "value") && Number.isInteger(entries[k].value) && entries[k].value >= 0 && entries[k].value <= 63)) return null;
16
+ return Object.freeze({ id, generation, hotspot: Object.freeze([entries[0].value, entries[1].value]) });
17
+ } catch { return null; }
18
+ }
19
+ function cursorMatches(value, url, hotspot) {
20
+ const m = typeof value === "string" && /^url\((["']?)(blob:[^"'()\s]+)\1\)\s+(\d+)\s+(\d+),\s*auto$/.exec(value);
21
+ return !!m && m[2] === url && Number(m[3]) === hotspot[0] && Number(m[4]) === hotspot[1];
22
+ }
23
+ /** readImage must be the existing shared loader's settled cursor operation. enabled is a host gate, never effect data. */
24
+ export function createEffectCursor({ window: host, current, readImage, onChange = () => {} } = {}) {
25
+ const unavailable = { ok: false, update() {}, snapshot: () => ({ status: "unavailable", id: null }), dispose() {} };
26
+ if (!host?.document || owners.has(host) || ![current, readImage, onChange, host.getComputedStyle, host.Blob, host.URL?.createObjectURL, host.URL?.revokeObjectURL, host.AbortController, host.setTimeout, host.clearTimeout, host.performance?.now].every(x => typeof x === "function")) return unavailable;
27
+ const token = {}; owners.set(host, token);
28
+ const doc = host.document; let dead = false, previous = null, wanted = null, pending = null, active = null, blocked = null, generation = 0, checking = false, notifying = false, status = "off";
29
+ const safe = fn => { try { return fn(); } catch { return null; } };
30
+ const release = () => { if (dead && !pending && owners.get(host) === token) owners.delete(host); };
31
+ function alive() {
32
+ if (dead || checking) return false;
33
+ checking = true; const value = safe(() => current() === true && host.document === doc); checking = false;
34
+ return !dead && value === true;
35
+ }
36
+ function now() {
37
+ const value = safe(() => host.performance.now());
38
+ if (!finite(value) || previous !== null && value < previous) { dispose(); return null; }
39
+ previous = value; return value;
40
+ }
41
+ function target() {
42
+ const all = doc.querySelectorAll("#app > .shell"), el = all.length === 1 ? all[0] : null;
43
+ return el?.ownerDocument === doc && el.isConnected === true ? el : null;
44
+ }
45
+ function remove(row) {
46
+ if (!row) return;
47
+ // Removing our exact attribute disarms even a stylesheet whose CSSOM has become unreadable.
48
+ safe(() => { if (row.element.getAttribute(ATTRIBUTE) === row.token) row.element.removeAttribute(ATTRIBUTE); });
49
+ safe(() => row.rule?.style.removeProperty("cursor"));
50
+ safe(() => { const at = [...row.sheet.cssRules].indexOf(row.rule); if (at >= 0) row.sheet.deleteRule(at); });
51
+ if (row.url) { safe(() => host.URL.revokeObjectURL(row.url)); row.url = null; }
52
+ }
53
+ function clear() { const old = active; active = null; remove(old); }
54
+ function tell() { if (dead || notifying) return; notifying = true; try { safe(() => onChange(snapshot())); } finally { notifying = false; } }
55
+ function snapshot() { return Object.freeze({ status: dead ? "unavailable" : status, id: wanted?.selection.id ?? null }); }
56
+ function dispose() {
57
+ if (dead) return; dead = true; generation++; wanted = null; clear();
58
+ if (pending) { safe(() => pending.controller.abort()); remove(pending); }
59
+ safe(() => host.removeEventListener?.("pagehide", dispose)); release();
60
+ }
61
+ if (!alive() || now() === null || !effectHostStyle(host)) { dispose(); return unavailable; }
62
+ safe(() => host.addEventListener?.("pagehide", dispose));
63
+ function valid(op) {
64
+ if (!alive() || pending !== op || op.generation !== generation || op.controller.signal.aborted || wanted !== op.wanted) return false;
65
+ const t = now();
66
+ return t !== null && t >= op.began && t - op.began < 10000 && target() === op.element && effectHostStyle(host) === op.sheet;
67
+ }
68
+ function fail(op) {
69
+ if (!dead && pending === op && generation === op.generation && wanted === op.wanted) { blocked = wanted; status = "unavailable"; tell(); }
70
+ }
71
+ async function load(op) {
72
+ try {
73
+ if (!valid(op)) return;
74
+ const bytes = await readImage(op.wanted.selection, op.controller.signal);
75
+ if (!valid(op)) return;
76
+ const checked = checkStaticEffectCursor(bytes); if (!checked) { fail(op); return; }
77
+ const view = new DataView(checked.buffer, checked.byteOffset, checked.byteLength), width = view.getUint32(16), height = view.getUint32(20);
78
+ if (op.wanted.selection.hotspot.some((v, n) => v >= (n === 0 ? width : height))) { fail(op); return; }
79
+ op.url = host.URL.createObjectURL(new host.Blob([checked], { type: "image/png" }));
80
+ const url = new URL(op.url); if (url.protocol !== "blob:" || url.origin !== host.location.origin || !/^blob:[^"'()\s\\]+$/.test(op.url)) { fail(op); return; }
81
+ const image = doc.createElement("img"); image.alt = ""; image.src = op.url; op.image = image;
82
+ if (typeof image.decode !== "function") { fail(op); return; }
83
+ await image.decode();
84
+ if (!valid(op)) return;
85
+ if (image.naturalWidth !== width || image.naturalHeight !== height) { fail(op); return; }
86
+ if (op.element.hasAttribute(ATTRIBUTE)) { fail(op); return; }
87
+ op.token = `agend-effect-cursor-${++serial}`;
88
+ const index = op.sheet.insertRule(`#app > .shell[${ATTRIBUTE}="${op.token}"]{}`, op.sheet.cssRules.length); op.rule = op.sheet.cssRules[index];
89
+ const hotspot = op.wanted.selection.hotspot, value = `url("${op.url}") ${hotspot[0]} ${hotspot[1]}, auto`;
90
+ op.rule.style.setProperty("cursor", value); op.element.setAttribute(ATTRIBUTE, op.token);
91
+ if (!valid(op) || !cursorMatches(op.rule.style.getPropertyValue("cursor"), op.url, hotspot) || !cursorMatches(host.getComputedStyle(op.element).cursor, op.url, hotspot)) { fail(op); return; }
92
+ active = op; status = "running"; tell();
93
+ } catch { fail(op); }
94
+ finally {
95
+ safe(() => host.clearTimeout(op.timer));
96
+ if (active !== op) remove(op);
97
+ if (pending === op) { pending = null; if (!dead && wanted === op.wanted && !active && blocked !== wanted) { blocked = wanted; status = "unavailable"; tell(); } }
98
+ release(); if (!dead) start();
99
+ }
100
+ }
101
+ function start() {
102
+ if (!alive() || pending || active || !wanted || blocked === wanted) return;
103
+ const began = now(), element = safe(target), sheet = effectHostStyle(host);
104
+ if (began === null || !element || !sheet || element.hasAttribute(ATTRIBUTE)) { blocked = wanted; status = "unavailable"; tell(); return; }
105
+ const op = { wanted, generation, began, element, sheet, controller: new host.AbortController(), url: null, rule: null, token: null, timer: null }; pending = op; status = "loading";
106
+ op.timer = host.setTimeout(() => { if (pending !== op) return; op.controller.abort(); remove(op); fail(op); }, 10000);
107
+ tell(); void load(op);
108
+ }
109
+ function update(value, enabled) {
110
+ if (!alive() || now() === null) { dispose(); return; }
111
+ const next = enabled === true ? selection(value) : null;
112
+ const same = !!next && wanted && next.id === wanted.selection.id && next.generation === wanted.selection.generation && next.hotspot.every((n, i) => n === wanted.selection.hotspot[i]);
113
+ if (!same) {
114
+ generation++; wanted = next ? { selection: next } : null; blocked = null; clear();
115
+ if (pending) { safe(() => pending.controller.abort()); remove(pending); }
116
+ status = next ? "loading" : "off";
117
+ }
118
+ if (active) {
119
+ const row = active;
120
+ if (safe(target) !== row.element || effectHostStyle(host) !== row.sheet || row.element.getAttribute(ATTRIBUTE) !== row.token || !cursorMatches(safe(() => host.getComputedStyle(row.element).cursor), row.url, row.wanted.selection.hotspot)) { clear(); blocked = wanted; status = "unavailable"; }
121
+ }
122
+ start(); tell();
123
+ }
124
+ return { ok: true, update, snapshot, dispose };
125
+ }
@@ -0,0 +1,127 @@
1
+ /** #1629: parent-owned Canvas Worker transport; the Chat adapter owns product admission. */
2
+ import { maySendEffectEvent } from "./effect-policy.js";
3
+ const plain = v => !!v && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
4
+ const exact = (v, keys) => plain(v) && Object.keys(v).sort().join(",") === keys.slice().sort().join(",");
5
+ const finite = v => typeof v === "number" && Number.isFinite(v);
6
+ const bytes = v => new TextEncoder().encode(v).byteLength;
7
+
8
+ /** Snapshot only host-validated code, primitives and raster bytes; no HTML or DOM interface exists. */
9
+ function snapshotRender(raw) {
10
+ if (!exact(raw, ["code", "params", "instance", "width", "height", "dpr", "assets", "wants"]) || typeof raw.code !== "string" || !plain(raw.params) || !Array.isArray(raw.assets) || raw.assets.length > 16 || !Array.isArray(raw.wants) || Array.from(raw.wants).some(v => !["pointer", "state", "reply", "mention", "theme"].includes(v)) || new Set(raw.wants).size !== raw.wants.length) return null;
11
+ if (raw.instance !== null && (typeof raw.instance !== "string" || raw.instance.length < 1 || raw.instance.length > 128)) return null;
12
+ if (!Number.isInteger(raw.width) || raw.width < 1 || raw.width > 4096 || !Number.isInteger(raw.height) || raw.height < 1 || raw.height > 4096 || raw.width * raw.height > 8388608 || !finite(raw.dpr) || raw.dpr < 0.5 || raw.dpr > 3) return null;
13
+ const params = Object.create(null), keys = Object.keys(raw.params);
14
+ if (keys.length > 16) return null;
15
+ for (const key of keys) {
16
+ const value = raw.params[key];
17
+ if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || !(typeof value === "boolean" || finite(value) || typeof value === "string" && value.length <= 128)) return null;
18
+ params[key] = value;
19
+ }
20
+ let total = bytes(raw.code) + bytes(JSON.stringify(params));
21
+ if (total > 1048576) return null;
22
+ const assets = [], names = new Set();
23
+ for (const a of raw.assets) {
24
+ if (!exact(a, ["name", "mime", "bytes"]) || typeof a.name !== "string" || !/^assets\/[a-z0-9][a-z0-9_-]{0,63}\.(?:png|jpe?g|webp|gif)$/.test(a.name) || names.has(a.name) || !["image/png", "image/jpeg", "image/webp", "image/gif"].includes(a.mime) || !(a.bytes instanceof Uint8Array)) return null;
25
+ total += a.bytes.byteLength;
26
+ if (total > 1048576) return null;
27
+ names.add(a.name);
28
+ assets.push({ name: a.name, mime: a.mime, bytes: new Uint8Array(a.bytes) });
29
+ }
30
+ return { code: raw.code, params: { ...params }, instance: raw.instance, width: raw.width, height: raw.height, dpr: raw.dpr, assets, wants: [...raw.wants] };
31
+ }
32
+
33
+ /**
34
+ * All options come from the host, never from an agent event or manifest origin. The registry supplies an admitted
35
+ * root and geometry in a later integration. This primitive alone does not implement caps, clipping or profiling.
36
+ */
37
+ export function createEffectFrame({ window: host, root, origin, boot, render, clock = () => performance.now(), onReady = () => {}, onRemove = () => {} } = {}) {
38
+ let url;
39
+ try { url = new URL(origin); } catch { return { ok: false, reason: "preview_unknown" }; }
40
+ if (origin !== url.origin || !["http:", "https:"].includes(url.protocol) || typeof boot !== "string" || !/^[0-9a-f]{32}$/.test(boot)) return { ok: false, reason: "preview_unknown" };
41
+ let hostOrigin; try { hostOrigin = new URL(host.location.origin); } catch { return { ok: false, reason: "preview_unknown" }; }
42
+ if (!["http:", "https:"].includes(hostOrigin.protocol) || origin === hostOrigin.origin || hostOrigin.protocol === "https:" && url.protocol !== "https:") return { ok: false, reason: "preview_unknown" };
43
+ const payload = snapshotRender(render);
44
+ if (!payload || !host || !root || root.ownerDocument !== host.document || typeof root.append !== "function" || typeof host.MessageChannel !== "function" || typeof clock !== "function" || typeof onReady !== "function" || typeof onRemove !== "function") return { ok: false, reason: "unsupported" };
45
+ let mounted;
46
+ try { mounted = clock(); } catch { return { ok: false, reason: "clock_unavailable" }; }
47
+ if (!finite(mounted) || mounted < 0) return { ok: false, reason: "clock_unavailable" };
48
+ const frame = host.document.createElement("iframe");
49
+ frame.setAttribute("sandbox", "allow-scripts");
50
+ frame.setAttribute("tabindex", "-1");
51
+ frame.setAttribute("aria-hidden", "true");
52
+ frame.setAttribute("referrerpolicy", "no-referrer");
53
+ frame.className = "agend-effect-frame";
54
+ let removed = false, ready = false, transferred = false, loads = 0, channel = null, timer = null, last = mounted;
55
+ const time = () => {
56
+ let now; try { now = clock(); } catch { return null; }
57
+ if (!finite(now) || now < last) return null;
58
+ last = now; return now;
59
+ };
60
+ const state = () => ({ ready, removed, loads });
61
+ const remove = reason => {
62
+ if (removed) return;
63
+ removed = true;
64
+ host.removeEventListener("message", hello);
65
+ frame.removeEventListener("load", load);
66
+ if (timer !== null) host.clearTimeout(timer);
67
+ if (channel) {
68
+ channel.port1.removeEventListener("message", portReady);
69
+ try { channel.port1.close(); } catch { /* already detached */ }
70
+ try { channel.port2.close(); } catch { /* already transferred */ }
71
+ }
72
+ frame.remove();
73
+ try { onRemove(reason); } catch { /* host callback cannot revive a removed frame */ }
74
+ };
75
+ const load = () => { loads++; if (loads > 1) remove("navigation"); };
76
+ const current = () => {
77
+ if (removed) return false;
78
+ if (frame.parentNode !== root || frame.isConnected !== true) { remove("detached"); return false; }
79
+ const now = time();
80
+ if (now === null) { remove("clock_unavailable"); return false; }
81
+ if (!ready && now - mounted >= 10000) { remove("startup_timeout"); return false; }
82
+ return true;
83
+ };
84
+ const portReady = event => {
85
+ if (removed || ready) return;
86
+ let data; try { data = event.data; } catch { return; }
87
+ if (!exact(data, ["type"]) || data.type !== "ready") return;
88
+ if (!current()) return;
89
+ ready = true;
90
+ host.clearTimeout(timer); timer = null;
91
+ channel.port1.removeEventListener("message", portReady);
92
+ try { onReady(); } catch { remove("host_callback"); }
93
+ };
94
+ const hello = event => {
95
+ if (removed || transferred) return;
96
+ let data; try { data = event.data; } catch { return; }
97
+ // Opaque sandbox messages have origin null. Only this exact WindowProxy and boot may receive the private port.
98
+ if (event.source !== frame.contentWindow || event.origin !== "null" || event.ports?.length || !exact(data, ["v", "type", "ch", "boot"]) || data.v !== 1 || data.type !== "ready" || data.ch !== null || data.boot !== boot) return;
99
+ if (!current()) return;
100
+ transferred = true;
101
+ host.removeEventListener("message", hello);
102
+ try {
103
+ channel = new host.MessageChannel();
104
+ channel.port1.addEventListener("message", portReady);
105
+ channel.port1.start();
106
+ const nonce = new Uint8Array(16); host.crypto.getRandomValues(nonce);
107
+ const ch = Array.from(nonce, b => b.toString(16).padStart(2, "0")).join("");
108
+ const { wants, ...message } = payload;
109
+ // '*' is required for the opaque destination; the source WindowProxy was checked immediately above.
110
+ frame.contentWindow.postMessage({ v: 1, type: "render", ch, ...message }, "*", [channel.port2]);
111
+ } catch { remove("transport"); }
112
+ };
113
+ const deadline = () => {
114
+ if (!current() || ready) return;
115
+ timer = host.setTimeout(deadline, Math.max(1, 10000 - (last - mounted)));
116
+ };
117
+ host.addEventListener("message", hello);
118
+ frame.addEventListener("load", load);
119
+ timer = host.setTimeout(deadline, 10000);
120
+ try { frame.src = `${origin}/effect`; root.append(frame); } catch { remove("mount_failed"); }
121
+ return { ok: true, element: frame, state, remove,
122
+ send(event) {
123
+ if (!ready || !current() || !maySendEffectEvent(event, { instance: payload.instance, wants: payload.wants })) return false;
124
+ try { channel.port1.postMessage(event); return true; } catch { remove("transport"); return false; }
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,122 @@
1
+ /** #1629: host-only fixed viewport geometry and trust holes through the already-loaded, fixed same-origin app stylesheet. No inline attributes. */
2
+ import { effectHostStyle } from "./effect-host-style.js";
3
+ import { effectTrustClip } from "./effect-policy.js";
4
+ import { effectStockSnapshot, effectStockAllows } from "./effect-stock-snapshot.js";
5
+ let serial = 0;
6
+ // Native CSSOM canonicalizes SVG command whitespace. Compare bounded tokens, never whitespace-stripped digits.
7
+ function sameClip(expected, written) {
8
+ if (expected === "none") return written === "none";
9
+ const tokens = value => {
10
+ if (typeof value !== "string" || value.length > 100000) return null;
11
+ const m = /^path\(\s*evenodd\s*,\s*["']([^"']*)["']\s*\)$/.exec(value);
12
+ if (!m) return null;
13
+ const out = [], rest = m[1].replace(/[MHVZ]|-?\d+(?:\.\d+)?/g, token => { out.push(/[MHVZ]/.test(token) ? token : Number(token)); return ""; });
14
+ return /^[\s,]*$/.test(rest) ? JSON.stringify(out) : null;
15
+ };
16
+ const a = tokens(expected), b = tokens(written);
17
+ return a !== null && a === b;
18
+ }
19
+ const number = n => typeof n === "number" && Number.isFinite(n) && Math.abs(n) <= 1e7;
20
+ /** A fixed frame must share viewport axes. Unknown transformed/filter/contained ancestors fail closed. */
21
+ export function hostEffectSpace(host, element) {
22
+ try {
23
+ if (element?.ownerDocument !== host.document || element.isConnected !== true || typeof host.getComputedStyle !== "function") return false;
24
+ let el = element, seen = 0;
25
+ while (el) {
26
+ if (++seen > 128 || el.ownerDocument !== host.document) return false;
27
+ const s = host.getComputedStyle(el);
28
+ for (const key of ["transform", "perspective", "filter", "backdropFilter", "rotate", "scale", "translate", "offsetPath"]) if (s[key] !== undefined && s[key] !== "" && s[key] !== "none") return false;
29
+ if (s.contain !== "none" || s.willChange !== "auto" || s.contentVisibility !== undefined && s.contentVisibility !== "" && s.contentVisibility !== "visible" || s.zoom !== undefined && !["", "normal", "1"].includes(s.zoom)) return false;
30
+ if (el === host.document.documentElement) return true;
31
+ el = el.parentElement;
32
+ }
33
+ return false;
34
+ } catch { return false; }
35
+ }
36
+ export function hostEffectRect(element) {
37
+ try {
38
+ if (element?.isConnected !== true) return null;
39
+ const r = element.getBoundingClientRect();
40
+ if (![r.x, r.y, r.width, r.height].every(number) || r.width < 1 || r.height < 1) return null;
41
+ const x = Math.floor(r.x), y = Math.floor(r.y), width = Math.ceil(r.x + r.width) - x, height = Math.ceil(r.y + r.height) - y;
42
+ return width <= 4096 && height <= 4096 && width * height <= 8388608 ? { x, y, width, height } : null;
43
+ } catch { return null; }
44
+ }
45
+ export function createEffectGeometry({ window: host } = {}) {
46
+ let sheet;
47
+ try {
48
+ if (!host?.document || host.CSS?.supports("clip-path", 'path(evenodd, "M0 0H1V1H0Z")') !== true) return { ok: false, reason: "geometry_unsupported" };
49
+ sheet = effectHostStyle(host); if (!sheet) throw Error("app stylesheet unavailable");
50
+ } catch { return { ok: false, reason: "geometry_unsupported" }; }
51
+ const bound = new Set(); let disposed = false;
52
+ function bind(frame, { layer, by } = {}) {
53
+ if (disposed || !hostEffectSpace(host, frame) || !["front", "behind"].includes(layer) || !frame.classList) {
54
+ // The caller must also close its factory handle. Never leave its own rejected frame painting.
55
+ if (frame?.ownerDocument === host.document && typeof frame.remove === "function") frame.remove();
56
+ return { ok: false, reason: "geometry_unknown" };
57
+ }
58
+ const token = `agend-effect-geometry-${++serial}`;
59
+ let rule;
60
+ try {
61
+ const at = sheet.insertRule(`.${token}{}`, sheet.cssRules.length); rule = sheet.cssRules[at];
62
+ if (!rule?.style) throw Error("rule unavailable");
63
+ for (const [key, value] of Object.entries({ position: "fixed", border: "0px", margin: "0px", padding: "0px", "pointer-events": "none", "z-index": layer === "front" ? "20" : "0", opacity: layer === "front" && by !== "human" ? "0.85" : "1", visibility: "hidden", "background-color": "transparent", "box-sizing": "border-box", "box-shadow": "none", outline: "none", transition: "none", animation: "none", "min-width": "0px", "min-height": "0px", "max-width": "none", "max-height": "none" })) rule.style.setProperty(key, value, "important");
64
+ frame.classList.add(token);
65
+ // Binding is accepted BEFORE the first sync/ready. Prove its initial hidden state before the caller may yield.
66
+ if (host.getComputedStyle(frame).visibility !== "hidden") throw Error("initial hiding cascade lost");
67
+ } catch {
68
+ try { const at = [...sheet.cssRules].indexOf(rule); if (at >= 0) sheet.deleteRule(at); } catch { /* cleanup cannot precede physical refusal */ }
69
+ try { frame.classList.remove(token); } catch { /* the refused frame will not remain in the document */ }
70
+ frame.remove();
71
+ return { ok: false, reason: "geometry_unknown" };
72
+ }
73
+ let dead = false;
74
+ const hide = () => {
75
+ try {
76
+ rule.style.setProperty("visibility", "hidden", "important");
77
+ if (frame.isConnected === true && host.getComputedStyle(frame).visibility !== "hidden") frame.remove();
78
+ } catch { frame.remove(); }
79
+ };
80
+ const handle = { ok: true, hide,
81
+ /** Same-turn host paint snapshot is mandatory, including behind/human placements. No DOM fallback. */
82
+ sync(rect, stock) {
83
+ hide();
84
+ const proof = effectStockSnapshot(stock);
85
+ if (!effectStockAllows(proof, { layer, by })) return false;
86
+ if (dead || disposed || !hostEffectSpace(host, frame) || !rect || ![rect.x, rect.y, rect.width, rect.height].every(number) || ![rect.x, rect.y, rect.width, rect.height].every(Number.isInteger) || rect.width < 1 || rect.height < 1 || rect.width > 4096 || rect.height > 4096 || rect.width * rect.height > 8388608) { hide(); return false; }
87
+ let path = "none";
88
+ if (layer === "front" && by !== "human") {
89
+ const clipped = effectTrustClip(rect, proof.rects);
90
+ if (!clipped.ok) return false;
91
+ path = clipped.path;
92
+ }
93
+
94
+ try {
95
+ for (const key of ["x", "y", "width", "height"]) rule.style.setProperty(key === "x" ? "left" : key === "y" ? "top" : key, `${rect[key]}px`, "important");
96
+ rule.style.setProperty("clip-path", path, "important");
97
+ if (!sameClip(path, rule.style.getPropertyValue("clip-path"))) return false;
98
+ // The stylesheet can lose the cascade to another host rule. Verify the actual viewport and clip
99
+ // before revealing: holes expressed in planned coordinates cannot protect a transformed frame.
100
+ const style = host.getComputedStyle(frame), actual = frame.getBoundingClientRect();
101
+ if (style.position !== "fixed" || style.pointerEvents !== "none" || style.visibility !== "hidden" || style.opacity !== (layer === "front" && by !== "human" ? "0.85" : "1") || style.zIndex !== (layer === "front" ? "20" : "0") || !sameClip(path, style.clipPath) || !["x", "y", "width", "height"].every(key => number(actual[key]) && Math.abs(actual[key] - rect[key]) < 0.02)) return false;
102
+ rule.style.setProperty("visibility", "visible", "important");
103
+ return true;
104
+ } catch { hide(); return false; }
105
+ },
106
+ remove() {
107
+ if (dead) return; dead = true; hide(); bound.delete(handle);
108
+ if (frame.isConnected === true) frame.remove();
109
+ try { frame.classList.remove(token); const at = [...sheet.cssRules].indexOf(rule); if (at >= 0) sheet.deleteRule(at); } catch { frame.remove(); }
110
+ },
111
+ };
112
+ bound.add(handle); return handle;
113
+ }
114
+ return { ok: true, bind,
115
+ hide() { for (const h of bound) h.hide(); },
116
+ dispose() {
117
+ if (disposed) return; disposed = true;
118
+ for (const h of [...bound]) h.remove();
119
+ // The host link and its other rules remain intact. Each binding removed only its own rule.
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,10 @@
1
+ /** Fixed same-origin host stylesheet only. No new sheet, inline style, owner CSS or supplied URL. */
2
+ export function effectHostStyle(host) {
3
+ try {
4
+ const link = host?.document?.querySelector('link[rel="stylesheet"][href="/assets/app.css"]');
5
+ const href = new URL("/assets/app.css", host.location.origin).href;
6
+ if (!link || link.tagName !== "LINK" || link.ownerDocument !== host.document || link.isConnected !== true || link.disabled !== false || !link.sheet || link.sheet.ownerNode !== link || link.sheet.href !== href || link.sheet.disabled !== false) return null;
7
+ const sheet = link.sheet;
8
+ return typeof sheet.insertRule === "function" && typeof sheet.deleteRule === "function" && sheet.cssRules ? sheet : null;
9
+ } catch { return null; }
10
+ }
@@ -0,0 +1,74 @@
1
+ /** #1629: host-only personal region targets on the currently proven stock shell. No selection, code or frame authority. */
2
+ import { createEffectGeometry, hostEffectRect, hostEffectSpace } from "./effect-geometry.js";
3
+ import { effectStockSnapshot } from "./effect-stock-snapshot.js";
4
+ const finite = n => typeof n === "number" && Number.isFinite(n);
5
+ function viewport(host) {
6
+ const width = host.innerWidth, height = host.innerHeight;
7
+ return [width, height].every(n => Number.isInteger(n) && n >= 1 && n <= 4096) && width * height <= 8388608 ? { x: 0, y: 0, width, height } : null;
8
+ }
9
+ /** An anchored box uses edge padding; center ignores the offset. Oversized boxes shrink within that padding. */
10
+ export function ownerEffectBoxRect(box, size) {
11
+ try {
12
+ if (!box || ![Object.prototype, null].includes(Object.getPrototypeOf(box))) return null;
13
+ const fields = Object.getOwnPropertyDescriptors(box), keys = Reflect.ownKeys(fields);
14
+ if (keys.length !== 4 || !["anchor", "width", "height", "offset"].every(k => fields[k] && Object.hasOwn(fields[k], "value"))) return null;
15
+ const b = Object.fromEntries(Object.entries(fields).map(([k, d]) => [k, d.value]));
16
+ if (!["top-left", "top-right", "bottom-left", "bottom-right", "center"].includes(b.anchor) || ![b.width, b.height].every(n => Number.isInteger(n) && n >= 32 && n <= 480) || !Number.isInteger(b.offset) || b.offset < 0 || b.offset > 64 || !size || ![size.width, size.height].every(n => Number.isInteger(n) && n >= 1 && n <= 4096) || size.width * size.height > 8388608) return null;
17
+ const pad = b.anchor === "center" ? 0 : b.offset, width = Math.min(b.width, size.width - pad * 2), height = Math.min(b.height, size.height - pad * 2);
18
+ if (width < 1 || height < 1) return null;
19
+ const x = b.anchor === "center" ? Math.floor((size.width - width) / 2) : b.anchor.endsWith("right") ? size.width - pad - width : pad;
20
+ const y = b.anchor === "center" ? Math.floor((size.height - height) / 2) : b.anchor.startsWith("bottom") ? size.height - pad - height : pad;
21
+ return { x, y, width, height };
22
+ } catch { return null; }
23
+ }
24
+ /** Caller supplies the authenticated lease and fresh stock proof. Header/sidebar are real stock elements;
25
+ * page/box are empty hidden host DOM references, never author DOM. They consume no frame/cap slot.
26
+ * A null result requires removing that descriptor; this primitive cannot retire a caller-owned effect frame. */
27
+ export function createOwnerEffectRegions({ window: host, current, factories = {} } = {}) {
28
+ let dead = false, root = null, geometry = null;
29
+ const targets = new Map();
30
+ const dispose = () => { if (dead) return; dead = true; geometry?.dispose?.(); root?.remove(); targets.clear(); };
31
+ const refuse = () => { for (const row of targets.values()) row.binding.hide(); return null; };
32
+ const unavailable = () => ({ ok: false, dispose });
33
+ const alive = () => { if (dead) return false; const valid = current() === true; return !dead && valid && host.document.body.dataset.mode === "full"; };
34
+ try {
35
+ if (!host?.document || typeof current !== "function" || typeof host.getComputedStyle !== "function" || !alive()) return unavailable();
36
+ root = host.document.createElement("div"); root.className = "agend-effect-layer"; host.document.body.append(root);
37
+ geometry = (factories.geometry ?? createEffectGeometry)({ window: host }); if (!geometry.ok) { dispose(); return unavailable(); }
38
+ } catch { dispose(); return unavailable(); }
39
+ function panel() {
40
+ const nodes = host.document.querySelectorAll(".main > .panel"); if (nodes.length !== 1) return null;
41
+ const el = nodes[0], kind = el.getAttribute("data-effect-panel");
42
+ return kind === null ? el.classList.contains("p-chat") ? el : null
43
+ : ["fleet", "needs", "settings", "view", "log", "details"].includes(kind) && el.classList.contains(`p-${kind}`) ? el : null;
44
+ }
45
+ function resolve(slot, box, stock) {
46
+ try {
47
+ if (!alive() || !effectStockSnapshot(stock)) return refuse();
48
+ const surface = panel(), size = viewport(host); if (!surface || !size) return refuse();
49
+ if (slot === "fx.header" || slot === "fx.sidebar") {
50
+ const selector = slot === "fx.header" ? ".main > .panel > .panel-head[data-trust=header]" : ".shell > .sidebar";
51
+ const nodes = host.document.querySelectorAll(selector); if (nodes.length !== 1) return null;
52
+ const el = nodes[0], s = host.getComputedStyle(el), rect = hostEffectRect(el);
53
+ return alive() && panel() === surface && s.display !== "none" && s.visibility === "visible" && hostEffectSpace(host, el) && rect && rect.x + rect.width > 0 && rect.y + rect.height > 0 && rect.x < size.width && rect.y < size.height ? el : null;
54
+ }
55
+ if (!["fx.page", "fx.box"].includes(slot)) return null;
56
+ const rect = slot === "fx.page" ? size : ownerEffectBoxRect(box, size); if (!rect) return refuse();
57
+ let row = targets.get(slot);
58
+ if (!row) {
59
+ const element = host.document.createElement("div"); element.className = "agend-owner-effect-target"; element.setAttribute("aria-hidden", "true"); root.append(element);
60
+ const binding = geometry.bind(element, { layer: "behind", by: "human" });
61
+ if (!binding.ok) { element.remove(); return null; }
62
+ row = { element, binding }; targets.set(slot, row);
63
+ }
64
+ // Empty reference only: the synchronous existing hidden-first verification is followed by hide before yielding.
65
+ // The future registry owns the actual effect's layer/provenance/caps and independent fresh paint check.
66
+ if (!row.binding.sync(rect, stock)) { row.binding.remove(); targets.delete(slot); return null; }
67
+ row.binding.hide();
68
+ const actual = hostEffectRect(row.element);
69
+ if (!alive() || panel() !== surface || row.element.isConnected !== true || host.getComputedStyle(row.element).visibility !== "hidden" || !actual || !["x", "y", "width", "height"].every(k => finite(actual[k]) && actual[k] === rect[k])) { row.binding.remove(); targets.delete(slot); return null; }
70
+ return row.element;
71
+ } catch { return refuse(); }
72
+ }
73
+ return { ok: true, root, resolve, dispose };
74
+ }