diff --git a/.changeset/17147-plugin-disclosure-not-enforced.md b/.changeset/17147-plugin-disclosure-not-enforced.md
new file mode 100644
index 0000000000..a1054cdb84
--- /dev/null
+++ b/.changeset/17147-plugin-disclosure-not-enforced.md
@@ -0,0 +1,12 @@
+---
+'@object-ui/app-shell': patch
+'@object-ui/i18n': patch
+---
+
+The marketplace consent panel no longer promises confinement the runtime does not provide.
+
+`PluginDisclosure` introduced a code-bearing package's structured permission set with "On install, this package will be granted:". A list of grants on a security panel is read as a confinement promise — the complement assumed denied — and on this platform it is not. The consented set is persisted (`sys_package_installation.granted_permissions`), re-confirmed on a widening upgrade, and registered on the runtime's `PluginPermissionEnforcer` at load; it is queried by nothing, because `SecurePluginContext` has zero production construction sites and the fs/network gates have no caller at all (measured on objectstack `9bd4344e4`; objectstack#17147).
+
+`marketplace.disclosure.grantsIntro` now states a REQUEST — "This package requests:" — and a new `marketplace.disclosure.notEnforced` line beside the list says the set is recorded at install, re-confirmed if a later version asks for more, and not yet a runtime restriction. Both land in all ten locale packs; the `ja` value stays predicate-final so that pack's halfwidth-colon rule still decides it.
+
+The trust-tier badge is deliberately untouched: it is objectstack#11330's half of the same panel.
diff --git a/packages/app-shell/src/console/marketplace/PluginDisclosure.tsx b/packages/app-shell/src/console/marketplace/PluginDisclosure.tsx
index f20a51c038..7e9f9f4f10 100644
--- a/packages/app-shell/src/console/marketplace/PluginDisclosure.tsx
+++ b/packages/app-shell/src/console/marketplace/PluginDisclosure.tsx
@@ -106,7 +106,7 @@ export function PluginDisclosure({ version }: { version?: MarketplacePackageVers
{hasAny ? (
diff --git a/packages/app-shell/src/console/marketplace/__tests__/pluginDisclosureNotEnforced-17147.test.tsx b/packages/app-shell/src/console/marketplace/__tests__/pluginDisclosureNotEnforced-17147.test.tsx
new file mode 100644
index 0000000000..f2a1e9ab33
--- /dev/null
+++ b/packages/app-shell/src/console/marketplace/__tests__/pluginDisclosureNotEnforced-17147.test.tsx
@@ -0,0 +1,100 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * objectstack#17147 — the consent panel says what its permission list DOES.
+ *
+ * ## What was misleading
+ *
+ * The panel listed a code-bearing package's structured permission set under
+ * *"On install, this package will be granted:"*. A list of grants on a security
+ * panel is read as a confinement promise — the complement is assumed denied —
+ * and on this platform it is not. Measured on objectstack `9bd4344e4`: the
+ * consented set is persisted (`sys_package_installation.granted_permissions`),
+ * re-confirmed on a widening upgrade (cloud answers 409 without `reconsent`),
+ * and REGISTERED on the runtime's `PluginPermissionEnforcer` at load — and
+ * queried by nothing, because `SecurePluginContext` has no production
+ * construction site and the fs/network gates have no caller at all.
+ *
+ * Maintainer ruling 2026-09-12 took option B — the same option #11330 took on
+ * the trust-tier half of the same claim: say it truthfully now. So the intro
+ * became a REQUEST ("This package requests:") and a note beside the list says
+ * the runtime does not yet restrict the package to it.
+ *
+ * ## Why a render test here, unlike `plugin-runtime-tier-3846`
+ *
+ * That file says a render test would have been green both ways and proved
+ * nothing, because the tier labels were byte-identical before and after. This
+ * change is the opposite: what moved IS the rendered copy, and the note is a
+ * whole element that did not exist. Both halves are therefore asserted on the
+ * rendered output, and the retracted sentence is asserted ABSENT — without that
+ * negative, a future edit could restore "will be granted" beside the note and
+ * the positive assertion would stay green.
+ *
+ * ⛔ This is the objectui end of a claim pinned in three repos. The framework
+ * pin `granted-permissions-not-enforced.pin.test.ts` (`@objectstack/core`) goes
+ * red the day a production `SecurePluginContext` construction site appears —
+ * i.e. the day the ADR-0025 materialize seam makes this list a real gate — and
+ * its failure message names this file. Rewrite them together.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { PluginDisclosure } from '../PluginDisclosure';
+import type { MarketplacePackageVersion } from '../marketplaceApi';
+
+const codeBearing = (permissions: MarketplacePackageVersion['permissions']) =>
+ ({
+ contains_code: true,
+ runtime: 'sandbox',
+ permissions,
+ } as unknown as MarketplacePackageVersion);
+
+describe('objectstack#17147 — the consent panel does not promise confinement', () => {
+ it('introduces the list as a REQUEST, not as a grant that binds the runtime', () => {
+ render(
);
+
+ expect(screen.getByText('This package requests:')).toBeTruthy();
+ // The negative half. `queryAllByText` rather than a throwing getter so the
+ // failure reads as "the retracted sentence is back", not as a lookup error.
+ expect(
+ screen.queryAllByText(/will be granted/),
+ 'the retracted "On install, this package will be granted:" must not return',
+ ).toEqual([]);
+ });
+
+ it('renders the not-enforced note beside the list', () => {
+ render(
);
+
+ // The two load-bearing clauses, probed separately so a reword that keeps the
+ // fact keeps the pin, and a reword that drops either clause does not.
+ expect(screen.getByText(/does not yet restrict the package to this list/)).toBeTruthy();
+ expect(screen.getByText(/re-confirmed if a later version asks for more/)).toBeTruthy();
+ });
+
+ it('says nothing about enforcement when the package requests nothing', () => {
+ // Anti-vacuity for the case above: the note rides the LIST, so a package
+ // with no requested surface has no list and nothing to qualify.
+ render(
);
+
+ expect(screen.getByText('Requests no special permissions.')).toBeTruthy();
+ expect(screen.queryAllByText(/does not yet restrict/)).toEqual([]);
+ });
+
+ it('renders nothing at all for a package that carries no code', () => {
+ // The panel's own precondition (`if (!version?.contains_code) return null`),
+ // asserted so the cases above cannot be satisfied by a component that
+ // renders its copy unconditionally.
+ const { container } = render(
+
,
+ );
+ expect(container.innerHTML).toBe('');
+ });
+});
diff --git a/packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx b/packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx
index 69a23903af..2ad0300a79 100644
--- a/packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx
+++ b/packages/i18n/src/__tests__/marketplace-preview-namespace-3546.test.tsx
@@ -333,14 +333,20 @@ describe('objectui#3546 slice five — the marketplace and preview namespaces',
// grid.import.mappingTemplate, auth.verifyEmail.sentTo,
// preview.changes.detailChangedKeys, …).
//
- // Both of this slice's ja colons end in a predicate (`付与されます`,
+ // Both of this slice's ja colons end in a predicate (`要求します`,
// `読み込めませんでした`), so both are halfwidth. `loadFailed` additionally
// matches its same-name sibling `preview.changes.loadFailed` byte for byte;
// `grantsIntro` has no sibling and is decided by the split alone. Written
// down because the first draft of this file got `grantsIntro` wrong (it had
// the fullwidth form) and only the census caught it.
+ // [objectstack#17147] The sentence changed — "will be granted" became
+ // "requests", because the runtime does not yet restrict the package to this
+ // list — but the CONDITION this case pins did not: the ja value is still
+ // predicate-final (`要求します`), so it still takes the halfwidth colon. ⛔ A
+ // future reword that ends this value on a NOUN must flip it to `:` and move
+ // it to the other side of the split, not keep the byte and lose the rule.
expect(at(builtInLocales.ja, 'marketplace.disclosure.grantsIntro')).toBe(
- 'インストール時に、このパッケージには次の権限が付与されます:',
+ 'このパッケージは次の権限を要求します:',
);
expect(at(builtInLocales.ja, 'preview.history.loadFailed')).toBe('履歴を読み込めませんでした:');
for (const key of ['marketplace.disclosure.grantsIntro', 'preview.history.loadFailed']) {
@@ -751,7 +757,7 @@ describe('objectui#3546 slice five — the marketplace and preview namespaces',
['pt', 'marketplace.disclosure.fs', 'Acesso ao sistema de arquivos'],
['ru', 'marketplace.disclosure.unreviewed', 'Ещё не проверено'],
['ja', 'preview.history.revertFailed', '取り消しに失敗しました'],
- ['ko', 'marketplace.disclosure.grantsIntro', '설치하면 이 패키지에 다음 권한이 부여됩니다:'],
+ ['ko', 'marketplace.disclosure.grantsIntro', '이 패키지가 요청하는 권한:'],
['ar', 'preview.unpublishedBar.published', 'تم النشر! التطبيق مرئي الآن لمستخدميك.'],
])('%s renders a user-visible string from the pack', (lang, key, expected) => {
// One pinned surface per remaining pack, across four writing systems, so a
diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts
index 259bf7a2bc..393b54faea 100644
--- a/packages/i18n/src/locales/ar.ts
+++ b/packages/i18n/src/locales/ar.ts
@@ -3210,7 +3210,8 @@ const ar = {
reviewed: "تمت المراجعة والموافقة",
unreviewed: "لم تتم المراجعة بعد",
signed: "موقَّع",
- grantsIntro: "عند التثبيت، ستُمنح هذه الحزمة:",
+ grantsIntro: "تطلب هذه الحزمة:",
+ notEnforced: "تُسجَّل هذه القائمة عند التثبيت ويُعاد تأكيدها إذا طلب إصدار لاحق المزيد، لكن بيئة التشغيل لا تقصر الحزمة على هذه القائمة بعد.",
services: "خدمات المنصة",
hooks: "خطافات دورة الحياة",
network: "الوصول إلى الشبكة",
diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts
index 10ed5d5594..119831977e 100644
--- a/packages/i18n/src/locales/de.ts
+++ b/packages/i18n/src/locales/de.ts
@@ -3203,7 +3203,8 @@ const de = {
reviewed: "Geprüft und genehmigt",
unreviewed: "Noch nicht geprüft",
signed: "Signiert",
- grantsIntro: "Bei der Installation erhält dieses Paket:",
+ grantsIntro: "Dieses Paket fordert an:",
+ notEnforced: "Diese Liste wird bei der Installation erfasst und erneut bestätigt, wenn eine spätere Version mehr anfordert – die Laufzeit beschränkt das Paket jedoch noch nicht darauf.",
services: "Plattformdienste",
hooks: "Lifecycle-Hooks",
network: "Netzwerkzugriff",
diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts
index 6a6314192f..4f3613e919 100644
--- a/packages/i18n/src/locales/en.ts
+++ b/packages/i18n/src/locales/en.ts
@@ -3615,7 +3615,8 @@ const en = {
reviewed: 'Reviewed & approved',
unreviewed: 'Not yet reviewed',
signed: 'Signed',
- grantsIntro: 'On install, this package will be granted:',
+ grantsIntro: 'This package requests:',
+ notEnforced: 'Recorded at install, and re-confirmed if a later version asks for more — but the runtime does not yet restrict the package to this list.',
services: 'Platform services',
hooks: 'Lifecycle hooks',
network: 'Network access',
diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts
index 67ae51006c..b5cf431653 100644
--- a/packages/i18n/src/locales/es.ts
+++ b/packages/i18n/src/locales/es.ts
@@ -3207,7 +3207,8 @@ const es = {
reviewed: "Revisado y aprobado",
unreviewed: "Sin revisar todavía",
signed: "Firmado",
- grantsIntro: "Al instalarlo, este paquete recibirá:",
+ grantsIntro: "Este paquete solicita:",
+ notEnforced: "Esta lista se registra al instalar y se vuelve a confirmar si una versión posterior solicita más, pero el runtime aún no restringe el paquete a ella.",
services: "Servicios de la plataforma",
hooks: "Hooks de ciclo de vida",
network: "Acceso a la red",
diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts
index 6b3a4c6a24..d15ef7b6ac 100644
--- a/packages/i18n/src/locales/fr.ts
+++ b/packages/i18n/src/locales/fr.ts
@@ -3205,7 +3205,8 @@ const fr = {
reviewed: "Examiné et approuvé",
unreviewed: "Pas encore examiné",
signed: "Signé",
- grantsIntro: "À l'installation, ce paquet obtiendra :",
+ grantsIntro: "Ce paquet demande :",
+ notEnforced: "Cette liste est enregistrée à l'installation et reconfirmée si une version ultérieure en demande davantage, mais l'exécution ne restreint pas encore le paquet à cette liste.",
services: "Services de la plateforme",
hooks: "Hooks de cycle de vie",
network: "Accès réseau",
diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts
index d1d27c4854..10f6931fbf 100644
--- a/packages/i18n/src/locales/ja.ts
+++ b/packages/i18n/src/locales/ja.ts
@@ -3205,7 +3205,8 @@ const ja = {
reviewed: "審査・承認済み",
unreviewed: "未審査",
signed: "署名済み",
- grantsIntro: "インストール時に、このパッケージには次の権限が付与されます:",
+ grantsIntro: "このパッケージは次の権限を要求します:",
+ notEnforced: "この一覧はインストール時に記録され、新しいバージョンがより多くを要求した場合は再確認されます。ただしランタイムはまだ、このパッケージをこの一覧に制限しません。",
services: "プラットフォームサービス",
hooks: "ライフサイクルフック",
network: "ネットワークアクセス",
diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts
index 7501a8d4ef..a629b38d59 100644
--- a/packages/i18n/src/locales/ko.ts
+++ b/packages/i18n/src/locales/ko.ts
@@ -3202,7 +3202,8 @@ const ko = {
reviewed: "검토 및 승인됨",
unreviewed: "아직 검토되지 않음",
signed: "서명됨",
- grantsIntro: "설치하면 이 패키지에 다음 권한이 부여됩니다:",
+ grantsIntro: "이 패키지가 요청하는 권한:",
+ notEnforced: "이 목록은 설치 시 기록되며 이후 버전이 더 많은 권한을 요청하면 다시 확인합니다. 다만 런타임은 아직 패키지를 이 목록으로 제한하지 않습니다.",
services: "플랫폼 서비스",
hooks: "라이프사이클 후크",
network: "네트워크 액세스",
diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts
index 95f605c735..3ed585d38c 100644
--- a/packages/i18n/src/locales/pt.ts
+++ b/packages/i18n/src/locales/pt.ts
@@ -3202,7 +3202,8 @@ const pt = {
reviewed: "Revisado e aprovado",
unreviewed: "Ainda não revisado",
signed: "Assinado",
- grantsIntro: "Na instalação, este pacote receberá:",
+ grantsIntro: "Este pacote solicita:",
+ notEnforced: "Esta lista é registada na instalação e reconfirmada se uma versão posterior pedir mais, mas o runtime ainda não restringe o pacote a ela.",
services: "Serviços da plataforma",
hooks: "Hooks de ciclo de vida",
network: "Acesso à rede",
diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts
index b544418e17..86aab8add6 100644
--- a/packages/i18n/src/locales/ru.ts
+++ b/packages/i18n/src/locales/ru.ts
@@ -3216,7 +3216,8 @@ const ru = {
reviewed: "Проверено и одобрено",
unreviewed: "Ещё не проверено",
signed: "Подписано",
- grantsIntro: "При установке пакет получит:",
+ grantsIntro: "Пакет запрашивает:",
+ notEnforced: "Список записывается при установке и подтверждается заново, если более поздняя версия запросит больше, но среда выполнения пока не ограничивает пакет этим списком.",
services: "Сервисы платформы",
hooks: "Хуки жизненного цикла",
network: "Доступ к сети",
diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts
index a9ce007799..573706b1b8 100644
--- a/packages/i18n/src/locales/zh.ts
+++ b/packages/i18n/src/locales/zh.ts
@@ -3333,7 +3333,8 @@ const zh = {
reviewed: '已审核并批准',
unreviewed: '尚未审核',
signed: '已签名',
- grantsIntro: '安装后,此软件包将获得以下权限:',
+ grantsIntro: '此软件包请求以下权限:',
+ notEnforced: '安装时会记录这份清单,后续版本要求更多权限时会再次征求同意;但运行时目前还不会把该软件包限制在此清单内。',
services: '平台服务',
hooks: '生命周期钩子',
network: '网络访问',