feat(box): inject box.local.env into every exec payload

This commit is contained in:
Nikarhub
2026-09-16 16:47:27 +08:00
parent 60bb67f025
commit 680049319a
3 changed files with 80 additions and 0 deletions
+27
View File
@@ -124,6 +124,7 @@ class BoxService:
self.profile = self._load_profile()
self.custom_image = self._load_custom_image()
self.workspace_quota_mb = self._load_workspace_quota_mb()
self.default_env = self._load_default_env()
self._admission_policy = (
require_cloud_admission_policy(_get_box_config(ap).get('admission')) if self._cloud_managed else None
)
@@ -1394,6 +1395,10 @@ class BoxService:
def build_spec(self, spec_payload: dict, skip_host_mount_validation: bool = False) -> BoxSpec:
spec_payload = dict(spec_payload)
spec_payload.setdefault('env', {})
if self.default_env:
merged_env = dict(self.default_env)
merged_env.update(spec_payload.get('env') or {})
spec_payload['env'] = merged_env
if spec_payload.get('host_path') in (None, '') and self.default_workspace is not None:
spec_payload['host_path'] = self.default_workspace
if spec_payload.get('workspace_quota_mb') in (None, '') and self.workspace_quota_mb is not None:
@@ -1831,6 +1836,28 @@ class BoxService:
raise BoxValidationError('workspace_quota_mb must be greater than or equal to 0')
return value
def _load_default_env(self) -> dict[str, str]:
"""Return ``box.local.env`` as operator-provided sandbox defaults.
These values are merged into every Box exec payload so sandbox commands
can read settings such as a service base URL. A per-call ``env`` supplied
by the agent or caller overrides the matching defaults. Non-dict configs
and non-string keys or values are ignored.
"""
raw_env = self._local_config().get('env')
if not isinstance(raw_env, dict):
return {}
env: dict[str, str] = {}
for key, value in raw_env.items():
if not isinstance(key, str) or not isinstance(value, str):
continue
normalized_key = key.strip()
if not normalized_key:
continue
env[normalized_key] = value
return env
def _ensure_default_workspace(self):
if self.default_workspace is None:
return
+3
View File
@@ -383,6 +383,9 @@ box:
- './data/box'
- '/tmp'
workspace_quota_mb: null # Optional disk quota override (>= 0). null = profile default.
# Extra environment variables injected into every Box exec payload.
# The agent-supplied per-call env overrides matching keys. Values must be strings.
env: {}
# Default nsjail cgroup memory limit for each MCP stdio server process, in MB.
# Node.js MCP servers (npx/bunx) need more memory than Python ones because V8
# and WebAssembly modules (e.g. undici llhttp) reserve large virtual address
+50
View File
@@ -416,6 +416,56 @@ def test_separated_box_runtime_allows_box_owned_missing_host_path(tmp_path):
assert not (host_root / 'default').exists()
def test_box_build_spec_injects_local_default_env():
logger = Mock()
app = make_app(logger)
app.instance_config.data['box']['local']['env'] = {'JENKINS_BASE_URL': 'https://jenkins.example.com'}
service = BoxService(app, client=Mock())
spec = service.build_spec({'cmd': 'echo hi', 'session_id': 'default-env'})
assert spec.env == {'JENKINS_BASE_URL': 'https://jenkins.example.com'}
def test_box_build_spec_per_call_env_overrides_local_default():
logger = Mock()
app = make_app(logger)
app.instance_config.data['box']['local']['env'] = {
'JENKINS_BASE_URL': 'https://default.example.com',
'KEEP_ME': 'yes',
}
service = BoxService(app, client=Mock())
spec = service.build_spec(
{'cmd': 'echo hi', 'session_id': 'override-env', 'env': {'JENKINS_BASE_URL': 'https://override.example.com'}}
)
assert spec.env == {'JENKINS_BASE_URL': 'https://override.example.com', 'KEEP_ME': 'yes'}
def test_box_build_spec_ignores_invalid_local_env_entries():
logger = Mock()
app = make_app(logger)
app.instance_config.data['box']['local']['env'] = {
'OK': 'value',
'': 'empty-key',
' ': 'blank-key',
'BAD_VALUE': 123,
3: 'non-string-key',
}
service = BoxService(app, client=Mock())
spec = service.build_spec({'cmd': 'echo hi', 'session_id': 'invalid-env'})
assert spec.env == {'OK': 'value'}
app.instance_config.data['box']['local']['env'] = 'not-a-dict'
service = BoxService(app, client=Mock())
spec = service.build_spec({'cmd': 'echo hi', 'session_id': 'non-dict-env'})
assert spec.env == {}
@pytest.mark.asyncio
async def test_box_service_get_sessions_delegates_to_client():
client = Mock()