From f82cb0cc33e5148dbce639ec92bd3faa47d2906e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Sat, 12 Sep 2026 10:32:44 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(marketplace):=20the=20consent=20panel?= =?UTF-8?q?=20promised=20confinement=20the=20runtime=20does=20not=20provid?= =?UTF-8?q?e=20=E2=80=94=20say=20what=20the=20permission=20list=20actually?= =?UTF-8?q?=20does=20(objectstack#17147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PluginDisclosure` 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 assumed denied — and on this platform it is not. Measured on objectstack `9bd4344e4`: the consented set IS persisted (`sys_package_installation.granted_permissions`), IS re-confirmed on a widening upgrade (cloud answers 409 without `reconsent`), and IS registered on the runtime's `PluginPermissionEnforcer` at load (objectstack#13457). It is queried by NOTHING — `SecurePluginContext` has zero production construction sites, and the fs/network gates have no caller at all — so the list is real and auditable, and it is not a gate. Maintainer ruling 2026-09-12 took option B, the same option objectstack#11330 took on the trust-tier half of the same claim: say it truthfully now. Enforcing is the ADR-0025 materialize seam, a separately designed direction. • `grantsIntro` becomes a REQUEST ("This package requests:") in all ten packs • new `notEnforced` line beside the list, in all ten packs: recorded at install, re-confirmed on a widening upgrade, and not yet a runtime restriction The `ja` value stays PREDICATE-final (`要求します`) so that pack's documented halfwidth-colon rule still decides it; slice five's colon case is updated to the new bytes with the rule it pins spelled out, and its `ko` value pin follows. New: `pluginDisclosureNotEnforced-17147.test.tsx` — both halves on the rendered output, the retracted sentence asserted ABSENT, plus the no-permissions and no-code branches so the positives cannot be satisfied by a component that renders unconditionally. Ablated twice (retracted intro restored · note element removed); each turns it red. The framework end of this claim is objectstack-ai/objectstack#17753, whose pin `granted-permissions-not-enforced.pin.test.ts` goes red the day the seam lands and names this file among the texts that then become false. Refs objectstack#17147 · objectstack#11333 · objectstack#13457 · objectstack#11330 Co-Authored-By: Claude Opus 5 --- .../console/marketplace/PluginDisclosure.tsx | 22 +++- ...pluginDisclosureNotEnforced-17147.test.tsx | 100 ++++++++++++++++++ ...arketplace-preview-namespace-3546.test.tsx | 12 ++- packages/i18n/src/locales/ar.ts | 3 +- packages/i18n/src/locales/de.ts | 3 +- packages/i18n/src/locales/en.ts | 3 +- packages/i18n/src/locales/es.ts | 3 +- packages/i18n/src/locales/fr.ts | 3 +- packages/i18n/src/locales/ja.ts | 3 +- packages/i18n/src/locales/ko.ts | 3 +- packages/i18n/src/locales/pt.ts | 3 +- packages/i18n/src/locales/ru.ts | 3 +- packages/i18n/src/locales/zh.ts | 3 +- 13 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 packages/app-shell/src/console/marketplace/__tests__/pluginDisclosureNotEnforced-17147.test.tsx 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 ? (
- {t('marketplace.disclosure.grantsIntro', { defaultValue: 'On install, this package will be granted:' })} + {t('marketplace.disclosure.grantsIntro', { defaultValue: 'This package requests:' })}
+ {/* + objectstack#17147 — say what this list DOES, because a permission + panel that only lists grants is read as a confinement promise. + Measured on objectstack `9bd4344e4`: the consented set is persisted + (`sys_package_installation.granted_permissions`), re-confirmed on a + widening upgrade (cloud returns 409 without `reconsent`), and + REGISTERED on the runtime's `PluginPermissionEnforcer` at load — and + queried by nothing, because `SecurePluginContext` has no production + construction site. So the list is real and auditable; it is not yet a + gate. ⛔ Delete this line only together with the framework pin + `granted-permissions-not-enforced.pin.test.ts`, which goes red the + day the ADR-0025 materialize seam makes it a gate. + */} +
+ {t('marketplace.disclosure.notEnforced', { + // One string literal, not a concatenation, so `check:i18n-keys` + // compares it against the en pack byte for byte. + defaultValue: '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.', + })} +
) : (
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: '网络访问', From 8f7a9f6683fdbc0afe267e495907c4bbb91a63e6 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Sat, 12 Sep 2026 10:37:10 +0800 Subject: [PATCH 2/2] chore(changeset): declare the consent-panel copy correction (objectstack#17147) Co-Authored-By: Claude Opus 5 --- .changeset/17147-plugin-disclosure-not-enforced.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/17147-plugin-disclosure-not-enforced.md 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.