feat(extensions): add browser-local extension settings surface (#5094)

This commit is contained in:
Rod Boev committed 2026-06-28 17:28:20 -04:00
1 parent e3271b099a
commit a76c794d6d
10 files changed
+895 -9

No files matched your search

+165 -4
View File
@@ -72,8 +72,10 @@ _EXTENSION_STATE_FILENAME = "extension-overrides.json"
_MAX_EXTENSION_STATE_BYTES = 32 * 1024
_MAX_DISABLED_EXTENSION_IDS = 512
_EXTENSION_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
_EXTENSION_SETTINGS_KEY_RE = re.compile(r"^[A-Za-z][A-Za-z0-9._-]{0,63}$")
_EXTENSION_STATE_WARNING_SOURCE = "extension_state"
_EXTENSION_STATE_LOCK = threading.Lock()
_EXTENSION_SETTING_TYPES = {"boolean", "string", "number", "integer", "enum"}
_GALLERY_INSTALL_STATE_FILENAME = "extension-install-manifest.json"
_MAX_INSTALL_MANIFEST_BYTES = 128 * 1024
@@ -521,6 +523,116 @@ def _manifest_entry_text(entry: Dict[str, object], key: str) -> str:
return ""
return value.strip()
def _manifest_entry_storage_owned(entry: Dict[str, object]) -> bool:
permissions = entry.get("permissions")
if not isinstance(permissions, dict):
return False
storage = permissions.get("storage")
return isinstance(storage, dict) and storage.get("owned") is True
def _settings_text(value: object, *, max_len: int = 160) -> str:
if not isinstance(value, str):
return ""
text = value.strip()
return text[:max_len]
def _normalize_enum_options(options: object) -> Optional[List[Dict[str, str]]]:
if not isinstance(options, list) or not options:
return None
normalized: List[Dict[str, str]] = []
seen: Set[str] = set()
for option in options:
if isinstance(option, str):
value = option.strip()
label = value
elif isinstance(option, dict):
raw_value = option.get("value")
if not isinstance(raw_value, str):
return None
value = raw_value.strip()
label = _settings_text(option.get("label")) or value
else:
return None
if not value or value in seen:
return None
seen.add(value)
normalized.append({"value": value, "label": label})
return normalized
def _normalize_settings_default(field_type: str, raw_default: object, options: Optional[List[Dict[str, str]]] = None) -> Tuple[bool, object]:
if field_type == "boolean":
if raw_default is None:
return True, False
return (True, raw_default) if isinstance(raw_default, bool) else (False, None)
if field_type == "string":
if raw_default is None:
return True, ""
return (True, raw_default) if isinstance(raw_default, str) else (False, None)
if field_type == "number":
if raw_default is None:
return True, 0
return (True, raw_default) if isinstance(raw_default, (int, float)) and not isinstance(raw_default, bool) else (False, None)
if field_type == "integer":
if raw_default is None:
return True, 0
return (True, raw_default) if isinstance(raw_default, int) and not isinstance(raw_default, bool) else (False, None)
if field_type == "enum" and options:
values = [option["value"] for option in options]
if raw_default is None:
return True, values[0]
return (True, raw_default) if isinstance(raw_default, str) and raw_default in values else (False, None)
return False, None
def _settings_schema_values(raw_schema: object) -> List[object]:
if isinstance(raw_schema, list):
return raw_schema
if isinstance(raw_schema, dict) and isinstance(raw_schema.get("fields"), list):
return raw_schema["fields"]
return []
def _sanitize_settings_schema(entry: Dict[str, object]) -> List[Dict[str, object]]:
if not _manifest_entry_storage_owned(entry):
return []
fields: List[Dict[str, object]] = []
seen_keys: Set[str] = set()
for raw_field in _settings_schema_values(entry.get("settings_schema")):
if not isinstance(raw_field, dict):
continue
if raw_field.get("sensitive") is True:
continue
key = raw_field.get("key")
if not isinstance(key, str):
continue
key = key.strip()
if not _EXTENSION_SETTINGS_KEY_RE.fullmatch(key):
continue
field_type = raw_field.get("type")
if not isinstance(field_type, str):
continue
field_type = field_type.strip().lower()
if field_type not in _EXTENSION_SETTING_TYPES:
continue
options = _normalize_enum_options(raw_field.get("options")) if field_type == "enum" else None
if field_type == "enum" and options is None:
continue
ok, default = _normalize_settings_default(field_type, raw_field.get("default"), options)
if not ok:
continue
if key in seen_keys:
continue
seen_keys.add(key)
field: Dict[str, object] = {
"key": key,
"type": field_type,
"label": _settings_text(raw_field.get("label")) or key,
"description": _settings_text(raw_field.get("description"), max_len=300),
"default": default,
}
if options is not None:
field["options"] = options
fields.append(field)
return fields
def _normalize_loopback_sidecar_origin(value: object) -> Optional[str]:
"""Return a canonical loopback origin or None when unsafe.
@@ -735,7 +847,7 @@ def _gallery_installed_runtime_manifest(
asset_base = ext_id
if isinstance(manifest, dict):
top_entry: Dict[str, object] = {"id": ext_id}
for key in ("name", "enabled", "scripts", "stylesheets", "sidecar"):
for key in ("name", "enabled", "scripts", "stylesheets", "sidecar", "permissions", "settings_schema"):
if key in manifest:
top_entry[key] = manifest[key]
if any(key in top_entry for key in ("scripts", "stylesheets", "sidecar")):
@@ -836,6 +948,7 @@ def _manifest_extension_state(
effective_enabled = manifest_enabled and not user_disabled
if not manifest_enabled:
manifest_disabled_ids.add(ext_id)
settings_schema = _sanitize_settings_schema(entry)
extension_entries.append(
{
"id": ext_id,
@@ -846,6 +959,8 @@ def _manifest_extension_state(
"effective_enabled": effective_enabled,
"can_toggle": can_toggle,
"reload_required": True,
"storage_owned": _manifest_entry_storage_owned(entry),
"settings_schema": settings_schema,
"status": (
"manifest_disabled"
if not manifest_enabled
@@ -866,6 +981,33 @@ def _manifest_extension_state(
"manifest_disabled_ids": manifest_disabled_ids,
}
def _extension_runtime_entries(
manifest: object, disabled_ids: Optional[Set[str]] = None
) -> List[Dict[str, object]]:
"""Return enabled extension metadata injected before extension scripts run."""
disabled_ids = disabled_ids or set()
extensions: List[Dict[str, object]] = []
seen_ids: Set[str] = set()
for _source, _index, entry in _manifest_extension_entries(manifest):
raw_id = _manifest_entry_text(entry, "id")
if not _valid_extension_id(raw_id):
continue
ext_id = raw_id.strip()
if ext_id in seen_ids:
continue
seen_ids.add(ext_id)
if entry.get("enabled", True) is False or ext_id in disabled_ids:
continue
extensions.append(
{
"id": ext_id,
"name": _manifest_entry_text(entry, "name") or ext_id,
"storage_owned": _manifest_entry_storage_owned(entry),
"settings_schema": _sanitize_settings_schema(entry),
}
)
return extensions
def _read_manifest_urls_with_diagnostics(
root: Path,
@@ -950,8 +1092,14 @@ def get_extension_config() -> Dict[str, Any]:
return {"enabled": False, "script_urls": [], "stylesheet_urls": []}
state = _load_extension_state()
disabled_ids = set(state.get("disabled_extensions") or [])
manifest_scripts, manifest_stylesheets = _read_manifest_urls(root, disabled_ids=disabled_ids)
return {
manifest, manifest_status = _load_manifest_with_status(root)
manifest_scripts, manifest_stylesheets, _, _ = _read_manifest_urls_with_diagnostics(
root,
disabled_ids=disabled_ids,
manifest=manifest,
manifest_status=manifest_status,
)
config = {
"enabled": True,
"script_urls": _read_url_list(
_EXTENSION_SCRIPT_URLS_ENV, manifest_scripts or None
@@ -960,6 +1108,10 @@ def get_extension_config() -> Dict[str, Any]:
_EXTENSION_STYLESHEET_URLS_ENV, manifest_stylesheets or None
),
}
runtime_entries = _extension_runtime_entries(manifest, disabled_ids) if manifest is not None else []
if runtime_entries:
config["extensions"] = runtime_entries
return config
@@ -1387,6 +1539,15 @@ def inject_extension_tags(index_html: str) -> str:
'<script src="{}" defer></script>'.format(html.escape(url, quote=True))
for url in config["script_urls"]
]
runtime_config = {
"extensions": config.get("extensions", []),
}
runtime_json = json.dumps(runtime_config, ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c")
runtime_tag = (
"<script>window.__HERMES_EXTENSION_CONFIG__={};"
"if(window.HermesExtensionSettings)window.HermesExtensionSettings.configure(window.__HERMES_EXTENSION_CONFIG__);"
"</script>"
).format(runtime_json)
if stylesheet_tags:
head_marker = "</head>"
@@ -1398,7 +1559,7 @@ def inject_extension_tags(index_html: str) -> str:
if script_tags:
body_marker = "</body>"
block = "\n".join(script_tags) + "\n"
block = runtime_tag + "\n" + "\n".join(script_tags) + "\n"
if body_marker in result:
result = result.replace(body_marker, block + body_marker, 1)
else: