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
12 changes: 12 additions & 0 deletions .changeset/17147-plugin-disclosure-not-enforced.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 21 additions & 1 deletion packages/app-shell/src/console/marketplace/PluginDisclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function PluginDisclosure({ version }: { version?: MarketplacePackageVers
{hasAny ? (
<div className="space-y-2">
<div className="text-xs text-muted-foreground">
{t('marketplace.disclosure.grantsIntro', { defaultValue: 'On install, this package will be granted:' })}
{t('marketplace.disclosure.grantsIntro', { defaultValue: 'This package requests:' })}
</div>
<PermissionGroup
icon={Boxes}
Expand All @@ -128,6 +128,26 @@ export function PluginDisclosure({ version }: { version?: MarketplacePackageVers
label={t('marketplace.disclosure.fs', { defaultValue: 'Filesystem access' })}
items={perms.fs ?? undefined}
/>
{/*
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.
*/}
<div className="text-[11px] text-muted-foreground/80 leading-snug">
{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.',
})}
</div>
</div>
) : (
<div className="text-xs text-muted-foreground">
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<PluginDisclosure version={codeBearing({ services: ['object'] })} />);

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(<PluginDisclosure version={codeBearing({ network: ['api.acme.com'] })} />);

// 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(<PluginDisclosure version={codeBearing({})} />);

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(
<PluginDisclosure
version={{ contains_code: false, permissions: { services: ['object'] } } as unknown as MarketplacePackageVersion}
/>,
);
expect(container.innerHTML).toBe('');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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']) {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3210,7 +3210,8 @@ const ar = {
reviewed: "تمت المراجعة والموافقة",
unreviewed: "لم تتم المراجعة بعد",
signed: "موقَّع",
grantsIntro: "عند التثبيت، ستُمنح هذه الحزمة:",
grantsIntro: "تطلب هذه الحزمة:",
notEnforced: "تُسجَّل هذه القائمة عند التثبيت ويُعاد تأكيدها إذا طلب إصدار لاحق المزيد، لكن بيئة التشغيل لا تقصر الحزمة على هذه القائمة بعد.",
services: "خدمات المنصة",
hooks: "خطافات دورة الحياة",
network: "الوصول إلى الشبكة",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3205,7 +3205,8 @@ const ja = {
reviewed: "審査・承認済み",
unreviewed: "未審査",
signed: "署名済み",
grantsIntro: "インストール時に、このパッケージには次の権限が付与されます:",
grantsIntro: "このパッケージは次の権限を要求します:",
notEnforced: "この一覧はインストール時に記録され、新しいバージョンがより多くを要求した場合は再確認されます。ただしランタイムはまだ、このパッケージをこの一覧に制限しません。",
services: "プラットフォームサービス",
hooks: "ライフサイクルフック",
network: "ネットワークアクセス",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3202,7 +3202,8 @@ const ko = {
reviewed: "검토 및 승인됨",
unreviewed: "아직 검토되지 않음",
signed: "서명됨",
grantsIntro: "설치하면 이 패키지에 다음 권한이 부여됩니다:",
grantsIntro: "이 패키지가 요청하는 권한:",
notEnforced: "이 목록은 설치 시 기록되며 이후 버전이 더 많은 권한을 요청하면 다시 확인합니다. 다만 런타임은 아직 패키지를 이 목록으로 제한하지 않습니다.",
services: "플랫폼 서비스",
hooks: "라이프사이클 후크",
network: "네트워크 액세스",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3216,7 +3216,8 @@ const ru = {
reviewed: "Проверено и одобрено",
unreviewed: "Ещё не проверено",
signed: "Подписано",
grantsIntro: "При установке пакет получит:",
grantsIntro: "Пакет запрашивает:",
notEnforced: "Список записывается при установке и подтверждается заново, если более поздняя версия запросит больше, но среда выполнения пока не ограничивает пакет этим списком.",
services: "Сервисы платформы",
hooks: "Хуки жизненного цикла",
network: "Доступ к сети",
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3333,7 +3333,8 @@ const zh = {
reviewed: '已审核并批准',
unreviewed: '尚未审核',
signed: '已签名',
grantsIntro: '安装后,此软件包将获得以下权限:',
grantsIntro: '此软件包请求以下权限:',
notEnforced: '安装时会记录这份清单,后续版本要求更多权限时会再次征求同意;但运行时目前还不会把该软件包限制在此清单内。',
services: '平台服务',
hooks: '生命周期钩子',
network: '网络访问',
Expand Down
Loading