Skip to content

feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-459] - #87

Open
piyushsarin-sib wants to merge 14 commits into
mainfrom
feat/iframe-extension-authoring
Open

feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-459]#87
piyushsarin-sib wants to merge 14 commits into
mainfrom
feat/iframe-extension-authoring

Conversation

@piyushsarin-sib

@piyushsarin-sib piyushsarin-sib commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Jira: BEX-459 · Epic: BEX-211

What

Phase 1 (authoring release) of the iframe-extension launch, CLI side. The validator, the upload diff (modal URL: row), the wire-only strip and the write-back already spoke iframeExtension — only the prompt withheld it (removed in 63bc9ee while authoring wasn't ready).

  • Prompt: the UI-app integration-type prompt offers Iframe (Embeds your page in a modal) next to Link — restored enabled, not as the old disabled "coming soon" entry. Private apps only: on a public app the choice is hidden (not disabled), because iframe extensions are private-only in v1 and a disabled entry would advertise a combination every layer refuses.
  • Iframe branch: the URL question becomes Iframe URL — the page Brevo embeds in the modal (record context arrives as query params) and the answer lands in each entry's modal_iframe_url instead of redirect_link. No size prompt (decided): the modal's dimensions are hand-authored per entry in app-config.json.
  • Private-only, locally too: uiAppType.validateConfig refuses a hand-authored iframeExtension block on a public app before any round trip, with the same wording as bo-be's 400. (readProjectConfig defaults an absent distribution_type to private, so only an explicit public reaches it.)
  • Smoke: yarn smoke --suite=ui grows an iframe leg — create (pty) → upload no-op → install → uninstall → delete — that skips rather than fails on a build whose prompt has no Iframe choice or an environment whose registry has no slot enabled for iframeExtension yet (pre-flip). New prompt patterns are pinned against messages in en.test.ts like the existing ones (the PR fix: harmonize app-create prompt copy (distribution & UI-app fields) #73 pty-timeout precedent).
  • Docs in the same PR (repo rule): agent-context/SKILL.md, agent-context/AGENTS.md, CLAUDE.md. One changeset, minor.

Tests

  • create: Link+Iframe both enabled on private; Iframe hidden on public; iframe branch authors modal_iframe_url (never redirect_link/link_target) and narrows both registry reads by iframeExtension; iframe-specific URL prompt copy.
  • upload: private-only refusal before any round trip; existing iframe upload/diff tests unchanged.
  • 1415 tests green, lint + prettier clean, smoke scripts typecheck under scripts/tsconfig.json.

Sequencing

The staging registry flip lands before this releases (bo-be PR DTSL/app-store-bo-be#404 carries the SQL): on an un-flipped environment the restored choice dead-ends in APP_CREATE_UI_POINTS_NONE_FOR_TYPE, which reads as a CLI bug and isn't. The prod flip follows this CLI release.

Part of the iframe-extension Phase 1 (epic BEX-281). Counterparts: bo-be DTSL/app-store-bo-be#404 (registry flip + server-side private-only rule); kit PR (sandbox + modal size) to follow.

🤖 Generated with Claude Code


Update 2026-09-03 — rebased onto main (#92 + #97)

Main gained the registry default_size seeding (#92, the cherry-picked successor of #91, released in 2.2.1) and the non-interactive actionLink UI creation (#97, released in 2.2.2). Reconciliation on this branch:

1434 tests green, lint + prettier clean, smoke scripts typecheck.


Update 2026-09-03 (2) — inline iframe cards

An iframeExtension entry on a widget slot can now author layout: "inline" (page embedded directly in the card body) vs "modal" (the default — never written, so layout-less configs are byte-identical). app create asks the question only for iframe widget placements; validateUiApp refuses the field on actionLink entries; the diff/box/install summary gain a layout: row via VALUE_ROWS. Counterparts: bo-be #404 (validation + storage, widget-only rule from the registry), app-store-backend #757 (serves appConfigs.layout, cacheSchemaVersion v9→v10), kit #317 (inline routing). Deploy order: bo-be → backend → kit → CLI.


Update 2026-09-04 — layout is widget-only, and what still lands in this PR

Rule. layout: "inline" is only meaningful on the 6 widget slots (contactDetails|dealDetails|companyDetails × overviewMain.widget|overviewSidebar.widget), the registry rows whose component_type is widget and therefore render a card. The 3 header-menu slots (*.headerMenu.action, component_type: "action") yield a menu entry that must open something, so there is nothing to be inline in. The slot kind is judged from the registry row, never from a local list (CLAUDE.md: no registry mirror in the CLI).

What this PR does today

  • Prompt gatepromptIframeLayout (src/app-types/ui/authoring.ts) asks How should it appear on the page? only when the picked placement's row has component_type === 'widget'. A header-menu placement never sees the question and its entry is written without layout. buildSurfacePointList can only ever write inline; modal is the default and is never stamped.
  • ValidationvalidateUiApp pins the vocabulary ("<value>" is not supported — use "modal" or "inline") and refuses layout on any actionLink entry. It deliberately has no local slot-kind check; the platform's validateLayoutPerEntry answers that on create and upload with:
    400 ui_app.surface_point_list authors layout "inline" on slot(s) that render no card: <slug> (component_type "action")
  • Non-interactive--ui-app / --ui-config stay actionLink-only; layout is not part of their input.

Landing before merge (same PR)

  1. Make the create-time gate a positive assertion: if any path tries to write layout for a row whose component_type is not widget, throw locally naming the entry (ui_app.surface_point_list["<slug>"].layout: "inline" needs a widget slot; <slug> renders a menu entry). Same outcome as the skip today, but a future multi-select or a new component_type value can no longer author inline silently.
  2. Map bo-be's layout 400 to an en.ts message so brevo app upload of a hand-edited inline on a header-menu slot reads like every other per-entry refusal.
  3. Tests: validator cases for the vocabulary pin and the actionLink refusal (none exist yet), en.test.ts pins for the three layout prompt strings, and a create test that the question is absent for a header-menu placement (exists) plus one that a widget placement gets it.
  4. --ui-config refuses a layout key explicitly instead of dropping it.

Planned in this PR: modal size presets. For an iframe entry that opens a modal (widget slot with modal layout, or any header-menu slot) app create asks Modal sizeSmall / Medium / Large, default Large. Large is the platform's current behaviour (the kit's ModalDialog is hard-coded size="lg"), so an unanswered or Large answer writes nothing and layout-less configs stay byte-identical. Wire: per-entry modal_size ("small" | "medium" | "large", iframeExtension only, refused on actionLink), validated by bo-be, served as appConfigs.modalSize inside the same v9→v10 cache bump as layout, mapped by the kit onto the dialog size. This is a preset for the dialog, distinct from the widget card's size, which is still seeded from the slot's registry default (BEX-461) and hand-edited in px/%; the CLI still asks no px/% question. Counterparts to follow in bo-be #404, backend #757, kit #317. Tracked under BEX-211 / BEX-467.

Sandbox stored in the snapshot, never authorable (2026-09-04). bo-be now stamps sandbox into the stored ui_app snapshot at the root with the default allow-scripts allow-same-origin allow-forms allow-popups, server-side only. The CLI API neither accepts it (an authored sandbox in app-config.json is a 400 unknown key) nor echoes it. This PR adds sandbox to uiAppType.wireOnlyKeys so a value can never reach app-config.json even if it were echoed, exactly like link_target and extension_point_name. In M1 the value is store-only: the manifest does not serve it and the kit keeps its constant.


Update 2026-09-04 (3) — plan sync

  • Phase 2c exists now. ui.showConfirm is no longer dropped from v1; it moved to Phase 2c (host confirm dialogs for inline cards, M2c 2026-11-13). No change to this PR.
  • Explicit app type in app-config.json (Phase 1, sibling change). create will write an app-type field (OAuth vs UI) so the file states what it is instead of relying on the presence of ui_app; upload checks it agrees with the ui_app / auth block and refuses a mismatch with a migration hint; isUiAppConfig() stays the single reader; files without the field keep working. Open: wire (bo-be reflected key sets + openapi) vs file-only key stripped before upload. Lands in its own PR on top of this branch; CLAUDE.md's "there is no appType key" note changes with it.
  • Test iframe app. The smoke iframe leg and the POM cases will target a Brevo-owned test iframe app on staging (echoes context params, renders modal S/M/L and inline, one entry per slot kind) instead of an ad-hoc URL. Built in brevo-cli-apps in W1–W2.
  • No review gate before 2a. Contract details for the trust layer are decided inside the backend mint PR and mirrored in the kit PR; no security-review kickoff or PII review is planned, and the context vocabulary stays identifiers-only.
    Plan of record: https://app.notion.com/p/sendinblue/Iframe-Embed-timelines-3d1449002dcb80fc8c3ff1a6a0c7c0b0

@piyushsarin-sib piyushsarin-sib changed the title feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-281] feat(app-create): iframe-extension authoring — Iframe prompt choice, private-only rule [BEX-459] Sep 1, 2026
piyushsarin-sib and others added 2 commits September 3, 2026 13:05
…nsions, private-only [BEX-281]

The iframe-extension launch, CLI side:

- The UI-app integration-type prompt offers Iframe (Embeds your page in
  a modal) next to Link, on a private app only — iframe extensions are
  private-only in v1. The choice is hidden (not disabled) on a public
  app: every layer refuses the combination.
- The iframe branch asks for the embed URL and writes it to each
  entry's modal_iframe_url instead of redirect_link; the example
  context-URL lines in the created-app box read either destination.
- Private-only enforced locally too: uiAppType.validateConfig refuses
  an iframeExtension block on a public app before any round trip
  (APP_UI_IFRAME_PRIVATE_ONLY), mirroring bo-be's 400.
- Smoke: yarn smoke --suite=ui grows an iframe leg (create → upload
  no-op → install → uninstall → delete) that skips on a build without
  the choice or an environment without the registry flip; its new
  prompt pins are covered by en.test.ts like the existing ones.
- Docs updated in the same PR (repo rule): agent-context/SKILL.md,
  agent-context/AGENTS.md, CLAUDE.md. One changeset (minor).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and size seeding

Rebase fallout from #92 (registry default_size seeding) and #97
(non-interactive actionLink UI creation) landing on main first:

- resolveUiAppOrRedirectUris threads the distribution answer through to
  resolveUiApp, which gates the Iframe choice (private-only). The
  non-interactive path deliberately doesn't take it — that route is
  actionLink-only by design, so its buildSurfacePointList call pins
  urlField: 'redirect_link'.
- buildSurfacePointList carries both #92's sizeFor and this branch's
  urlField/url; its unit tests updated to the merged signature.
- Agent docs merged: the non-interactive flags (#97) and the Iframe
  choice documented side by side, both noting the non-interactive routes
  stay actionLink-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@piyushsarin-sib
piyushsarin-sib force-pushed the feat/iframe-extension-authoring branch from 5b957d8 to 7c31953 Compare September 3, 2026 07:38
piyushsarin-sib and others added 12 commits September 3, 2026 17:42
An iframeExtension entry on a widget slot can author layout: "inline"
(the page embedded directly in the card body) vs "modal" (the default —
never written, so layout-less configs stay byte-identical).

- create asks the question only for an iframe WIDGET placement (an
  action slot's menu entry is a modal by definition) and writes the key
  only for inline.
- validateUiApp refuses layout on actionLink entries and pins the
  vocabulary; the widget-only rule stays server-side (registry fact).
- fields.ts gains the layout: row, so the upload diff, created-app box
  and install summary all show it.
- Docs (SKILL/AGENTS) + changeset extended; 1437 tests green.

Requires bo-be with per-entry layout support (same-branch PR #404) —
an older server 400s the key as unknown, the standard sequencing rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…[BEX-467]

bo-be stamps a root `sandbox` onto the stored snapshot — the iframe policy
the platform decides, not the partner. Left in the wire-only list's blind
spot, the first successful upload writes it into app-config.json and every
upload after that reports drift on a field nobody can edit.

`stripUiAppWireOnlyKeys` is the single reader and strips at every depth, so
naming the key in `uiAppType.wireOnlyKeys` is the whole change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…[BEX-459]

Four rules that finish per-entry iframe authoring, all of them about a value
the CLI would otherwise write and something downstream would then ignore or
reject.

`modal_size` — `small` / `medium` / `large`, asked for every iframe entry that
actually OPENS a modal. That is deliberately a different set of placements from
the layout question's: layout is widget-only (an action slot's menu entry has
one presentation), while the menu entry opens a modal too, and a widget card
answered `inline` opens none. Large is the default, is pre-selected rather than
listed first, and writes nothing — so a default answer leaves app-config.json
byte-identical to one authored before the question existed, the same contract
`layout: "modal"` has. `validateUiApp` pins the vocabulary and refuses the field
on an `actionLink`; the `modal size:` row is one more `VALUE_ROWS` entry, so the
upload diff, the created-app box and `app install`'s summary all gain it at once.

`buildSurfacePointList` now refuses a `layout` on a row that renders no card,
naming the entry the way the validator does. The prompt never asks for one
there, so this cannot fire from the interactive flow — but the builder is what
stamps the field and it stamps whatever row it is handed, so an unchecked caller
would author a block the upload endpoint rejects and the partner would meet the
rule one round trip later, phrased by the server.

`--ui-config` refuses `layout` and `modal_size` by name instead of dropping
them. The file's key set is fixed and everything else is discarded, so a
silently ignored presentation field means the created app renders differently
from the file that asked for it, with nothing said anywhere.

`app upload` translates the platform's own layout refusal — narrowly, exactly
like `isPublicDistributionRefusal`: only a 400 that mentions `layout` is
relabelled, so an unrelated 400 keeps the server's text. No local slot list:
whether a slot renders a card is a registry fact and the CLI holds no copy of
the registry, so the server stays the authority and this only says what to edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ui suite's iframe leg went URL -> output directory. Two questions can
render in between and neither is guaranteed: the layout list appears only when
the first iframe-enabled registry row on the picked page is a widget, and the
modal-size list only when a modal actually opens. Which row the registry serves
first is not this suite's business, so the leg cannot know in advance, and
either question is also missing from an older build under --against=published.

The pty runner gains `optional` on an exchange: a prompt that hasn't appeared is
skipped once a LATER one has — proof the flow moved past it, rather than that it
is merely slow — and trailing optionals left unmatched at exit are not a broken
flow. Distinct from the existing `send: () => null` abort, which says the prompt
rendered without the choice this leg needs and skips the whole leg.

Both new questions are answered with their default (Enter), and the leg now
asserts neither wrote a key — the only part of the omit-the-default contract a
real run can prove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y [BEX-459]

Both agent docs gain `modal_size` next to `layout`, and say plainly that the
two are gated differently — an agent that assumes they cover the same
placements will author a size on an inline card, or omit one on a menu entry.
Also: `--ui-config` refuses either field by name, and `sandbox` joins the
server-stamped keys that must never be written into app-config.json.

Appended to the branch's existing changeset rather than adding a second file;
the bump level stays minor, since this is all new surface on an unreleased
feature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings the branch up to date with main through 2.3.0 (#100 Brevo
Functions, #104 BEX-470 snake_case app-config.json keys, #106 BEX-471
--json snake_case twins, #107 release).

Conflicts resolved by keeping both sides:

- src/lang/en.ts — main's APP_UPLOAD_APP_TYPE_MISMATCH plus the branch's
  APP_UPLOAD_UI_LAYOUT_REJECTED; APP_UPLOAD_UI_APP_AUTH_HAS_OAUTH_FIELDS
  takes main's snake_case `redirect_uris` wording.
- src/__tests__/commands/app/upload.test.ts — import union; both the
  app_type-agreement (BEX-468) and layout-400 / diff-row suites kept.
- agent-context/SKILL.md, agent-context/AGENTS.md — main's app_type,
  --json-twin and `brevo function` reference merged with the branch's
  iframe / layout / modal_size / sandbox reference. Both files describe
  three app types and stay in sync with each other.

Semantic (non-marker) collisions verified after the auto-merge:

- resolveUiAppOrRedirectUris (create.ts) carries BOTH main's `appType`
  and the branch's trailing `distribution`; the `function` early-return
  precedes resolveUiApp(distribution), and the single call site passes
  both.
- scripts/smoke/ui-app.ts — the branch's restructured createUiSmokeApp
  picked up main's snake_case reads (configField 'app_id' / 'app_name',
  configRedirectUris) cleanly; the new iframe must() blocks only read
  per-entry ui_app keys, which were already snake_case.
- UI_APP_WIRE_ONLY_KEYS keeps 'sandbox', and capabilities.test.ts
  asserts wireOnlyKeys for oauth, ui and function.

No branch code reads an app-config.json key in camelCase, and the branch
adds no new --json output keys (the snake_case twinning in
withSnakeCaseAliases is generic and applies to any new top-level key).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ness [BEX-467]

The `sandbox` bullet above `UI_APP_WIRE_ONLY_KEYS` justified keeping the key
out of `app-config.json` partly on the value being inert — "a value the
partner can edit and the platform ignores". That half expires: the plan is
for the extensibility UI kit to read the sandbox from the manifest in a later
phase, at which point the platform reads the stored value rather than
ignoring it.

Rewrite the bullet (and the matching comment on the strip's test) to rest on
the durable reason, which was already stated first: the sandbox is
server-derived policy the partner may not author, and bo-be 400s an authored
one. Note the planned manifest read in a clause, since it makes the
not-authorable rule matter more rather than less.

Comments only. `UI_APP_WIRE_ONLY_KEYS`, the strip, validation and every test
assertion are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A product decision applied across all four repos in the chain at once. The
old name became a misnomer once `layout: "inline"` existed — an inline card
embeds the page directly and opens no modal — so the field is named for what
it holds rather than how it happens to be presented. The manifest and UI kit
spelling is `iframeHref`; the authoring spelling here and on the bo-be wire
is `iframe_href`.

No migration and no alias, deliberately: the registry flip that would let an
`iframeExtension` app exist has not been applied on staging or production, so
no `app-config.json` in the wild can carry the old key. In particular the key
is NOT added to `LEGACY_KEY_ALIASES` in `src/lib/config.ts` — those tables
exist for the BEX-470 camelCase→snake_case migration of files that really do
have a population.

Surface: the `SurfacePointEntry` field, the create prompt and
`buildSurfacePointList`'s `urlField`, the validator's root-migration hint and
both per-type refusals, the message key
(`APP_CREATE_UI_MODAL_IFRAME_URL_PROMPT` → `APP_CREATE_UI_IFRAME_HREF_PROMPT`),
the scaffold templates, the agent docs and the smoke assertions.

Two things followed the field rather than being left behind:

- The upload diff's row is now `iframe URL:` instead of `modal URL:`, named
  for the field for the same reason the field is.
- The two refusal messages that asserted the URL always opens in a modal are
  now layout-neutral. They still refuse exactly what they refused before.

The user-visible prompt copy is unchanged, so the smoke's prompt patterns and
their `en.test.ts` pins still match.

`CHANGELOG.md` keeps the old name in the released 2.3.0 entry — that is
history, not a live reference.

Also converts `scaffold.test.ts`'s two bare `stdin.isTTY = …` assignments to
the descriptor form the same file already uses. Most suites define the
property with `{ value, configurable: true }` and no `writable`, which
defaults to non-writable; Jest reuses a worker across files, so an assignment
throws once one of those has run in the same worker. It passed only by
scheduling luck, and this rename's byte-size changes reshuffled the schedule
enough to surface it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…st [BEX-467]

The `sandbox` bullet above `UI_APP_WIRE_ONLY_KEYS` described the manifest read
as a later phase. That phase has landed — app-store-backend is serving the
stored `sandbox` and the UI kit is rendering from it rather than the fixed
constant it used to — so this is a tense correction, nothing more.

No CLI behaviour changes: `sandbox` stays in `UI_APP_WIRE_ONLY_KEYS`, stays
unauthorable, and stays out of `app-config.json`. The durable reason is
unchanged and deliberately left intact — it is server-derived policy a partner
may not author, and bo-be 400s an authored one.

The "don't re-justify this by who *reads* the value" warning is kept, and is
now more relevant rather than less: a real reader makes it tempting to explain
the strip by the read, which would be the wrong reason for the right rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`parseUiConfigFile` reads a fixed key set, so anything else in the file
vanished with no error. `UI_CONFIG_UNSUPPORTED_KEYS` existed to close
exactly that gap for `layout` and `modal_size`, but `iframe_href` — the
destination itself — was not on it: a file asking for an embedded page
created an actionLink app pointing somewhere else, silently.

Two classes now, two messages, because they are wrong for two different
reasons and one sentence cannot honestly cover both:

- iframe-only (`iframe_href`, `layout`, `modal_size`) — authorable, just
  not on this actionLink-only route, so the message says to create the
  app interactively or put the field in app-config.json and upload. The
  same three `validateUiApp` refuses on an actionLink entry.
- platform-owned (`link_target`, `sandbox`, `extension_point_name`,
  `version`) — not authorable anywhere, so the message says to remove
  the key. Derived from `uiAppType.wireOnlyKeys` rather than re-listed,
  so it cannot drift from the keys the CLI strips off the echo; a fifth
  stamped key is refused for free.

`sandbox` is the one with no prior local refusal at all: `validateUiApp`
checks named fields and has no unknown-key sweep, so an authored one
reached the wire and came back a 400. Naming it before any registry read
beats a server error for a value the partner should never have written.

Both loops run ahead of every network call, like the other
non-interactive guards. SKILL.md and AGENTS.md updated together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`stripUiAppWireOnlyKeys` covers the inbound direction only: it runs on what
the server echoes. `app upload` sends the file's `ui_app` block verbatim
(plus the injected `link_target`), so a `sandbox` hand-added to
app-config.json travelled outbound unexamined and came back an opaque bo-be
400. Tolerable for the other three wire-only keys — `version` and
`extension_point_name` are inert, `link_target` is pinned per entry — but
not for this one.

`sandbox` is the embedded frame's whole permission set, and nothing local
holds a copy of it any more: bo-be stamps it at write time,
app-store-backend serves the stored value on the manifest, and the kit
applies it verbatim with no default of its own (absent ⇒ the fail-closed
`sandbox=""`). An authored value is therefore not a preference weighed
against the platform's — it is a partner writing the attributes their own
frame runs under.

`validateUiApp` now refuses it by name at the block root and on every
`surface_point_list` entry, the two depths the strip recurses through,
ahead of the per-entry checks so a blank label on entry one cannot decide
whether the security-relevant problem is the message the partner sees.
Refused rather than stripped on the way out, deliberately: dropping the key
silently would leave a partner believing their value is in force. That
makes the pair symmetric — `--ui-config` already refuses it off the same
`wireOnlyKeys` list on the non-interactive route.

The scaffolded AGENTS.md / CLAUDE.md / README.md say so too, and gain the
`layout` and `modal_size` rows they were missing — both authorable, both
undocumented in the copies a partner actually reads. The `iframeExtension`
line there no longer claims the page is always in a modal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dtsl/extensibility-ui-kit 0.11.0 is released and deleted
EXTENSIBILITY_IFRAME_SANDBOX outright. The kit now applies the served
app_configs.sandbox verbatim and has no default behind it: an entry
served without one renders sandbox="" — an inert frame — plus a
[extensibility:missing-iframe-sandbox] dev warning.

No CLI behaviour changes. `sandbox` stays in UI_APP_WIRE_ONLY_KEYS,
stays unauthorable, stays out of app-config.json and stays refused by
--ui-config. Only the explanation was wrong.

The `sandbox` bullet above UI_APP_WIRE_ONLY_KEYS said the kit "renders
from" the manifest "rather than the fixed constant it used to", which
still left a fallback implied. It now says the stamp is load-bearing:
the platform stamps the policy, the manifest serves it, the kit applies
it verbatim with no default, so this is not merely a field the partner
may not author but one whose absence breaks the app. The durable reason
(server-derived policy; bo-be refuses an authored one) and the
"don't re-justify this by who reads the value" warning are both kept —
the warning is now more relevant, not less.

Also corrected the strip test's comment, which still described the
manifest read as "planned ... in a later phase", and dropped the
"stored snapshot's root" wording in both places: the kit serves the
policy per entry, and the strip recurses at every depth regardless.

SKILL.md and AGENTS.md said the kit "applies the stored value verbatim"
without the no-default consequence; both now carry it, in matching
wording. Neither mentions modal_iframe_url anywhere — checked, since
the kit removed modalIframeUrl rather than aliasing it and this branch
already renamed the authoring key to iframe_href.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant