@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.
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +25 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +13 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.js +17 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/launch-effort.js +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +17 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +8 -1
- package/dist/channel/adapters/telegram.js +74 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +34 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +15 -0
- package/dist/config-validator.js +17 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +244 -20
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-manager.d.ts +89 -0
- package/dist/fleet-manager.js +401 -25
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/instance-avatar.js +3 -0
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +5 -0
- package/dist/outbound-handlers.js +30 -9
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +64 -0
- package/dist/outbound-schemas.js +50 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +14 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tool-permissions.js +50 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +10 -6
- package/dist/topic-commands.js +21 -17
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +4 -4
- package/dist/ui/chat-store.js +9 -3
- package/dist/ui/chat-strings.js +15 -0
- package/dist/ui/chat-thread.js +47 -21
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +85 -17
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +3 -3
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +8 -5
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +245 -0
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +2 -0
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +12 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +214 -1
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +1 -1
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +81 -8
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +30 -0
- package/dist/ui/signin.html +1 -0
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +188 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +42 -0
- package/dist/web-api.js +896 -36
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +13 -0
- package/dist/web-chat-history.js +23 -2
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +48 -0
- package/dist/web-prefs.js +209 -33
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.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 +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/** #1629: bounded host HTTP projection. No product startup, frame, content event or retry. */
|
|
2
|
+
import { checkStaticEffectPoster } from "./effect-poster-reader.js";
|
|
3
|
+
const loaders = new WeakMap();
|
|
4
|
+
const ID = /^[a-f0-9]{32}$/, EPOCH = /^[a-f0-9]{16}$/, PARAM = /^[a-z][a-z0-9_]{0,31}$/;
|
|
5
|
+
const REGIONS = ["chat", "sidebar", "header", "page", "box"], WANTS = ["pointer", "state", "reply", "mention", "theme"];
|
|
6
|
+
const SLOTS = ["fx.sidebar", "fx.header", "fx.page", "fx.box"];
|
|
7
|
+
const MIB = 1048576, WIRE = 8 * MIB;
|
|
8
|
+
const plain = x => !!x && (Object.getPrototypeOf(x) === Object.prototype || Object.getPrototypeOf(x) === null);
|
|
9
|
+
const finite = x => typeof x === "number" && Number.isFinite(x);
|
|
10
|
+
const integer = (x, lo, hi) => Number.isSafeInteger(x) && x >= lo && x <= hi;
|
|
11
|
+
const exact = (o, keys) => plain(o) && Object.keys(o).sort().join(",") === [...keys].sort().join(",");
|
|
12
|
+
const subset = (o, keys) => plain(o) && Object.keys(o).every(k => keys.includes(k));
|
|
13
|
+
const instanceOK = x => typeof x === "string" && x.length >= 1 && x.length <= 128;
|
|
14
|
+
const enumeration = (x, values, min = 0) => Array.isArray(x) && x.length >= min && x.length <= values.length && new Set(x).size === x.length && x.every(v => values.includes(v));
|
|
15
|
+
/** Copy own data before inspecting it. Refuse accessors/symbols, non-finite values and unbounded trees. */
|
|
16
|
+
function data(value, maxNodes = 1024) {
|
|
17
|
+
let nodes = 0;
|
|
18
|
+
function copy(x, depth) {
|
|
19
|
+
if (++nodes > maxNodes || depth > 8) throw Error("shape");
|
|
20
|
+
if (x === null || typeof x === "string" || typeof x === "boolean" || finite(x)) return x;
|
|
21
|
+
const array = Array.isArray(x);
|
|
22
|
+
if (array ? Object.getPrototypeOf(x) !== Array.prototype || x.length > 32 : !plain(x)) throw Error("shape");
|
|
23
|
+
const out = array ? [] : Object.create(null), keys = Reflect.ownKeys(x);
|
|
24
|
+
if (keys.length > 33) throw Error("shape");
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
if (array && key === "length") continue;
|
|
27
|
+
if (typeof key !== "string" || array && (!/^(0|[1-9][0-9]*)$/.test(key) || !Number.isSafeInteger(Number(key)) || Number(key) >= x.length)) throw Error("shape");
|
|
28
|
+
const d = Object.getOwnPropertyDescriptor(x, key); if (!d || !("value" in d)) throw Error("shape");
|
|
29
|
+
out[key] = copy(d.value, depth + 1);
|
|
30
|
+
}
|
|
31
|
+
if (array && Object.keys(out).length !== x.length) throw Error("shape");
|
|
32
|
+
return Object.freeze(out);
|
|
33
|
+
}
|
|
34
|
+
try { return copy(value, 0); } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
// Same normalized one-line contract as web-effect.ts text()/web-theme.ts canonicalText.
|
|
37
|
+
const FORBIDDEN_TEXT = /(?!\u200d)[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
38
|
+
const VISIBLE_TEXT = /[^\p{M}\p{C}\p{Z}\u115f\u1160\u3164\uffa0\u2800]/u;
|
|
39
|
+
let graphemes = null;
|
|
40
|
+
function displayText(s, max, min = 1) {
|
|
41
|
+
if (typeof s !== "string" || s.length > max * 10) return false;
|
|
42
|
+
if (min === 0 && s === "") return true;
|
|
43
|
+
if (s.normalize("NFC") !== s || FORBIDDEN_TEXT.test(s) || s.trim() !== s || !VISIBLE_TEXT.test(s)) return false;
|
|
44
|
+
try {
|
|
45
|
+
graphemes ??= new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
46
|
+
let count = 0; for (const _part of graphemes.segment(s)) if (++count > max) return false;
|
|
47
|
+
return count >= min;
|
|
48
|
+
} catch { return false; }
|
|
49
|
+
}
|
|
50
|
+
function manifest(x) {
|
|
51
|
+
if (!subset(x, ["v", "kind", "runtime", "worker", "name", "title", "description", "poster", "regions", "layer", "box", "cursor", "wants", "params"]) || x.v !== 1 || x.kind !== "effect" || x.runtime !== "canvas-worker-v1" || x.worker !== "worker.js"
|
|
52
|
+
|| typeof x.name !== "string" || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(x.name) || !displayText(x.title, 40)
|
|
53
|
+
|| x.description !== undefined && !displayText(x.description, 200, 0) || x.poster !== undefined && x.poster !== "poster.png"
|
|
54
|
+
|| !enumeration(x.regions, REGIONS, 1) || !["behind", "front"].includes(x.layer) || !enumeration(x.wants, WANTS)) return false;
|
|
55
|
+
if (x.box !== undefined && (!exact(x.box, ["anchor", "width", "height", "offset"]) || !["top-left", "top-right", "bottom-left", "bottom-right", "center"].includes(x.box.anchor) || !integer(x.box.width, 32, 480) || !integer(x.box.height, 32, 480) || !integer(x.box.offset, 0, 64))) return false;
|
|
56
|
+
if (x.cursor !== undefined && (!exact(x.cursor, ["hotspot"]) || !Array.isArray(x.cursor.hotspot) || x.cursor.hotspot.length !== 2 || !x.cursor.hotspot.every(v => integer(v, 0, 63)))) return false;
|
|
57
|
+
if (x.params === undefined) return true;
|
|
58
|
+
if (!plain(x.params) || Object.keys(x.params).length > 16) return false;
|
|
59
|
+
return Object.entries(x.params).every(([k, d]) => {
|
|
60
|
+
if (!PARAM.test(k) || !plain(d)) return false;
|
|
61
|
+
if (d.type === "number" || d.type === "int") return subset(d, ["type", "min", "max", "default", ...(d.type === "number" ? ["step"] : [])]) && [d.min, d.max, d.default].every(v => finite(v) && Math.abs(v) <= 1e9 && (d.type !== "int" || Number.isInteger(v)))
|
|
62
|
+
&& d.min <= d.max && d.default >= d.min && d.default <= d.max && (d.step === undefined || finite(d.step) && d.step > 0 && d.step <= 2e9 && aligned(d.default, d));
|
|
63
|
+
if (d.type === "bool") return exact(d, ["type", "default"]) && typeof d.default === "boolean";
|
|
64
|
+
if (d.type === "color") return exact(d, ["type", "default"]) && typeof d.default === "string" && /^#[0-9a-f]{6}$/.test(d.default);
|
|
65
|
+
return d.type === "enum" && exact(d, ["type", "default", "values"]) && Array.isArray(d.values) && d.values.length >= 1 && d.values.length <= 16 && new Set(d.values).size === d.values.length && d.values.every(v => typeof v === "string" && /^[a-z0-9][a-z0-9-]{0,31}$/.test(v)) && d.values.includes(d.default);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function aligned(v, d) { const n = (v - d.min) / d.step; return Number.isFinite(n) && Math.abs(n - Math.round(n)) <= 1e-8; }
|
|
69
|
+
function params(m, values) {
|
|
70
|
+
const defs = m.params ?? {};
|
|
71
|
+
return plain(values) && Object.keys(values).sort().join(",") === Object.keys(defs).sort().join(",") && Object.entries(defs).every(([key, d]) => {
|
|
72
|
+
const v = values[key];
|
|
73
|
+
if (d.type === "number" || d.type === "int") return finite(v) && v >= d.min && v <= d.max && (d.type !== "int" || Number.isSafeInteger(v)) && (d.step === undefined || aligned(v, d));
|
|
74
|
+
if (d.type === "bool") return typeof v === "boolean";
|
|
75
|
+
return d.type === "enum" ? d.values.includes(v) : typeof v === "string" && /^#[0-9a-f]{6}$/.test(v);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** HTTP data only: authoritative provenance comes from the host's persisted placement, never a manifest. */
|
|
79
|
+
export function checkEffectPlacements(input) {
|
|
80
|
+
const x = data(input); if (!exact(x, ["rev", "effects"]) || !integer(x.rev, 0, Number.MAX_SAFE_INTEGER) || !Array.isArray(x.effects) || x.effects.length > 2) return null;
|
|
81
|
+
const ids = new Set();
|
|
82
|
+
for (const p of x.effects) {
|
|
83
|
+
if (!exact(p, ["id", "by", "at", "manifest", "params"]) || typeof p.id !== "string" || !ID.test(p.id) || ids.has(p.id) || !["human", "agent"].includes(p.by) || !integer(p.at, 0, Number.MAX_SAFE_INTEGER) || !manifest(p.manifest) || !params(p.manifest, p.params)) return null;
|
|
84
|
+
ids.add(p.id);
|
|
85
|
+
}
|
|
86
|
+
return x;
|
|
87
|
+
}
|
|
88
|
+
/** Closed personal metadata. Uses the same CW1 manifest/text contract as resources, with no code or provenance grant. */
|
|
89
|
+
export function checkEffectOwnerSlots(input) {
|
|
90
|
+
const x = data(input, 4096), keys = [...SLOTS, "fx.cursor"];
|
|
91
|
+
if (!exact(x, ["rev", "slots"]) || !integer(x.rev, 0, Number.MAX_SAFE_INTEGER) || !exact(x.slots, keys)) return null;
|
|
92
|
+
for (const key of keys) {
|
|
93
|
+
const row = x.slots[key];
|
|
94
|
+
if (!plain(row)) return null;
|
|
95
|
+
if (row.status === "none") { if (!exact(row, ["status", "id"]) || row.id !== null) return null; }
|
|
96
|
+
else if (row.status === "unavailable") { if (!exact(row, ["status", "id"]) || typeof row.id !== "string" || !ID.test(row.id)) return null; }
|
|
97
|
+
else if (row.status === "available") {
|
|
98
|
+
if (!exact(row, ["status", "id", "manifest"]) || typeof row.id !== "string" || !ID.test(row.id) || !manifest(row.manifest)) return null;
|
|
99
|
+
if (key === "fx.cursor" ? !row.manifest.cursor : !row.manifest.regions.includes(key.slice(3)) || key === "fx.box" && !row.manifest.box) return null;
|
|
100
|
+
} else return null;
|
|
101
|
+
}
|
|
102
|
+
return x;
|
|
103
|
+
}
|
|
104
|
+
/** Checked local-admin Start response only; this projection never starts or authorizes a trial. */
|
|
105
|
+
export function checkEffectTrialStart(input, expectedOffer, expectedId) {
|
|
106
|
+
const x = data(input);
|
|
107
|
+
if (typeof expectedOffer !== "string" || !ID.test(expectedOffer) || typeof expectedId !== "string" || !ID.test(expectedId)
|
|
108
|
+
|| !exact(x, ["trial", "offer", "remainingMs", "manifest", "params"]) || typeof x.trial !== "string" || !ID.test(x.trial)
|
|
109
|
+
|| !integer(x.remainingMs, 1, 600000) || !exact(x.offer, ["v", "offer", "id", "title", "expiresAt"])
|
|
110
|
+
|| x.offer.v !== 1 || x.offer.offer !== expectedOffer || x.offer.id !== expectedId || !displayText(x.offer.title, 40)
|
|
111
|
+
|| !integer(x.offer.expiresAt, 0, Number.MAX_SAFE_INTEGER) || !manifest(x.manifest) || x.manifest.title !== x.offer.title || !params(x.manifest, x.params)) return null;
|
|
112
|
+
return x;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Never include host poster/cursor bytes in the Worker asset list. The factory snapshots again. */
|
|
116
|
+
export function checkEffectResource(input, expectedId) {
|
|
117
|
+
const x = data(input); if (!exact(x, ["id", "manifest", "params", "wants", "code", "assets"]) || typeof expectedId !== "string" || !ID.test(expectedId) || x.id !== expectedId || !manifest(x.manifest) || !params(x.manifest, x.params)
|
|
118
|
+
|| !enumeration(x.wants, WANTS) || JSON.stringify(x.wants) !== JSON.stringify(x.manifest.wants) || typeof x.code !== "string" || x.code.length > MIB || !Array.isArray(x.assets) || x.assets.length > 16) return null;
|
|
119
|
+
const encoder = new TextEncoder(), code = encoder.encode(x.code); let bytes = code.length + encoder.encode(JSON.stringify(x.params)).length;
|
|
120
|
+
if (bytes > MIB || new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(code) !== x.code) return null;
|
|
121
|
+
const names = new Set(), assets = [];
|
|
122
|
+
for (const a of x.assets) {
|
|
123
|
+
if (!exact(a, ["name", "mime", "data_b64"]) || 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) || typeof a.data_b64 !== "string" || a.data_b64.length > Math.ceil(MIB / 3) * 4 || a.data_b64.length % 4 !== 0) return null;
|
|
124
|
+
const ext = a.name.slice(a.name.lastIndexOf(".") + 1), mime = ["jpg", "jpeg"].includes(ext) ? "image/jpeg" : `image/${ext}`;
|
|
125
|
+
if (a.mime !== mime) return null;
|
|
126
|
+
let raw; try { raw = atob(a.data_b64); if (btoa(raw) !== a.data_b64) return null; } catch { return null; }
|
|
127
|
+
if (!raw.length || (bytes += raw.length) > MIB) return null;
|
|
128
|
+
names.add(a.name); assets.push({ name: a.name, mime, bytes: Uint8Array.from(raw, c => c.charCodeAt(0)) });
|
|
129
|
+
}
|
|
130
|
+
return { id: x.id, manifest: x.manifest, params: x.params, wants: x.wants, code: x.code, assets };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Static host cursor only. Native decode/hotspot checks remain the mount controller's responsibility. */
|
|
134
|
+
export function checkStaticEffectCursor(bytes) {
|
|
135
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > 32768) return null;
|
|
136
|
+
const image = checkStaticEffectPoster(bytes); if (!image) return null;
|
|
137
|
+
const view = new DataView(image.buffer, image.byteOffset, image.byteLength), width = view.getUint32(16), height = view.getUint32(20);
|
|
138
|
+
return width >= 1 && width <= 64 && height === width ? image : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The host freezes ownerEpoch/current at page creation; neither is taken from a response or an effect. */
|
|
142
|
+
export function createEffectRuntimeLoader({ window: host, ownerEpoch, current, onRevoke = () => {} } = {}) {
|
|
143
|
+
if (!host || typeof host !== "object" || loaders.has(host) || typeof host.fetch !== "function" || typeof host.AbortController !== "function" || typeof host.setTimeout !== "function" || typeof host.clearTimeout !== "function" || typeof ownerEpoch !== "string" || !EPOCH.test(ownerEpoch) || typeof current !== "function" || typeof onRevoke !== "function") return { ok: false };
|
|
144
|
+
const reservation = {}; loaders.set(host, reservation);
|
|
145
|
+
let dead = false, revoked = false, previous = null; const pending = new Set();
|
|
146
|
+
const releaseWindow = () => { if (dead && pending.size === 0 && loaders.get(host) === reservation) loaders.delete(host); };
|
|
147
|
+
// Terminal state is latched before callbacks. Abort releases logical callers, never physical reservations.
|
|
148
|
+
function revoke(reason) {
|
|
149
|
+
if (dead || revoked) return;
|
|
150
|
+
revoked = true;
|
|
151
|
+
try { onRevoke(Object.freeze({ reason })); } catch { /* host notification cannot preserve authority */ }
|
|
152
|
+
for (const p of [...pending]) { try { p.abort?.(); } catch { /* cancel the other operations */ } }
|
|
153
|
+
}
|
|
154
|
+
const now = () => { let t; try { t = host.performance.now(); } catch { revoke("clock_unavailable"); return null; } if (!finite(t) || t < 0 || previous !== null && t < previous) { revoke("clock_unavailable"); return null; } previous = t; return t; };
|
|
155
|
+
const alive = () => { if (dead || revoked) return false; try { if (current() === true) return !dead && !revoked; } catch { /* unknown */ } revoke("owner_unavailable"); return false; };
|
|
156
|
+
if (!alive() || now() === null) { dead = true; releaseWindow(); return { ok: false }; }
|
|
157
|
+
function run(kind, path, project, signal, settled = false, binary = false) {
|
|
158
|
+
if (dead || revoked) return Promise.resolve(null);
|
|
159
|
+
const began = now(); if (began === null) return Promise.resolve(null);
|
|
160
|
+
if (!alive() || signal?.aborted || [...pending].filter(p => p.kind === kind).length >= (kind === "resource" ? 3 : 1)) return Promise.resolve(null);
|
|
161
|
+
const controller = new host.AbortController(), op = { kind, controller, abort: null }, limit = binary ? 32768 : kind === "placements" ? 256 * 1024 : WIRE; pending.add(op);
|
|
162
|
+
let release, reader = null, body = null, cancellation = null, timer = null, wasAborted = false;
|
|
163
|
+
// Cache the actual cancellation, but not a pre-response no-op: a late response still owns its body.
|
|
164
|
+
const cancelBody = () => {
|
|
165
|
+
if (!reader && !body) return Promise.resolve();
|
|
166
|
+
if (!cancellation) { try { cancellation = Promise.resolve(reader ? reader.cancel() : body.cancel()).catch(() => {}); } catch { cancellation = Promise.resolve(); } }
|
|
167
|
+
return cancellation;
|
|
168
|
+
};
|
|
169
|
+
const cancelled = new Promise(resolve => { release = resolve; });
|
|
170
|
+
const abort = () => { wasAborted = true; controller.abort(); void cancelBody(); release(null); };
|
|
171
|
+
op.abort = abort;
|
|
172
|
+
const valid = () => { if (!alive() || controller.signal.aborted) { abort(); return false; } const t = now(); if (t === null || t < began || t - began >= 10000) { abort(); return false; } return true; };
|
|
173
|
+
const work = Promise.resolve().then(async () => {
|
|
174
|
+
if (!valid()) return null;
|
|
175
|
+
const response = await host.fetch(path, { credentials: "same-origin", cache: "no-store", redirect: "error", signal: controller.signal });
|
|
176
|
+
body = response.body;
|
|
177
|
+
if (!valid()) return null;
|
|
178
|
+
if ([401, 403, 409].includes(response.status)) { revoke("authorization_rejected"); return null; }
|
|
179
|
+
if (!response.ok || response.redirected || !((binary ? /^image\/png(?:\s*;|$)/i : /^application\/json(?:\s*;|$)/i).test(response.headers.get("content-type") || ""))) return null;
|
|
180
|
+
const length = response.headers.get("content-length");
|
|
181
|
+
if (length !== null && (!/^(0|[1-9][0-9]*)$/.test(length) || !integer(Number(length), 0, limit))) return null;
|
|
182
|
+
if (!body || typeof body.getReader !== "function") return null;
|
|
183
|
+
reader = body.getReader(); const chunks = []; let size = 0;
|
|
184
|
+
for (;;) {
|
|
185
|
+
const part = await reader.read(); if (!valid()) return null;
|
|
186
|
+
if (part.done) break;
|
|
187
|
+
if (!(part.value instanceof Uint8Array) || (size += part.value.length) > limit) { abort(); return null; }
|
|
188
|
+
chunks.push(part.value.slice());
|
|
189
|
+
}
|
|
190
|
+
const bytes = new Uint8Array(size); let offset = 0; for (const part of chunks) { bytes.set(part, offset); offset += part.length; }
|
|
191
|
+
let value = bytes; if (!binary) { try { value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); } catch { return null; } }
|
|
192
|
+
if (!valid()) return null;
|
|
193
|
+
const answer = project(value); return valid() ? answer : null;
|
|
194
|
+
}).catch(() => null).finally(async () => { controller.abort(); await cancelBody(); pending.delete(op); releaseWindow(); host.clearTimeout(timer); signal?.removeEventListener("abort", abort); }).then(value => {
|
|
195
|
+
// Cleanup aborts the controller even on success; only an actual abort or stale completion revokes the result.
|
|
196
|
+
if (!alive() || wasAborted || signal?.aborted) return null;
|
|
197
|
+
const t = now(); return t !== null && t >= began && t - began < 10000 ? value : null;
|
|
198
|
+
});
|
|
199
|
+
timer = host.setTimeout(abort, 10000); signal?.addEventListener("abort", abort, { once: true });
|
|
200
|
+
return settled ? work : Promise.race([work, cancelled]);
|
|
201
|
+
}
|
|
202
|
+
const placementRead = (instance, signal, settled = false) => instanceOK(instance) ? run("placements", `/ui/effects/placements?instance=${encodeURIComponent(instance)}&ownerEpoch=${ownerEpoch}`, checkEffectPlacements, signal, settled) : Promise.resolve(null);
|
|
203
|
+
function resourceRead(input, signal, settled = false) {
|
|
204
|
+
const spec = data(input);
|
|
205
|
+
if (!subset(spec, ["id", "instance", "slot"]) || typeof spec.id !== "string" || !ID.test(spec.id) || (spec.instance !== undefined) === (spec.slot !== undefined) || spec.instance !== undefined && !instanceOK(spec.instance) || spec.slot !== undefined && !SLOTS.includes(spec.slot)) return Promise.resolve(null);
|
|
206
|
+
const id = spec.id, scope = spec.instance !== undefined ? `instance=${encodeURIComponent(spec.instance)}` : `slot=${encodeURIComponent(spec.slot)}`;
|
|
207
|
+
return run("resource", `/ui/effects/resource/${id}?${scope}&ownerEpoch=${ownerEpoch}`, value => checkEffectResource(value, id), signal, settled);
|
|
208
|
+
}
|
|
209
|
+
function trialResourceRead(input, signal) {
|
|
210
|
+
const spec = data(input);
|
|
211
|
+
if (!exact(spec, ["trial", "id", "instance"]) || typeof spec.trial !== "string" || !ID.test(spec.trial)
|
|
212
|
+
|| typeof spec.id !== "string" || !ID.test(spec.id) || !instanceOK(spec.instance)) return Promise.resolve(null);
|
|
213
|
+
const id = spec.id;
|
|
214
|
+
return run("resource", `/ui/effect-trials/${spec.trial}/resource?instance=${encodeURIComponent(spec.instance)}&ownerEpoch=${ownerEpoch}`, value => checkEffectResource(value, id), signal, true);
|
|
215
|
+
}
|
|
216
|
+
const ownerRead = (signal, settled = false) => run("placements", `/ui/effects/owner?ownerEpoch=${ownerEpoch}`, checkEffectOwnerSlots, signal, settled);
|
|
217
|
+
return { ok: true,
|
|
218
|
+
owner(signal) { return ownerRead(signal); },
|
|
219
|
+
placements(instance, signal) { return placementRead(instance, signal); },
|
|
220
|
+
resource(input, signal) { return resourceRead(input, signal); },
|
|
221
|
+
// Host scheduler seams: logical abort is prompt; these promises retain ownership until physical cleanup.
|
|
222
|
+
ownerSettled(signal) { return ownerRead(signal, true); },
|
|
223
|
+
placementsSettled(instance, signal) { return placementRead(instance, signal, true); },
|
|
224
|
+
resourceSettled(input, signal) { return resourceRead(input, signal, true); },
|
|
225
|
+
trialResourceSettled(input, signal) { return trialResourceRead(input, signal); },
|
|
226
|
+
// One binary physical lane in the same loader: no response-derived URL, code, metadata or ACL fallback.
|
|
227
|
+
cursorImageSettled(id, signal) {
|
|
228
|
+
return typeof id === "string" && ID.test(id)
|
|
229
|
+
? run("cursor", `/ui/effects/image/${id}/cursor.png?slot=fx.cursor&ownerEpoch=${ownerEpoch}`, checkStaticEffectCursor, signal, true, true)
|
|
230
|
+
: Promise.resolve(null);
|
|
231
|
+
},
|
|
232
|
+
dispose() { if (dead) return; dead = true; for (const p of pending) p.abort(); releaseWindow(); },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** #1629: synchronous browser probation/history participant. No product startup or frame creation. */
|
|
2
|
+
import { newEffectPauseHistory, recordEffectPause } from "./effect-profiler.js";
|
|
3
|
+
|
|
4
|
+
const PREFIX = "agend_effect_safety_v1:", MAX_KEYS = 4096, MAX_RECORDS = 2048, MAX_BYTES = 512 * 1024;
|
|
5
|
+
const plain = o => !!o && (Object.getPrototypeOf(o) === Object.prototype || Object.getPrototypeOf(o) === null);
|
|
6
|
+
const idOK = x => typeof x === "string" && /^[a-f0-9]{32}$/.test(x);
|
|
7
|
+
const deviceOK = x => x === "standard" || x === "constrained";
|
|
8
|
+
const wallOK = x => typeof x === "number" && Number.isSafeInteger(x) && x >= 0;
|
|
9
|
+
const fields = (o, expected) => plain(o) && Object.keys(o).sort().join(",") === expected.split(",").sort().join(",");
|
|
10
|
+
const keyFor = r => `${PREFIX}${r.kind}:${r.id}:${r.device}:${r.token}`;
|
|
11
|
+
function valid(r) {
|
|
12
|
+
if (!plain(r) || r.v !== 1 || !idOK(r.id) || !deviceOK(r.device) || !idOK(r.token)) return false;
|
|
13
|
+
if (r.kind === "probation") return fields(r, "v,kind,id,device,token,owner,at") && idOK(r.owner) && wallOK(r.at);
|
|
14
|
+
if (r.kind === "pause") return fields(r, "v,kind,id,device,token,at") && wallOK(r.at);
|
|
15
|
+
return r.kind === "resume" && fields(r, "v,kind,id,device,token");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Storage is local to this browser/origin. Append-only pause receipts avoid read/modify/write lost updates
|
|
20
|
+
* between tabs. Explicit human Resume acknowledges exact pause ids; it never deletes history or sticky off.
|
|
21
|
+
* Any unreadable/unknown receipt, quota failure or incomplete cleanup latches this participant closed.
|
|
22
|
+
*/
|
|
23
|
+
export function createEffectSafety({ storage, crypto, wall = () => Date.now() } = {}) {
|
|
24
|
+
let fault = false, owner = null;
|
|
25
|
+
const owned = new Map();
|
|
26
|
+
const fail = () => { fault = true; return null; };
|
|
27
|
+
const nonce = () => {
|
|
28
|
+
try {
|
|
29
|
+
const bytes = new Uint8Array(16); if (crypto.getRandomValues(bytes) !== bytes) return fail();
|
|
30
|
+
return [...bytes].map(x => x.toString(16).padStart(2, "0")).join("");
|
|
31
|
+
} catch { return fail(); }
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
if (!storage || ["key", "getItem", "setItem", "removeItem"].some(k => typeof storage[k] !== "function") || typeof crypto?.getRandomValues !== "function" || typeof wall !== "function") fault = true;
|
|
35
|
+
else owner = nonce();
|
|
36
|
+
} catch { fault = true; }
|
|
37
|
+
function read() {
|
|
38
|
+
if (fault) return null;
|
|
39
|
+
try {
|
|
40
|
+
const length = storage.length;
|
|
41
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > MAX_KEYS) return fail();
|
|
42
|
+
const rows = new Map(), seen = new Set(); let bytes = 0;
|
|
43
|
+
for (let i = 0; i < length; i++) {
|
|
44
|
+
const key = storage.key(i);
|
|
45
|
+
if (typeof key !== "string" || seen.has(key)) return fail();
|
|
46
|
+
seen.add(key);
|
|
47
|
+
if (!key.startsWith(PREFIX)) continue;
|
|
48
|
+
const text = storage.getItem(key);
|
|
49
|
+
if (typeof text !== "string" || text.length > 1024) return fail();
|
|
50
|
+
bytes += (key.length + text.length) * 2;
|
|
51
|
+
if (bytes > MAX_BYTES || rows.size >= MAX_RECORDS) return fail();
|
|
52
|
+
const value = JSON.parse(text);
|
|
53
|
+
if (!valid(value) || keyFor(value) !== key) return fail();
|
|
54
|
+
rows.set(key, { value, text });
|
|
55
|
+
}
|
|
56
|
+
if (storage.length !== length) return fail();
|
|
57
|
+
// A still-owned probation receipt cannot disappear or change beneath a live page. Another tab's
|
|
58
|
+
// explicit Resume may remove it; that page must stop rather than silently treating the loss as healthy.
|
|
59
|
+
for (const item of owned.values()) if (rows.get(item.key)?.text !== item.text) return fail();
|
|
60
|
+
for (const { value: r } of rows.values()) if (r.kind === "resume" && !rows.has(keyFor({ ...r, kind: "pause" }))) return fail();
|
|
61
|
+
return rows;
|
|
62
|
+
} catch { return fail(); }
|
|
63
|
+
}
|
|
64
|
+
function inspect(rows, id, device) {
|
|
65
|
+
const relevant = [...rows.values()].map(x => x.value).filter(r => r.id === id && r.device === device);
|
|
66
|
+
const pauses = relevant.filter(r => r.kind === "pause").sort((a, b) => a.at - b.at || a.token.localeCompare(b.token));
|
|
67
|
+
let history = newEffectPauseHistory(id, device, 0);
|
|
68
|
+
for (const r of pauses) { const next = recordEffectPause(history, r.at); if (!next.ok) return fail(); history = next.history; }
|
|
69
|
+
const interrupted = relevant.some(r => r.kind === "probation" && r.owner !== owner);
|
|
70
|
+
const paused = pauses.some(r => !rows.has(keyFor({ ...r, kind: "resume" })));
|
|
71
|
+
return { state: history.off ? "off" : interrupted ? "interrupted" : paused ? "paused" : "ok", history };
|
|
72
|
+
}
|
|
73
|
+
function put(record, rows) {
|
|
74
|
+
try {
|
|
75
|
+
const key = keyFor(record), text = JSON.stringify(record), previous = storage.getItem(key);
|
|
76
|
+
if (previous !== null) { if (previous === text) return true; return !!fail(); }
|
|
77
|
+
if (rows.size >= MAX_RECORDS || [...rows].reduce((n, [k, r]) => n + (k.length + r.text.length) * 2, (key.length + text.length) * 2) > MAX_BYTES) return !!fail();
|
|
78
|
+
storage.setItem(key, text);
|
|
79
|
+
if (storage.getItem(key) !== text) return !!fail();
|
|
80
|
+
return true;
|
|
81
|
+
} catch { return !!fail(); }
|
|
82
|
+
}
|
|
83
|
+
function remove(key, text) {
|
|
84
|
+
try {
|
|
85
|
+
if (storage.getItem(key) !== text) return !!fail();
|
|
86
|
+
storage.removeItem(key);
|
|
87
|
+
if (storage.getItem(key) !== null) return !!fail();
|
|
88
|
+
return true;
|
|
89
|
+
} catch { return !!fail(); }
|
|
90
|
+
}
|
|
91
|
+
function stamp(rows, id, device) {
|
|
92
|
+
try {
|
|
93
|
+
const now = wall(); if (!wallOK(now)) return fail();
|
|
94
|
+
return Math.max(now, ...[...rows.values()].map(x => x.value).filter(r => r.id === id && r.device === device && "at" in r).map(r => r.at));
|
|
95
|
+
} catch { return fail(); }
|
|
96
|
+
}
|
|
97
|
+
const status = (id, device) => {
|
|
98
|
+
if (!idOK(id) || !deviceOK(device)) return { state: "unknown" };
|
|
99
|
+
const rows = read(), result = rows && inspect(rows, id, device);
|
|
100
|
+
return result ? { state: result.state, history: { ...result.history, pauses: [...result.history.pauses] } } : { state: "unknown" };
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
status,
|
|
104
|
+
allowed(id, device) { return status(id, device).state === "ok"; },
|
|
105
|
+
begin(spec) {
|
|
106
|
+
if (!plain(spec) || !idOK(spec.id) || !deviceOK(spec.device) || typeof spec.key !== "string" || spec.key.length < 1 || spec.key.length > 256) return null;
|
|
107
|
+
const rows = read(), state = rows && inspect(rows, spec.id, spec.device);
|
|
108
|
+
if (!state || state.state !== "ok") return null;
|
|
109
|
+
const token = nonce(), at = stamp(rows, spec.id, spec.device);
|
|
110
|
+
if (token === null || at === null) return null;
|
|
111
|
+
const record = { v: 1, kind: "probation", id: spec.id, device: spec.device, token, owner, at };
|
|
112
|
+
// Never overwrite a colliding marker, even one this page already owns.
|
|
113
|
+
if (rows.has(keyFor(record))) return fail();
|
|
114
|
+
if (!put(record, rows)) return null;
|
|
115
|
+
const fresh = read(), verified = fresh && inspect(fresh, spec.id, spec.device);
|
|
116
|
+
if (!verified || verified.state !== "ok" || fresh.get(keyFor(record))?.text !== JSON.stringify(record)) { if (!fault) remove(keyFor(record), JSON.stringify(record)); return null; }
|
|
117
|
+
owned.set(token, { key: keyFor(record), text: JSON.stringify(record) });
|
|
118
|
+
return token;
|
|
119
|
+
},
|
|
120
|
+
finish(token, reason) {
|
|
121
|
+
if (fault || !idOK(token) || typeof reason !== "string" || reason.length < 1 || reason.length > 128) return false;
|
|
122
|
+
const item = owned.get(token); if (!item) return false;
|
|
123
|
+
if (!remove(item.key, item.text)) return false;
|
|
124
|
+
owned.delete(token); return true;
|
|
125
|
+
},
|
|
126
|
+
pause(record) {
|
|
127
|
+
if (!plain(record) || !idOK(record.id) || !deviceOK(record.device) || record.reason !== "host_budget") return false;
|
|
128
|
+
const rows = read(); if (!rows) return false;
|
|
129
|
+
const token = nonce(), at = stamp(rows, record.id, record.device);
|
|
130
|
+
if (token === null || at === null) return false;
|
|
131
|
+
const value = { v: 1, kind: "pause", id: record.id, device: record.device, token, at };
|
|
132
|
+
if (rows.has(keyFor(value))) return !!fail();
|
|
133
|
+
return put(value, rows);
|
|
134
|
+
},
|
|
135
|
+
/** Human action only: acknowledges the observed pauses, leaving every immutable history receipt intact. */
|
|
136
|
+
resume(id, device) {
|
|
137
|
+
if (!idOK(id) || !deviceOK(device)) return false;
|
|
138
|
+
const rows = read(), state = rows && inspect(rows, id, device);
|
|
139
|
+
if (!state || state.state === "off") return false;
|
|
140
|
+
for (const [key, { value: r, text }] of rows) {
|
|
141
|
+
if (r.id !== id || r.device !== device) continue;
|
|
142
|
+
if (r.kind === "probation" && r.owner !== owner && !remove(key, text)) return false;
|
|
143
|
+
if (r.kind === "pause" && !rows.has(keyFor({ ...r, kind: "resume" }))) {
|
|
144
|
+
const ack = { v: 1, kind: "resume", id, device, token: r.token };
|
|
145
|
+
if (!put(ack, rows)) return false;
|
|
146
|
+
rows.set(keyFor(ack), { value: ack, text: JSON.stringify(ack) });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const fresh = read(), verified = fresh && inspect(fresh, id, device);
|
|
150
|
+
return verified?.state === "ok";
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** #1629: a bounded copy of host stock paint evidence. Never reads an accessor or a DOMRect. */
|
|
2
|
+
const snapshots = new WeakSet();
|
|
3
|
+
const plain = value => !!value && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
4
|
+
const finite = value => typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= 1e7;
|
|
5
|
+
function fields(value, required, optional = []) {
|
|
6
|
+
if (!plain(value)) return null;
|
|
7
|
+
const descriptors = Object.getOwnPropertyDescriptors(value), keys = Reflect.ownKeys(descriptors);
|
|
8
|
+
if (keys.some(key => typeof key !== "string" || !required.includes(key) && !optional.includes(key) || !Object.hasOwn(descriptors[key], "value")) || required.some(key => !Object.hasOwn(descriptors, key))) return null;
|
|
9
|
+
return Object.fromEntries(keys.map(key => [key, descriptors[key].value]));
|
|
10
|
+
}
|
|
11
|
+
export function effectStockSnapshot(value) {
|
|
12
|
+
try {
|
|
13
|
+
if (snapshots.has(value)) return value;
|
|
14
|
+
const data = fields(value, ["ok", "front", "agentFront", "rects"], ["reason", "agentReason", "settingsHref"]);
|
|
15
|
+
if (!data || data.ok !== true || typeof data.front !== "boolean" || typeof data.agentFront !== "boolean" || data.agentFront && !data.front || !Array.isArray(data.rects) || data.rects.length > 128) return null;
|
|
16
|
+
// Optional host display metadata is not copied into the paint evidence.
|
|
17
|
+
for (const key of ["reason", "agentReason", "settingsHref"]) if (Object.hasOwn(data, key) && data[key] !== null && (typeof data[key] !== "string" || data[key].length > 256)) return null;
|
|
18
|
+
const entries = Object.getOwnPropertyDescriptors(data.rects), keys = Reflect.ownKeys(entries);
|
|
19
|
+
if (keys.length !== data.rects.length + 1 || keys.some(key => key !== "length" && (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= data.rects.length || !Object.hasOwn(entries[key], "value")))) return null;
|
|
20
|
+
const rects = [];
|
|
21
|
+
for (let i = 0; i < data.rects.length; i++) {
|
|
22
|
+
const r = fields(entries[i].value, ["x", "y", "width", "height"]);
|
|
23
|
+
if (!r || ![r.x, r.y, r.width, r.height].every(finite) || r.width < 0 || r.height < 0) return null;
|
|
24
|
+
rects.push(Object.freeze(r));
|
|
25
|
+
}
|
|
26
|
+
const result = Object.freeze({ ok: true, front: data.front, agentFront: data.agentFront, rects: Object.freeze(rects) });
|
|
27
|
+
snapshots.add(result); return result;
|
|
28
|
+
} catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
/** Missing provenance receives the agent rule. Behind still requires proven stock ordering. */
|
|
31
|
+
export function effectStockAllows(snapshot, { layer, by } = {}) {
|
|
32
|
+
const proof = effectStockSnapshot(snapshot);
|
|
33
|
+
return !!proof && (layer === "behind" || layer === "front" && proof.front && (by === "human" || proof.agentFront));
|
|
34
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/** #1629: stock shell inventory/paint prerequisite. No frame creation, effect code, content or network reads. */
|
|
2
|
+
const SPECS = [
|
|
3
|
+
["composer", ".shell .p-chat .chat-main > .dock"],
|
|
4
|
+
["reply", ".shell .thread .rb-row, .shell .thread .poll-voters"],
|
|
5
|
+
["navigation", ".shell .side-head, .shell .side-nav, .shell .side-section, .shell .side-foot, .shell .tabs, .shell .skip, .shell .side-resize"],
|
|
6
|
+
["header", ".shell .panel-head"], ["status", ".shell .main > .conn"], ["usage", ".shell > .u-rail"],
|
|
7
|
+
["widget", ".shell .aw, .shell .side-widget"], ["card", ".shell .p-chat > .chat-card"], ["permission", ".shell .n-item, .shell .dock .prompts"],
|
|
8
|
+
["menu", ".shell .menu-list[role=menu]"], ["session", ".shell .side-foot .pop[role=dialog]"],
|
|
9
|
+
["popover", ".shell .prefs-pop > .pop"], ["dialog", ".shell dialog.dlg"], ["lightbox", ".shell dialog.lb"],
|
|
10
|
+
["tour", "body > #tour.tour[role=dialog]"], ["keep", ".shell > .oc-keep"], ["toast", ".shell > .toasts"],
|
|
11
|
+
["confirmation", ".shell > .pending-stack, .shell .p-settings > .panel-body > .col > .pending-stack.inline"], ["preview", ".shell .chat-split > .pv-panel, .shell .chat-split > .pv-divider"],
|
|
12
|
+
["preview-menu", ".shell .thread details.pv-menu"], ["palette", ".shell .dock .cmd-palette"],
|
|
13
|
+
["drop", ".shell .p-chat > .drop-overlay"],
|
|
14
|
+
["thread-control", ".shell .p-chat .jump-latest, .shell .thread .msg-tools, .shell .thread .st-more, .shell .thread .cb-head, .shell .thread .cb-more, .shell .thread .tc-head, .shell .thread .tc-more, .shell .thread .pv-head, .shell .thread .pv-note, .shell .thread .pv-banner, .shell .thread .effect-trial-offer, .shell .thread .click-chip"],
|
|
15
|
+
["link", ".shell .thread .md a, .shell .thread .st-text > a.st-link, .shell .dock .prompts .txt > a.st-link"], ["attachment", ".shell .thread .atts"], ["media", ".shell .thread .att-media"],
|
|
16
|
+
["panel-navigation", ".shell .p-fleet > .seg, .shell .p-settings > .seg"],
|
|
17
|
+
["panel-content", ".shell .p-fleet > .panel-body, .shell .p-needs > .panel-body, .shell .p-settings > .panel-body, .shell .p-log > .panel-body, .shell .p-details > .panel-body, .shell .p-view > .panel-body, .shell .p-view > .v-term, .shell .p-view > .v-id, .shell .p-view > .v-card"],
|
|
18
|
+
["panel-controls", ".shell .p-log > .log-bar, .shell .p-settings > .s-pending"],
|
|
19
|
+
["session-ended", "body > .agend-session-ended[role=alert]"],
|
|
20
|
+
];
|
|
21
|
+
export const STOCK_TRUST_INVENTORY = Object.freeze(SPECS.map(([kind, selector]) => Object.freeze({ kind, selector })));
|
|
22
|
+
const STRUCTURAL = ["html", "body", "#app", "#app > .shell", ".shell > .main", ".main > .p-chat", ".p-chat > .chat-split", ".chat-split > .chat-main"];
|
|
23
|
+
const CONTENT = [".main > .conn", ".p-chat > .panel-head", ".p-chat > .chat-card", ".p-chat > .aw-chat_header", ".chat-main > .scroller", ".chat-main > .dock", ".sidebar > .side-head", ".sidebar > .side-nav", ".sidebar > .side-section", ".sidebar > .side-widget", ".sidebar > .side-foot", ".u-rail > .u-rail-head", ".u-rail > .u-rail-body"];
|
|
24
|
+
// Closed producer/topology inventory. Bare Outlet loading/error panels never become an effect surface.
|
|
25
|
+
const PANELS = Object.freeze({
|
|
26
|
+
fleet: { required: [".panel-head", ".seg", ".panel-body"], optional: [] },
|
|
27
|
+
needs: { required: [".panel-head", ".panel-body"], optional: [] },
|
|
28
|
+
settings: { required: [".panel-head", ".seg", ".panel-body"], optional: [".s-pending"] },
|
|
29
|
+
log: { required: [".panel-head", ".panel-body"], optional: [".log-bar"] },
|
|
30
|
+
details: { required: [".panel-head", ".panel-body"], optional: [] },
|
|
31
|
+
view: { required: [".panel-head"], optional: [".panel-body", ".v-term", ".v-id", ".v-card"] },
|
|
32
|
+
});
|
|
33
|
+
function panelSurface(doc, chat) {
|
|
34
|
+
if (chat) return { panel: chat, structural: STRUCTURAL, content: CONTENT, kind: "chat" };
|
|
35
|
+
const panel = doc.querySelector(".main > .panel"), kind = panel?.getAttribute("data-effect-panel"), spec = Object.hasOwn(PANELS, kind) ? PANELS[kind] : null;
|
|
36
|
+
if (!spec || panel.className !== `panel p-${kind}` || [...panel.childNodes].some(node => node.nodeType === 3 && node.textContent.trim())) return null;
|
|
37
|
+
const children = [...panel.children], count = selector => children.filter(el => el.matches(selector)).length;
|
|
38
|
+
if (spec.required.some(selector => count(selector) !== 1) || spec.optional.some(selector => count(selector) > 1)
|
|
39
|
+
|| children.some(el => !el.matches([...spec.required, ...spec.optional, "dialog.dlg", "dialog.lb"].join(",")))) return null;
|
|
40
|
+
if (kind === "view" && !(count(".panel-body") === 1 && !count(".v-term") && !count(".v-id") && !count(".v-card")
|
|
41
|
+
|| !count(".panel-body") && count(".v-term") === 1 && count(".v-card") === 1)) return null;
|
|
42
|
+
const prefix = `.p-${kind} > `;
|
|
43
|
+
return { panel, kind, structural: [...STRUCTURAL.slice(0, 5), `.main > .p-${kind}`],
|
|
44
|
+
content: [...CONTENT.filter(selector => !selector.startsWith(".p-chat") && !selector.startsWith(".chat-main")),
|
|
45
|
+
...[...spec.required, ...spec.optional].map(selector => prefix + selector)] };
|
|
46
|
+
}
|
|
47
|
+
const BARRIERS = '[role=menu],[role=dialog],[role=alertdialog],[role=listbox],dialog[open],details[open],.toast,.pending-stack:not(.inline),.oc-keep,.shell.sb-open,.p-chat.dragging,.agend-session-ended,audio[controls],video[controls],.effect-trial-panel';
|
|
48
|
+
/** Shared closed overlay list; a trial may exclude only its own positively identified panel. */
|
|
49
|
+
export const STOCK_EFFECT_BARRIERS = BARRIERS;
|
|
50
|
+
const finite = n => typeof n === "number" && Number.isFinite(n) && Math.abs(n) <= 1e7;
|
|
51
|
+
const none = x => x === "" || x === undefined || x === "none";
|
|
52
|
+
// The stock narrow header fades its own pixels inside its already bounded content context.
|
|
53
|
+
// This exception is never applied to an ancestor or another content surface.
|
|
54
|
+
function stockWidgetMask(host, selector, style) {
|
|
55
|
+
return selector === ".p-chat > .aw-chat_header" && host.innerWidth < 900
|
|
56
|
+
&& typeof style.maskImage === "string"
|
|
57
|
+
&& /^linear-gradient\(to right, (?:rgb\(0, 0, 0\)|rgb\(0 0 0\)) calc\(100% - 32px\), (?:rgba\(0, 0, 0, 0\)|rgb\(0 0 0 \/ 0\))\)$/.test(style.maskImage);
|
|
58
|
+
}
|
|
59
|
+
function neutral(style, allowStockMask = false) {
|
|
60
|
+
return !!style && ["transform", "perspective", "filter", "backdropFilter", "rotate", "scale", "translate", "offsetPath", "clipPath"].every(k => none(style[k])) && (none(style.maskImage) || allowStockMask)
|
|
61
|
+
&& style.opacity === "1" && style.isolation === "auto" && style.mixBlendMode === "normal" && style.contain === "none" && style.willChange === "auto"
|
|
62
|
+
&& (style.contentVisibility === undefined || ["", "visible"].includes(style.contentVisibility)) && (style.zoom === undefined || ["", "1", "normal"].includes(style.zoom));
|
|
63
|
+
}
|
|
64
|
+
function rect(el) {
|
|
65
|
+
const r = el.getBoundingClientRect();
|
|
66
|
+
return [r.x, r.y, r.width, r.height].every(finite) && r.width >= 0 && r.height >= 0 ? { x: r.x, y: r.y, width: r.width, height: r.height } : null;
|
|
67
|
+
}
|
|
68
|
+
function visible(host, el) {
|
|
69
|
+
if (!el || el.isConnected !== true) return false;
|
|
70
|
+
// A descendant of a closed details/dialog may have a styled box but no painted client rect.
|
|
71
|
+
const s = host.getComputedStyle(el);
|
|
72
|
+
return s.display !== "none" && s.visibility === "visible" && s.opacity !== "0" && el.getClientRects().length > 0;
|
|
73
|
+
}
|
|
74
|
+
/** Conservatively include stock outer shadows and focus outlines, not only the border box. Unknown paint refuses. */
|
|
75
|
+
function outset(style) {
|
|
76
|
+
let pad = 0;
|
|
77
|
+
if (!none(style.textShadow)) return null;
|
|
78
|
+
if (style.outlineStyle !== "none") {
|
|
79
|
+
const width = parseFloat(style.outlineWidth), offset = parseFloat(style.outlineOffset);
|
|
80
|
+
if (![width, offset].every(Number.isFinite)) return null;
|
|
81
|
+
pad = Math.max(pad, width + Math.max(0, offset));
|
|
82
|
+
}
|
|
83
|
+
if (!none(style.boxShadow)) {
|
|
84
|
+
// Computed colors may contain commas; remove closed color functions before splitting the shadow list.
|
|
85
|
+
const shadow = style.boxShadow.replace(/(?:rgba?|color|color-mix)\([^()]*\)/g, "color");
|
|
86
|
+
for (const part of shadow.split(",")) {
|
|
87
|
+
if (/\binset\b/.test(part)) continue;
|
|
88
|
+
const lengths = [...part.matchAll(/(-?\d+(?:\.\d+)?)px\b/g)].map(m => Number(m[1]));
|
|
89
|
+
if (lengths.length < 2 || lengths.length > 4 || !lengths.every(Number.isFinite)) return null;
|
|
90
|
+
const [x, y, blur = 0, spread = 0] = lengths;
|
|
91
|
+
if (blur < 0) return null;
|
|
92
|
+
// Stock blur support is bounded generously; no arbitrary constant padding is the security argument.
|
|
93
|
+
pad = Math.max(pad, Math.abs(x) + 3 * blur + Math.max(0, spread), Math.abs(y) + 3 * blur + Math.max(0, spread));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return pad <= 256 ? Math.ceil(pad) : null;
|
|
97
|
+
}
|
|
98
|
+
function protectedPaint(host, root, budget) {
|
|
99
|
+
const nodes = [root, ...root.querySelectorAll("*")]; budget.n += nodes.length;
|
|
100
|
+
if (budget.n > 4096) return null;
|
|
101
|
+
let union = null;
|
|
102
|
+
for (const el of nodes) {
|
|
103
|
+
if (!visible(host, el)) continue;
|
|
104
|
+
const style = host.getComputedStyle(el), r = rect(el), pad = outset(style);
|
|
105
|
+
if (!r || pad === null || !none(style.filter) || !none(style.backdropFilter)) return null;
|
|
106
|
+
const a = { x: r.x - pad, y: r.y - pad, width: r.width + pad * 2, height: r.height + pad * 2 };
|
|
107
|
+
if (!union) union = a;
|
|
108
|
+
else { const x = Math.min(union.x, a.x), y = Math.min(union.y, a.y); union = { x, y, width: Math.max(union.x + union.width, a.x + a.width) - x, height: Math.max(union.y + union.height, a.y + a.height) - y }; }
|
|
109
|
+
}
|
|
110
|
+
return union || { x: 0, y: 0, width: 0, height: 0 };
|
|
111
|
+
}
|
|
112
|
+
/** Snapshot immediately before reveal; callers must hide first on mutations/scroll/resize. No observer is installed here. */
|
|
113
|
+
export function stockEffectTrust(host, { frameRoot = null, ownerCss = "unknown" } = {}) {
|
|
114
|
+
const fail = reason => ({ ok: false, reason, rects: [], front: false });
|
|
115
|
+
try {
|
|
116
|
+
const doc = host.document, app = doc.getElementById("app"), shell = doc.querySelector("#app > .shell"), chat = doc.querySelector(".main > .p-chat");
|
|
117
|
+
if (!app || app.parentElement !== doc.body || !shell || doc.querySelectorAll("#app > .shell").length !== 1 || doc.querySelectorAll(".main > .panel").length !== 1) return fail("stock_route_unknown");
|
|
118
|
+
const surface = panelSurface(doc, chat);
|
|
119
|
+
if (!surface) return fail("stock_route_unknown");
|
|
120
|
+
if (frameRoot && (frameRoot.ownerDocument !== doc || frameRoot.parentElement !== doc.body || !frameRoot.classList.contains("agend-effect-layer") || !neutral(host.getComputedStyle(frameRoot)) || !["static", "relative"].includes(host.getComputedStyle(frameRoot).position) || host.getComputedStyle(frameRoot).zIndex !== "auto")) return fail("stock_frame_root_unknown");
|
|
121
|
+
for (const selector of surface.structural) {
|
|
122
|
+
const el = doc.querySelector(selector), s = el && host.getComputedStyle(el);
|
|
123
|
+
if (!el || el.isConnected !== true || !neutral(s) || s.zIndex !== "auto" || !["static", "relative"].includes(s.position)) return fail("stock_order_unknown");
|
|
124
|
+
}
|
|
125
|
+
for (const selector of surface.content) for (const el of doc.querySelectorAll(selector)) {
|
|
126
|
+
if (!visible(host, el)) { const children = [...el.querySelectorAll("*")]; if (children.length > 4096 || children.some(child => visible(host, child))) return fail("stock_order_unknown"); continue; }
|
|
127
|
+
const s = host.getComputedStyle(el);
|
|
128
|
+
if (s.position !== (selector === ".p-settings > .s-pending" ? "sticky" : "relative") || s.zIndex !== (selector.endsWith(" > .panel-head") ? "2" : "1") || !neutral(s, stockWidgetMask(host, selector, s))) return fail("stock_order_unknown");
|
|
129
|
+
}
|
|
130
|
+
// The resize separator has no bounded content parent; prove its own level and neutral sidebar/usage ancestors.
|
|
131
|
+
for (const el of doc.querySelectorAll(".shell > .sidebar, .shell > .u-rail")) {
|
|
132
|
+
if (!visible(host, el)) { const children = [...el.querySelectorAll("*")]; if (children.length > 4096 || children.some(child => visible(host, child))) return fail("stock_order_unknown"); continue; }
|
|
133
|
+
const s = host.getComputedStyle(el);
|
|
134
|
+
if (!neutral(s) || s.zIndex !== "auto" || !["static", "relative"].includes(s.position)) return fail("stock_order_unknown");
|
|
135
|
+
}
|
|
136
|
+
for (const el of doc.querySelectorAll(".shell .side-resize")) if (visible(host, el)) {
|
|
137
|
+
const s = host.getComputedStyle(el);
|
|
138
|
+
if (!neutral(s) || s.position !== "absolute" || s.zIndex !== "5") return fail("stock_order_unknown");
|
|
139
|
+
}
|
|
140
|
+
// Desktop split preview is a stock level1 content owner too. Narrow preview is a modal sheet, handled below.
|
|
141
|
+
if (host.innerWidth >= 900) for (const el of doc.querySelectorAll(".chat-split > .pv-panel, .chat-split > .pv-divider")) {
|
|
142
|
+
if (!visible(host, el)) { const children = [...el.querySelectorAll("*")]; if (children.length > 4096 || children.some(child => visible(host, child))) return fail("stock_order_unknown"); continue; }
|
|
143
|
+
const s = host.getComputedStyle(el);
|
|
144
|
+
if (s.position !== "relative" || s.zIndex !== "1" || !neutral(s)) return fail("stock_order_unknown");
|
|
145
|
+
}
|
|
146
|
+
const marked = [...doc.querySelectorAll("[data-trust]")], expected = new Set(), rects = [], budget = { n: 0 };
|
|
147
|
+
if (marked.length > 128) return fail("stock_trust_unknown");
|
|
148
|
+
for (const { kind, selector } of STOCK_TRUST_INVENTORY) for (const el of doc.querySelectorAll(selector)) {
|
|
149
|
+
if (el.ownerDocument !== doc || el.isConnected !== true || el.getAttribute("data-trust") !== kind) return fail("stock_trust_unknown");
|
|
150
|
+
expected.add(el);
|
|
151
|
+
}
|
|
152
|
+
if (marked.some(el => !expected.has(el)) || surface.kind === "chat" && !chat.querySelector('.dock[data-trust="composer"]') || !surface.panel.querySelector('.panel-head[data-trust="header"]')) return fail("stock_trust_unknown");
|
|
153
|
+
for (const el of marked) {
|
|
154
|
+
const bounds = protectedPaint(host, el, budget); if (!bounds) return fail("stock_paint_unknown");
|
|
155
|
+
if (bounds.width && bounds.height) rects.push(bounds);
|
|
156
|
+
}
|
|
157
|
+
if (doc.querySelector(".agend-session-ended")) return fail("session_ended");
|
|
158
|
+
let barrier = [...doc.querySelectorAll(BARRIERS)].some(el => visible(host, el));
|
|
159
|
+
if (host.innerWidth < 900 && [...doc.querySelectorAll(".pv-panel")].some(el => visible(host, el))) barrier = true;
|
|
160
|
+
const active = doc.activeElement;
|
|
161
|
+
if (doc.fullscreenElement || doc.pictureInPictureElement) barrier = true;
|
|
162
|
+
if (active && active !== doc.body && active !== doc.documentElement && (active.matches("input,select,textarea,[contenteditable]") || active.isContentEditable)) barrier = true;
|
|
163
|
+
return { ok: true, rects, front: !barrier, reason: barrier ? "stock_overlay" : "stock", agentFront: !barrier && ownerCss === "off", agentReason: barrier ? "stock_overlay" : ownerCss === "off" ? "stock" : "owner_css", settingsHref: "/settings/appearance" };
|
|
164
|
+
} catch { return fail("stock_trust_unknown"); }
|
|
165
|
+
}
|