mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-08 07:52:20 +02:00
Merge frozen lab b1666951 (Wave 3) into Wave 4 effects provenance
Integrates the merged and frozen Wave 3 lab commit b1666951faf8285054e1ca90f11533b0fb53fb57 with a normal merge, preserving every Wave 4 commit unchanged. Conflict: src/agent_runtime/resources.py. Wave 3's _control_plane_snapshot() / _control_plane_path(path, *, snapshot=None) split is kept. The snapshot adds the effect-store directories to its prefix set after the recursive job-dir inventory and no longer references path (the auto-merged prefix check would have raised NameError there). _control_plane_path calls _aliases_effect_store after its os.stat, only for multiply linked files, so single-link files never list the store. Semantic reconciliation (no textual conflict): bg_monitor keeps launch settlement right after the first successful validate_job and before the authority check, with Wave 3's post-drain revalidation intact. The deleted-session branch, terminal before linkage validation, now settles a validated launch too: that job is later pruned and its publication retired, which would otherwise leave its effect RUNNING. Regression tests cover the snapshot form of the effect-store check and both deleted-session linkage outcomes.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from unittest.mock import AsyncMock
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -11,6 +12,11 @@ from src.host_docker_access import HOST_DOCKER_ACCESS_HINT
|
||||
from tests.helpers.unix_sockets import bound_unix_socket
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def authenticated_admin_mode(monkeypatch):
|
||||
monkeypatch.setenv("AUTH_ENABLED", "true")
|
||||
|
||||
|
||||
def _model_serve_endpoint():
|
||||
router = cookbook_routes.setup_cookbook_routes()
|
||||
for route in router.routes:
|
||||
@@ -27,6 +33,8 @@ def _admin_request() -> Request:
|
||||
"path": "/api/model/serve",
|
||||
"headers": [],
|
||||
"state": {},
|
||||
"app": SimpleNamespace(state=SimpleNamespace(auth_manager=SimpleNamespace(
|
||||
is_configured=True, is_admin=lambda user: user == "admin"))),
|
||||
}
|
||||
)
|
||||
request.state.current_user = "admin"
|
||||
@@ -139,7 +147,6 @@ async def test_local_container_serve_returns_host_docker_opt_in_hint(
|
||||
assert cookbook_routes.shutil.which(binary) == "/usr/bin/docker"
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(cookbook_routes, "require_admin", lambda request: None)
|
||||
monkeypatch.setattr(cookbook_routes, "_binary_available", binary_available)
|
||||
monkeypatch.setattr(cookbook_routes, "running_in_container", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
@@ -199,7 +206,6 @@ async def test_local_container_serve_allows_generated_docker_exec_when_enabled(
|
||||
launched_commands.append(command)
|
||||
return _Process()
|
||||
|
||||
monkeypatch.setattr(cookbook_routes, "require_admin", lambda request: None)
|
||||
monkeypatch.setattr(cookbook_routes, "_binary_available", binary_available)
|
||||
monkeypatch.setattr(cookbook_routes, "running_in_container", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -329,6 +329,14 @@ def test_hardlinked_effect_state_is_control_plane_without_scanning_the_store(tmp
|
||||
alias = workspace / "sneaky.jsonl"
|
||||
os.link(store / f"{7:032x}.jsonl", alias)
|
||||
assert resources._control_plane_path(str(alias)) is True
|
||||
# Wave 3's scan-local snapshot form: the store is a prefix, not inventory.
|
||||
snapshot = resources._control_plane_snapshot()
|
||||
assert str(store) in snapshot[0]
|
||||
assert resources._control_plane_path(str(store / "new.jsonl"), snapshot=snapshot) is True
|
||||
listed.clear()
|
||||
assert resources._control_plane_path(str(ordinary), snapshot=snapshot) is False
|
||||
assert str(store) not in listed
|
||||
assert resources._control_plane_path(str(alias), snapshot=snapshot) is True
|
||||
assert str(store) not in globbed, "the effect store is listed one level, never recursively inventoried"
|
||||
# Multiply linked files elsewhere stay ordinary.
|
||||
elsewhere = tmp_path / "other.txt"
|
||||
|
||||
@@ -330,7 +330,7 @@ async def test_anonymous_native_cookbook_control_rejected_before_producer(monkey
|
||||
monkeypatch.setattr(asyncio, "create_subprocess_shell", lambda *a, **k: pytest.fail("Anonymous producer reached"))
|
||||
app = FastAPI()
|
||||
app.include_router(cookbook_routes.setup_cookbook_routes())
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://local") as client:
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, client=("192.0.2.1", 123)), base_url="http://local") as client:
|
||||
result = await client.post(path, json=payload)
|
||||
assert result.status_code == 403
|
||||
|
||||
@@ -352,3 +352,156 @@ async def test_direct_local_cookbook_control_does_not_enroll_discovered_processe
|
||||
# mint a process resource even when the UI supplies a matching name.
|
||||
result = await cookbook._cookbook_kill_session("serve-unowned")
|
||||
assert result["failure_kind"] == "resource_identity_denied"
|
||||
|
||||
|
||||
def test_direct_containment_attachment_skips_unobservable_token(workspace, monkeypatch):
|
||||
import uuid
|
||||
from src.agent_runtime.resources import ProcessResource
|
||||
|
||||
# 1. Unobservable child token: pid exists, start_token is None
|
||||
op = ExactOperation.normalize("bash", "printf test")
|
||||
bound = resources.resolve_process_operation(authority(workspace), op, NativeBackendResource("bash"))
|
||||
launch = bound.launch
|
||||
containment_id = uuid.uuid4().hex
|
||||
|
||||
resources.publish_launch(launch, authority(workspace), containment_id)
|
||||
containment._save_records({
|
||||
containment_id: {
|
||||
"id": containment_id,
|
||||
"launch_generation": launch.generation,
|
||||
"workspace": launch.scope.root.path,
|
||||
"pid": 54321,
|
||||
"start_token": None,
|
||||
"pgid": 54321,
|
||||
"mechanism": "process_group",
|
||||
}
|
||||
})
|
||||
|
||||
# Guard: ensure no attempt is made to rediscover/rebind from process table
|
||||
monkeypatch.setattr(process_ownership, "process_table", lambda *a, **k: pytest.fail("re-read process table"))
|
||||
monkeypatch.setattr(process_ownership, "start_token", lambda *a, **k: pytest.fail("re-read current PID start_token"))
|
||||
|
||||
# Must NOT raise
|
||||
resources.attach_containment_processes(launch, containment_id)
|
||||
|
||||
# Publication remains valid
|
||||
pub_path = resources.launch_path(launch.generation)
|
||||
published = json.loads(pub_path.read_text())
|
||||
assert published["launch"] == launch.to_dict()
|
||||
assert published["containment_id"] == containment_id
|
||||
assert published["processes"] == []
|
||||
|
||||
# 2. Record with pid + valid token still publishes exact ProcessResource
|
||||
op_valid = ExactOperation.normalize("bash", "printf valid")
|
||||
bound_valid = resources.resolve_process_operation(authority(workspace), op_valid, NativeBackendResource("bash"))
|
||||
launch_valid = bound_valid.launch
|
||||
cid_valid = uuid.uuid4().hex
|
||||
|
||||
resources.publish_launch(launch_valid, authority(workspace), cid_valid)
|
||||
containment._save_records({
|
||||
cid_valid: {
|
||||
"id": cid_valid,
|
||||
"launch_generation": launch_valid.generation,
|
||||
"workspace": launch_valid.scope.root.path,
|
||||
"pid": 65432,
|
||||
"start_token": "procfs:boot:token65432",
|
||||
"pgid": 65432,
|
||||
"mechanism": "process_group",
|
||||
}
|
||||
})
|
||||
|
||||
resources.attach_containment_processes(launch_valid, cid_valid)
|
||||
published_valid = json.loads(resources.launch_path(launch_valid.generation).read_text())
|
||||
assert len(published_valid["processes"]) == 1
|
||||
leader_res = ProcessResource.from_dict(published_valid["processes"][0])
|
||||
assert leader_res.role == "leader"
|
||||
assert leader_res.identity.pid == 65432
|
||||
assert leader_res.identity.start_token == "procfs:boot:token65432"
|
||||
assert leader_res.identity.pgid == 65432
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool,command,expected_out", [
|
||||
("bash", "printf hi", "hi"),
|
||||
("python", "print('hi', end='')", "hi"),
|
||||
])
|
||||
async def test_end_to_end_fast_exit_preserves_command_result(workspace, monkeypatch, tool, command, expected_out):
|
||||
import os
|
||||
original_capture = process_ownership.capture
|
||||
def mocked_capture(pid):
|
||||
if pid == os.getpid():
|
||||
return original_capture(pid)
|
||||
return {"pid": pid, "start_token": None}
|
||||
monkeypatch.setattr(process_ownership, "capture", mocked_capture)
|
||||
|
||||
# Observe the real attachment before foreground lifecycle retirement.
|
||||
published = []
|
||||
attach = resources.attach_containment_processes
|
||||
def observe_attachment(launch, containment_id):
|
||||
attach(launch, containment_id)
|
||||
published.append(json.loads(resources.launch_path(launch.generation).read_text()))
|
||||
monkeypatch.setattr(resources, "attach_containment_processes", observe_attachment)
|
||||
|
||||
auth = authority(workspace, tool=tool)
|
||||
approval = approval_for(auth, tool, command)
|
||||
_, result = await dispatch(auth, tool, command, approval)
|
||||
|
||||
assert result["exit_code"] == 0
|
||||
assert result.get("output") == expected_out
|
||||
assert "failure_kind" not in result or result["failure_kind"] != "resource_linkage_unavailable"
|
||||
|
||||
launches_dir = resources._LAUNCH_DIR
|
||||
launch_files = list(launches_dir.glob("*.json"))
|
||||
assert not launch_files
|
||||
cid = result.get("containment", {}).get("id")
|
||||
assert cid
|
||||
matching = [record for record in published if record.get("containment_id") == cid]
|
||||
assert len(matching) == 1
|
||||
assert matching[0]["processes"] == []
|
||||
|
||||
|
||||
def test_missing_start_token_security_negative(workspace, monkeypatch):
|
||||
import uuid
|
||||
import src.process_lifecycle as pl
|
||||
from src.process_lifecycle import ProcessIdentity
|
||||
|
||||
op = ExactOperation.normalize("bash", "printf test")
|
||||
bound = resources.resolve_process_operation(authority(workspace), op, NativeBackendResource("bash"))
|
||||
launch = bound.launch
|
||||
cid = uuid.uuid4().hex
|
||||
|
||||
resources.publish_launch(launch, authority(workspace), cid)
|
||||
containment._save_records({
|
||||
cid: {
|
||||
"id": cid,
|
||||
"launch_generation": launch.generation,
|
||||
"workspace": launch.scope.root.path,
|
||||
"pid": 77777,
|
||||
"start_token": None,
|
||||
"pgid": 77777,
|
||||
"mechanism": "process_group",
|
||||
}
|
||||
})
|
||||
|
||||
created_identities = []
|
||||
orig_identity_init = ProcessIdentity.__init__
|
||||
def spy_identity_init(self, pid, start_token, pgid=None):
|
||||
created_identities.append((pid, start_token, pgid))
|
||||
return orig_identity_init(self, pid, start_token, pgid=pgid)
|
||||
|
||||
monkeypatch.setattr(ProcessIdentity, "__init__", spy_identity_init)
|
||||
monkeypatch.setattr(process_ownership, "process_table", lambda *a, **k: pytest.fail("PID rediscovery attempted via process_table"))
|
||||
monkeypatch.setattr(process_ownership, "start_token", lambda *a, **k: pytest.fail("PID rediscovery attempted via start_token"))
|
||||
|
||||
resources.attach_containment_processes(launch, cid)
|
||||
|
||||
# 1. No ProcessIdentity created for this unobservable process
|
||||
assert not any(pid == 77777 for pid, token, pgid in created_identities)
|
||||
|
||||
# 2. No process authority published
|
||||
published = json.loads(resources.launch_path(launch.generation).read_text())
|
||||
assert published["processes"] == []
|
||||
|
||||
# 3. No signal authority
|
||||
fake_ident = ProcessIdentity(77777, None, 77777)
|
||||
assert fake_ident.verdict() == process_ownership.UNVERIFIABLE
|
||||
assert pl.signal_identity(fake_ident, 15) is False
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Permanent linkage loss suppresses continuation without granting authority."""
|
||||
from types import SimpleNamespace
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from src import bg_jobs, bg_monitor
|
||||
from src.agent_runtime import process_resources as resources
|
||||
from tests.test_background_resource_identity import store, seed
|
||||
from src.agent_runtime.resources import ResourceIdentityError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def monitor_session(monkeypatch):
|
||||
messages = []
|
||||
sess = SimpleNamespace(id='thread', owner='alice', model='test-model', get_context_messages=lambda: [])
|
||||
sm = SimpleNamespace(get_session=lambda sid: sess, add_message=lambda *args: messages.append(args), save_sessions=lambda: None)
|
||||
import src.ai_interaction as ai
|
||||
monkeypatch.setattr(ai, 'get_session_manager', lambda: sm)
|
||||
import src.agent_runs
|
||||
monkeypatch.setattr(src.agent_runs, 'is_active', lambda sid: False)
|
||||
async def drain(*args, **kwargs):
|
||||
messages.append('drained')
|
||||
return 'continued', []
|
||||
monkeypatch.setattr(bg_monitor, '_drain_agent', drain)
|
||||
return messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize('damage', ['missing', 'corrupt', 'wrong_owner', 'wrong_pid'])
|
||||
async def test_invalid_linkage_is_terminal_without_message(store, monkeypatch, monitor_session, damage):
|
||||
resource, rec = seed(store, status='done')
|
||||
sidecar = bg_jobs._JOBS_DIR / 'job.authority.json'
|
||||
if damage == 'missing': sidecar.unlink()
|
||||
elif damage == 'corrupt': sidecar.write_text('{}')
|
||||
else:
|
||||
jobs = bg_jobs._load()
|
||||
if damage == 'wrong_owner': jobs['job']['resource_identity']['owner'] = 'bob'
|
||||
else: jobs['job']['pid'] = 99999
|
||||
bg_jobs._save(jobs)
|
||||
rec = jobs['job']
|
||||
# Invalid data must not even be rendered into a synthetic result message.
|
||||
monkeypatch.setattr(bg_monitor, '_background_result_message', lambda rec: pytest.fail('Invalid result rendered'))
|
||||
assert await bg_monitor._process_followup(rec) is bg_monitor.FollowupResult.TERMINAL_UNFOLLOWABLE
|
||||
assert not monitor_session
|
||||
assert not bg_jobs.pending_followups()
|
||||
assert bg_jobs.peek('job')['followup_state'] == 'terminal_unfollowable'
|
||||
assert not bg_jobs.peek('job').get('followed_up')
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resources.validate_job(resource)
|
||||
|
||||
|
||||
async def test_busy_session_retries_then_continues(store, monkeypatch, monitor_session):
|
||||
_, rec = seed(store, status='done')
|
||||
import src.agent_runs
|
||||
monkeypatch.setattr(src.agent_runs, 'is_active', lambda sid: True)
|
||||
assert await bg_monitor._process_followup(rec) is bg_monitor.FollowupResult.RETRYABLE_LATER
|
||||
assert bg_jobs.pending_followups() and not monitor_session
|
||||
monkeypatch.setattr(src.agent_runs, 'is_active', lambda sid: False)
|
||||
assert await bg_monitor._process_followup(rec) is bg_monitor.FollowupResult.COMPLETED
|
||||
assert bg_jobs.peek('job')['followed_up']
|
||||
assert monitor_session and not bg_jobs.pending_followups()
|
||||
|
||||
|
||||
async def test_terminal_record_prunes_exact_generation(store, monitor_session):
|
||||
resource, rec = seed(store, status='done')
|
||||
rec['ended_at'] = time.time() - bg_jobs._RETENTION_S - 10
|
||||
bg_jobs._save({'job': rec})
|
||||
(bg_jobs._JOBS_DIR / 'job.authority.json').unlink()
|
||||
assert await bg_monitor._process_followup(rec) is bg_monitor.FollowupResult.TERMINAL_UNFOLLOWABLE
|
||||
assert not bg_jobs.pending_followups()
|
||||
assert bg_jobs.peek('job') is None
|
||||
assert not resources.launch_path(resource.generation).exists()
|
||||
assert not monitor_session
|
||||
|
||||
|
||||
def test_stale_terminal_snapshot_cannot_suppress_new_generation(store):
|
||||
_, old = seed(store, status='done')
|
||||
new, _ = seed(store, status='done')
|
||||
assert not bg_jobs.mark_unfollowable('job', expected_record=old)
|
||||
assert 'followup_state' not in bg_jobs.peek('job')
|
||||
assert resources.launch_path(new.generation).exists()
|
||||
|
||||
|
||||
async def test_linkage_lost_during_continuation_cannot_deliver(store, monkeypatch, monitor_session):
|
||||
_, rec = seed(store, status='done')
|
||||
async def interrupted(*args, **kwargs):
|
||||
(bg_jobs._JOBS_DIR / 'job.authority.json').unlink()
|
||||
return 'must not be delivered', []
|
||||
monkeypatch.setattr(bg_monitor, '_drain_agent', interrupted)
|
||||
assert await bg_monitor._process_followup(rec) is bg_monitor.FollowupResult.TERMINAL_UNFOLLOWABLE
|
||||
assert not monitor_session
|
||||
assert not bg_jobs.pending_followups()
|
||||
|
||||
|
||||
async def test_stale_terminal_outcome_retries_current_record(store, monkeypatch, monitor_session):
|
||||
_, old = seed(store, status='done')
|
||||
seed(store, status='done')
|
||||
async def terminal(rec): return bg_monitor.FollowupResult.TERMINAL_UNFOLLOWABLE
|
||||
monkeypatch.setattr(bg_monitor, '_run_followup', terminal)
|
||||
assert await bg_monitor._process_followup(old) is bg_monitor.FollowupResult.RETRYABLE_LATER
|
||||
assert bg_jobs.pending_followups()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('linkage', ['valid', 'damaged'])
|
||||
async def test_deleted_session_settles_only_a_validated_launch(store, monkeypatch, monitor_session, linkage):
|
||||
"""Wave 4: a job retired for a deleted session must not leave its launch effect RUNNING."""
|
||||
resource, rec = seed(store, status='done')
|
||||
if linkage == 'damaged':
|
||||
(bg_jobs._JOBS_DIR / 'job.authority.json').write_text('{}')
|
||||
import src.ai_interaction as ai
|
||||
monkeypatch.setattr(ai, 'get_session_manager', lambda: SimpleNamespace(get_session=lambda sid: None))
|
||||
settled = []
|
||||
monkeypatch.setattr(bg_monitor, '_settle_launch_effect', lambda job, record: settled.append(job))
|
||||
assert await bg_monitor._process_followup(rec) is bg_monitor.FollowupResult.TERMINAL_UNFOLLOWABLE
|
||||
assert settled == ([resource] if linkage == 'valid' else [])
|
||||
assert not monitor_session
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Wave 3 metadata requires a real allowlisted Linux producer artifact."""
|
||||
import pytest
|
||||
from src import browser_identity as browser
|
||||
from src.agent_runtime.resources import ResourceIdentityError
|
||||
|
||||
|
||||
@pytest.mark.parametrize('system,machine', [('Darwin', 'x86_64'), ('Darwin', 'arm64'),
|
||||
('Windows', 'AMD64'), ('Windows', 'ARM64'), ('Linux', 'riscv64')])
|
||||
async def test_unsupported_platform_fails_before_producer_execution(monkeypatch, system, machine):
|
||||
monkeypatch.setattr(browser.platform, 'system', lambda: system)
|
||||
monkeypatch.setattr(browser.platform, 'machine', lambda: machine)
|
||||
async def forbidden(*args, **kwargs): pytest.fail('Unsupported producer was executed')
|
||||
monkeypatch.setattr(browser, 'run_client', forbidden)
|
||||
with pytest.raises(ResourceIdentityError, match='Unsupported browser producer platform'):
|
||||
await browser.trusted_producer()
|
||||
|
||||
|
||||
def test_observed_release_hash_contract_is_explicit():
|
||||
assert set(browser.PRODUCER_HASHES) == {'linux-x64', 'linux-arm64'}
|
||||
assert browser.PRODUCER_VERSION == '0.35.0'
|
||||
assert browser.SESSION_ACTIONS == {'session_info'}
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Unexpected programming defects must not look like successful policy denial."""
|
||||
import pytest
|
||||
from src import tool_execution
|
||||
from src.agent_runtime.authority import ExactOperation, OperationGrant, RequestAuthority
|
||||
from src.agent_runtime.resources import ResourceIdentityError
|
||||
from src.tool_capabilities import ToolRunSecurityContext
|
||||
from src.tool_types import ToolBlock
|
||||
|
||||
|
||||
@pytest.mark.parametrize('seam,tool,content', [
|
||||
('bind_backend_for_operation', 'bash', 'printf probe'),
|
||||
('resolve_process_operation', 'bash', 'printf probe'),
|
||||
('admit_owned_operation', 'edit_document', '{"document_id":"doc","content":"changed"}'),
|
||||
])
|
||||
@pytest.mark.parametrize('error_type', [AttributeError, ResourceIdentityError, ValueError, TypeError])
|
||||
async def test_binding_errors_keep_diagnostic_identity(tmp_path, monkeypatch, seam, tool, content, error_type):
|
||||
authority = RequestAuthority('request', 'alice', 'thread', str(tmp_path), (OperationGrant(tool),))
|
||||
monkeypatch.setattr(tool_execution, '_owner_is_admin', lambda owner: True)
|
||||
def broken(*args, **kwargs):
|
||||
raise error_type('injected defect')
|
||||
monkeypatch.setattr(tool_execution, seam, broken)
|
||||
args = dict(owner='alice', session_id='thread', workspace=str(tmp_path), request_authority=authority,
|
||||
security_context=ToolRunSecurityContext())
|
||||
if error_type is AttributeError:
|
||||
with pytest.raises(AttributeError, match='injected defect'):
|
||||
await tool_execution.execute_tool_block(ToolBlock(tool, content), **args)
|
||||
else:
|
||||
_, result = await tool_execution.execute_tool_block(ToolBlock(tool, content), **args)
|
||||
assert result['failure_kind'] == 'resource_identity_denied' and result['blocked']
|
||||
|
||||
|
||||
async def test_argument_normalization_defect_propagates(tmp_path, monkeypatch):
|
||||
authority = RequestAuthority('request', 'alice', 'thread', str(tmp_path), (OperationGrant('bash'),))
|
||||
def broken(*args, **kwargs): raise AttributeError('internal-only diagnostic')
|
||||
monkeypatch.setattr(ExactOperation, 'normalize', broken)
|
||||
with pytest.raises(AttributeError):
|
||||
await tool_execution.execute_tool_block(ToolBlock('bash', 'printf probe'), owner='alice',
|
||||
session_id='thread', workspace=str(tmp_path), request_authority=authority,
|
||||
security_context=ToolRunSecurityContext())
|
||||
|
||||
|
||||
@pytest.mark.parametrize('content', ['{invalid', None])
|
||||
async def test_expected_bad_input_still_has_authority_denial(tmp_path, content):
|
||||
authority = RequestAuthority('request', 'alice', 'thread', str(tmp_path), (OperationGrant('api_call'),))
|
||||
_, result = await tool_execution.execute_tool_block(ToolBlock('api_call', content), owner='alice',
|
||||
session_id='thread', workspace=str(tmp_path), request_authority=authority,
|
||||
security_context=ToolRunSecurityContext())
|
||||
assert result['failure_kind'] == 'request_authority_denied'
|
||||
@@ -0,0 +1,217 @@
|
||||
"""Structural dispatch cost and exact publication lifetime regressions."""
|
||||
import asyncio
|
||||
from dataclasses import replace
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from core.atomic_io import atomic_write_json
|
||||
from src import bg_jobs, containment, process_ownership
|
||||
from src.agent_runtime import resources as identities
|
||||
from src.agent_runtime.authority import ExactOperation, bind_request_authority
|
||||
from src.agent_runtime.resources import NativeBackendResource, ResourceIdentityError
|
||||
from src.agent_tools.subprocess_tools import BashTool
|
||||
from src.process_lifecycle import ProcessIdentity
|
||||
from tests.test_runtime_resource_integration import workspace, authority, dispatch
|
||||
from tests.test_background_resource_identity import seed
|
||||
from src.agent_runtime import process_resources as resources
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tool,content', [('bash', 'printf guarded'), ('python', 'print("guarded")')])
|
||||
async def test_real_dispatch_scans_workspace_once_per_binding(workspace, monkeypatch, tool, content):
|
||||
(workspace / 'child').mkdir()
|
||||
(workspace / 'child' / 'link').symlink_to(workspace / 'child')
|
||||
calls = []
|
||||
walk = os.walk
|
||||
def counted(*args, **kwargs):
|
||||
calls.append(args[0])
|
||||
return walk(*args, **kwargs)
|
||||
monkeypatch.setattr(os, 'walk', counted)
|
||||
admitted = authority(workspace, tool)
|
||||
for _ in range(2):
|
||||
calls.clear()
|
||||
_, result = await dispatch(admitted, tool, content)
|
||||
assert result['exit_code'] == 0, result
|
||||
assert calls == [workspace]
|
||||
assert not list(resources._LAUNCH_DIR.glob('*.json'))
|
||||
|
||||
|
||||
async def test_alias_created_after_resolution_is_denied_at_binding(workspace):
|
||||
admitted = authority(workspace)
|
||||
op = ExactOperation.normalize('bash', 'printf safe')
|
||||
bound = resources.resolve_process_operation(admitted, op, NativeBackendResource('bash'))
|
||||
resources._LAUNCH_DIR.mkdir(parents=True)
|
||||
state = resources._LAUNCH_DIR / ('a' * 32 + '.json')
|
||||
state.write_text('{}')
|
||||
(workspace / 'alias').symlink_to(state)
|
||||
with bind_request_authority(admitted), pytest.raises(ResourceIdentityError):
|
||||
with resources.bind_process_operation(bound):
|
||||
pytest.fail('New control-plane alias admitted')
|
||||
|
||||
|
||||
async def test_publication_retained_during_launch_and_retired_after_teardown(workspace, monkeypatch):
|
||||
entered, resume = asyncio.Event(), asyncio.Event()
|
||||
run = containment.run
|
||||
paths = []
|
||||
async def held(grant, command, **kwargs):
|
||||
launch = resources.active_process_operation().launch
|
||||
path = resources.launch_path(launch.generation)
|
||||
assert path.is_file()
|
||||
paths.append(path)
|
||||
entered.set()
|
||||
await resume.wait()
|
||||
return await run(grant, command, **kwargs)
|
||||
monkeypatch.setattr(containment, 'run', held)
|
||||
task = asyncio.create_task(dispatch(authority(workspace), 'bash', 'printf foreground'))
|
||||
await asyncio.wait_for(entered.wait(), 5)
|
||||
assert paths[0].is_file()
|
||||
resume.set()
|
||||
_, result = await task
|
||||
assert result['exit_code'] == 0 and result['teardown']['dead']
|
||||
assert not paths[0].exists()
|
||||
|
||||
|
||||
async def test_retired_publication_cannot_replay_bound_reservation(workspace):
|
||||
admitted = authority(workspace)
|
||||
op = ExactOperation.normalize('bash', 'printf once')
|
||||
bound = resources.resolve_process_operation(admitted, op, NativeBackendResource('bash'))
|
||||
from src import tool_execution
|
||||
token = tool_execution._active_workspace.set(str(workspace))
|
||||
try:
|
||||
with bind_request_authority(admitted), resources.bind_process_operation(bound):
|
||||
ctx = {'owner': 'alice', 'session_id': 'thread'}
|
||||
first = await BashTool().execute(op.input, ctx)
|
||||
assert first['exit_code'] == 0
|
||||
assert not resources.launch_path(bound.launch.generation).exists()
|
||||
second = await BashTool().execute(op.input, ctx)
|
||||
assert second['failure_kind'] == 'resource_identity_denied'
|
||||
copy = replace(bound, exact_approval=None)
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
with resources.bind_process_operation(copy):
|
||||
pytest.fail('Approval copy renewed a consumed launch')
|
||||
finally:
|
||||
tool_execution._active_workspace.reset(token)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('publication', [[], None, 'malformed'])
|
||||
def test_nonobject_publication_cannot_be_retired(workspace, publication):
|
||||
resource, rec = seed(workspace, status='done')
|
||||
path = resources.launch_path(resource.generation)
|
||||
path.write_text(json.dumps(publication))
|
||||
launch = identities.ProcessLaunchResource.from_dict(rec['launch_resource'])
|
||||
assert not resources.retire_launch(launch, resource.containment_id, job=resource)
|
||||
assert json.loads(path.read_text()) == publication
|
||||
|
||||
|
||||
async def test_corrupt_publication_retirement_preserves_command_result(workspace, monkeypatch):
|
||||
attach = resources.attach_containment_processes
|
||||
paths = []
|
||||
def corrupt_after_attachment(launch, containment_id):
|
||||
observed = attach(launch, containment_id)
|
||||
path = resources.launch_path(launch.generation)
|
||||
path.write_text('[]')
|
||||
paths.append(path)
|
||||
return observed
|
||||
monkeypatch.setattr(resources, 'attach_containment_processes', corrupt_after_attachment)
|
||||
_, result = await dispatch(authority(workspace), 'bash', 'printf completed')
|
||||
assert result['exit_code'] == 0 and result['output'] == 'completed', result
|
||||
assert paths[0].read_text() == '[]'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('status,followed_up,old,removed', [
|
||||
('running', True, True, False), ('done', False, True, False),
|
||||
('done', True, False, False), ('done', True, True, True), ('failed', True, True, True),
|
||||
])
|
||||
def test_background_publication_tracks_supported_history_lifetime(workspace, status, followed_up, old, removed):
|
||||
resource, rec = seed(workspace, status=status)
|
||||
rec.update(followed_up=followed_up, ended_at=time.time() - (bg_jobs._RETENTION_S + 10 if old else 0))
|
||||
jobs = {'job': rec}
|
||||
bg_jobs._save(jobs)
|
||||
assert resources.launch_path(resource.generation).exists()
|
||||
bg_jobs._prune(jobs, time.time())
|
||||
assert resources.launch_path(resource.generation).exists() is not removed
|
||||
assert ('job' not in jobs) is removed
|
||||
if removed:
|
||||
bg_jobs._save(jobs)
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resources.validate_job(resource)
|
||||
|
||||
|
||||
def test_old_generation_retirement_cannot_delete_replacement(workspace):
|
||||
old, rec = seed(workspace, status='done')
|
||||
new, _ = seed(workspace, status='done')
|
||||
old_launch = identities.ProcessLaunchResource.from_dict(rec['launch_resource'])
|
||||
assert resources.retire_launch(old_launch, old.containment_id, job=old)
|
||||
assert resources.launch_path(new.generation).is_file()
|
||||
# Even a replaced file at the old generation's slot is not deletable by old linkage.
|
||||
replacement = json.loads(resources.launch_path(new.generation).read_text())
|
||||
atomic_write_json(resources.launch_path(old.generation), replacement)
|
||||
assert not resources.retire_launch(old_launch, old.containment_id, job=old)
|
||||
assert resources.launch_path(old.generation).is_file()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('manager,release,retired', [
|
||||
(process_ownership.OWNED, False, False), (process_ownership.UNVERIFIABLE, False, False),
|
||||
(process_ownership.OWNED, True, False), (process_ownership.UNVERIFIABLE, True, False),
|
||||
(process_ownership.GONE, False, True), (process_ownership.FOREIGN, False, True),
|
||||
(process_ownership.GONE, True, True),
|
||||
])
|
||||
def test_startup_retirement_does_not_invent_process_death(workspace, monkeypatch, manager, release, retired):
|
||||
admitted = authority(workspace)
|
||||
launch = resources.resolve_process_operation(admitted, ExactOperation.normalize('bash', 'printf recovery'), NativeBackendResource('bash')).launch
|
||||
cid = 'receipt'
|
||||
resources.publish_launch(launch, admitted, cid)
|
||||
atomic_write_json(containment._store_path(), {cid: {'id': cid, 'launch_generation': launch.generation,
|
||||
'manager_pid': 123, 'manager_token': 'old-manager', 'release': {'dead': release}}})
|
||||
monkeypatch.setattr(process_ownership, 'verify', lambda *args: manager)
|
||||
assert resources.prune_foreground_publications() == int(retired)
|
||||
assert resources.launch_path(launch.generation).exists() is not retired
|
||||
assert containment._load_records()[cid]['release']['dead'] is release
|
||||
|
||||
|
||||
def test_restart_never_prunes_background_linkage(workspace, monkeypatch):
|
||||
resource, _ = seed(workspace, status='done')
|
||||
monkeypatch.setattr(process_ownership, 'verify', lambda *args: process_ownership.GONE)
|
||||
assert resources.prune_foreground_publications() == 0
|
||||
assert resources.launch_path(resource.generation).is_file()
|
||||
|
||||
|
||||
def test_snapshot_is_rebuilt_for_each_guard(workspace):
|
||||
resources._LAUNCH_DIR.mkdir(parents=True)
|
||||
target = workspace / 'data'; target.write_text('ordinary')
|
||||
(workspace / 'link').symlink_to(target)
|
||||
resources.guard_launch_workspace(identities.FilesystemRoot.seal(workspace))
|
||||
os.link(target, resources._LAUNCH_DIR / ('b' * 32 + '.json'))
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
resources.guard_launch_workspace(identities.FilesystemRoot.seal(workspace))
|
||||
|
||||
|
||||
def test_missing_receipt_publication_cannot_recover_authority(workspace):
|
||||
admitted = authority(workspace)
|
||||
launch = resources.resolve_process_operation(admitted, ExactOperation.normalize('bash', 'printf recovery'), NativeBackendResource('bash')).launch
|
||||
resources.publish_launch(launch, admitted, 'missing-receipt')
|
||||
assert resources.prune_foreground_publications() == 1
|
||||
assert not resources.launch_path(launch.generation).exists()
|
||||
assert not containment._load_records()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('receipt_data', ['{corrupt', '[]', '{"receipt":null}'])
|
||||
def test_unreadable_receipts_cannot_retire_live_consumers(workspace, receipt_data):
|
||||
admitted = authority(workspace)
|
||||
launch = resources.resolve_process_operation(admitted, ExactOperation.normalize('bash', 'printf pending'), NativeBackendResource('bash')).launch
|
||||
resources.publish_launch(launch, admitted, 'receipt')
|
||||
containment._store_path().write_text(receipt_data)
|
||||
assert resources.prune_foreground_publications() == 0
|
||||
assert resources.launch_path(launch.generation).is_file()
|
||||
|
||||
|
||||
def test_missing_manager_identity_cannot_retire_attachment(workspace, monkeypatch):
|
||||
admitted = authority(workspace)
|
||||
launch = resources.resolve_process_operation(admitted, ExactOperation.normalize('bash', 'printf pending'), NativeBackendResource('bash')).launch
|
||||
resources.publish_launch(launch, admitted, 'receipt')
|
||||
atomic_write_json(containment._store_path(), {'receipt': {'id': 'receipt',
|
||||
'launch_generation': launch.generation, 'release': {'dead': True}}})
|
||||
monkeypatch.setattr(process_ownership, 'verify', lambda *args: pytest.fail('Missing manager treated as observed'))
|
||||
assert resources.prune_foreground_publications() == 0
|
||||
assert resources.launch_path(launch.generation).is_file()
|
||||
@@ -0,0 +1,246 @@
|
||||
"""Local administration and exact Cookbook caller-to-route regressions."""
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import replace
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from starlette.requests import Request
|
||||
|
||||
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN, INTERNAL_TOOL_USER
|
||||
from routes import cookbook_routes, shell_routes
|
||||
from src import builtin_actions, tool_execution
|
||||
from src.agent_runtime.authority import ExactOperation, OperationGrant, RequestAuthority, bind_request_authority, seal_task_authority, restore_task_authority
|
||||
from src.agent_runtime.remote_resources import bind_backend_for_operation, bind_backend_operation
|
||||
from src.agent_runtime.local_model_control import CAPABILITY_HEADER, model_control_headers
|
||||
from src.agent_runtime.resources import ResourceIdentityError
|
||||
from src.tools import cookbook
|
||||
from src.tool_capabilities import ToolRunSecurityContext
|
||||
from src.tool_types import ToolBlock
|
||||
|
||||
|
||||
def request(host='127.0.0.1', headers=None, user=None):
|
||||
req = Request({'type': 'http', 'method': 'POST', 'scheme': 'http', 'path': '/api/shell/exec',
|
||||
'server': ('127.0.0.1', 7000), 'client': (host, 1234),
|
||||
'headers': [(k.lower().encode(), v.encode()) for k, v in (headers or {}).items()],
|
||||
'app': SimpleNamespace(state=SimpleNamespace(auth_manager=SimpleNamespace(is_admin=lambda u: u == 'alice')))})
|
||||
req.state.current_user = user
|
||||
return req
|
||||
|
||||
|
||||
@pytest.mark.parametrize('host,headers,allowed', [
|
||||
('127.0.0.1', {}, True), ('::1', {}, True), ('192.0.2.1', {}, False),
|
||||
('127.0.0.1', {'x-forwarded-for': '192.0.2.1'}, False),
|
||||
('127.0.0.1', {'forwarded': 'for=192.0.2.1'}, False),
|
||||
('127.0.0.1', {'cf-ray': 'proxy'}, False),
|
||||
('127.0.0.1', {'x-forwarded-proto': 'https'}, False),
|
||||
('127.0.0.1', {'sec-fetch-site': 'cross-site'}, False),
|
||||
('127.0.0.1', {'origin': 'https://evil.example'}, False),
|
||||
('127.0.0.1', {INTERNAL_TOOL_HEADER: 'forged'}, False),
|
||||
('127.0.0.1', {INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN}, False),
|
||||
])
|
||||
def test_auth_disabled_operator_transport(monkeypatch, host, headers, allowed):
|
||||
monkeypatch.setenv('AUTH_ENABLED', 'false')
|
||||
req = request(host, headers)
|
||||
if allowed:
|
||||
shell_routes._require_admin(req)
|
||||
else:
|
||||
with pytest.raises(HTTPException) as error:
|
||||
shell_routes._require_admin(req)
|
||||
assert error.value.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.parametrize('user,allowed', [('alice', True), ('bob', False), (None, False), ('api', False), (INTERNAL_TOOL_USER, False)])
|
||||
def test_auth_enabled_administration(monkeypatch, user, allowed):
|
||||
monkeypatch.setenv('AUTH_ENABLED', 'true')
|
||||
if allowed:
|
||||
shell_routes._require_admin(request('192.0.2.1', user=user))
|
||||
else:
|
||||
with pytest.raises(HTTPException):
|
||||
shell_routes._require_admin(request(user=user))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def control_app(tmp_path, monkeypatch):
|
||||
# Auth tests can reload middleware after collection. Authenticate the live
|
||||
# transport token used by real producers, rather than a collection snapshot.
|
||||
from core.middleware import INTERNAL_TOOL_HEADER, INTERNAL_TOOL_TOKEN, INTERNAL_TOOL_USER
|
||||
monkeypatch.setenv('AUTH_ENABLED', 'true')
|
||||
manager = SimpleNamespace(is_configured=True, users={'alice': {}}, is_admin=lambda u: u == 'alice')
|
||||
import core.auth
|
||||
monkeypatch.setattr(core.auth, 'AuthManager', lambda: manager)
|
||||
monkeypatch.setattr(tool_execution, '_owner_is_admin', lambda u: u == 'alice')
|
||||
monkeypatch.setattr(cookbook_routes, 'TMUX_LOG_DIR', tmp_path / 'tmux')
|
||||
state = tmp_path / 'cookbook.json'
|
||||
state.write_text(json.dumps({'presets': [{'name': 'preset', 'model': 'samplepkg', 'cmd': 'python -m pip install samplepkg'}]}))
|
||||
monkeypatch.setattr(cookbook_routes, 'COOKBOOK_STATE_FILE', str(state))
|
||||
monkeypatch.setattr(builtin_actions, 'COOKBOOK_STATE_FILE', str(state))
|
||||
spawned = []
|
||||
async def spawn(command, **kwargs):
|
||||
spawned.append(command)
|
||||
async def wait(): return 0
|
||||
async def read(): return b''
|
||||
return SimpleNamespace(returncode=0, wait=wait, stderr=SimpleNamespace(read=read))
|
||||
monkeypatch.setattr(asyncio, 'create_subprocess_shell', spawn)
|
||||
async def remote_probe(*args, **kwargs):
|
||||
async def communicate(): return b'tmux', b''
|
||||
return SimpleNamespace(returncode=0, communicate=communicate)
|
||||
monkeypatch.setattr(asyncio, 'create_subprocess_exec', remote_probe)
|
||||
import src.assistant_log
|
||||
monkeypatch.setattr(src.assistant_log, 'log_to_assistant', lambda *a, **k: None)
|
||||
async def endpoint(**kwargs): return {'added': True, 'endpoint_id': 'endpoint'}
|
||||
monkeypatch.setattr(cookbook, '_ensure_served_endpoint', endpoint)
|
||||
app = FastAPI()
|
||||
app.state.auth_manager = manager
|
||||
@app.middleware('http')
|
||||
async def attribution(req, next):
|
||||
if req.headers.get(INTERNAL_TOOL_HEADER) == INTERNAL_TOOL_TOKEN:
|
||||
req.state.current_user = req.headers.get('X-Odysseus-Owner') or INTERNAL_TOOL_USER
|
||||
return await next(req)
|
||||
app.include_router(cookbook_routes.setup_cookbook_routes())
|
||||
app.include_router(shell_routes.setup_shell_routes())
|
||||
real_client = httpx.AsyncClient
|
||||
def client_factory(*args, **kwargs):
|
||||
kwargs.setdefault('transport', httpx.ASGITransport(app=app))
|
||||
return real_client(*args, **kwargs)
|
||||
monkeypatch.setattr(httpx, 'AsyncClient', client_factory)
|
||||
return app, spawned, tmp_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tool,args', [
|
||||
('download_model', {'repo_id': 'org/model', 'local': True}),
|
||||
('serve_model', {'repo_id': 'samplepkg', 'cmd': 'python -m pip install samplepkg', 'local': True}),
|
||||
('serve_preset', {'name': 'preset'}),
|
||||
])
|
||||
async def test_real_local_tool_dispatch_reaches_real_model_route(control_app, tool, args):
|
||||
app, spawned, work = control_app
|
||||
authority = RequestAuthority('request', 'alice', 'thread', str(work), (OperationGrant(tool),))
|
||||
_, result = await tool_execution.execute_tool_block(ToolBlock(tool, json.dumps(args)), owner='alice',
|
||||
session_id='thread', workspace=str(work), request_authority=authority, security_context=ToolRunSecurityContext())
|
||||
assert result['exit_code'] == 0, result
|
||||
assert result['session_id'].startswith('cookbook-' if tool == 'download_model' else 'serve-')
|
||||
assert len(spawned) == 1 and 'tmux new-session' in spawned[0]
|
||||
|
||||
|
||||
async def test_real_scheduled_local_action_uses_restored_exact_authority(control_app):
|
||||
app, spawned, work = control_app
|
||||
command = json.dumps({'repo_id': 'samplepkg', 'cmd': 'python -m pip install samplepkg', 'set_default': False})
|
||||
snapshot = seal_task_authority(command, 'action', 'cookbook_serve', owner='alice')
|
||||
authority = restore_task_authority(snapshot, command, 'action', 'cookbook_serve', owner='alice')
|
||||
with bind_request_authority(authority):
|
||||
message, ok = await builtin_actions.action_cookbook_serve('alice', command=command)
|
||||
assert ok, message
|
||||
assert len(spawned) == 1
|
||||
with bind_request_authority(authority):
|
||||
_, ok = await builtin_actions.action_cookbook_serve('alice', command=command.replace('samplepkg', 'changedpkg'))
|
||||
assert not ok and len(spawned) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('host,headers', [
|
||||
('127.0.0.1', {INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN}),
|
||||
('192.0.2.1', {INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN}),
|
||||
('127.0.0.1', {INTERNAL_TOOL_HEADER: 'forged'}),
|
||||
('127.0.0.1', {CAPABILITY_HEADER: 'forged', INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN}),
|
||||
])
|
||||
async def test_header_only_cannot_launch(control_app, host, headers):
|
||||
app, spawned, _ = control_app
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, client=(host, 123)), base_url='http://127.0.0.1') as client:
|
||||
for path in ('/api/model/download', '/api/model/serve', '/api/shell/exec'):
|
||||
r = await client.post(path, json={'repo_id': 'org/model', 'cmd': 'printf nope', 'command': 'printf nope'}, headers=headers)
|
||||
assert r.status_code == 403
|
||||
assert not spawned
|
||||
|
||||
|
||||
@pytest.mark.parametrize('substitute', ['body', 'route', 'owner', 'remote', 'proxy', 'replay'])
|
||||
async def test_capability_exact_transport_binding(control_app, substitute):
|
||||
app, spawned, work = control_app
|
||||
content = json.dumps({'repo_id': 'org/model', 'local': True})
|
||||
authority = RequestAuthority('request', 'alice', 'thread', str(work), (OperationGrant('download_model'),))
|
||||
operation = ExactOperation.normalize('download_model', content)
|
||||
backend = bind_backend_for_operation(authority, operation)
|
||||
body = {'repo_id': 'org/model'}
|
||||
with bind_request_authority(authority), bind_backend_operation(backend), model_control_headers('download_model', content, 'alice', body) as headers:
|
||||
changed = dict(headers); payload = dict(body); path = '/api/model/download'; host = '127.0.0.1'
|
||||
if substitute == 'body': payload['repo_id'] = 'org/changed'
|
||||
if substitute == 'route': path = '/api/model/serve'
|
||||
if substitute == 'owner': changed['X-Odysseus-Owner'] = 'bob'
|
||||
if substitute == 'remote': host = '192.0.2.1'
|
||||
if substitute == 'proxy': changed['x-forwarded-for'] = '192.0.2.1'
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, client=(host, 123)), base_url='http://127.0.0.1') as client:
|
||||
if substitute == 'replay':
|
||||
assert (await client.post(path, json=payload, headers=changed)).status_code == 200
|
||||
r = await client.post(path, json=payload, headers=changed)
|
||||
assert r.status_code == 403
|
||||
assert len(spawned) == (1 if substitute == 'replay' else 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field', ['owner', 'request_id', 'session_id'])
|
||||
def test_producer_wrong_application_binding(control_app, field):
|
||||
_, _, work = control_app
|
||||
content = '{"repo_id":"org/model","local":true}'
|
||||
authority = RequestAuthority('request', 'alice', 'thread', str(work), (OperationGrant('download_model'),))
|
||||
backend = bind_backend_for_operation(authority, ExactOperation.normalize('download_model', content))
|
||||
changed = replace(authority, **{field: 'replacement'}, resource_roots=None, backend_resources=None, owned_scopes=None)
|
||||
with bind_request_authority(changed), bind_backend_operation(backend), pytest.raises(ResourceIdentityError):
|
||||
with model_control_headers('download_model', content, 'alice', {'repo_id': 'org/model'}):
|
||||
pytest.fail('Substituted producer obtained a capability')
|
||||
|
||||
|
||||
async def test_remote_route_semantics_remain_unchanged(control_app):
|
||||
app, spawned, _ = control_app
|
||||
async with httpx.AsyncClient(base_url='http://127.0.0.1') as client:
|
||||
r = await client.post('/api/model/download', json={'repo_id': 'org/model', 'remote_host': 'gpu.example'},
|
||||
headers={INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN})
|
||||
assert r.status_code == 200 and r.json()['ok'], r.text
|
||||
assert len(spawned) == 1 and 'ssh ' in spawned[0]
|
||||
|
||||
|
||||
async def test_auth_disabled_local_route_usage(control_app, monkeypatch):
|
||||
app, spawned, _ = control_app
|
||||
monkeypatch.setenv('AUTH_ENABLED', 'false')
|
||||
async with httpx.AsyncClient(base_url='http://127.0.0.1') as client:
|
||||
shell = await client.post('/api/shell/exec', json={'command': ''})
|
||||
state = await client.post('/api/cookbook/state', json={'tasks': []})
|
||||
launch = await client.post('/api/model/download', json={'repo_id': 'org/model'})
|
||||
assert shell.status_code == state.status_code == launch.status_code == 200
|
||||
assert launch.json()['ok'] and len(spawned) == 1
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, client=('192.0.2.1', 1)), base_url='http://127.0.0.1') as client:
|
||||
for path, body in [('/api/shell/exec', {'command': ''}), ('/api/cookbook/state', {'tasks': []}), ('/api/model/download', {'repo_id': 'org/model'})]:
|
||||
assert (await client.post(path, json=body)).status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.parametrize('host,internal', [('127.0.0.1', True), ('192.0.2.1', False)])
|
||||
async def test_scoped_wrapper_cannot_bypass_native_control(control_app, monkeypatch, host, internal):
|
||||
from routes.codex_routes import setup_codex_routes
|
||||
app, spawned, _ = control_app
|
||||
app.include_router(setup_codex_routes())
|
||||
monkeypatch.setenv('AUTH_ENABLED', 'false')
|
||||
headers = {INTERNAL_TOOL_HEADER: INTERNAL_TOOL_TOKEN} if internal else {}
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app, client=(host, 123)), base_url='http://127.0.0.1') as client:
|
||||
r = await client.post('/api/codex/cookbook/serve', json={'repo_id': 'samplepkg', 'cmd': 'python -m pip install samplepkg'}, headers=headers)
|
||||
assert r.status_code == 403 and not spawned
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path', ['/api/codex/cookbook/serve', '/api/codex/cookbook/stop/job', '/api/codex/%63ookbook/serve'])
|
||||
async def test_generic_app_api_cannot_substitute_scoped_wrapper(control_app, path):
|
||||
_, spawned, work = control_app
|
||||
authority = RequestAuthority('request', 'alice', 'thread', str(work), (OperationGrant('app_api'),))
|
||||
content = json.dumps({'action': 'call', 'method': 'POST', 'path': path, 'body': {'repo_id': 'samplepkg', 'cmd': 'python -m pip install samplepkg'}})
|
||||
_, result = await tool_execution.execute_tool_block(ToolBlock('app_api', content), owner='alice',
|
||||
session_id='thread', workspace=str(work), request_authority=authority, security_context=ToolRunSecurityContext())
|
||||
assert result['failure_kind'] == 'resource_identity_denied' and not spawned
|
||||
|
||||
|
||||
async def test_direct_endpoint_call_keeps_producer_gate(control_app, monkeypatch):
|
||||
from routes.cookbook_helpers import ModelDownloadRequest
|
||||
app, spawned, _ = control_app
|
||||
router = cookbook_routes.setup_cookbook_routes()
|
||||
endpoint = next(route.endpoint for route in router.routes if getattr(route, 'path', '') == '/api/model/download')
|
||||
monkeypatch.setenv('AUTH_ENABLED', 'false')
|
||||
req = request('192.0.2.1')
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await endpoint(req, ModelDownloadRequest(repo_id='org/model'))
|
||||
assert exc.value.status_code == 403 and not spawned
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Closed inheritance is the complete subprocess environment boundary."""
|
||||
from src import tool_execution
|
||||
|
||||
def test_closed_subprocess_environment_drops_all_unlisted_credentials(monkeypatch):
|
||||
from unittest.mock import patch
|
||||
import os
|
||||
ambient = {'PATH': '/usr/bin', 'LANG': 'C.UTF-8', 'OPENAI_API_KEY': 'secret', 'HF_TOKEN': 'secret',
|
||||
'AUTH_ENABLED': 'false', 'DATABASE_URL': 'secret', 'PATH_TOKEN': 'secret',
|
||||
'AWS_SECRET_ACCESS_KEY': 'secret', 'ARBITRARY': 'secret', 'HOME': '/server/secret'}
|
||||
with patch.dict(os.environ, ambient, clear=True):
|
||||
child = tool_execution._agent_subprocess_env()
|
||||
assert child['PATH'] == '/usr/bin'
|
||||
assert child['HOME'] == tool_execution._AGENT_WORKDIR
|
||||
assert set(child) <= tool_execution._SAFE_SUBPROCESS_VARS | {'HOME', 'TERM', 'COLUMNS', 'LINES'}
|
||||
assert all(child.get(name) != value for name, value in ambient.items() if name not in {'PATH', 'LANG'})
|
||||
|
||||
|
||||
async def test_real_python_child_does_not_inherit_ambient_credentials(workspace, monkeypatch):
|
||||
from tests.test_runtime_resource_integration import authority, dispatch
|
||||
for name in ('OPENAI_API_KEY', 'HF_TOKEN', 'PATH_TOKEN', 'DATABASE_URL', 'ODYSSEUS_INTERNAL_TOKEN'):
|
||||
monkeypatch.setenv(name, 'never-inherit-this-value')
|
||||
_, result = await dispatch(authority(workspace, 'python'), 'python',
|
||||
'import os\nprint(any(v == "never-inherit-this-value" for v in os.environ.values()))')
|
||||
assert result['exit_code'] == 0 and result['output'] == 'False'
|
||||
|
||||
|
||||
from tests.test_runtime_resource_integration import workspace
|
||||
Reference in New Issue
Block a user