diff --git a/services/core/tools/e2b-provider/README.md b/services/core/tools/e2b-provider/README.md index b872e1f84..10a98a695 100644 --- a/services/core/tools/e2b-provider/README.md +++ b/services/core/tools/e2b-provider/README.md @@ -50,7 +50,7 @@ An unknown Create is never repeated. A Create whose connection material was lost ## Inspection and cleanup -Inspection uses SDK metadata and ID reads only. SDK `connect` is never used because it can resume paused compute. The version-pinned constructor that restores a client from saved connection material is confined to [`sdk.py`](sdk.py) and covered by a no-connect, no-create test. Resource drift fails inspection but still permits ownership-based cleanup. +Inspection accepts an omitted `SandboxInfo.sandbox_domain`, as allowed by the pinned SDK; an explicit domain must match the configured domain or its subdomains. Create connection objects and clients restored from saved connection material must always have a matching domain before envd access. Ownership, template and resource checks apply regardless of the observation domain. Inspection uses SDK metadata and ID reads only. SDK `connect` is never used because it can resume paused compute. The version-pinned constructor that restores a client from saved connection material is confined to [`sdk.py`](sdk.py) and covered by a no-connect, no-create test. Resource drift fails inspection but still permits ownership-based cleanup. `CreateSettled` proves that the original Create and bootstrap can no longer mutate; it is independent of `BootstrapComplete`. An empty lookup never settles an unknown Create. Kill destroys every matching sandbox, confirms that none remains and only then records a settled tombstone; it returns `State=absent` with `CreateSettled`. A settled rejected Create with no sandbox IDs proves absence without a cloud request, so GetInfo and Kill still succeed when the key is invalid. Ordinary missing compute has no such proof. diff --git a/services/core/tools/e2b-provider/observation_domain_test.py b/services/core/tools/e2b-provider/observation_domain_test.py new file mode 100644 index 000000000..644e35e30 --- /dev/null +++ b/services/core/tools/e2b-provider/observation_domain_test.py @@ -0,0 +1,118 @@ +"""Optional SDK observation metadata never relaxes connection validation.""" +from pathlib import Path +from types import SimpleNamespace +import unittest + +import provider_test + + +class ObservationDomainTest(unittest.TestCase): + setUp = provider_test.ProviderTest.setUp + call = provider_test.ProviderTest.call + record = provider_test.ProviderTest.record + + def observed(self, **changes): + fields = dict(sandbox_id=self.cloud.sandbox_id, sandbox_domain=None, + metadata=self.cloud.metadata, template_id=self.cloud.template_id, + cpu_count=2, memory_mb=2048, state=self.cloud.state) + fields.update(changes) + return SimpleNamespace(**fields) + + def test_create_and_inspect_missing_observation_domain_use_saved_connection(self): + self.config['Resources'] = {'cpus': 2, 'memory_mib': 2048} + self.api.get_info.side_effect = lambda *a, **k: self.observed() + self.assertEqual(self.call('create')['ErrorCode'], '') + self.assertTrue(self.call('inspect')['Info']['BootstrapComplete']) + self.assertEqual(self.call('renew')['ErrorCode'], '') + self.assertEqual(self.record()['connection']['sandbox_domain'], 'e2b.app') + self.api.connect.assert_not_called() + self.api.create.assert_called_once() + + def test_paused_inspection_without_domain_does_not_wake(self): + self.api.get_info.side_effect = lambda *a, **k: self.observed() + self.assertEqual(self.call('create')['ErrorCode'], '') + self.cloud.state = 'paused' + result = self.call('inspect') + self.assertEqual(result['ErrorCode'], '') + self.assertEqual(result['Info']['State'], 'paused') + self.api.connect.assert_not_called() + + def test_unknown_create_list_without_domain_never_recreates_or_attaches(self): + self.api.create.side_effect = TimeoutError('unknown create') + self.assertEqual(self.call('create')['ErrorCode'], 'unconfirmed') + self.api.list.return_value = SimpleNamespace( + has_next=True, next_items=lambda **kwargs: [self.observed()]) + # One page exhausts the SDK paginator. + def page(**kwargs): + self.api.list.return_value.has_next = False + return [self.observed()] + self.api.list.return_value.next_items = page + result = self.call('inspect') + self.assertEqual(result['ErrorCode'], '') + self.assertFalse(result['Info']['BootstrapComplete']) + self.api.create.assert_called_once() + self.api.connect.assert_not_called() + self.cloud.files.write.assert_not_called() + + def test_missing_domain_does_not_bypass_identity_or_resource_checks(self): + self.config['Resources'] = {'cpus': 2, 'memory_mib': 2048} + for changes, error in (({'metadata': {}}, 'ownership'), + ({'template_id': 'other'}, 'invalid'), + ({'cpu_count': 4}, 'invalid'), + ({'memory_mb': 8192}, 'invalid')): + with self.subTest(changes=changes): + for path in Path(self.temporary.name).glob('*.json'): + path.unlink() + self.api.get_info.return_value = self.observed(**changes) + self.assertEqual(self.call('create')['ErrorCode'], error) + self.cloud.files.write.assert_not_called() + self.api.connect.assert_not_called() + + def test_explicit_invalid_observation_domain_rejected_before_credentials(self): + for domain in ('foreign.example', 'e2b.app.foreign.example', '', 3): + with self.subTest(domain=domain): + for path in Path(self.temporary.name).glob('*.json'): + path.unlink() + self.api.get_info.return_value = self.observed(sandbox_domain=domain) + self.assertEqual(self.call('create')['ErrorCode'], 'ownership') + self.cloud.files.write.assert_not_called() + self.api.connect.assert_not_called() + + def test_rejected_connection_cannot_reenter_through_missing_observation_domain(self): + for domain in ('foreign.example', None): + with self.subTest(domain=domain): + for path in Path(self.temporary.name).glob('*.json'): + path.unlink() + self.cloud.sandbox_domain = domain + self.api.get_info.return_value = self.observed() + self.assertEqual(self.call('create')['ErrorCode'], 'ownership') + self.assertEqual(self.call('inspect')['ErrorCode'], 'ownership') + self.cloud.files.read.assert_not_called() + self.cloud.files.write.assert_not_called() + self.api.connect.assert_not_called() + + def test_rejected_observation_cannot_resume_bootstrap_on_later_missing_domain(self): + self.api.get_info.return_value = self.observed(sandbox_domain='foreign.example') + self.assertEqual(self.call('create')['ErrorCode'], 'ownership') + self.assertEqual(self.record()['status'], 'configuration_rejected') + self.api.get_info.return_value = self.observed() + result = self.call('inspect') + self.assertEqual(result['ErrorCode'], '') + self.assertFalse(result['Info']['BootstrapComplete']) + self.assertTrue(result['Info']['CreateSettled']) + self.assertEqual(self.record()['status'], 'configuration_rejected') + self.cloud.files.read.assert_not_called() + self.cloud.files.write.assert_not_called() + self.api.connect.assert_not_called() + + def test_missing_actual_connection_domain_is_still_rejected(self): + self.api.get_info.return_value = self.observed() + self.cloud.sandbox_domain = None + self.assertEqual(self.call('create')['ErrorCode'], 'ownership') + self.api.get_info.assert_not_called() + self.cloud.files.write.assert_not_called() + self.api.connect.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/services/core/tools/e2b-provider/provider.py b/services/core/tools/e2b-provider/provider.py index 76557b500..e2e3c0a90 100644 --- a/services/core/tools/e2b-provider/provider.py +++ b/services/core/tools/e2b-provider/provider.py @@ -127,15 +127,18 @@ def owns(self, cloud): raise Failure('ownership') return cloud - def check_domain(self, cloud): + def check_domain(self, sandbox_domain): domain = self.options()['domain'] - sandbox_domain = cloud.sandbox_domain if not isinstance(sandbox_domain, str) or not ( sandbox_domain == domain or sandbox_domain.endswith('.' + domain)): raise Failure('ownership') def qualified(self, cloud): - self.check_domain(cloud) + # SandboxInfo omits the optional domain on the official API. It is an + # observation, not connection material; Create and saved clients still + # require a qualified domain before any envd access. + if cloud.sandbox_domain is not None: + self.check_domain(cloud.sandbox_domain) template = self.config['Template'].split(':', 1)[0] if cloud.template_id not in (template, self.config['Template']): raise Failure('invalid') @@ -173,6 +176,7 @@ def client(self, cloud): material = (self.receipt.data or {}).get('connection') if not material or material['sandbox_id'] != cloud.sandbox_id: raise Failure('unconfirmed') + self.check_domain(material['sandbox_domain']) return restore(material, self.options()) def inspect(self): @@ -189,7 +193,7 @@ def inspect(self): self.qualified(cloud) record = self.receipt.data or {} if (cloud.state == 'running' and not record.get('bootstrap_complete') and record.get('connection') - and record.get('status') not in ('bootstrap_failed', 'killed')): + and record.get('status') not in ('configuration_rejected', 'bootstrap_failed', 'killed')): try: receipt = json.loads(self.client(cloud).files.read('/root/.oac/e2b/managed-ready.json', user='root', request_timeout=self.remaining())) @@ -222,7 +226,7 @@ def create(self): raise Failure('unconfirmed') from None self.receipt.save(status='created', ids=[cloud.sandbox_id], connection=connection_material(cloud)) # A create response must not steer envd traffic to an unrelated host. - self.check_domain(cloud) + self.check_domain(cloud.sandbox_domain) # SDK Create returns connection material, but no metadata or resources. # Read its exact ID before writing credentials, even when Core adopts the # template's resources and does not supply explicit limits.