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:
Alexandre Teixeira
2026-10-03 01:13:36 +01:00
31 changed files with 1416 additions and 89 deletions
+8 -2
View File
@@ -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(
+8
View File
@@ -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"
+154 -1
View File
@@ -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
+115
View File
@@ -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
+21
View File
@@ -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'}
+48
View File
@@ -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'
+217
View File
@@ -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()
+246
View 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