---
url: https://nsh.dev/ru/docs/sandbox-and-capabilities
description: "Ограничения, в которых работает каждое приложение NSH (память, время, узлы), усиление защиты языка в релизных сборках, а также capabilities, которые объявляются в nsh.json и предоставляются хост-приложениями."
---

# Песочница и capabilities

Приложение на Rust — это нативный код, скомпилированный в поставляющее его приложение так же, как и остальная его часть, и он работает с его уровнем доверия. Его вызовы сервисов NSH (`fetch`, изображения, веб-представления, строка состояния) проходят те же нативные проверки, что и вызовы из TSX, в рамках того, что разрешает хост. TSX и JavaScript, которые выполняет приложение, собственные или из загружаемого им [подписанного пакета](https://nsh.dev/translations/ru/docs/signed-packages.md), работают в виртуальной машине QuickJS на каждое представление, а всё, что они могут делать за пределами этой VM, проходит через нативный рантайм NSH. Благодаря этому можно запускать код, который вы не писали, например сторонние мини-приложения или загружаемые обновления, по правилам, которые контролирует хост-приложение:

- **Лимиты** ограничивают память, время и нативные ресурсы, доступные одному приложению.
- **Усиление защиты** в релизных сборках убирает все способы превратить строки в код.
- **Capabilities** перечисляют, до чего приложение может дотянуться: с каких хостов загружать данные, какие страницы может показывать веб-представление, может ли оно менять строку состояния. Приложение объявляет их сами; хост-приложение может выдать меньше.

Бандл содержит только код приложения: реактивный граф, рендерер и каждый импортируемый приложением модуль нативные. Поэтому ничто в JS не является доверенным, а каждый лимит и каждая проверка применяются в Rust; в JS нет кода, который приложение могло бы подменить, чтобы обойти их.

<a id="limits"></a>

## Лимиты

Лимиты ограничивают VM: они действуют для TSX и JavaScript. Приложение, встроенное в другое приложение, работает в пределах лимитов, которые определяет его хост. Автономное приложение (упакованное с помощью `nsh package`, включая веб, или запущенное с помощью `nsh run`) — это ваш собственный код, как и нативное приложение: оно использует память и время, которые предоставляет устройство, а стек интерпретатора в 4096 КБ превращает глубокую рекурсию в `RangeError`, которую приложение может перехватить.

У встроенного приложения действуют следующие лимиты:

| Лимит | По умолчанию | Что происходит при достижении |
|---|---|---|
| Куча JS | 256 МБ | VM останавливается. |
| Стек интерпретатора | 4096 КБ (это и максимум) | Глубокая рекурсия выбрасывает `RangeError`, которую приложение может перехватить. |
| Время вычисления бандла | 10 с | Вычисление прерывается. |
| Время любого другого вызова в JS | 2 с | Вызов прерывается; приложение продолжает работу. |
| Хранимые узлы | 200 000 | VM останавливается. |
| Изменения за один раз (операции в очереди) | 8 000 000 значений | VM останавливается. |
| Операции в одной отрисовке canvas | 1 000 000 | VM останавливается. |

**Время ограничивается на каждый вызов, а не на приложение.** «Вызов» — это одно действие, которое рантайм просит выполнить JS: обработать одно событие (включая вызванный им рендеринг), привязать одну строку списка, выполнить один таймер, выполнить колбэки `requestAnimationFrame` одного кадра, опустошить очередь микрозадач. Долго работающее приложение никогда не накапливает время в счёт лимита; прерывается только отдельный вызов, не вернувшийся вовремя. Вызовы, вложенные в другой вызов, разделяют бюджет внешнего.

Прерванный вызов завершается исключением, которое не может поглотить ни один `catch`, а об ошибке сообщается с указанием превышенного бюджета:

```text
the script was interrupted: a call into JS exceeded its budget of 2000 ms
```

VM продолжает работать: следующее событие, таймер или кадр выполняются как обычно.

**Нехватка памяти или превышение квоты останавливает VM.** Об ошибке сообщается один раз (обработчику ошибок хост-приложения, а в сборках для разработки ещё и поверх приложения), после этого JS не выполняется, а представления остаются в прежнем состоянии. Квоты на узлы и операции существуют потому, что нативные представления и очередь изменений живут в нативной памяти, на которую лимит кучи JS не распространяется: без них приложение могло бы исчерпать память устройства через UI.

> [!TIP]
> Эти лимиты щедры для обычных приложений. Если вы упираетесь в бюджет вызова в 2 с, работа в любом случае слишком длинна для одного события: она заморозила бы обновления приложения на это время. Разбейте её с помощью `setTimeout` или вынесите из обработчика события.

Хост-приложения задают лимиты для каждого представления: `NshAppView.limits` на [iOS](https://nsh.dev/translations/ru/docs/ios.md#limits), `NshView.setLimits` на [Android](https://nsh.dev/translations/ru/docs/android.md#limits), `AppOptions::limits` / `AppView::set_limits` в [приложении GPUI](https://nsh.dev/translations/ru/docs/gpui.md#embedding-in-a-gpui-app). Они применяются со следующей загрузки.

Чтобы увидеть в разработке, как TSX-приложение работает в рамках лимитов хост-приложения, запустите его с [`--limits`](https://nsh.dev/translations/ru/docs/cli.md#nsh-run): `nsh run App.tsx --limits` использует значения по умолчанию, указанные выше, а `nsh run --limits memory=128MB,call=500ms` задаёт эти два лимита так, как это делало бы хост-приложение, остальные при этом сохраняют значения по умолчанию.

<a id="hardening"></a>

## Усиление защиты

Релизные сборки (всё, что не загружается с dev-сервера) усиливаются при загрузке:

- `eval` удаляется, а конструкторы `Function`, `AsyncFunction`, `GeneratorFunction` и `AsyncGeneratorFunction` выбрасывают исключение. NSH не выполняет код, созданный из строк:

  ```text
  TypeError: the Function constructor is disabled: nsh runs no code from strings (development builds allow it)
  ```

- Прототипы, общие для всех модулей (`Object`, `Array`, `Function`, `String`, `Number`, `Promise`, `Map`, типизированные массивы и остальные), заморожены, поэтому один модуль (или одна библиотека) не может изменить `Array.prototype` для всех остальных. `Error.prototype` остаётся доступным для записи, чтобы подклассы ошибок могли задавать `this.name`.

Заморозка прототипов обычно ломает распространённый паттерн, «override mistake»: присваивание `Sub.prototype.constructor = Sub` или `obj.toString = …` выбрасывает исключение, если унаследованное свойство заморожено. NSH превращает наиболее часто переопределяемые члены (`constructor`, `toString`, `valueOf`, `toJSON`, ...) в аксессоры, сеттер которых определяет свойство на объекте, которому присваивают, поэтому такой код (и библиотеки вроде axios, которые его используют) продолжает работать.

Бандлы, которые отдают `nsh dev` / `nsh run` (с `--release` или без него), не усиливаются, поэтому их можно отлаживать с помощью `eval`. Проверяйте поведение релиза с помощью `nsh test` (он включает усиление) или упакованной сборки, прежде чем поставлять код, зависящий от динамического кода.

## Capabilities

Приложение объявляет, до чего оно может дотянуться за пределами своей VM, в `nsh.json`:

```json
{
  "name": "Quotes",
  "id": "com.example.quotes",
  "version": "1.0.0",
  "build": 3,
  "capabilities": {
    "network": {
      "hosts": ["api.example.com", "*.cdn.example.com"],
      "http": [{ "host": "quotes.example.com", "methods": ["GET"], "pathPrefix": "/v1/" }],
      "sockets": ["feed.example.com:7000"]
    },
    "webview": { "hosts": ["*.example.com"], "input": true },
    "statusBar": true,
    "clipboard": true,
    "picker": true,
    "speech": true,
    "host": ["getUserInfo", "bridge.*"],
    "openUrl": { "schemes": ["https", "mailto"] }
  }
}
```

**Приложению без `"capabilities"` разрешено всё**: автономное приложение принадлежит его разработчику, и ничего не ограничивается. Сырые TCP-сокеты выдаются по имени в каждом приложении через `network.sockets`, а функции хоста — через `host`. **Приложению с `"capabilities"` разрешено только то, что в нём перечислено**: всё остальное запрещено. Неизвестные ключи — это ошибки сборки, поэтому опечатка обнаруживается при сборке, а не превращается в отсутствующее разрешение во время выполнения. [Подписанный пакет](https://nsh.dev/translations/ru/docs/signed-packages.md#packages-from-newer-versions-of-nsh) от более новой версии NSH может объявлять capabilities, которых более старый рантайм не знает: в нём они не выдаются.

### `network`

Охватывает `fetch` (включая каждый редирект), WebSocket, удалённые изображения (`<Image>`, `placeholder`, изображения canvas, `Image.getSize` / `Image.prefetch`), источники видео и сырые TCP-сокеты (`net.connect`).

| Ключ | Тип | Описание |
|---|---|---|
| `hosts` | `string[]` | Хосты, к которым приложение может обращаться любым методом и по любой схеме (`http`, `https`, `ws`, `wss`). |
| `http` | `object[]` | Более точные разрешения: хост, суженный любым из ключей ниже. |
| `http[].host` | `string` | Хост или шаблон хоста (обязательно). |
| `http[].methods` | `string[]` | Разрешённые методы, например `["GET", "HEAD"]`. Если не указано: любые. |
| `http[].schemes` | `string[]` | Разрешённые схемы (`scheme`: одна, в виде строки). Если не указано: любая из `http`, `https`, `ws`, `wss`. |
| `http[].port` | `number` | Порт; у URL без порта используется порт по умолчанию для его схемы (443 для `https`). Если не указано: любой. |
| `http[].paths` | `string[]` | Точные пути, например `["/v1/quote"]` (запрос не входит в путь). |
| `http[].pathPrefixes` | `string[]` | Префиксы путей, охватывающие целые сегменты: `/v1` охватывает `/v1` и `/v1/quote`, `/v1/` — всё, что следует за ним. `pathPrefix` (один, в виде строки) и `path_prefixes` читаются одинаково. |
| `sockets` | `string[]` | Адреса назначения для сырых TCP-соединений через [`net.connect`](https://nsh.dev/translations/ru/modules/standard-modules.md), каждый — точная пара `"host:port"` (`"[::1]:9000"` для IPv6). |

Правило `http` охватывает все пути, если в нём не указаны `paths` и `pathPrefixes`, а если указаны оба — путь, подходящий под любой из них.

Шаблоны хостов: `*` (любой хост), `*.example.com` (любой поддомен `example.com`, но не сам `example.com`) или точное имя хоста. При сопоставлении регистр и порт игнорируются. Хост берётся после последнего `@`, поэтому `https://api.example.com@evil.io/` проверяется как `evil.io`.

Рукопожатие WebSocket — это HTTP GET: `ws://` проверяется как `http://`, а `wss://` — как `https://`, поэтому разрешения в hosts и `http`, допускающие GET для этого URL, допускают и WebSocket. Сырой сокет обходит все HTTP-правила, поэтому `sockets` перечисляет каждый хост и порт точно, и разрешает их только `sockets`.

URI `asset:` (собственные файлы приложения) разрешены всегда; изображения и видео могут также использовать URI `data:` и относительные пути.

### `webview`

| Ключ | Тип | Описание |
|---|---|---|
| `hosts` | `string[]` | Хосты, которые может показывать [веб-представление](https://nsh.dev/translations/ru/docs/web-view-and-video.md) и на которые оно может переходить (`http` и `https`). Без `webview` приложение вообще не может показывать веб-страницы. |
| `input` | `boolean` | Ref веб-представления может передавать своей странице доверенный ввод ([`sendInput`](https://nsh.dev/translations/ru/docs/web-view-and-video.md#input-as-the-users-own)). Никогда не выдаётся, если не объявлено, в том числе для приложения без `"capabilities"`. |

Переходы верхнего уровня внутри веб-представления тоже проверяются делегатом навигации платформы до загрузки. Собственные подресурсы страницы (её скрипты, изображения, XHR) — дело веб-контента и не проверяются. `about:blank` и встроенный HTML (`html`) требуют лишь того, чтобы веб-представления были разрешены вообще: `"webview": { "hosts": [] }` допускает их, не допуская ни одной удалённой страницы.

`input` отделён от `hosts`, потому что доверенный ввод несёт пользовательскую активацию, которой `injectJavaScript` никогда не получает: тогда страница может открывать окна, записывать в буфер обмена, открывать системную панель выбора файлов, запускать WebAuthn или запросы разрешений и нажимать любые кнопки, проверяющие `isTrusted`. Без него ядро отбрасывает `sendInput` и записывает в лог, какой ключ нужно добавить.

### `statusBar`

`"statusBar": true` позволяет приложению менять строку состояния (`statusBar` страниц и диалогов в [`nsh/base`](https://nsh.dev/translations/ru/modules/nsh-base.md#status-bar) и значение по умолчанию `"statusBar"` у `nsh.json`).

### `clipboard`

`"clipboard": true` позволяет приложению читать и записывать системный буфер обмена ([`Clipboard`](https://nsh.dev/translations/ru/modules/nsh.md#clipboard)). Копирование из меню редактирования выделяемого текста — собственное действие пользователя и не требует разрешения.

### `picker`

`"picker": true` позволяет приложению показывать системный выбор фотографий ([`Picker`](https://nsh.dev/translations/ru/modules/nsh.md#picker)): пользователь сам выбирает, что попадёт в приложение.

### `speech`

`"speech": true` позволяет приложению распознавать речь пользователя ([`Speech`](https://nsh.dev/translations/ru/modules/nsh.md#speech)): микрофон и системный распознаватель речи, транскрипция которого попадает в приложение. Система по-прежнему один раз спрашивает пользователя.

### `openUrl`

Позволяет приложению открывать URL за его пределами ([`openUrl`](https://nsh.dev/translations/ru/modules/nsh.md#openurl), ссылки `TextView` с `links="open"`): `"openUrl": true` для всех схем, `"openUrl": { "schemes": ["https", "mailto"] }` для перечисленных схем. URL, схема которого не разрешена, отклоняется с `ERR_ACCESS_DENIED` (`permission` `"openUrl"`, `resource` — URL). URL, которые выполняли бы код или обращались бы к собственным данным приложения (`javascript:`, `data:`, `file:`, ...), никогда не открываются, независимо от разрешения.

### Storage

Каждое приложение может хранить свои данные на устройстве с помощью [`Storage`](https://nsh.dev/translations/ru/modules/nsh.md#storage) без какого-либо разрешения: хранилище содержит только данные этого приложения, а место хранения определяет встраивающее приложение.

### `host`

`host` перечисляет функции приложения, встраивающего NSH, которые может вызывать `host.call(name, args)` из `nsh`: точные имена (`"getUserInfo"`) или префиксы, оканчивающиеся на `*` (`"bridge.*"` соответствует `bridge.call` и `bridge.share`). `"host": true` разрешает все функции. Функция, которой нет в списке, отклоняется, в том числе в приложении без `"capabilities"`: функции хоста никогда не выдаются по умолчанию, а сборка приложения, импортирующего `host` из `nsh` без `host`, предупреждает, что каждый вызов будет отклонён. Имена содержат от 1 до 128 символов из `A`–`Z`, `a`–`z`, `0`–`9`, `_`, `.`, `:` и `-`, без `..`; вызов любого другого имени отклоняется до чтения разрешений. Оканчивайте префикс разделителем (`"bridge.*"`): `"bridge*"` соответствует и `bridgeAdmin`. События хоста (`host.on(event, listener)`) доходят до слушателей приложения в том виде, в каком их отправляет хост.

```ts
import { host } from "nsh";

if (host.available()) {
  const user = await host.call("getUserInfo");
  const stop = host.on("loginChanged", (payload) => console.log(payload));
  stop();
}
```

`host.available()` равно true на платформах, встраивающих хост; на остальных `host.call` отклоняется с `code` `"ERR_FEATURE_UNAVAILABLE_ON_PLATFORM"`.
<a id="what-a-refusal-looks-like"></a>

### Как выглядит отказ

| Запрос | Результат |
|---|---|
| `fetch` | Промис отклоняется с `TypeError`, как при любом неудачном запросе; его `code` равно `ERR_ACCESS_DENIED`, `permission` `"network"` и `resource` URL (запроса или редиректа); запрос никогда не доходит до сети. |
| `Image.getSize`, `Image.prefetch` | Промис отклоняется с `ERR_ACCESS_DENIED`, `permission` `"network"` и `resource` URL; изображение не запрашивается. |
| Удалённое изображение, источник видео, источник web view или `html` | Ничего не показывается, а `onError` элемента сообщает об этом с `code` `"ERR_ACCESS_DENIED"`. Переход web view на хост, к которому нет доступа, отменяется и сообщается так же. |
| Изменение Status bar, изображение на canvas | Ничего не меняется и не рисуется; отказ записывается в лог. |
| Clipboard | Каждая функция `Clipboard` отклоняется с `ERR_ACCESS_DENIED`, `permission` `"clipboard"` и `resource` `""`; системный буфер обмена никогда не затрагивается. |
| Picker | `pickImages` ничего не показывает и отклоняется с `ERR_ACCESS_DENIED`, `permission` `"picker"` и `resource` `""`. |
| Speech | `Speech.start` выбрасывает `ERR_ACCESS_DENIED`, `permission` `"speech"` и `resource` `""`; микрофон остаётся выключенным. |
| `openUrl` | Промис отклоняется с `ERR_ACCESS_DENIED`, `permission` `"openUrl"` и `resource` URL; ничего не открывается. |
| Вызов хоста | `host.call` отклоняется с `ERR_ACCESS_DENIED`, `permission` `"host"` и `resource` имя функции (`host.post` выбрасывает его); вызов не доходит до хоста. |
| Файлы (`fs/promises`) | Промис отклоняется с `ERR_ACCESS_DENIED`, `permission` `"fs"` и `resource` путь; диск никогда не затрагивается. |
| Процессы (`process`) | `run` отклоняется, а `spawn` и `exit` выбрасывают ошибку с `ERR_ACCESS_DENIED`, `permission` `"process"` и `resource` команду (`"exit"` для `exit`); дочерний процесс не запускается. |
| TCP-сокет (`net.connect`) | Промис отклоняется с `ERR_ACCESS_DENIED`, `permission` `"network"` и `resource` `"host:port"`, до любого DNS-запроса. |
| WebSocket | `connect` модуля `websocket` отклоняется с `ERR_ACCESS_DENIED`, `permission` `"network"` и `resource` URL; глобальный `WebSocket` вызывает `error`, а затем `close` (код 1006), как при неудачном соединении. |

Ошибки [стандартных модулей](https://nsh.dev/translations/ru/modules/standard-modules.md#failures) несут `code` `"ERR_ACCESS_DENIED"`, поэтому приложение отличает отказ от неудачной операции, а их `permission` и `resource` называют capability и то, что было запрошено. Модули [`nsh`](https://nsh.dev/translations/ru/modules/nsh.md) отказывают так же: их промисы отклоняются с `ERR_ACCESS_DENIED`, `permission` и `resource` (`fetch` с `TypeError`, классом любого неудачного запроса).

```ts
import { Clipboard } from "nsh";

try {
  await Clipboard.writeText(link);
} catch (error) {
  if (error.code !== "ERR_ACCESS_DENIED") throw error;
  console.warn(`${error.permission} is not granted`); // "clipboard"
}
```

Компоненты на Rust получают те же отказы в терминах Rust: отклонённое чтение clipboard возвращает то же, что и пустой clipboard, отклонённый выбор — ни одного файла, отклонённый `open_url` `false`, а отклонённый `fetch` возвращает `Err` с сообщением.

Каждый отказ записывается в лог один раз с указанием, какой ключ нужно добавить:

```text
fetch GET https://evil.example.org/x is not granted: add `evil.example.org` to capabilities.network in nsh.json
```

Редиректы выполняет NSH, а не HTTP-стек платформы, поэтому каждый переход проверяется на соответствие capabilities. (Потоковое тело запроса не отправляется дважды, поэтому редирект `307` / `308` при потоковой выгрузке завершается ошибкой.)

<a id="where-capabilities-travel"></a>

### Куда попадает декларация

Декларация следует за приложением, куда бы оно ни попало:

- `nsh build` и `nsh package` записывают её рядом с бандлом как `capabilities.json`;
- `nsh publish` помещает её в заголовок [подписанного пакета](https://nsh.dev/translations/ru/docs/signed-packages.md), и она защищена подписью;
- `nsh dev` отправляет её с каждой сборкой, поэтому в разработке действуют те же правила. Изменение `capabilities` перезагружает приложение вместо горячего патча.

<a id="grants-what-a-host-app-allows"></a>

## Гранты: что разрешает хост-приложение

Хост-приложение, встраивающее NSH, может выдать **не больше** некоторых capabilities в той же JSON-форме:

```rust
// iOS, in Rust (nsh_platform_ios::AppView)
let grant = nsh_engine::Capabilities::from_json(&serde_json::json!({
    "network": { "hosts": ["api.example.com"] }
}))?;
app.set_granted_capabilities(Some(grant));
```

```swift
// iOS
try app.grantCapabilities(#"{ "network": { "hosts": ["api.example.com"] } }"#)
```

```java
// Android
nsh.setGrantedCapabilities("{ \"network\": { \"hosts\": [\"api.example.com\"] } }");
```

Запрос должен быть разрешён **и** декларацией приложения, **и** грантом хоста. При приведённом выше гранте приложение, которое объявляет `"hosts": ["*"]`, всё равно сможет обращаться только к `api.example.com` и не сможет показывать веб-страницы или менять Status bar. Без гранта (`nil` / `null`, по умолчанию) действует то, что объявляет каждое приложение.

Гранты применяются со следующей загрузки. Типичная схема для сторонних мини-приложений: выдать каждому только его собственный API-хост, запускать его из [подписанного пакета](https://nsh.dev/translations/ru/docs/signed-packages.md), декларацию которого вы проверили, и оставить лимиты по умолчанию или снизить их.

> [!WARNING]
> [Нативные компоненты](https://nsh.dev/translations/ru/docs/native-components.md), которые регистрирует хост-приложение, — это собственный код хоста: capabilities на них не распространяются. Регистрируйте только те компоненты, которые безопасны для любого приложения, способного работать в этом view.

<a id="summary"></a>

## Итог

| Уровень | Кто обеспечивает | Кто задаёт |
|---|---|---|
| Лимиты (heap, stack, время, узлы) | runtime, для каждой VM | хост-приложение для каждого view (встраиваемые приложения) |
| Hardening | runtime, при загрузке | всегда включён вне разработки |
| Capabilities | runtime (fetch, изображения, видео, web view, Status bar, файлы, процессы, сокеты) | приложение (`nsh.json`) и хост-приложение (грант) |
| Целостность кода | проверка подписи | `nsh publish`; доверенные корни хост-приложения |

<a id="see-also"></a>

## См. также

- [Подписанные пакеты](https://nsh.dev/translations/ru/docs/signed-packages.md)
- [Работа с сетью](https://nsh.dev/translations/ru/docs/networking.md)
- [Встраивание в приложения iOS](https://nsh.dev/translations/ru/docs/ios.md), [Встраивание в приложения Android](https://nsh.dev/translations/ru/docs/android.md)
