feat(box): SaaS guard to force a single global sandbox scope

Add system.limitation.force_box_session_id_template: when non-empty it
overrides every pipeline's box-session-id-template at resolve time, pinning
all queries to one shared sandbox (e.g. {global}). This is the authoritative,
unbypassable guard — it runs on every exec call, so editing the pipeline
config via API cannot escape it. The web UI locks the Sandbox Scope selector
via a combined box_scope_editable flag (box available AND not forced).
This commit is contained in:
RockChinQ
2026-06-08 06:38:52 -04:00
parent c460bd7814
commit 3b5e89f17f
6 changed files with 138 additions and 20 deletions
+33 -7
View File
@@ -221,13 +221,25 @@ class BoxService:
return self._serialize_result(result)
def resolve_box_session_id(self, query: pipeline_query.Query) -> str:
"""Resolve the Box session_id from the pipeline's template and query variables."""
template = (
(query.pipeline_config or {})
.get('ai', {})
.get('local-agent', {})
.get('box-session-id-template', '{launcher_type}_{launcher_id}')
)
"""Resolve the Box session_id from the pipeline's template and query variables.
When ``system.limitation.force_box_session_id_template`` is set to a
non-empty value, that template overrides whatever the pipeline
configured. This is the authoritative SaaS guard: it runs on every
``exec`` call, so a tenant cannot escape a single shared sandbox even
by editing the pipeline config directly through the API (which only
gates the web UI).
"""
forced_template = self._forced_box_session_id_template()
if forced_template:
template = forced_template
else:
template = (
(query.pipeline_config or {})
.get('ai', {})
.get('local-agent', {})
.get('box-session-id-template', '{launcher_type}_{launcher_id}')
)
variables = dict(query.variables or {})
launcher_type = getattr(query, 'launcher_type', None)
if hasattr(launcher_type, 'value'):
@@ -606,6 +618,20 @@ class BoxService:
raw = str(self._local_config().get('image', '') or '').strip()
return raw or None
def _forced_box_session_id_template(self) -> str:
"""Return the SaaS-forced sandbox-scope template, or '' when unset.
Read from ``system.limitation.force_box_session_id_template``. A
non-empty value pins every pipeline to a single sandbox scope
(e.g. ``'{global}'``) and cannot be overridden per-pipeline.
"""
limitation = (
(self.ap.instance_config.data or {}).get('system', {}).get('limitation', {})
if getattr(self.ap, 'instance_config', None) is not None
else {}
)
return str(limitation.get('force_box_session_id_template', '') or '').strip()
def _load_workspace_quota_mb(self) -> int | None:
raw_value = self._local_config().get('workspace_quota_mb')
if raw_value in (None, ''):
+6
View File
@@ -25,6 +25,12 @@ system:
max_bots: -1
max_pipelines: -1
max_extensions: -1
# When set to a non-empty string, every pipeline is forced to use this
# Box sandbox-scope template regardless of its own configuration, and
# the per-pipeline "Sandbox Scope" selector is locked in the web UI.
# Used by SaaS deployments to confine a tenant to a single shared
# sandbox (set to '{global}'). Empty string = no restriction.
force_box_session_id_template: ''
task_retention:
# Keep at most this many completed async task records in memory
completed_limit: 200
+12 -11
View File
@@ -152,21 +152,22 @@ stages:
es_ES: Determina cómo se comparten los entornos sandbox entre mensajes.
ru_RU: Определяет, как песочницы используются совместно между сообщениями.
disable_if:
field: __system.box_available
field: __system.box_scope_editable
operator: eq
value: false
disabled_tooltip:
en_US: >-
Box sandbox is disabled or unavailable. Enable it in config.yaml
(box.enabled = true) and ensure the runtime is reachable to change
this setting.
zh_Hans: Box 沙箱已禁用或不可用。请在配置中启用(box.enabled = true)并确认运行时连接正常,才能修改此项。
zh_Hant: Box 沙箱已用或無法使用。請在設定中啟用(box.enabled = true)並確認執行時連線正常,才能修改此項。
ja_JP: Box サンドボックスが無効または利用できません。設定で有効化(box.enabled = true)し、ランタイムが接続できることを確認してから変更してください。
vi_VN: Sandbox Box đã tắt hoặc không khả dụng. Hãy bật trong cấu hình (box.enabled = true) và đảm bảo runtime hoạt động để chỉnh sửa.
th_TH: Sandbox Box ถูกปิดใช้งานหรือไม่พร้อมใช้งาน กรุณาเปิดใช้งานในการตั้งค่า (box.enabled = true) และตรวจสอบว่ารันไทม์เชื่อมต่อปกติก่อนปรับค่า
es_ES: El sandbox de Box está desactivado o no disponible. Actívelo en la configuración (box.enabled = true) y asegúrese de que el runtime esté conectado para modificar este ajuste.
ru_RU: Песочница Box отключена или недоступна. Включите её в конфигурации (box.enabled = true) и убедитесь, что среда выполнения работает, чтобы изменить эту настройку.
Sandbox scope can't be changed: either the Box sandbox is disabled
or unavailable (enable it in config.yaml with box.enabled = true and
ensure the runtime is reachable), or this deployment pins all
pipelines to a fixed scope.
zh_Hans: "无法修改沙箱作用域:Box 沙箱已用或不可用(请在配置中启用 box.enabled = true 并确认运行时连接正常),或本部署已将所有流水线固定为统一作用域。"
zh_Hant: "無法修改沙箱作用域:Box 沙箱已停用或無法使用(請在設定中啟用 box.enabled = true 並確認執行時連線正常),或本部署已將所有流水線固定為統一作用域。"
ja_JP: "サンドボックススコープを変更できません:Box サンドボックスが無効/利用不可(設定で box.enabled = true にしてランタイム接続を確認)、またはこのデプロイがすべてのパイプラインを固定スコープに制限しています。"
vi_VN: "Không thể thay đổi phạm vi sandboxBox sandbox bị tắt hoặc không khả dụng (bật box.enabled = true và đảm bảo runtime hoạt động), hoặc bản triển khai này cố định mọi pipeline về một phạm vi."
th_TH: "ไม่สามารถเปลี่ยนขอบเขต Sandbox:Box sandbox ถูกปิดหรือไม่พร้อมใช้งาน (เปิด box.enabled = true และตรวจสอบรันไทม์) หรือการ deploy นี้ล็อกทุก pipeline ไว้ที่ขอบเขตเดียว"
es_ES: "No se puede cambiar el alcance del sandbox: el sandbox de Box está desactivado o no disponible (actívelo con box.enabled = true y verifique el runtime), o este despliegue fija todas las pipelines a un alcance único."
ru_RU: "Невозможно изменить область песочницы: песочница Box отключена или недоступна (включите box.enabled = true и проверьте среду выполнения), либо это развёртывание фиксирует единую область для всех конвейеров."
type: select
required: false
default: "{launcher_type}_{launcher_id}"