Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/facade/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

- Target Web Host 1.0.62 and schema wippy-context-2.1.json.
- Forward model selection, session selector visibility, and sanitizer tags as shared top-level policy. Existing requirement names stay unchanged.
- Preserve older and private hosts with automatic legacy policy mirroring; add `host_policy_mode` for explicit legacy or shared policy.
- Preserve Attention configuration and the existing transport and theming scopes.

## [0.6.42](https://github.com/wippyai/framework/compare/facade-v0.6.41...facade-v0.6.42) (2026-10-09)


Expand Down
2 changes: 1 addition & 1 deletion src/facade/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Run `make sync` after every Wippy Web Host version bump to pull fresh copies.

# Web Host CDN base — update version when Wippy Web Host releases
WEB_HOST_CDN = https://web-host.wippy.ai/webcomponents-1.0.59
WEB_HOST_CDN = https://web-host.wippy.ai/webcomponents-1.0.62

# Files to sync from CDN into public/@wippy-fe/
CDN_FILES = loading.js
Expand Down
32 changes: 22 additions & 10 deletions src/facade/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ These fields are NOT configurable via requirements — they are computed at runt

| Requirement | Default | Description |
|---|---|---|
| `fe_facade_url` | `https://web-host.wippy.ai/webcomponents-1.0.59` | CDN base URL for the Web Host frontend bundle |
| `fe_facade_url` | `https://web-host.wippy.ai/webcomponents-1.0.62` | CDN base URL for the Web Host frontend bundle |
| `host_policy_mode` | `auto` | `auto` detects official CDN versions and preserves legacy policy for unknown hosts; `legacy` mirrors policy into `hostConfig`; `shared` keeps root-only policy for custom modern hosts. Invalid values fail the config endpoint. |
| `fe_entry_path` | `/iframe.html` | Iframe HTML entry point path (appended to `fe_facade_url`) |
| `fe_mode` | `compat` | `compat` (default — loads `module.js`) or `managed` (loads `managed-layout.js` for declarative multi-panel apps). See [Modes](#modes) above |
| `render_engine` | `iframe` | Global page render engine for packaged `view.page` micro-frontends: `iframe` (default — legacy srcdoc iframe) or `fragment` (Web Fragment / reframed realm reflected into a shadow root). `fragment` **requires** the `wippy/views` fragment gateway and a fragment-capable host bundle serving `/@wippy-fe/proxy-fragment.js`. Flows to the host as `hostConfig.renderEngine`; the child app is engine-agnostic (same package renders under either engine). |
Expand Down Expand Up @@ -124,7 +125,7 @@ These accept JSON strings for complex configuration:
| `api_routes` | `{}` | `apiRoutes` | API route overrides — top-level, not under hostConfig (e.g. `{"agents":{"list":"/custom/agents"}}`) |
| `additional_nav_items` | `[]` | `hostConfig.additionalNavItems` | Extra sidebar nav items as JSON array |
| `state_cache` | `{}` | `hostConfig.stateCache` | Child state LRU config (e.g. `{"maxPages":50,"maxSizePerPage":1048576}`) |
| `allow_additional_tags` | `{}` | `hostConfig.allowAdditionalTags` | HTML sanitizer tag whitelist (e.g. `{"w-chart":["data","type"]}`) |
| `allow_additional_tags` | `{}` | `allowAdditionalTags` | HTML sanitizer tag whitelist (e.g. `{"w-chart":["data","type"]}`) |
| `chat` | `{}` | `hostConfig.chat` | Chat config (e.g. `{"convertPasteToFile":{"enabled":true,"minFileSize":1024,"allowHtml":false}}`) |
| `axios_defaults` | `{}` | `axiosDefaults` | HTTP client defaults (e.g. `{"timeout":30000}`) — top-level, not under hostConfig |
| `tanstack` | `{}` | `tanstack` | TanStack Query defaults — top-level, not under hostConfig. `{ default?, content?, lists? }`: `default` applies to all queries, `content` to single-resource renders, `lists` to navigation/index queries. Host default is `refetchOnWindowFocus:false` (e.g. `{"lists":{"refetchOnWindowFocus":true}}`) |
Expand Down Expand Up @@ -365,7 +366,7 @@ Scripts are fetched in parallel and awaited before the Web Host bundle is import

### `GET /api/public/facade/config`

Returns the full facade configuration as JSON (wippy-context-2.0 format). Used by `index.html` on load; see [Config Response](#config-response) below.
Returns the full facade configuration as JSON (wippy-context-2.1 format). Used by `index.html` on load; see [Config Response](#config-response) below.

### `GET /api/public/facade/variables.css`

Expand All @@ -386,13 +387,13 @@ Returns an empty body (200 OK) when no variables are configured. Response has `C

## Config Response

`GET /api/public/facade/config` returns (wippy-context-2.0 format):
`GET /api/public/facade/config` returns (wippy-context-2.1 format):

```json
{
"facade_url": "https://web-host.wippy.ai/webcomponents-1.0.59",
"facade_url": "https://web-host.wippy.ai/webcomponents-1.0.62",
"iframe_origin": "https://web-host.wippy.ai",
"iframe_url": "https://web-host.wippy.ai/webcomponents-1.0.59/iframe.html?waitForCustomConfig",
"iframe_url": "https://web-host.wippy.ai/webcomponents-1.0.62/iframe.html?waitForCustomConfig",
"login_path": "/login.html",
"login_redirect_param": null,
"env": {
Expand All @@ -401,6 +402,9 @@ Returns an empty body (200 OK) when no variables are configured. Response has `C
"APP_WEBSOCKET_URL": "ws://localhost:8085"
},
"routePrefix": "http://localhost:8085",
"allowSelectModel": false,
"hideSessionSelector": false,
"allowAdditionalTags": {},
"theming": {
"global": {
"customCSS": "@import url('https://fonts.googleapis.com/css2?family=Poppins...');"
Expand All @@ -414,11 +418,9 @@ Returns an empty body (200 OK) when no variables are configured. Response has `C
"session": { "type": "non-persistent" },
"history": "hash",
"showAdmin": true,
"allowSelectModel": false,
"startNavOpen": false,
"hideNavBar": false,
"disableRightPanel": false,
"hideSessionSelector": false
"disableRightPanel": false
},
"extraScripts": null
}
Expand All @@ -434,10 +436,20 @@ fetch /api/public/facade/config
→ listen for 'authExpired' and 'error' events → redirect to login_path
```

The backend returns config in wippy-context-2.0 shape. `index.html` only adds `$schema` (from `facade_url`), `auth` (from localStorage), and `context` (empty default). All other fields pass through from the backend unchanged.
The backend returns config in wippy-context-2.1 shape. `index.html` only adds `$schema` (from `facade_url`), `auth` (from localStorage), and `context` (empty default). All other fields pass through from the backend unchanged.

If any step fails (config fetch, CDN import, missing `initWippyApp`), the page shows a themed `<wippy-error>` screen with title and details. No external CSS is required — both `<wippy-loading>` and `<wippy-error>` are self-contained web components with Shadow DOM styles.

## Shared chat policy

The existing `allow_select_model`, `hide_session_selector`, and `allow_additional_tags` requirements produce top-level camelCase fields. With `host_policy_mode = auto`, official CDN Host versions before 1.0.62 also receive the same resolved values in `hostConfig`. Unknown and private URLs receive this legacy mirror too, preserving existing deployments. Official CDN Host 1.0.62 and newer keep the fields only at the root so live root policy updates remain effective. Both shells forward the supplied scopes. Existing deployment requirement names stay valid. An empty allowlist is encoded as `{}`.

Set `host_policy_mode = shared` when a custom URL serves Host 1.0.62 or newer. Set `legacy` to force the mirror regardless of URL. The automatic detector accepts only the exact `https://web-host.wippy.ai/webcomponents-MAJOR.MINOR.PATCH` URL, with an optional trailing slash. Invalid modes return HTTP 500 with a config error naming the accepted values.

`hostConfig` describes the Web Host wrapper. Child applications do not receive it. A custom parent may still supply these fields in `hostConfig` to override built-in host chat; the shared reader uses `hostConfig[key] ?? config[key] ?? default`. A child inherits the top-level policy and can replace it through `wippy.configOverrides`. Chat WC attributes override the policy for one instance. Managed layouts have no built-in chat by default, so shared application policy belongs at the root.

Web Host 1.0.62 uses the root policy for child chat components. Earlier hosts, including 1.0.61, use the mirrored `hostConfig` policy for built-in chat. Older facade output remains supported by the new frontend, including its host-only fields.

## Publishing

```bash
Expand Down
19 changes: 16 additions & 3 deletions src/facade/_index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,20 @@ entries:
targets:
- entry: wippy.facade:fe_facade_url
path: .default
default: https://web-host.wippy.ai/webcomponents-1.0.59
default: https://web-host.wippy.ai/webcomponents-1.0.62

- name: host_policy_mode
kind: ns.requirement
meta:
description: |
Shared chat policy compatibility. auto uses root-only policy for
official CDN Host 1.0.62 and newer, and mirrors policy in hostConfig
for older or unknown hosts. shared opts custom modern hosts into
root-only policy; legacy forces the mirror. Invalid modes fail config.
targets:
- entry: wippy.facade:host_policy_mode
path: .default
default: auto

- name: fe_entry_path
kind: ns.requirement
Expand Down Expand Up @@ -130,7 +143,7 @@ entries:
- name: allow_select_model
kind: ns.requirement
meta:
description: Allow user to select LLM model
description: Shared allowSelectModel policy for built-in chat and child chat components
targets:
- entry: wippy.facade:allow_select_model
path: .default
Expand Down Expand Up @@ -326,7 +339,7 @@ entries:
path: .default
default: ""

# Additional hostConfig options (JSON)
# Additional facade configuration options
- name: hide_session_selector
kind: ns.requirement
meta:
Expand Down
46 changes: 43 additions & 3 deletions src/facade/config_handler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ local function non_empty_array_or_nil(a: any): {any}?
return a :: {any}
end

local function needs_legacy_policy(facade_url: string, mode: string): (boolean?, string?)
if mode == "legacy" then
return true, nil
end
if mode == "shared" then
return false, nil
end
if mode ~= "auto" then
return nil, "invalid host_policy_mode: expected auto, legacy or shared"
end
local major, minor, patch = facade_url:match("^https://web%-host%.wippy%.ai/webcomponents%-(%d+)%.(%d+)%.(%d+)/?$")
if not major then
-- Private and unversioned hosts may still use the legacy policy shape.
return true, nil
end
local major_version = tonumber(major) or 0
local minor_version = tonumber(minor) or 0
local patch_version = tonumber(patch) or 0
local shared = major_version > 1
or (major_version == 1 and (minor_version > 0 or patch_version >= 62))
return not shared, nil
end

local function handler()
local res = http.response()
if not res then
Expand Down Expand Up @@ -76,6 +99,13 @@ local function handler()
end

local facade_url = get_req("fe_facade_url")
local legacy_policy, policy_err = needs_legacy_policy(facade_url, get_req("host_policy_mode"))
if policy_err then
res:set_status(http.STATUS.INTERNAL_SERVER_ERROR)
res:set_content_type(http.CONTENT.JSON)
res:write('{"error":"invalid host_policy_mode: expected auto, legacy or shared"}')
return nil, policy_err
end
local entry_path = get_req("fe_entry_path")
local fe_mode = get_req("fe_mode")

Expand Down Expand Up @@ -141,15 +171,15 @@ local function handler()
},
}

local allow_select_model = get_req("allow_select_model") == "true"
local hide_session_selector = get_req("hide_session_selector") == "true"
local host_config: {[string]: any} = {
session = { type = non_empty_or_nil(get_req("session_type")) },
history = non_empty_or_nil(get_req("history_mode")),
showAdmin = get_req("show_admin") ~= "false",
allowSelectModel = get_req("allow_select_model") == "true",
startNavOpen = get_req("start_nav_open") == "true",
hideNavBar = get_req("hide_nav_bar") == "true",
disableRightPanel = get_req("disable_right_panel") == "true",
hideSessionSelector = get_req("hide_session_selector") == "true",
renderEngine = render_engine,
}

Expand All @@ -168,7 +198,14 @@ local function handler()
local attention = non_empty_map_or_nil(get_req_json_any("attention"))

local additional_tags = non_empty_map_or_nil(get_req_json_any("allow_additional_tags"))
if additional_tags then
if not additional_tags then
additional_tags = table.create(0, 1)
end
-- Legacy hosts read policy only from hostConfig. Shared mode keeps policy
-- at the root so later root updates are not shadowed by a legacy mirror.
if legacy_policy then
host_config.allowSelectModel = allow_select_model
host_config.hideSessionSelector = hide_session_selector
host_config.allowAdditionalTags = additional_tags
end

Expand Down Expand Up @@ -233,6 +270,9 @@ local function handler()
themeStorageKey = theme_storage_key,
apiRoutes = api_routes,
attention = attention,
allowSelectModel = allow_select_model,
hideSessionSelector = hide_session_selector,
allowAdditionalTags = additional_tags,
axiosDefaults = axios_defaults,
tanstack = tanstack,
extraScripts = extra_scripts,
Expand Down
11 changes: 7 additions & 4 deletions src/facade/index.jet
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@
}

var stored = localStorage.getItem(STORAGE_KEY);
if (!stored) { goToLogin(); throw new Error('no token'); }
if (!stored) { goToLogin(); return; }

var token;
try { token = JSON.parse(stored).token; }
catch (e) { goToLogin(); throw new Error('bad token'); }
catch (e) { goToLogin(); return; }

if (Array.isArray(cfg.extraScripts) && cfg.extraScripts.length) {
await Promise.all(cfg.extraScripts.map(function (entry) {
Expand Down Expand Up @@ -153,9 +153,9 @@
throw new Error('initWippyApp not found');
}

// Backend returns config in wippy-context-2.0 shape — just add auth + schema
// Backend returns config in wippy-context-2.1 shape — just add auth + schema
var events = window.initWippyApp({
$schema: cfg.facade_url + '/schemas/wippy-context-2.0.xsd',
$schema: cfg.facade_url + '/schemas/wippy-context-2.1.json',
auth: { token: token, expiresAt: new Date(Date.now() + 86400000).toISOString() },
env: cfg.env,
routePrefix: cfg.routePrefix,
Expand All @@ -164,6 +164,9 @@
themeMode: (window.wippyThemePersist && window.wippyThemePersist.read()) || cfg.themeMode,
apiRoutes: cfg.apiRoutes,
attention: cfg.attention,
allowSelectModel: cfg.allowSelectModel,
hideSessionSelector: cfg.hideSessionSelector,
allowAdditionalTags: cfg.allowAdditionalTags,
axiosDefaults: cfg.axiosDefaults,
theming: cfg.theming,
hostConfig: cfg.hostConfig,
Expand Down
11 changes: 7 additions & 4 deletions src/facade/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@
}

var stored = localStorage.getItem(STORAGE_KEY);
if (!stored) { goToLogin(); throw new Error('no token'); }
if (!stored) { goToLogin(); return; }

var token;
try { token = JSON.parse(stored).token; }
catch (e) { goToLogin(); throw new Error('bad token'); }
catch (e) { goToLogin(); return; }

if (Array.isArray(cfg.extraScripts) && cfg.extraScripts.length) {
await Promise.all(cfg.extraScripts.map(function (entry) {
Expand Down Expand Up @@ -148,9 +148,9 @@
throw new Error('initWippyApp not found');
}

// Backend returns config in wippy-context-2.0 shape — just add auth + schema
// Backend returns config in wippy-context-2.1 shape — just add auth + schema
var events = window.initWippyApp({
$schema: cfg.facade_url + '/schemas/wippy-context-2.0.xsd',
$schema: cfg.facade_url + '/schemas/wippy-context-2.1.json',
auth: { token: token, expiresAt: new Date(Date.now() + 86400000).toISOString() },
env: cfg.env,
routePrefix: cfg.routePrefix,
Expand All @@ -159,6 +159,9 @@
themeMode: (window.wippyThemePersist && window.wippyThemePersist.read()) || cfg.themeMode,
apiRoutes: cfg.apiRoutes,
attention: cfg.attention,
allowSelectModel: cfg.allowSelectModel,
hideSessionSelector: cfg.hideSessionSelector,
allowAdditionalTags: cfg.allowAdditionalTags,
axiosDefaults: cfg.axiosDefaults,
theming: cfg.theming,
hostConfig: cfg.hostConfig,
Expand Down
15 changes: 15 additions & 0 deletions src/facade/test/_index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ entries:
- registry

# Tests
- name: policy_compatibility_test
kind: function.lua
meta:
type: test
suite: facade
comment: Real config endpoint policy compatibility for older and newer hosts
source: file://policy_compatibility_test.lua
method: run
imports:
test: wippy.test:test
modules:
- http_client
- json
- registry

- name: config_handler_test
kind: function.lua
meta:
Expand Down
2 changes: 1 addition & 1 deletion src/facade/test/config_dependency_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ local function run()
local entry = registry.get(NS .. "fe_facade_url")
test.not_nil(entry)
test.not_nil(entry.data)
test.eq(entry.data.default, "https://web-host.wippy.ai/webcomponents-1.0.59")
test.eq(entry.data.default, "https://web-host.wippy.ai/webcomponents-1.0.62")

entry = registry.get(NS .. "app_title")
test.not_nil(entry)
Expand Down
Loading
Loading