diff --git a/.changeset/30326-install-result-protocol-check-skipped.md b/.changeset/30326-install-result-protocol-check-skipped.md
new file mode 100644
index 00000000000..76bed3f0ee4
--- /dev/null
+++ b/.changeset/30326-install-result-protocol-check-skipped.md
@@ -0,0 +1,13 @@
+---
+'@object-ui/app-shell': patch
+'@object-ui/i18n': patch
+---
+
+feat(console): the install result says when a package was installed without a protocol compatibility check
+
+Clause-②: yes (widening)
+Metadata-protocol: none
+
+- **What the administrator now reads.** `POST /api/v1/marketplace/install-local` answers `data.protocolCheckSkipped` when its protocol handshake admitted the package without checking it: the package declares no `engines.protocol` (`no-range`), or a range the runtime cannot read (`unparsed-range`). The three Console surfaces that show an install result now draw it as a caution line under the success sentence: the package page's Install result, the catalog's organization install message, and Setup → Packages → Import. The line names the package, the platform protocol it was installed under and, for `unparsed-range`, the range it declared, in the reader's locale. A `reason` this Console does not know shows the server's own `message`. An install whose range was checked shows nothing new.
+- **Types.** `LocalManagedInstallResult` and `LocalUnmanagedInstallResult` gain the optional `protocolCheckSkipped`.
+- **New strings.** `@object-ui/i18n` gains `marketplace.install.protocolCheckSkipped.noRange` and `.unparsedRange` in all 10 locales.
diff --git a/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx b/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx
index 83e5abf5fd6..877027030aa 100644
--- a/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx
+++ b/ui/packages/app-shell/src/console/marketplace/MarketplacePackagePage.tsx
@@ -82,6 +82,8 @@ import {
type CloudInstallationInfo,
} from './marketplaceApi.js';
import { useDependencyMissingText } from './dependencyMissing.js';
+import { useProtocolCheckSkippedText } from './protocolCheckSkipped.js';
+import { ProtocolCheckSkippedNote } from './ProtocolCheckSkippedNote.js';
import { getCloudBase, getRuntimeConfig, isMarketplaceEnabled } from '../../runtime-config.js';
import { emitMetadataRefresh } from '../../assistant/assistantBus.js';
import { useMetadata } from '../../providers/MetadataProvider.js';
@@ -148,6 +150,7 @@ export function MarketplacePackagePage() {
const { isAdmin, isResolved } = useWorkspaceAdminStatus();
const { t, language } = useObjectTranslation();
const dependencyMissingText = useDependencyMissingText();
+ const protocolCheckSkippedText = useProtocolCheckSkippedText();
// Dates and numbers on this surface read the display locale; a bare
// `toLocale*()` call used the MACHINE's locale (objectui#9909).
const displayLocale = useDisplayLocale();
@@ -267,6 +270,9 @@ export function MarketplacePackagePage() {
message: string;
unmanaged?: { strippedLocks: LocalInstallStrippedLock[] };
credentials?: { packageId: string; note?: string };
+ // objectstack#30326 — the install answer's `protocolCheckSkipped`, drawn:
+ // installed, but its platform protocol range was not checked.
+ protocolNote?: string;
} | null>(null);
// ADR-0131 D6 — the install modes the package's latest manifest declares,
// read from the manifest install-local judges, and the operator's pick among
@@ -601,6 +607,10 @@ export function MarketplacePackagePage() {
try {
const result = await installLocal({ packageId, ...(installMode ? { installMode } : {}) });
const name = data?.package ? localizePackage(data.package as any, language).displayName : result.manifestId;
+ // objectstack#30326 — every success answer can say the package was
+ // admitted with its protocol range unchecked; each note below carries it.
+ const protocolNote = protocolCheckSkippedText(result) ?? undefined;
+ const withProtocolNote = protocolNote ? { protocolNote } : {};
// An unmanaged copy (ADR-0131 D6): the package's metadata is this
// runtime's own from now on, in the base the answer names; the answer
// lists every protection lock the copy dropped, with the package's reason.
@@ -614,6 +624,7 @@ export function MarketplacePackagePage() {
ok: true,
message: t('marketplace.install.unmanagedSuccess', { version: result.version, name, base: result.targetPackageId }),
unmanaged: { strippedLocks: result.strippedLocks },
+ ...withProtocolNote,
});
emitMetadataRefresh();
return;
@@ -629,8 +640,8 @@ export function MarketplacePackagePage() {
// and the page says when the app will appear.
if (!result.hotLoaded) {
setLocalResult(ignoredCopy
- ? { ok: false, message: t('marketplace.install.unmanagedIgnored', { name }) }
- : { ok: true, message: t('marketplace.install.localLoadsAtRestart', { version: result.version, name }) });
+ ? { ok: false, message: t('marketplace.install.unmanagedIgnored', { name }), ...withProtocolNote }
+ : { ok: true, message: t('marketplace.install.localLoadsAtRestart', { version: result.version, name }), ...withProtocolNote });
return;
}
try {
@@ -639,11 +650,12 @@ export function MarketplacePackagePage() {
// Non-fatal: the install succeeded; the app list will refresh on next navigation.
}
setLocalResult(ignoredCopy
- ? { ok: false, message: t('marketplace.install.unmanagedIgnored', { name }) }
+ ? { ok: false, message: t('marketplace.install.unmanagedIgnored', { name }), ...withProtocolNote }
: {
ok: true,
message: t('marketplace.install.localSuccess', { version: result.version, name }),
credentials: { packageId: result.manifestId, note: result.credentialNote },
+ ...withProtocolNote,
});
emitMetadataRefresh();
} catch (e: any) {
@@ -996,6 +1008,7 @@ export function MarketplacePackagePage() {
{localResult.ok ? : }
{localResult.message}
+ {localResult.protocolNote ?
: null}
{/* ADR-0131 D6 — each protection lock the unmanaged copy dropped, as
the answer states it: the item, the lock and the package's reason
verbatim, so the operator reads why the vendor locked an item
diff --git a/ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx b/ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx
index d0ec1afc1d2..8035064a8f8 100644
--- a/ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx
+++ b/ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx
@@ -52,6 +52,8 @@ import { appRouteSegment, filterActiveApps } from '../../utils/index.js';
import { waitForServedApp, type ServedApp } from './waitForServedApp.js';
import { splitPackageSuffix } from './middleTruncate.js';
import { useDependencyMissingText } from './dependencyMissing.js';
+import { useProtocolCheckSkippedText } from './protocolCheckSkipped.js';
+import { ProtocolCheckSkippedNote } from './ProtocolCheckSkippedNote.js';
import { ConnectorCredentialsStep } from './ConnectorCredentialsStep.js';
import { packagesHref } from '../../views/metadata-admin/packageSource.js';
@@ -310,6 +312,7 @@ export function MarketplacePage() {
const { isAdmin, isResolved } = useWorkspaceAdminStatus();
const { t, language } = useObjectTranslation();
const dependencyMissingText = useDependencyMissingText();
+ const protocolCheckSkippedText = useProtocolCheckSkippedText();
const { appLabel } = useServedAppLabel();
// The adapter `MetadataProvider` reads through: the deploy wait asks the
// same `GET /meta/app` the cache would, without the cache persisting it.
@@ -356,6 +359,8 @@ export function MarketplacePage() {
ok: boolean;
text: string;
credentials?: { packageId: string; note?: string };
+ // objectstack#30326 — the answer's `protocolCheckSkipped`, drawn.
+ protocolNote?: string;
} | null>(null);
const [orgDeploy, setOrgDeploy] = useState
(null);
const [installedIds, setInstalledIds] = useState>(new Set());
@@ -491,6 +496,8 @@ export function MarketplacePage() {
try {
if (getRuntimeConfig().features.installLocal) {
const result = await installLocal({ packageId: pkg.manifest_id || pkg.id, versionId: 'latest' });
+ // objectstack#30326 — installed, its platform protocol range unchecked.
+ const protocolNote = protocolCheckSkippedText(result);
// objectui#12098 — `hotLoaded: false` is the runtime's lenient path
// (objectstack#22695): installed, loaded at its next restart. Nothing
// of it is served until then, so the strip says so and nothing is
@@ -501,6 +508,7 @@ export function MarketplacePage() {
? t('marketplace.org.installed', { defaultValue: `Installed ${pkg.display_name}`, name: pkg.display_name })
: t('marketplace.install.localLoadsAtRestart', { version: result.version, name: pkg.display_name }),
...(result.hotLoaded ? { credentials: { packageId: result.manifestId, note: result.credentialNote } } : {}),
+ ...(protocolNote ? { protocolNote } : {}),
});
await load();
// On `hotLoaded` the runtime registered the package before it answered,
@@ -738,6 +746,7 @@ export function MarketplacePage() {
{orgMsg && (
{orgMsg.text}
+ {orgMsg.protocolNote ?
: null}
{orgMsg.ok && orgMsg.credentials ? (
+
+ {text}
+
+ );
+}
diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.protocolCheckSkipped-30326.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.protocolCheckSkipped-30326.test.tsx
new file mode 100644
index 00000000000..ecea7dca144
--- /dev/null
+++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePackagePage.protocolCheckSkipped-30326.test.tsx
@@ -0,0 +1,155 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * The package page's Install result says when the install door admitted the
+ * package WITHOUT checking its platform protocol range: the answer's
+ * `data.protocolCheckSkipped` (objectstack#30326), drawn under the success
+ * sentence. Managed (loaded now, loaded at restart) and unmanaged answers all
+ * carry it.
+ *
+ * `marketplaceApi` is NOT mocked: one stubbed `fetch` answers by path, so the
+ * notice the page draws is the one that crossed the wire. `t` echoes the KEY
+ * and the notice's facts.
+ */
+
+import '@testing-library/jest-dom/vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
+
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => vi.fn(),
+ useParams: () => ({ packageId: 'pkg_psa' }),
+}));
+
+vi.mock('@object-ui/auth', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }),
+}));
+
+vi.mock('@object-ui/i18n', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useObjectTranslation: () => ({
+ t: (key: string, o?: Record) => {
+ const facts = ['packageId', 'protocolVersion', 'requiredRange'].filter((k) => o?.[k] !== undefined).map((k) => `${k}=${String(o?.[k])}`);
+ return facts.length > 0 ? `«${key}|${facts.join('|')}»` : `«${key}»`;
+ },
+ language: 'en',
+ }),
+}));
+
+vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} }));
+vi.mock('../../../providers/MetadataProvider', () => ({ useMetadata: () => ({ refresh: async () => {} }) }));
+vi.mock('../../../components/SuggestedBindingsPanel', () => ({
+ SuggestedBindingsPanel: () => ,
+}));
+vi.mock('../ConnectorCredentialsStep', () => ({
+ ConnectorCredentialsStep: () => ,
+}));
+
+import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../../runtime-config';
+import { MarketplacePackagePage } from '../MarketplacePackagePage';
+
+const INSTALL_PATH = '/api/v1/marketplace/install-local';
+const MANIFEST_PATH = '/api/v1/marketplace/packages/pkg_psa/versions/latest/manifest';
+const CATALOG_PACKAGE = {
+ package: { id: 'pkg_psa', manifest_id: 'com.acme.psa', display_name: 'PSA', latest_version: { id: 'ver_1', version: '1.0.0' } },
+ versions: [{ id: 'ver_1', version: '1.0.0' }],
+};
+const SERVER_MESSAGE = 'SERVER-ENGLISH sentinel: installed without a compatibility check.';
+const NO_RANGE = { reason: 'no-range', packageId: 'com.acme.psa', protocolVersion: '18.1.0', message: SERVER_MESSAGE };
+
+const answer = (status: number, body: unknown) => ({
+ ok: status >= 200 && status < 300,
+ status,
+ statusText: '',
+ headers: new Headers({ 'content-type': 'application/json' }),
+ json: async () => body,
+ text: async () => JSON.stringify(body),
+});
+
+async function boot(installData: Record, installModes?: string[]) {
+ resetRuntimeConfigForTesting();
+ vi.stubGlobal('fetch', vi.fn(async (input: unknown, init?: { method?: string }) => {
+ const path = new URL(String(input), 'http://console.test').pathname;
+ const method = (init?.method ?? 'GET').toUpperCase();
+ if (path === '/api/v1/runtime/config') {
+ return answer(200, {
+ cloudUrl: '', singleEnvironment: true,
+ features: { installLocal: true, marketplace: true, aiStudio: true, autoPublishAiBuilds: true },
+ branding: { productName: 'ObjectOS', productShortName: 'ObjectOS' },
+ });
+ }
+ if (path === INSTALL_PATH && method === 'GET') return answer(200, { success: true, data: { items: [], total: 0 } });
+ if (path === INSTALL_PATH && method === 'POST') return answer(200, { success: true, data: installData });
+ if (path === MANIFEST_PATH) {
+ return answer(200, {
+ success: true,
+ data: { manifest: { id: 'com.acme.psa', version: '1.0.0', type: 'app', ...(installModes ? { installModes } : {}) }, version: '1.0.0', version_id: 'ver_1' },
+ });
+ }
+ if (path === '/api/v1/marketplace/packages/pkg_psa') return answer(200, { success: true, data: CATALOG_PACKAGE });
+ return answer(404, { success: false, error: { code: 'NOT_FOUND', message: `no route ${method} ${path}` } });
+ }));
+ await initRuntimeConfig();
+}
+
+async function install() {
+ render();
+ const button = await screen.findByRole('button', { name: '«marketplace.action.install»' });
+ await act(async () => {});
+ fireEvent.click(button);
+}
+
+const MANAGED = { manifestId: 'com.acme.psa', version: '1.0.0', versionId: 'ver_1', installedAt: '2026-11-05T00:00:00Z', upgradedFrom: null };
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+ resetRuntimeConfigForTesting();
+});
+
+describe('the package page\'s Install result draws protocolCheckSkipped (objectstack#30326)', () => {
+ it('a managed install the kernel loaded: the success sentence, then the unchecked-protocol line', async () => {
+ await boot({ ...MANAGED, hotLoaded: true, protocolCheckSkipped: NO_RANGE });
+ await install();
+
+ expect(await screen.findByText('«marketplace.install.localSuccess»')).toBeInTheDocument();
+ expect(screen.getByTestId('protocol-check-skipped')).toHaveTextContent(
+ '«marketplace.install.protocolCheckSkipped.noRange|packageId=com.acme.psa|protocolVersion=18.1.0|requiredRange=»',
+ );
+ expect(screen.queryByText(SERVER_MESSAGE)).not.toBeInTheDocument();
+ });
+
+ it('a managed install loaded at restart carries it too', async () => {
+ await boot({ ...MANAGED, hotLoaded: false, protocolCheckSkipped: NO_RANGE });
+ await install();
+
+ expect(await screen.findByText('«marketplace.install.localLoadsAtRestart»')).toBeInTheDocument();
+ expect(screen.getByTestId('protocol-check-skipped')).toHaveTextContent('marketplace.install.protocolCheckSkipped.noRange');
+ });
+
+ it('an unmanaged copy with an unreadable range: the range it declared', async () => {
+ await boot({
+ installMode: 'unmanaged', manifestId: 'com.acme.psa', version: '1.0.0', versionId: 'ver_1', installedAt: '2026-11-05T00:00:00Z',
+ installedFrom: { packageId: 'com.acme.psa' }, targetPackageId: 'com.acme.psa_copy', namespace: 'psa', copiedCount: 0, copied: [], strippedLocks: [],
+ protocolCheckSkipped: {
+ reason: 'unparsed-range', packageId: 'com.acme.psa', protocolVersion: '18.1.0',
+ requiredRange: 'latest', rangeSource: 'engines.protocol', message: SERVER_MESSAGE,
+ },
+ }, ['unmanaged']);
+ await install();
+
+ expect(await screen.findByText('«marketplace.install.unmanagedSuccess»')).toBeInTheDocument();
+ expect(screen.getByTestId('protocol-check-skipped')).toHaveTextContent(
+ '«marketplace.install.protocolCheckSkipped.unparsedRange|packageId=com.acme.psa|protocolVersion=18.1.0|requiredRange=latest»',
+ );
+ });
+
+ it('CONTROL: an install whose range was checked draws no such line', async () => {
+ await boot({ ...MANAGED, hotLoaded: true });
+ await install();
+
+ expect(await screen.findByText('«marketplace.install.localSuccess»')).toBeInTheDocument();
+ expect(screen.queryByTestId('protocol-check-skipped')).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.orgInstallProtocolCheckSkipped-30326.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.orgInstallProtocolCheckSkipped-30326.test.tsx
new file mode 100644
index 00000000000..d7aedd28d57
--- /dev/null
+++ b/ui/packages/app-shell/src/console/marketplace/__tests__/MarketplacePage.orgInstallProtocolCheckSkipped-30326.test.tsx
@@ -0,0 +1,133 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * The catalog page's org install on a self-hosted runtime (`installLocal`)
+ * says when the install door admitted the package WITHOUT checking its
+ * platform protocol range: the answer's `data.protocolCheckSkipped`
+ * (objectstack#30326), drawn under the installed sentence.
+ *
+ * The catalog lists are stubbed; `installLocal` is the REAL client, so the
+ * notice the strip draws is the one that crossed the wire.
+ */
+
+import '@testing-library/jest-dom/vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
+
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => vi.fn(),
+ useParams: () => ({}),
+}));
+
+vi.mock('@object-ui/auth', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }),
+ useAuth: () => ({ activeOrganization: { id: 'org_1' } }),
+}));
+
+vi.mock('@object-ui/i18n', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useObjectTranslation: () => ({
+ t: (key: string, o?: Record) => {
+ const facts = ['packageId', 'protocolVersion', 'requiredRange'].filter((k) => o?.[k] !== undefined).map((k) => `${k}=${String(o?.[k])}`);
+ return facts.length > 0 ? `«${key}|${facts.join('|')}»` : `«${key}»`;
+ },
+ language: 'en',
+ }),
+}));
+
+vi.mock('../ConnectorCredentialsStep', () => ({
+ ConnectorCredentialsStep: () => ,
+}));
+vi.mock('../../../assistant/assistantBus', () => ({ emitMetadataRefresh: () => {} }));
+
+const PKG = 'com.acme.psa';
+const ORG_ROW = {
+ id: 'pkg_psa',
+ manifest_id: PKG,
+ display_name: 'Acme PSA',
+ description: 'A PSA.',
+ category: null,
+ icon_url: null,
+ visibility: 'org',
+ latest_version: '1.0.0',
+};
+
+vi.mock('../marketplaceApi', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ listMarketplacePackages: async () => ({ items: [] }),
+ listLocalInstalls: async () => [],
+ listOrgPackages: async () => ({ connected: true, items: [ORG_ROW] }),
+ listInstalledPackages: async () => ({ connected: true, items: [] }),
+}));
+
+import { ActiveOrganizationStorage } from '@object-ui/auth';
+import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../../runtime-config';
+import { MarketplacePage } from '../MarketplacePage';
+
+const INSTALL_PATH = '/api/v1/marketplace/install-local';
+const SERVER_MESSAGE = 'SERVER-ENGLISH sentinel: installed without a compatibility check.';
+const MANAGED = { manifestId: PKG, version: '1.0.0', versionId: 'ver_1', installedAt: '2026-11-05T00:00:00Z', upgradedFrom: null };
+
+const answer = (status: number, body: unknown) => ({
+ ok: status >= 200 && status < 300,
+ status,
+ statusText: '',
+ headers: new Headers({ 'content-type': 'application/json' }),
+ json: async () => body,
+ text: async () => JSON.stringify(body),
+});
+
+async function boot(installData: Record) {
+ resetRuntimeConfigForTesting();
+ ActiveOrganizationStorage.set('org_1');
+ vi.stubGlobal('fetch', vi.fn(async (input: unknown, init?: { method?: string }) => {
+ const path = new URL(String(input), 'http://console.test').pathname;
+ const method = (init?.method ?? 'GET').toUpperCase();
+ if (path === INSTALL_PATH && method === 'POST') return answer(200, { success: true, data: installData });
+ if (path === '/api/v1/runtime/config') {
+ return answer(200, {
+ cloudUrl: 'https://cloud.example', singleEnvironment: true, defaultEnvironmentId: 'env_1',
+ features: { installLocal: true, marketplace: true, aiStudio: true, autoPublishAiBuilds: true },
+ branding: { productName: 'ObjectOS', productShortName: 'ObjectOS' },
+ });
+ }
+ return answer(404, { success: false, error: { code: 'NOT_FOUND', message: `no route ${method} ${path}` } });
+ }));
+ await initRuntimeConfig();
+}
+
+async function installFromOrgCard() {
+ render();
+ const card = await screen.findByTestId(`org-card-${PKG}`);
+ fireEvent.click(within(card).getByRole('button', { name: '«marketplace.org.install»' }));
+}
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+ resetRuntimeConfigForTesting();
+});
+
+describe('the catalog page\'s org install draws protocolCheckSkipped (objectstack#30326)', () => {
+ it('installed, its range unchecked: the installed sentence, then the unchecked-protocol line', async () => {
+ await boot({
+ ...MANAGED, hotLoaded: true,
+ protocolCheckSkipped: { reason: 'no-range', packageId: PKG, protocolVersion: '18.1.0', message: SERVER_MESSAGE },
+ });
+ await installFromOrgCard();
+
+ const line = await screen.findByTestId('protocol-check-skipped');
+ expect(line).toHaveTextContent('«marketplace.install.protocolCheckSkipped.noRange|packageId=com.acme.psa|protocolVersion=18.1.0|requiredRange=»');
+ expect(screen.getByText('«marketplace.org.installed»')).toBeInTheDocument();
+ expect(screen.queryByText(SERVER_MESSAGE)).not.toBeInTheDocument();
+ });
+
+ it('CONTROL: an install whose range was checked draws no such line', async () => {
+ await boot({ ...MANAGED, hotLoaded: true });
+ await installFromOrgCard();
+
+ expect(await screen.findByText('«marketplace.org.installed»')).toBeInTheDocument();
+ expect(screen.queryByTestId('protocol-check-skipped')).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/protocolCheckSkipped-30326.test.ts b/ui/packages/app-shell/src/console/marketplace/__tests__/protocolCheckSkipped-30326.test.ts
new file mode 100644
index 00000000000..2ce7f5254c5
--- /dev/null
+++ b/ui/packages/app-shell/src/console/marketplace/__tests__/protocolCheckSkipped-30326.test.ts
@@ -0,0 +1,54 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * The install answer's `protocolCheckSkipped` (objectstack#30326), drawn from
+ * its facts: one key per `reason`, the facts interpolated; an unknown `reason`
+ * draws the server's own `message`; an answer without the field draws nothing.
+ * `t` echoes the key and the facts, so the claim is WHICH sentence and WHAT
+ * facts, not the wording.
+ */
+
+import { describe, expect, it } from 'vitest';
+import { protocolCheckSkippedText } from '../protocolCheckSkipped';
+
+const t = (key: string, o?: Record) =>
+ `«${key}|${['packageId', 'protocolVersion', 'requiredRange'].map((k) => `${k}=${String(o?.[k])}`).join('|')}»`;
+
+const SERVER_MESSAGE = 'SERVER-ENGLISH sentinel: installed without a compatibility check.';
+
+describe('protocolCheckSkippedText (objectstack#30326)', () => {
+ it('no-range: the package and the protocol it was installed under', () => {
+ expect(protocolCheckSkippedText({
+ manifestId: 'com.acme.psa',
+ protocolCheckSkipped: { reason: 'no-range', packageId: 'com.acme.psa', protocolVersion: '18.1.0', message: SERVER_MESSAGE },
+ }, t)).toBe('«marketplace.install.protocolCheckSkipped.noRange|packageId=com.acme.psa|protocolVersion=18.1.0|requiredRange=»');
+ });
+
+ it('unparsed-range: the range it declared, too', () => {
+ expect(protocolCheckSkippedText({
+ protocolCheckSkipped: {
+ reason: 'unparsed-range', packageId: 'com.acme.psa', protocolVersion: '18.1.0',
+ requiredRange: 'latest', rangeSource: 'engines.protocol', message: SERVER_MESSAGE,
+ },
+ }, t)).toBe('«marketplace.install.protocolCheckSkipped.unparsedRange|packageId=com.acme.psa|protocolVersion=18.1.0|requiredRange=latest»');
+ });
+
+ it('a reason this reader does not know: the server\'s message as given', () => {
+ expect(protocolCheckSkippedText({
+ protocolCheckSkipped: { reason: 'some-later-reason', packageId: 'com.acme.psa', protocolVersion: '18.1.0', message: SERVER_MESSAGE },
+ }, t)).toBe(SERVER_MESSAGE);
+ });
+
+ it('an unparsed-range without its range: the server\'s message, not a sentence with a hole', () => {
+ expect(protocolCheckSkippedText({
+ protocolCheckSkipped: { reason: 'unparsed-range', packageId: 'com.acme.psa', protocolVersion: '18.1.0', message: SERVER_MESSAGE },
+ }, t)).toBe(SERVER_MESSAGE);
+ });
+
+ it('CONTROL: a checked install (no field), an empty answer and a notice with nothing readable draw nothing', () => {
+ expect(protocolCheckSkippedText({ manifestId: 'com.acme.psa', hotLoaded: true }, t)).toBeNull();
+ expect(protocolCheckSkippedText(null, t)).toBeNull();
+ expect(protocolCheckSkippedText(undefined, t)).toBeNull();
+ expect(protocolCheckSkippedText({ protocolCheckSkipped: { reason: 'later' } }, t)).toBeNull();
+ });
+});
diff --git a/ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts b/ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts
index f90f05c1e7b..157410caad6 100644
--- a/ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts
+++ b/ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts
@@ -147,6 +147,7 @@ export type { PluginRuntime } from '@objectstack/spec/kernel';
import type { MetadataLock, PluginPermissions, PluginRuntime } from '@objectstack/spec/kernel';
import { isPlainRecord } from '@objectstack/spec/data';
+import type { ProtocolCheckSkipped } from './protocolCheckSkipped.js';
export interface MarketplacePackageVersion {
id: string;
@@ -770,6 +771,12 @@ export interface LocalManagedInstallResult {
* as under a walled tenancy posture, which this sentence names the remedy for.
*/
credentialNote?: string;
+ /**
+ * Present only when the install door's protocol handshake admitted the
+ * package WITHOUT checking its range (objectstack#30326): it declares no
+ * `engines.protocol`, or one the runtime cannot read. Omitted when checked.
+ */
+ protocolCheckSkipped?: ProtocolCheckSkipped;
}
/**
@@ -806,6 +813,8 @@ export interface LocalUnmanagedInstallResult {
copied: Array<{ type: string; name: string }>;
strippedLocks: LocalInstallStrippedLock[];
note?: string;
+ /** As on the managed answer ({@link LocalManagedInstallResult.protocolCheckSkipped}). */
+ protocolCheckSkipped?: ProtocolCheckSkipped;
}
export type LocalInstallResult = LocalManagedInstallResult | LocalUnmanagedInstallResult;
diff --git a/ui/packages/app-shell/src/console/marketplace/protocolCheckSkipped.ts b/ui/packages/app-shell/src/console/marketplace/protocolCheckSkipped.ts
new file mode 100644
index 00000000000..1700af14036
--- /dev/null
+++ b/ui/packages/app-shell/src/console/marketplace/protocolCheckSkipped.ts
@@ -0,0 +1,74 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * The install answer's `data.protocolCheckSkipped` (objectstack#30326), in the
+ * reader's locale: the package IS installed, but the ADR-0087 D1 handshake
+ * admitted it without checking its platform protocol range, so it may not work
+ * on this platform version.
+ *
+ * `POST /api/v1/marketplace/install-local` carries it on a success answer only
+ * then (managed, unmanaged and paired installs alike), omitted when the range
+ * was checked:
+ *
+ * { reason: 'no-range', packageId, protocolVersion, message }
+ * { reason: 'unparsed-range', packageId, protocolVersion, requiredRange, rangeSource, message }
+ *
+ * `message` is the server's English sentence. The Console draws its own
+ * sentence from the facts instead, one key per `reason`; a `reason` this
+ * reader does not know draws the server's `message` as given, and an answer
+ * without the field draws nothing.
+ */
+
+import { useCallback } from 'react';
+import { useObjectTranslation } from '@object-ui/i18n';
+
+/** The two verdicts the handshake admits a package unchecked under. */
+export type ProtocolCheckSkippedReason = 'no-range' | 'unparsed-range';
+
+/** `data.protocolCheckSkipped`, as the install door answers it. */
+export type ProtocolCheckSkipped =
+ | { reason: 'no-range'; packageId: string; protocolVersion: string; message: string }
+ | {
+ reason: 'unparsed-range';
+ packageId: string;
+ protocolVersion: string;
+ requiredRange: string;
+ rangeSource: string;
+ message: string;
+ };
+
+const LINE_KEY: Record = {
+ 'no-range': 'marketplace.install.protocolCheckSkipped.noRange',
+ 'unparsed-range': 'marketplace.install.protocolCheckSkipped.unparsedRange',
+};
+
+const isText = (v: unknown): v is string => typeof v === 'string' && v !== '';
+
+type Translate = (key: string, options?: Record) => string;
+
+/**
+ * The notice's sentence for an install answer, or `null` when the answer
+ * carries none (the range was checked, or an older runtime answered).
+ */
+export function protocolCheckSkippedText(answer: unknown, t: Translate): string | null {
+ const notice = (answer as { protocolCheckSkipped?: unknown } | null | undefined)?.protocolCheckSkipped;
+ if (!notice || typeof notice !== 'object') return null;
+ const n = notice as Record;
+ const known = typeof n.reason === 'string' && n.reason in LINE_KEY
+ && isText(n.packageId) && isText(n.protocolVersion)
+ && (n.reason !== 'unparsed-range' || isText(n.requiredRange));
+ if (known) {
+ return t(LINE_KEY[n.reason as ProtocolCheckSkippedReason], {
+ packageId: n.packageId,
+ protocolVersion: n.protocolVersion,
+ requiredRange: isText(n.requiredRange) ? n.requiredRange : '',
+ });
+ }
+ return isText(n.message) ? n.message : null;
+}
+
+/** {@link protocolCheckSkippedText} in the current locale. */
+export function useProtocolCheckSkippedText(): (answer: unknown) => string | null {
+ const { t } = useObjectTranslation();
+ return useCallback((answer: unknown) => protocolCheckSkippedText(answer, t as Translate), [t]);
+}
diff --git a/ui/packages/app-shell/src/views/metadata-admin/PackagesPage.importProtocolCheckSkipped-30326.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/PackagesPage.importProtocolCheckSkipped-30326.test.tsx
new file mode 100644
index 00000000000..d3d58b1b069
--- /dev/null
+++ b/ui/packages/app-shell/src/views/metadata-admin/PackagesPage.importProtocolCheckSkipped-30326.test.tsx
@@ -0,0 +1,101 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Packages → Import says when the install door admitted the imported package
+ * WITHOUT checking its platform protocol range: the answer's
+ * `data.protocolCheckSkipped` (objectstack#30326), drawn under the success
+ * sentence. The REAL locale packs are mounted (`I18nProvider`), so the zh case
+ * asserts the sentence an administrator reads, not a key.
+ */
+
+import '@testing-library/jest-dom/vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { I18nProvider } from '@object-ui/i18n';
+
+vi.mock('@object-ui/auth', async (importOriginal) => ({
+ ...(await importOriginal>()),
+ useWorkspaceAdminStatus: () => ({ isAdmin: true, isResolved: true }),
+}));
+
+import { PackagesPage } from './PackagesPage';
+import { initRuntimeConfig, resetRuntimeConfigForTesting } from '../../runtime-config';
+
+const reply = (status: number, body: unknown) =>
+ new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
+
+const SERVER_MESSAGE = 'SERVER-ENGLISH sentinel: installed without a compatibility check.';
+const PSA = { id: 'com.acme.psa', name: 'PSA', version: '1.0.0', type: 'app' };
+const MANAGED = { manifestId: 'com.acme.psa', version: '1.0.0', versionId: 'ver_1', installedAt: '2026-11-05T00:00:00Z', hotLoaded: true, upgradedFrom: null };
+
+async function serve(installData: Record) {
+ resetRuntimeConfigForTesting();
+ vi.stubGlobal('fetch', vi.fn(async () => reply(200, { features: { installLocal: true } })));
+ await initRuntimeConfig();
+ vi.stubGlobal('fetch', vi.fn(async (input: unknown, init?: { method?: string }) => {
+ const path = new URL(String(input), 'http://console.test').pathname;
+ const method = (init?.method ?? 'GET').toUpperCase();
+ if (path === '/api/v1/packages') return reply(200, { success: true, data: { packages: [] } });
+ if (path === '/api/v1/cloud-connection/installed') return reply(404, {});
+ if (path === '/api/v1/marketplace/install-local' && method === 'GET') return reply(200, { success: true, data: { items: [] } });
+ if (path === '/api/v1/marketplace/install-local' && method === 'POST') return reply(200, { success: true, data: installData });
+ return reply(404, { success: false, error: { code: 'NOT_FOUND', message: `no route ${method} ${path}` } });
+ }));
+}
+
+async function importFile(lang: 'en' | 'zh') {
+ render(
+
+
+
+
+ ,
+ );
+ const input = await vi.waitFor(() => {
+ const el = document.querySelector('input[type="file"]');
+ if (!el) throw new Error('no import input yet');
+ return el as HTMLInputElement;
+ });
+ const file = new File([JSON.stringify(PSA)], 'psa.json', { type: 'application/json' });
+ fireEvent.change(input, { target: { files: [file] } });
+}
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+ resetRuntimeConfigForTesting();
+});
+
+describe('Packages → Import draws protocolCheckSkipped (objectstack#30326)', () => {
+ it('zh: no range — the package, the protocol it was installed under, in place of the server\'s English', async () => {
+ await serve({ ...MANAGED, protocolCheckSkipped: { reason: 'no-range', packageId: 'com.acme.psa', protocolVersion: '18.1.0', message: SERVER_MESSAGE } });
+ await importFile('zh');
+
+ const line = await screen.findByTestId('protocol-check-skipped');
+ expect(line).toHaveTextContent(/^com\.acme\.psa 未声明它支持的平台协议版本,因此已在平台协议 18\.1\.0 下安装,未做兼容性检查/);
+ expect(screen.queryByText(SERVER_MESSAGE)).not.toBeInTheDocument();
+ });
+
+ it('en: an unreadable range — the range it declared', async () => {
+ await serve({
+ ...MANAGED,
+ protocolCheckSkipped: {
+ reason: 'unparsed-range', packageId: 'com.acme.psa', protocolVersion: '18.1.0',
+ requiredRange: 'latest', rangeSource: 'engines.protocol', message: SERVER_MESSAGE,
+ },
+ });
+ await importFile('en');
+
+ const line = await screen.findByTestId('protocol-check-skipped');
+ expect(line).toHaveTextContent(/^com\.acme\.psa states its platform protocol as latest, which this runtime cannot read, so it was installed under protocol 18\.1\.0 without a compatibility check/);
+ });
+
+ it('CONTROL: an import whose range was checked draws no such line', async () => {
+ await serve({ ...MANAGED });
+ await importFile('en');
+
+ await screen.findByText(/com\.acme\.psa/);
+ expect(screen.queryByTestId('protocol-check-skipped')).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx b/ui/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx
index 9e267095a7c..617d1078962 100644
--- a/ui/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx
+++ b/ui/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx
@@ -90,6 +90,8 @@ import { getRuntimeConfig } from '../../runtime-config.js';
import { readMarketplaceInstalls, type LocalInstallEntry } from '../../console/marketplace/marketplaceApi.js';
import { MarketplaceNotLoaded, MarketplaceRowActions } from '../../console/marketplace/MarketplaceRowActions.js';
import { useDependencyMissingText } from '../../console/marketplace/dependencyMissing.js';
+import { useProtocolCheckSkippedText } from '../../console/marketplace/protocolCheckSkipped.js';
+import { ProtocolCheckSkippedNote } from '../../console/marketplace/ProtocolCheckSkippedNote.js';
import type { InstalledPackageAtEitherStage } from '@objectstack/spec/api-assembled';
import type { PackagePublishResult } from '@objectstack/spec/system';
import type { ReassignOrphanedMetadataResponse } from '@objectstack/spec/api';
@@ -1376,6 +1378,7 @@ export function PackagesPage() {
// ADR-0141 D2 — the import door's `DEPENDENCY_MISSING` refusal, one line per
// unmet dependency, from the locale packs (the `engine.*` table is en/zh only).
const dependencyMissingText = useDependencyMissingText();
+ const protocolCheckSkippedText = useProtocolCheckSkippedText();
const { pathname } = useLocation();
// App base = path up to (and excluding) `/component/...`, so links to
// `/apps/:app/metadata/...` work regardless of nesting.
@@ -1394,7 +1397,9 @@ export function PackagesPage() {
const [detailOpen, setDetailOpen] = React.useState(false);
const fileRef = React.useRef(null);
const [importing, setImporting] = React.useState(false);
- const [importMsg, setImportMsg] = React.useState<{ kind: 'ok' | 'err'; text: string } | null>(
+ // `protocolNote`: the import answer's `protocolCheckSkipped`, drawn
+ // (objectstack#30326) — imported, its platform protocol range unchecked.
+ const [importMsg, setImportMsg] = React.useState<{ kind: 'ok' | 'err'; text: string; protocolNote?: string } | null>(
null,
);
// objectui#12675 — every row with its Source; objectui#12790 — that Source is
@@ -1560,9 +1565,11 @@ export function PackagesPage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ manifest }),
});
+ const protocolNote = protocolCheckSkippedText(res);
setImportMsg({
kind: 'ok',
text: tFormat('engine.packages.import.success', locale, { id: res?.manifestId ?? manifest.id }),
+ ...(protocolNote ? { protocolNote } : {}),
});
await load();
} catch (err: any) {
@@ -1663,6 +1670,7 @@ export function PackagesPage() {
}`}
>
{importMsg.text}
+ {importMsg.protocolNote ? : null}
)}
diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts
index 92d81058632..b101bdee258 100644
--- a/ui/packages/i18n/src/locales/ar.ts
+++ b/ui/packages/i18n/src/locales/ar.ts
@@ -4560,6 +4560,10 @@ const ar = {
unreadableVersion: "ثبّت إصدارًا نهائيًا من {{dependency}} ضمن {{range}}. الإصدار المثبّت {{installedVersion}} ليس بصيغة MAJOR.MINOR.PATCH.",
unreadableDeclaration: "تعلن {{packageId}} عن تبعياتها بصيغة لا يستطيع التثبيت قراءتها. اكتبها كخريطة من معرّف الحزمة إلى نطاق الإصدارات ثم أعد نشر الحزمة.",
},
+ protocolCheckSkipped: {
+ noRange: "لا تحدد الحزمة {{packageId}} بروتوكول المنصة الذي تدعمه، لذلك ثُبّتت تحت البروتوكول {{protocolVersion}} دون فحص التوافق وقد لا تعمل على إصدار المنصة هذا. يُفحص عند التثبيت أي إصدار من الحزمة يصرّح بالبروتوكول الذي يدعمه (engines.protocol).",
+ unparsedRange: "تصرّح الحزمة {{packageId}} ببروتوكول المنصة على أنه {{requiredRange}}، وهو ما لا تستطيع بيئة التشغيل هذه قراءته، لذلك ثُبّتت تحت البروتوكول {{protocolVersion}} دون فحص التوافق وقد لا تعمل على إصدار المنصة هذا. يُفحص عند التثبيت أي إصدار من الحزمة يصرّح بنطاق مقروء (engines.protocol).",
+ },
localUnauthorized: "سجل الدخول إلى هذا الوقت التشغيل أولاً.",
localMarketplaceUnavailable: "هذا الوقت التشغيل لا يحتوي على OS_CLOUD_URL مُهيأ.",
updateTo: "تحديث → v{{version}}",
diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts
index f4845ac1cb9..31cac4b9fe8 100644
--- a/ui/packages/i18n/src/locales/de.ts
+++ b/ui/packages/i18n/src/locales/de.ts
@@ -4215,6 +4215,10 @@ const de = {
unreadableVersion: "Installieren Sie eine Release-Version von {{dependency}} innerhalb von {{range}}. Die installierte Version {{installedVersion}} hat nicht das Format MAJOR.MINOR.PATCH.",
unreadableDeclaration: "{{packageId}} deklariert seine Abhängigkeiten in einer Form, die die Installation nicht lesen kann. Schreiben Sie sie als Zuordnung von Paket-ID zu Versionsbereich und veröffentlichen Sie das Paket erneut.",
},
+ protocolCheckSkipped: {
+ noRange: "{{packageId}} gibt nicht an, welches Plattformprotokoll es unterstützt. Es wurde daher ohne Kompatibilitätsprüfung unter Protokoll {{protocolVersion}} installiert und funktioniert auf dieser Plattformversion möglicherweise nicht. Ein Build des Pakets, der sein unterstütztes Protokoll (engines.protocol) angibt, wird bei der Installation geprüft.",
+ unparsedRange: "{{packageId}} gibt sein Plattformprotokoll als {{requiredRange}} an, was diese Laufzeitumgebung nicht lesen kann. Es wurde daher ohne Kompatibilitätsprüfung unter Protokoll {{protocolVersion}} installiert und funktioniert auf dieser Plattformversion möglicherweise nicht. Ein Build des Pakets mit lesbarem Bereich (engines.protocol) wird bei der Installation geprüft.",
+ },
localUnauthorized: "Melden Sie sich zuerst in diesem Laufzeitsystem an.",
localMarketplaceUnavailable: "Dieses Laufzeitsystem hat keine OS_CLOUD_URL konfiguriert.",
updateTo: "Aktualisieren → v{{version}}",
diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts
index ca7b46ceacd..9aa1e4fc592 100644
--- a/ui/packages/i18n/src/locales/en.ts
+++ b/ui/packages/i18n/src/locales/en.ts
@@ -5016,6 +5016,11 @@ const en = {
unreadableVersion: 'Install a release version of {{dependency}} inside {{range}}. The installed version {{installedVersion}} is not MAJOR.MINOR.PATCH.',
unreadableDeclaration: '{{packageId}} declares its dependencies in a shape install cannot read. Write them as a map from package ID to version range and republish the package.',
},
+ // objectstack#30326 — the install answer's data.protocolCheckSkipped: installed, the protocol range unchecked.
+ protocolCheckSkipped: {
+ noRange: '{{packageId}} does not state which platform protocol it supports, so it was installed under protocol {{protocolVersion}} without a compatibility check and may not work on this platform version. A build of the package that declares the protocol it supports (engines.protocol) is checked at install.',
+ unparsedRange: '{{packageId}} states its platform protocol as {{requiredRange}}, which this runtime cannot read, so it was installed under protocol {{protocolVersion}} without a compatibility check and may not work on this platform version. A build of the package that declares a range this runtime reads (engines.protocol) is checked at install.',
+ },
localUnauthorized: 'Sign in to this runtime first, then try again.',
localMarketplaceUnavailable: 'This runtime has no OS_CLOUD_URL configured, so the marketplace catalog is unreachable.',
updateTo: 'Update → v{{version}}',
diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts
index 74f57d0b20a..b843d2d494c 100644
--- a/ui/packages/i18n/src/locales/es.ts
+++ b/ui/packages/i18n/src/locales/es.ts
@@ -4302,6 +4302,10 @@ const es = {
unreadableVersion: "Instale una versión publicada de {{dependency}} dentro de {{range}}. La versión instalada {{installedVersion}} no tiene el formato MAJOR.MINOR.PATCH.",
unreadableDeclaration: "{{packageId}} declara sus dependencias en una forma que la instalación no puede leer. Escríbalas como un mapa de ID de paquete a rango de versiones y vuelva a publicar el paquete.",
},
+ protocolCheckSkipped: {
+ noRange: "{{packageId}} no indica qué protocolo de plataforma admite, por lo que se instaló con el protocolo {{protocolVersion}} sin comprobar la compatibilidad y es posible que no funcione en esta versión de la plataforma. Una compilación del paquete que declare el protocolo que admite (engines.protocol) se comprueba al instalarla.",
+ unparsedRange: "{{packageId}} declara su protocolo de plataforma como {{requiredRange}}, que este entorno de ejecución no puede leer, por lo que se instaló con el protocolo {{protocolVersion}} sin comprobar la compatibilidad y es posible que no funcione en esta versión de la plataforma. Una compilación del paquete que declare un rango legible (engines.protocol) se comprueba al instalarla.",
+ },
localUnauthorized: "Primero inicie sesión en este runtime.",
localMarketplaceUnavailable: "Este runtime no tiene configurado OS_CLOUD_URL.",
updateTo: "Actualizar → v{{version}}",
diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts
index 91e95b0b8b3..1975d3616f1 100644
--- a/ui/packages/i18n/src/locales/fr.ts
+++ b/ui/packages/i18n/src/locales/fr.ts
@@ -4298,6 +4298,10 @@ const fr = {
unreadableVersion: "Installez une version publiée de {{dependency}} comprise dans {{range}}. La version installée {{installedVersion}} n’est pas au format MAJOR.MINOR.PATCH.",
unreadableDeclaration: "{{packageId}} déclare ses dépendances sous une forme que l’installation ne peut pas lire. Écrivez-les sous forme de correspondance entre ID de paquet et plage de versions, puis republiez le paquet.",
},
+ protocolCheckSkipped: {
+ noRange: "{{packageId}} n’indique pas quel protocole de plateforme il prend en charge : il a donc été installé sous le protocole {{protocolVersion}} sans vérification de compatibilité et risque de ne pas fonctionner sur cette version de la plateforme. Une version du paquet qui déclare le protocole pris en charge (engines.protocol) est vérifiée à l’installation.",
+ unparsedRange: "{{packageId}} déclare son protocole de plateforme comme {{requiredRange}}, que ce runtime ne sait pas lire : il a donc été installé sous le protocole {{protocolVersion}} sans vérification de compatibilité et risque de ne pas fonctionner sur cette version de la plateforme. Une version du paquet qui déclare une plage lisible (engines.protocol) est vérifiée à l’installation.",
+ },
localUnauthorized: "Connectez-vous d'abord à ce runtime.",
localMarketplaceUnavailable: "Ce runtime n'a pas de OS_CLOUD_URL configuré.",
updateTo: "Mettre à jour → v{{version}}",
diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts
index fd4368b2392..f54f97f29d5 100644
--- a/ui/packages/i18n/src/locales/ja.ts
+++ b/ui/packages/i18n/src/locales/ja.ts
@@ -4217,6 +4217,10 @@ const ja = {
unreadableVersion: "{{range}} の範囲内にある {{dependency}} のリリース版をインストールしてください(インストール済みの {{installedVersion}} は MAJOR.MINOR.PATCH 形式ではありません)。",
unreadableDeclaration: "{{packageId}} の依存関係の宣言を読み取れません。パッケージ ID からバージョン範囲へのマップとして記述し、パッケージを再公開してください。",
},
+ protocolCheckSkipped: {
+ noRange: "{{packageId}} は対応するプラットフォームプロトコルを宣言していないため、互換性チェックなしでプロトコル {{protocolVersion}} の下にインストールされました。このプラットフォームバージョンでは正しく動作しない可能性があります。対応プロトコル(engines.protocol)を宣言したパッケージのビルドはインストール時にチェックされます。",
+ unparsedRange: "{{packageId}} はプラットフォームプロトコルを {{requiredRange}} と宣言していますが、このランタイムでは解釈できないため、互換性チェックなしでプロトコル {{protocolVersion}} の下にインストールされました。このプラットフォームバージョンでは正しく動作しない可能性があります。解釈可能な範囲(engines.protocol)を宣言したパッケージのビルドはインストール時にチェックされます。",
+ },
localUnauthorized: "最初にこのランタイムにサインインしてから再試行してください。",
localMarketplaceUnavailable: "このランタイムにはOS_CLOUD_URLが設定されていないため、マーケットプレイスカタログにアクセスできません。",
updateTo: "更新 → v{{version}}",
diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts
index 9ead767b6d9..39bca83fc22 100644
--- a/ui/packages/i18n/src/locales/ko.ts
+++ b/ui/packages/i18n/src/locales/ko.ts
@@ -4214,6 +4214,10 @@ const ko = {
unreadableVersion: "{{range}} 범위 안의 {{dependency}} 정식 버전을 설치하세요. 설치된 버전 {{installedVersion}}은(는) MAJOR.MINOR.PATCH 형식이 아닙니다.",
unreadableDeclaration: "{{packageId}}의 종속성 선언 형식을 읽을 수 없습니다. 패키지 ID에서 버전 범위로의 맵으로 작성한 뒤 패키지를 다시 게시하세요.",
},
+ protocolCheckSkipped: {
+ noRange: "{{packageId}}은(는) 지원하는 플랫폼 프로토콜을 명시하지 않아 호환성 검사 없이 프로토콜 {{protocolVersion}}에서 설치되었으며, 이 플랫폼 버전에서 작동하지 않을 수 있습니다. 지원 프로토콜(engines.protocol)을 명시한 패키지 빌드는 설치 시 검사됩니다.",
+ unparsedRange: "{{packageId}}은(는) 플랫폼 프로토콜을 {{requiredRange}}(으)로 명시했지만 이 런타임이 읽을 수 없어 호환성 검사 없이 프로토콜 {{protocolVersion}}에서 설치되었으며, 이 플랫폼 버전에서 작동하지 않을 수 있습니다. 읽을 수 있는 범위(engines.protocol)를 명시한 패키지 빌드는 설치 시 검사됩니다.",
+ },
localUnauthorized: "먼저 이 런타임에 로그인하세요.",
localMarketplaceUnavailable: "이 런타임에 OS_CLOUD_URL이 구성되지 않았습니다.",
updateTo: "업데이트 → v{{version}}",
diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts
index ad1beabee58..a07a63391e5 100644
--- a/ui/packages/i18n/src/locales/pt.ts
+++ b/ui/packages/i18n/src/locales/pt.ts
@@ -4297,6 +4297,10 @@ const pt = {
unreadableVersion: "Instale uma versão publicada de {{dependency}} dentro de {{range}}. A versão instalada {{installedVersion}} não está no formato MAJOR.MINOR.PATCH.",
unreadableDeclaration: "{{packageId}} declara as suas dependências num formato que a instalação não consegue ler. Escreva-as como um mapa de ID de pacote para intervalo de versões e publique o pacote novamente.",
},
+ protocolCheckSkipped: {
+ noRange: "{{packageId}} não indica qual protocolo de plataforma suporta, por isso foi instalado sob o protocolo {{protocolVersion}} sem verificação de compatibilidade e pode não funcionar nesta versão da plataforma. Uma compilação do pacote que declara o protocolo suportado (engines.protocol) é verificada na instalação.",
+ unparsedRange: "{{packageId}} declara o seu protocolo de plataforma como {{requiredRange}}, que este runtime não consegue ler, por isso foi instalado sob o protocolo {{protocolVersion}} sem verificação de compatibilidade e pode não funcionar nesta versão da plataforma. Uma compilação do pacote que declara um intervalo legível (engines.protocol) é verificada na instalação.",
+ },
localUnauthorized: "Faça login neste runtime primeiro.",
localMarketplaceUnavailable: "Este runtime não tem OS_CLOUD_URL configurado.",
updateTo: "Atualizar → v{{version}}",
diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts
index 0d209989bfc..3df72e0b7b2 100644
--- a/ui/packages/i18n/src/locales/ru.ts
+++ b/ui/packages/i18n/src/locales/ru.ts
@@ -4398,6 +4398,10 @@ const ru = {
unreadableVersion: "Установите выпускную версию {{dependency}} в пределах {{range}}. Установленная версия {{installedVersion}} не в формате MAJOR.MINOR.PATCH.",
unreadableDeclaration: "{{packageId}} объявляет зависимости в форме, которую установка не может прочитать. Запишите их как сопоставление ID пакета с диапазоном версий и опубликуйте пакет повторно.",
},
+ protocolCheckSkipped: {
+ noRange: "{{packageId}} не указывает, какой протокол платформы он поддерживает, поэтому он установлен под протоколом {{protocolVersion}} без проверки совместимости и может не работать в этой версии платформы. Сборка пакета, в которой указан поддерживаемый протокол (engines.protocol), проверяется при установке.",
+ unparsedRange: "{{packageId}} указывает протокол платформы как {{requiredRange}}, но эта среда выполнения не может его прочитать, поэтому пакет установлен под протоколом {{protocolVersion}} без проверки совместимости и может не работать в этой версии платформы. Сборка пакета с читаемым диапазоном (engines.protocol) проверяется при установке.",
+ },
localUnauthorized: "Сначала войдите в этот рантайм.",
localMarketplaceUnavailable: "OS_CLOUD_URL не настроен.",
updateTo: "Обновить → v{{version}}",
diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts
index 4f4cba69104..c130205876f 100644
--- a/ui/packages/i18n/src/locales/zh.ts
+++ b/ui/packages/i18n/src/locales/zh.ts
@@ -4402,6 +4402,11 @@ const zh = {
unreadableVersion: '请安装 {{dependency}} 在 {{range}} 范围内的正式版本(当前安装的 {{installedVersion}} 不是“主版本.次版本.修订号”格式)。',
unreadableDeclaration: '{{packageId}} 的依赖声明格式无法识别,请写成“软件包 ID → 版本范围”的映射后重新发布软件包。',
},
+ // objectstack#30326 — the install answer's data.protocolCheckSkipped: installed, the protocol range unchecked.
+ protocolCheckSkipped: {
+ noRange: '{{packageId}} 未声明它支持的平台协议版本,因此已在平台协议 {{protocolVersion}} 下安装,未做兼容性检查,可能无法在当前平台版本上正常运行。声明了所支持协议(engines.protocol)的软件包版本会在安装时接受检查。',
+ unparsedRange: '{{packageId}} 声明的平台协议为 {{requiredRange}},本运行时无法识别,因此已在平台协议 {{protocolVersion}} 下安装,未做兼容性检查,可能无法在当前平台版本上正常运行。声明了可识别协议范围(engines.protocol)的软件包版本会在安装时接受检查。',
+ },
localUnauthorized: '请先登录本运行时再试。',
localMarketplaceUnavailable: '本运行时未配置 OS_CLOUD_URL,无法访问应用市场目录。',
updateTo: '更新 → v{{version}}',