refactor(skills): extract evaluation and audit orchestration (#6711)

* refactor(skills): extract evaluation and audit orchestration

* chore(security): scope OpenAPI digest Gitleaks exception

* refactor(skills): establish domain-oriented service boundaries

* refactor(skills): address architecture review feedback

* fix(skills): restore prompt seams and harden signature contracts

* test(skills): restore ReDoS coverage and simplify API baseline
This commit is contained in:
Alexandre Teixeira
2026-10-15 09:30:16 +01:00
committed by GitHub
parent f1a47f3812
commit b2e717e28f
19 changed files with 9190 additions and 1704 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -26,7 +26,7 @@ def _fast(fn, *args):
return result
# -- closed <think> blocks (skills_routes x4, memory_extractor.audit_memories) --
# -- closed <think> blocks (skills.evaluation x4, memory_extractor.audit_memories) --
_THINK_REF = re.compile(r"<think(?:ing)?>[\s\S]*?</think(?:ing)?>", re.I)
@@ -51,7 +51,11 @@ def test_strip_closed_think_blocks_unclosed_opener_flood_is_linear():
def test_lazy_think_regex_is_gone_from_llm_review_parsers():
for rel in ("routes/skills_routes.py", "services/memory/memory_extractor.py"):
for rel in (
"routes/skills_routes.py",
"services/skills/evaluation.py",
"services/memory/memory_extractor.py",
):
source = (_ROOT / rel).read_text(encoding="utf-8")
assert r"<think(?:ing)?>[\s\S]*?</think(?:ing)?>" not in source, rel
+2 -2
View File
@@ -3,7 +3,7 @@
Both had two adjacent `\\s`-matching quantifiers that backtrack O(n^2) when the
rest of the pattern fails on a whitespace flood:
* `routes/skills_routes.py` `_VERDICT_PROSE_RE` — `["\\'\\s:]*\\s*` (the class
* `services/skills/evaluation.py` `_VERDICT_PROSE_RE`: `["\\'\\s:]*\\s*` (the class
already matches `\\s`) over a teacher/verifier model's prose verdict.
* `src/agent_loop.py` `_EXPLICIT_CONTINUATION_RE` — `\\s*[.!?]*\\s*$` over a
user's terse reply.
@@ -18,7 +18,7 @@ import time
import pytest
import src.agent_tools # noqa: F401 (break agent_tools<->agent_loop import cycle)
from routes.skills_routes import _VERDICT_PROSE_RE
from services.skills.evaluation import _VERDICT_PROSE_RE
from src.agent_loop import _EXPLICIT_CONTINUATION_RE, _is_explicit_continuation
_BUDGET_S = 4.0
+485
View File
@@ -0,0 +1,485 @@
"""Offline contracts captured against the merged skills baseline before extraction."""
import asyncio
from dataclasses import fields, is_dataclass
from difflib import unified_diff
import inspect
import json
from contextvars import ContextVar
from pathlib import Path
from typing import Annotated, get_args, get_origin
import pytest
from fastapi import FastAPI
from starlette.requests import Request
from routes import skills_routes as routes
FIXTURES = Path(__file__).parent / 'fixtures'
def _qualified_name(value):
return f'{value.__module__}.{value.__qualname__}'
def _value_contract(value):
if value is inspect.Signature.empty:
return {'empty': True}
if value is None or type(value) in (bool, int, float, str):
return {'type': _qualified_name(type(value)), 'value': value}
if type(value) in (list, tuple):
return {'type': _qualified_name(type(value)),
'items': [_value_contract(item) for item in value]}
if type(value) is dict and all(type(key) is str for key in value):
return {'type': 'builtins.dict',
'items': {key: _value_contract(item) for key, item in value.items()}}
if is_dataclass(value) and not isinstance(value, type):
return {'type': _qualified_name(type(value)), 'fields': {
field.name: _value_contract(getattr(value, field.name))
for field in fields(value)
}}
raise TypeError(f'Unsupported contract value: {type(value)}')
def _annotation_contract(annotation):
if annotation is inspect.Signature.empty or annotation is None:
return _value_contract(annotation)
origin = get_origin(annotation)
if origin is Annotated:
base, *metadata = get_args(annotation)
return {'origin': _qualified_name(origin),
'annotation': _annotation_contract(base),
'metadata': [_value_contract(item) for item in metadata]}
if origin is not None:
return {'origin': _qualified_name(origin), 'module': annotation.__module__,
'arguments': [_annotation_contract(arg) for arg in get_args(annotation)]}
return {'type': _qualified_name(annotation)}
def _factory_contract(factory):
# Names do not identify closure state, partial arguments or arbitrary code.
# Only these known builtin constructors have a supported default contract.
if not any(factory is builtin for builtin in (list, tuple, dict, set, frozenset)):
raise TypeError('Unsupported default factory; requires an explicit compatibility contract')
return _qualified_name(factory)
def _signature_contract(target):
"""Freeze supported constructor structure without executing default factories."""
signature = inspect.signature(target)
model_fields = getattr(target, 'model_fields', {})
parameters = []
for parameter in signature.parameters.values():
field = model_fields.get(parameter.name)
if field is not None and field.default_factory is not None:
default = {'factory': _factory_contract(field.default_factory),
'marker': _qualified_name(type(parameter.default))}
else:
default = _value_contract(parameter.default)
parameters.append({'name': parameter.name, 'kind': parameter.kind.name,
'annotation': _annotation_contract(parameter.annotation),
'default': default})
contract = {'parameters': parameters,
'return': _annotation_contract(signature.return_annotation)}
if model_fields:
# Field names can be absent from the Python signature (validation aliases).
# These public FieldInfo attributes affect input validation independently
# of the generated signature and JSON schema. Rebuilt annotations retain
# field metadata, including constraints and strict/union validation modes.
contract['model_fields'] = {
name: {
'annotation': _annotation_contract(field.rebuild_annotation()),
'default': ({'factory': _factory_contract(field.default_factory),
'takes_validated_data': field.default_factory_takes_validated_data}
if field.default_factory is not None else
_value_contract(inspect.Signature.empty if field.is_required() else field.default)),
'validation': {attribute: _value_contract(getattr(field, attribute))
for attribute in ('alias', 'alias_priority', 'validation_alias',
'discriminator', 'validate_default')},
}
for name, field in model_fields.items()
}
# Includes inherited model settings such as validate_default and strict.
# Unsupported callables or values fail closed through _value_contract.
contract['model_config'] = _value_contract(target.model_config)
return contract
def _canonical_openapi(schema):
return json.dumps(schema, sort_keys=True, indent=2, ensure_ascii=True) + '\n'
def _assert_openapi_matches_baseline(schema):
expected = (FIXTURES / 'skills_openapi_baseline.json').read_text(encoding='utf-8')
actual = _canonical_openapi(schema)
if actual != expected:
baseline = json.loads(expected)
affected = []
for label, before, after in [
('paths', baseline.get('paths', {}), schema.get('paths', {})),
('schemas', baseline.get('components', {}).get('schemas', {}),
schema.get('components', {}).get('schemas', {})),
]:
changed = [key for key in sorted(before.keys() | after.keys())
if _canonical_openapi(before.get(key)) != _canonical_openapi(after.get(key))]
if changed:
affected.append(f'Changed {label}: {", ".join(changed)}\n')
pytest.fail('OpenAPI contract changed:\n' + ''.join(affected) + ''.join(unified_diff(
expected.splitlines(keepends=True), actual.splitlines(keepends=True),
fromfile='skills_openapi_baseline.json', tofile='generated OpenAPI',
)))
def endpoint(method, path, manager=None):
return next(r.endpoint for r in routes.setup_skills_routes(manager).routes
if method in r.methods and r.path == '/api/skills' + path)
def request(owner='alice', body=None):
async def receive():
return {'type': 'http.request', 'body': json.dumps(body or {}).encode()}
return Request({'type': 'http', 'method': 'POST', 'path': '/',
'headers': [(b'content-type', b'application/json')],
'state': {'user': owner}}, receive)
def test_api_and_python_signatures_match_baseline():
baseline = json.loads((FIXTURES / 'skills_api_baseline.json').read_text(encoding='utf-8'))
app = FastAPI()
router = routes.setup_skills_routes(object())
app.include_router(router)
schema = app.openapi()
_assert_openapi_matches_baseline(schema)
declarations = [[sorted(r.methods), r.path, r.name] for r in router.routes]
assert declarations == baseline['routes']
for name, signature in baseline['signatures'].items():
assert _signature_contract(getattr(routes, name)) == signature, name
def test_signature_contract_ignores_annotated_repr(monkeypatch):
target = routes.SkillImportUrlRequest
before = _signature_contract(target)
annotation = inspect.signature(target).parameters['url'].annotation
monkeypatch.setattr(type(annotation), '__repr__', lambda self: 'different Python rendering')
assert _signature_contract(target) == before
@pytest.mark.parametrize('change', ['name', 'order', 'kind', 'default', 'annotation', 'return'])
def test_signature_contract_detects_interface_changes(change):
def original(first: str, second: int = 1) -> bool:
pass
signature = inspect.signature(original)
first, second = signature.parameters.values()
parameters = [first, second]
if change == 'order':
parameters = [second.replace(default=inspect.Parameter.empty),
first.replace(default='')]
elif change == 'return':
signature = signature.replace(return_annotation=str)
else:
changes = {
'name': {'name': 'renamed'},
'kind': {'kind': inspect.Parameter.KEYWORD_ONLY},
'default': {'default': 2},
'annotation': {'annotation': float},
}
parameters[1] = second.replace(**changes[change])
def changed():
pass
changed.__signature__ = signature.replace(parameters=parameters)
assert _signature_contract(changed) != _signature_contract(original)
@pytest.mark.parametrize('change', ['constraint', 'factory'])
def test_signature_contract_detects_model_changes(change):
from pydantic import BaseModel, Field
class Original(BaseModel):
text: str = Field(max_length=8)
tags: list[str] = Field(default_factory=list)
class Changed(BaseModel):
text: str = Field(max_length=9 if change == 'constraint' else 8)
tags: list[str] = Field(default_factory=tuple if change == 'factory' else list)
assert _signature_contract(Changed) != _signature_contract(Original)
@pytest.mark.parametrize('validated_data', [False, True])
def test_signature_contract_rejects_custom_factories_without_calling_them(validated_data):
from pydantic import BaseModel, Field
calls = []
def make_factory(value):
def plain():
calls.append(value)
return [value]
def with_data(data):
calls.append(value)
return [data['prefix'], value]
return with_data if validated_data else plain
class Before(BaseModel):
prefix: str = 'validated'
tags: list[str] = Field(default_factory=make_factory('before'))
class After(BaseModel):
prefix: str = 'validated'
tags: list[str] = Field(default_factory=make_factory('after'))
assert Before().tags != After().tags
assert Before.model_fields['tags'].default_factory_takes_validated_data is validated_data
calls.clear()
for model in (Before, After):
with pytest.raises(TypeError, match='Unsupported default factory'):
_signature_contract(model)
assert calls == []
def test_signature_contract_detects_default_validation_outside_signature_and_openapi():
from pydantic import BaseModel, Field, ValidationError
class Before(BaseModel):
count: int = Field(default='invalid', validate_default=False)
class After(BaseModel):
count: int = Field(default='invalid', validate_default=True)
assert Before().count == 'invalid'
with pytest.raises(ValidationError):
After()
assert inspect.signature(Before) == inspect.signature(After)
before_schema, after_schema = Before.model_json_schema(), After.model_json_schema()
before_schema.pop('title')
after_schema.pop('title')
assert before_schema == after_schema
assert _signature_contract(Before) != _signature_contract(After)
def test_signature_contract_detects_validation_alias_paths():
from pydantic import AliasPath, BaseModel, Field, ValidationError
class Before(BaseModel):
count: int = Field(validation_alias=AliasPath('before', 0))
class After(BaseModel):
count: int = Field(validation_alias=AliasPath('after', 0))
assert Before(before=[1]).count == 1
with pytest.raises(ValidationError):
After(before=[1])
assert inspect.signature(Before) == inspect.signature(After)
before_schema, after_schema = Before.model_json_schema(), After.model_json_schema()
before_schema.pop('title')
after_schema.pop('title')
assert before_schema == after_schema
assert _signature_contract(Before) != _signature_contract(After)
@pytest.mark.parametrize('setting', ['validate_default', 'strict'])
def test_signature_contract_detects_model_validation_configuration(setting):
from pydantic import BaseModel, ConfigDict, ValidationError
class Before(BaseModel):
model_config = ConfigDict({setting: False})
count: int = 'invalid' if setting == 'validate_default' else 1
class After(BaseModel):
model_config = ConfigDict({setting: True})
count: int = 'invalid' if setting == 'validate_default' else 1
data = {} if setting == 'validate_default' else {'count': '1'}
Before(**data)
with pytest.raises(ValidationError):
After(**data)
assert inspect.signature(Before) == inspect.signature(After)
assert _signature_contract(Before) != _signature_contract(After)
def test_openapi_golden_is_canonical():
golden = (FIXTURES / 'skills_openapi_baseline.json').read_text(encoding='utf-8')
schema = json.loads(golden)
assert golden == _canonical_openapi(schema)
@pytest.mark.parametrize('section, key', [
('paths', '/api/skills/add'), ('schemas', 'SkillAddRequest'),
])
def test_openapi_failure_identifies_changed_endpoint_or_schema(section, key):
schema = json.loads((FIXTURES / 'skills_openapi_baseline.json').read_text(encoding='utf-8'))
target = schema['paths'] if section == 'paths' else schema['components']['schemas']
if section == 'paths':
target[key]['post']['responses']['200']['description'] = 'changed contract'
else:
target[key]['properties']['solution']['anyOf'][0]['maxLength'] = 4999
with pytest.raises(pytest.fail.Exception) as failure:
_assert_openapi_matches_baseline(schema)
diagnostic = str(failure.value)
assert '--- skills_openapi_baseline.json' in diagnostic
assert '+++ generated OpenAPI' in diagnostic
assert f'Changed {section}: {key}' in diagnostic
expected_change = '"description": "changed contract"' if section == 'paths' else '"maxLength": 4999'
assert any(line.startswith('+') and expected_change in line
for line in diagnostic.splitlines())
@pytest.mark.parametrize('raw, expected', [
('<think>{"verdict":"fail"}</think>{"verdict":"PASS","confidence":2,}', 'pass'),
('{"verdict":"fail"} text {"verdict":"needs_work"}', 'needs_work'),
('Verdict: INCONCLUSIVE', 'inconclusive'),
('<think>{"verdict":"pass"}', 'unknown'),
('{"verdict":"invalid"}', 'unknown'),
])
async def test_run_verdict_parsing_and_retry(monkeypatch, raw, expected):
calls = []
async def call(url, model, messages, **kwargs):
calls.append((messages, kwargs))
return raw
monkeypatch.setattr('src.llm_core.llm_call_async', call)
monkeypatch.setattr(routes, 'effort_for_call', lambda url, model, owner: 'high' if owner == 'alice' else None)
result = await routes._eval_skill_run('md', 'task', 'transcript', 'url', 'm', {},
workload='background', owner='alice')
assert result['verdict'] == expected
assert len(calls) == (2 if expected == 'unknown' else 1)
for _, kwargs in calls:
assert kwargs['reasoning_effort'] == 'high'
assert kwargs['workload'] == 'background'
assert kwargs['timeout'] == 180
assert kwargs['max_tokens'] == 32768
if len(calls) == 2:
assert "must START with '{'" in calls[1][0][0]['content']
async def test_review_transient_failure_and_result_coercion(monkeypatch):
calls = []
async def call(*args, **kwargs):
calls.append(kwargs)
if len(calls) == 1:
raise TimeoutError('offline')
return json.dumps({'verdict': 'pass', 'confidence': '-1', 'summary': 'x' * 500,
'issues': ['y' * 250] * 10, 'baseline_verdict': 'invalid',
'usefulness': '2', 'saved_turns': '-2', 'saved_tool_calls': 'bad'})
monkeypatch.setattr('src.llm_core.llm_call_async', call)
result = await routes._eval_skill_run('', '', '', 'url', 'm', None)
assert result == {'verdict': 'pass', 'confidence': 0.0, 'summary': 'x' * 400,
'issues': ['y' * 200] * 8, 'baseline_verdict': 'unknown',
'usefulness': 1.0, 'saved_turns': -2, 'saved_tool_calls': 0}
async def test_review_failure_schema_and_transcript_tail(monkeypatch):
seen = []
async def call(url, model, messages, **kwargs):
seen.append(messages)
raise RuntimeError('offline')
monkeypatch.setattr('src.llm_core.llm_call_async', call)
result = await routes._eval_skill_run('m' * 5000, 'TASK', 'HEAD' + 'x' * 30000 + 'TAIL', 'url', 'm', None)
assert result == {'verdict': 'unknown', 'confidence': 0,
'summary': 'Review call failed: offline', 'issues': []}
assert len(seen) == 2
assert 'HEAD' in seen[0][1]['content'] and 'TAIL' in seen[0][1]['content']
assert 'm' * 4001 not in seen[0][1]['content']
@pytest.mark.parametrize('name, raw, expected, tokens, timeout', [
('_eval_skill_necessity', '<think>x</think>{"necessary":false,"redundant_with":["demo"],"reason":"duplicate",}',
{'necessary': False, 'redundant_with': ['demo'], 'reason': 'duplicate'}, 8192, 120),
('_eval_skill_retrieval_precision', '{"ok":false,"summary":"broad","issues":["metadata: retrieval: narrow"]}',
{'ok': False, 'summary': 'broad', 'issues': ['metadata: retrieval: narrow']}, 4096, 90),
('_eval_skill_necessity', '<think>unfinished', None, 8192, 120),
('_eval_skill_retrieval_precision', '[]', None, 4096, 90),
])
async def test_advisory_schema_budget_and_owner(monkeypatch, name, raw, expected, tokens, timeout):
async def call(*args, **kwargs):
assert kwargs['max_tokens'] == tokens
assert kwargs['timeout'] == timeout
assert kwargs['reasoning_effort'] == 'medium'
return raw
monkeypatch.setattr('src.llm_core.llm_call_async', call)
monkeypatch.setattr(routes, 'effort_for_call', lambda url, model, owner: 'medium' if owner == 'alice' else None)
assert await getattr(routes, name)('md', [], 'url', 'm', None, owner='alice') == expected
@pytest.mark.parametrize('name', ['_eval_skill_necessity', '_eval_skill_retrieval_precision', '_improve_skill_md'])
async def test_advisory_exceptions_return_none(monkeypatch, name):
async def call(*args, **kwargs):
raise TimeoutError('offline')
monkeypatch.setattr('src.llm_core.llm_call_async', call)
args = ('md', {}, '', 'url', 'm', None) if name == '_improve_skill_md' else ('md', [], 'url', 'm', None)
assert await getattr(routes, name)(*args) is None
async def test_cancelled_audit_closes_stream_in_originating_context(monkeypatch):
bound = ContextVar('audit_characterization', default=None)
opened = asyncio.Event()
closed = []
async def stream(*args, **kwargs):
token = bound.set('bound')
try:
yield 'data: {"delta":"started"}\n\n'
opened.set()
await asyncio.Event().wait()
finally:
bound.reset(token)
closed.append(asyncio.current_task())
monkeypatch.setattr('src.agent_loop.stream_agent_loop', stream)
task = asyncio.create_task(routes._run_skill_audit_arm([], 'url', 'm', {}, 'alice'))
await opened.wait()
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
assert closed == [task]
assert bound.get() is None
async def test_cancelled_batch_skips_finalization_and_cleans_task(monkeypatch):
opened = asyncio.Event()
async def audit(*args, **kwargs):
opened.set()
await asyncio.Event().wait()
class Manager:
def load(self, owner=None):
assert owner == 'alice'
return [{'name': 'demo'}]
job = {'status': 'running', 'log': [], 'results': [], 'done': 0}
monkeypatch.setattr(routes, '_skill_audit_jobs', {('alice',): job})
monkeypatch.setattr(routes, '_audit_one_skill', audit)
monkeypatch.setattr(routes, '_finalize_audit_batch', lambda *args: pytest.fail('cancelled batch finalized'))
task = asyncio.create_task(routes._run_audit_all_job(('alice',), Manager(), ['demo'], 'url', 'm', {}, None, 'alice'))
job['task'] = task
await opened.wait()
task.cancel()
await task
assert job['status'] == 'cancelled'
assert job['current'] is None and 'task' not in job and 'finished' in job
assert job['results'] == [] and job['done'] == 0
async def test_test_polling_is_owner_scoped_and_hides_continuation(monkeypatch):
class Manager:
def load(self, owner=None):
return [{'name': 'demo'}]
monkeypatch.setattr(routes, 'get_current_user', lambda req: req.state.user)
job = {'status': 'awaiting_approval', 'task': 'task', 'model': 'm', 'log': [],
'verdict': None, 'approval': {'approval_id': 'id'},
'_run': {'headers': {'Authorization': 'secret'}}, '_transcript': ['private']}
monkeypatch.setattr(routes, '_skill_test_jobs', {('alice', 'demo'): job})
poll = endpoint('GET', '/{skill_id}/test-status', Manager())
assert await poll(request('bob'), 'demo') == {'status': 'none'}
assert await poll(request(), 'demo') == {k: job[k] for k in ('status', 'task', 'model', 'log', 'verdict', 'approval')}
def test_batch_finalizer_protects_approval_required_and_unprocessed_skills(monkeypatch):
class Manager:
def __init__(self):
self.updated = []
def load(self, owner=None):
assert owner == 'alice'
return [{'name': name, 'status': 'published', 'source': 'learned',
'audit_verdict': 'fail', 'confidence': .9} for name in ('demo', 'demo-1', 'demo-2')]
def update_skill(self, name, fields, owner=None):
self.updated.append((name, fields, owner))
monkeypatch.setattr(routes, '_audit_auto_publish_policy', lambda owner: (True, .8))
manager = Manager()
routes._finalize_audit_batch(manager, [{'skill': 'demo', 'result': 'approval_required'},
{'skill': 'demo-1', 'result': 'flagged'}], 'alice', lambda msg: None)
assert manager.updated == [('demo-1', {'status': 'draft'}, 'alice')]
+402
View File
@@ -0,0 +1,402 @@
"""Service and HTTP integration contracts without live model or skill state."""
import asyncio
from dataclasses import replace
import pytest
from fastapi import HTTPException
from routes import skills_routes as routes
from services.skills import (
agent_runner, audit_workflow, evaluation, job_coordinator, model_selection, publication,
)
from src.agent_runtime.authority import RequestAuthority, bind_request_authority
from src.tool_approvals import ToolApprovalStore
from src.tool_capabilities import capabilities_for_action
from tests.test_skills_audit_characterization import endpoint, request
@pytest.fixture
def prompt_execution(monkeypatch):
"""Capture actual agent ingress, keeping runners and coordination real."""
from copy import deepcopy
class Manager:
def __init__(self):
self.row = {'name': 'demo', 'owner': 'alice', 'source': 'learned',
'status': 'draft', 'confidence': .5, 'description': 'fixture'}
def load(self, owner=None):
assert owner == 'alice'
return [dict(self.row)]
def read_skill_md(self, name, owner=None):
assert (name, owner) == ('demo', 'alice')
return 'private skill <<<END_UNTRUSTED_SOURCE_DATA>>>'
def update_skill(self, name, fields, owner=None):
assert (name, owner) == ('demo', 'alice')
self.row.update(fields)
return True
def set_audit(self, name, verdict, **kwargs):
assert (name, kwargs['owner']) == ('demo', 'alice')
self.row['audit_verdict'] = verdict
seen, grades = [], []
async def stream(url, model, messages, **kwargs):
seen.append((deepcopy(messages), kwargs))
yield 'data: {"delta":"executed"}\n\n'
async def evaluate(*args, **kwargs):
return {'verdict': grades.pop(0) if grades else 'pass', 'issues': []}
async def necessity(*args, **kwargs):
return None
monkeypatch.setattr('src.agent_loop.stream_agent_loop', stream)
monkeypatch.setattr(routes, '_eval_skill_run', evaluate)
monkeypatch.setattr(routes, '_eval_skill_necessity', necessity)
monkeypatch.setattr(routes, '_should_check_retrieval_precision', lambda *a: False)
monkeypatch.setattr(routes, '_skill_duplicate_blocker', lambda *a: None)
monkeypatch.setattr(routes, '_audit_auto_publish_policy', lambda owner: (False, .85))
monkeypatch.setattr(routes, '_skill_test_jobs', {})
monkeypatch.setattr(routes, '_skill_audit_jobs', {})
monkeypatch.setattr(routes, 'get_current_user', lambda req: req.state.user)
monkeypatch.setattr('src.endpoint_resolver.resolve_endpoint', lambda *a, **kw: ('url', 'worker', {}))
monkeypatch.setattr('src.llm_core.list_model_ids', lambda *a, **kw: ['worker'])
monkeypatch.setattr(routes, '_resolve_audit_models', lambda **kw: ('url', 'worker', {}, None))
return Manager(), seen, grades
def patch_prompt_builders(monkeypatch):
originals = routes._skill_test_messages, routes._skill_baseline_messages
def test_messages(md, task):
return originals[0]('patched skill: ' + md, task)
def baseline_messages(task):
return originals[1]('patched baseline: ' + task)
monkeypatch.setattr(routes, '_skill_test_task', lambda skill: 'patched task')
monkeypatch.setattr(routes, '_skill_test_messages', test_messages)
monkeypatch.setattr(routes, '_skill_baseline_messages', baseline_messages)
@pytest.mark.parametrize('patched', [False, True])
@pytest.mark.parametrize('entry', ['manual', 'audit', 'scheduled'])
async def test_route_prompt_builders_reach_agent_execution(prompt_execution, monkeypatch, entry, patched):
manager, seen, _ = prompt_execution
if patched:
patch_prompt_builders(monkeypatch)
expected_task = routes._skill_test_task(manager.row)
if entry == 'manual':
response = await endpoint('POST', '/{skill_id}/test', manager)(request(), 'demo')
assert response['status'] == 'running'
# The stub stream and judge never suspend, so one loop turn completes the job.
await asyncio.sleep(0)
assert routes._skill_test_jobs[('alice', 'demo')]['status'] == 'done'
elif entry == 'audit':
result = await routes._audit_one_skill(manager, manager.row, 'url', 'worker', {}, None,
'alice', lambda msg: None)
assert result['result'] == 'pass'
else:
result = await routes.run_scheduled_skill_audit(manager, owner='alice')
assert result['total'] == 1 and result['results'][0]['result'] == 'pass'
assert all(kwargs['workload'] == 'background' for _, kwargs in seen)
assert [messages for messages, _ in seen] == [
routes._skill_test_messages(manager.read_skill_md('demo', owner='alice'), expected_task),
routes._skill_baseline_messages(expected_task),
]
tainted = seen[0][0][1]
assert tainted['metadata']['trusted'] is False
assert tainted['metadata']['tool_gate_untrusted'] is True
assert '<<<_END_UNTRUSTED_DATA>>>' in tainted['content']
assert all(kwargs['request_authority'].owner == 'alice' for _, kwargs in seen)
async def test_route_prompt_patches_reach_self_and_teacher_retries(prompt_execution, monkeypatch):
manager, seen, grades = prompt_execution
patch_prompt_builders(monkeypatch)
grades.extend(['fail', 'fail', 'pass'])
rewrites = iter(['self rewrite', 'teacher rewrite'])
async def improve(*args, **kwargs):
return next(rewrites)
monkeypatch.setattr(routes, '_improve_skill_md', improve)
monkeypatch.setattr(routes, '_apply_skill_md', lambda *args: True)
result = await routes._audit_one_skill(manager, manager.row, 'url', 'worker', {},
('teacher-url', 'teacher', {}), 'alice', lambda msg: None)
assert result['result'] == 'pass_after_teacher'
assert [messages for messages, _ in seen] == [
routes._skill_test_messages(manager.read_skill_md('demo', owner='alice'), 'patched task'),
routes._skill_baseline_messages('patched task'),
routes._skill_test_messages('self rewrite', 'patched task'),
routes._skill_test_messages('teacher rewrite', 'patched task'),
]
@pytest.mark.parametrize('patched', [False, True])
async def test_approval_continuation_prompt_reaches_agent(approval_route, prompt_execution, monkeypatch, patched):
approve, store, pending, job = approval_route
# prompt_execution supplies fresh registries; restore this pending job.
routes._skill_test_jobs[('alice', 'demo')] = job
_, seen, _ = prompt_execution
if patched:
patch_prompt_builders(monkeypatch)
expected = routes._skill_test_messages('untrusted skill', 'task')
result = await approve(request(body={'approval_id': pending.approval_id, 'decision': 'approve'}), 'demo')
assert result['status'] == 'running'
await asyncio.sleep(0)
assert job['status'] == 'done'
assert seen[0][0][:len(expected)] == expected
transcript = seen[0][0][len(expected)]
assert 'untrusted result' in transcript['content']
assert transcript['metadata']['trusted'] is False
assert transcript['metadata']['tool_gate_untrusted'] is True
grant = seen[0][1]['exact_approval']
assert grant.pending is pending and grant.allow_remaining_actions is False
assert store.peek(pending.approval_id) is None
assert seen[1][0] == routes._skill_baseline_messages('task')
def test_baseline_task_alias_uses_route_test_task_patch(monkeypatch):
monkeypatch.setattr(routes, '_skill_test_task', lambda skill: 'patched shared task')
assert routes._skill_baseline_task({'name': 'demo'}) == 'patched shared task'
async def test_evaluation_can_inject_model_call_and_owner_effort_without_network():
seen = []
async def call(url, model, messages, **kwargs):
seen.append(kwargs)
return '{"verdict":"pass"}'
def effort(url, model, owner):
assert (url, model, owner) == ('offline', 'worker', 'alice')
return 'high'
result = await evaluation.evaluate_run('md', 'task', 'done', 'offline', 'worker', None,
owner='alice', _llm_call=call, _effort_for_call=effort)
assert result['verdict'] == 'pass'
assert seen[0]['reasoning_effort'] == 'high'
async def test_rewrite_keeps_last_document_and_owner_effort():
async def call(*args, **kwargs):
assert kwargs['reasoning_effort'] == 'medium'
return '<think>reasoning</think>old\n---\nname: old\n---\nbody\n---\nname: demo\n---\nfixed'
assert await evaluation.improve_markdown('md', {}, 'transcript', 'url', 'm', {},
owner='alice', _llm_call=call, _effort_for_call=lambda u, m, o: 'medium') == '---\nname: demo\n---\nfixed'
def test_audit_model_resolution_uses_first_owner_scoped_utility_fallback(monkeypatch):
seen = []
def primary(role, owner=None):
seen.append(('primary', role, owner))
return None, None, {}
def fallback(owner=None):
seen.append(('fallback', owner))
return [('first', '/models/worker', {'Authorization': 'owner-key'}), ('second', 'other', {})]
def served(url, headers=None):
assert (url, headers) == ('first', {'Authorization': 'owner-key'})
return ['worker']
monkeypatch.setattr('src.endpoint_resolver.resolve_endpoint', primary)
monkeypatch.setattr('src.endpoint_resolver.resolve_utility_fallback_candidates', fallback)
monkeypatch.setattr('src.llm_core.list_model_ids', served)
monkeypatch.setattr('src.settings.get_setting', lambda key, default=None: default)
assert model_selection.resolve_audit_models(owner='alice') == ('first', 'worker', {'Authorization': 'owner-key'}, None)
assert seen == [('primary', 'utility', 'alice'), ('fallback', 'alice')]
async def test_audit_stream_failure_closes_and_restores_inherited_authority(monkeypatch):
authority = RequestAuthority.empty(owner='alice')
closed = []
async def stream(*args, **kwargs):
assert kwargs['owner'] == 'alice'
assert kwargs['request_authority'] is authority
assert kwargs['suppress_skills'] is True
try:
yield 'event: error\ndata: {"error":"offline"}\n\n'
finally:
closed.append(asyncio.current_task())
monkeypatch.setattr('src.agent_loop.stream_agent_loop', stream)
with bind_request_authority(authority):
with pytest.raises(agent_runner.SkillAuditUnavailable, match='offline'):
await agent_runner.run_audit_arm([], 'url', 'm', {}, 'alice')
assert closed == [asyncio.current_task()]
async def test_manual_exception_closes_stream_and_finishes_without_publishing(monkeypatch):
closed = []
async def stream(*args, **kwargs):
try:
yield 'data: {"delta":"partial"}\n\n'
raise RuntimeError('stream failed')
finally:
closed.append(asyncio.current_task())
async def arm(*args, **kwargs):
return 'baseline', {}, None
async def evaluate(*args, **kwargs):
return {'verdict': 'fail', 'summary': 'failed', 'issues': []}
class Manager:
def __init__(self):
self.updates = []
def set_audit(self, name, verdict, **kwargs):
assert name == 'demo' and verdict == 'fail' and kwargs['owner'] == 'alice'
def update_skill(self, name, fields, owner=None):
self.updates.append((fields, owner))
job = {'status': 'running', 'log': [], '_run': {}, '_transcript': []}
deps = replace(routes._audit_dependencies(), test_jobs={('alice', 'demo'): job},
run_arm=arm, evaluate_run=evaluate)
monkeypatch.setattr('src.agent_loop.stream_agent_loop', stream)
manager = Manager()
await agent_runner.run_test_job(('alice', 'demo'), 'demo', 'md', 'task', 'url', 'm', {},
'alice', manager, deps=deps)
assert closed == [asyncio.current_task()]
assert job['status'] == 'done' and '_run' not in job and '_transcript' not in job
assert {'type': 'error', 'error': 'stream failed'} in job['log']
assert manager.updates == [({'confidence': .4}, 'alice')]
async def test_batch_exception_records_progress_and_finalizes_once():
class Manager:
def __init__(self):
self.audit = []
def load(self, owner=None):
assert owner == 'alice'
return [{'name': name, 'status': 'draft'} for name in ('first', 'second')]
def set_audit(self, *args, **kwargs):
self.audit.append((args, kwargs))
async def audit(manager, skill, *args, **kwargs):
if skill['name'] == 'first':
raise RuntimeError('failed')
return {'skill': 'second', 'result': 'pass'}
finalized = []
job = {'status': 'running', 'log': [], 'results': [], 'done': 0, 'task': object()}
deps = replace(routes._audit_dependencies(), audit_jobs={('alice',): job}, audit_one=audit,
finalize_batch=lambda manager, results, owner, log: finalized.append((list(results), owner)))
manager = Manager()
await audit_workflow.run_audit_batch(('alice',), manager, ['first', 'second'], 'url', 'm', {}, None, 'alice', deps=deps)
assert job['status'] == 'done' and job['done'] == 2 and job['current'] is None
assert 'task' not in job and 'finished' in job
assert [r['result'] for r in job['results']] == ['error', 'pass']
assert manager.audit == [(('first', 'inconclusive'), {'worker_model': 'm', 'owner': 'alice'})]
assert finalized == [(job['results'], 'alice')]
@pytest.mark.parametrize('enabled,confidence,verdict,expected', [
(True, .85, 'pass', 'published'), (True, .84, 'pass', 'draft'),
(False, .95, 'pass', 'draft'), (True, .95, 'fail', 'draft'),
(False, .2, 'unknown', 'published'),
])
def test_policy_preserves_threshold_and_auto_approval(enabled, confidence, verdict, expected):
class Manager:
def __init__(self):
self.updates = []
def load(self, owner=None):
return [{'name': 'demo', 'status': 'published'}]
def update_skill(self, name, fields, owner=None):
assert owner == 'alice'
self.updates.append(fields)
deps = replace(routes._audit_dependencies(), auto_publish_policy=lambda owner: (enabled, .85),
duplicate_blocker=lambda *args: None)
manager = Manager()
assert publication.finalize_status(manager, 'demo', 'alice', verdict, confidence, deps=deps) == expected
assert manager.updates == ([] if verdict == 'unknown' else [{'status': expected}])
@pytest.mark.parametrize('enabled', [True, False])
def test_batch_duplicate_finalization_changes_only_processed_unprotected_skills(enabled):
class Manager:
def __init__(self):
self.updates = []
def load(self, owner=None):
assert owner == 'alice'
return [{'name': name, 'source': 'learned', 'status': 'draft',
'audit_verdict': 'pass', 'confidence': .95, 'uses': uses}
for name, uses in [('demo', 10), ('demo-1', 0), ('demo-2', 20), ('demo-3', 30)]]
def update_skill(self, name, fields, owner=None):
self.updates.append((name, fields, owner))
deps = replace(routes._audit_dependencies(), auto_publish_policy=lambda owner: (enabled, .85))
manager = Manager()
publication.finalize_batch(manager, [{'skill': 'demo', 'result': 'pass'},
{'skill': 'demo-1', 'result': 'pass'},
{'skill': 'demo-3', 'result': 'approval_required'}],
'alice', lambda message: None, deps=deps)
expected = [('demo', {'status': 'published'}, 'alice')] if enabled else []
assert manager.updates == expected + [('demo-1', {'status': 'binned'}, 'alice')]
@pytest.fixture
def approval_route(monkeypatch):
store = ToolApprovalStore()
monkeypatch.setattr('src.tool_approvals.tool_approval_store', store)
monkeypatch.setattr(routes, 'get_current_user', lambda req: req.state.user)
monkeypatch.setattr(routes, '_skill_test_jobs', {})
class Manager:
def load(self, owner=None):
return [{'name': 'demo', 'owner': owner}]
pending = store.create(owner='alice', session_id=None, origin_run_id='run', tool_name='bash',
content='printf approved', workspace=None, external_untrusted_context_seen=True,
capabilities=capabilities_for_action('bash', 'printf approved'))
job = {'status': 'awaiting_approval', 'log': [], 'task': 'task',
'approval': pending.public_payload(), '_transcript': ['untrusted result'],
'_run': {'owner': 'alice', 'md': 'untrusted skill', 'url': 'url', 'model': 'm'}}
routes._skill_test_jobs[('alice', 'demo')] = job
return endpoint('POST', '/{skill_id}/test-approval', Manager()), store, pending, job
async def test_denied_skill_approval_retires_state_without_spawning(approval_route, monkeypatch):
approve, store, pending, job = approval_route
async def run(*args, **kwargs):
pytest.fail('denial spawned a test')
monkeypatch.setattr(routes, '_run_skill_test_job', run)
result = await approve(request(body={'approval_id': pending.approval_id, 'decision': 'deny'}), 'demo')
assert result == {'ok': True, 'status': 'done', 'decision': 'deny'}
assert store.peek(pending.approval_id) is None
assert job['verdict']['verdict'] == 'inconclusive'
assert not {'approval', '_run', '_transcript'} & job.keys()
@pytest.mark.parametrize('case,status', [('other_owner', 409), ('mismatch', 409), ('consumed', 409), ('invalid_decision', 400)])
async def test_invalid_skill_approval_cannot_resume(approval_route, monkeypatch, case, status):
approve, store, pending, job = approval_route
async def run(*args, **kwargs):
pytest.fail('invalid approval resumed a test')
monkeypatch.setattr(routes, '_run_skill_test_job', run)
owner = 'bob' if case == 'other_owner' else 'alice'
if case == 'other_owner':
routes._skill_test_jobs[('bob', 'demo')] = job
if case == 'consumed':
store.consume(pending.approval_id, decision='deny', owner='alice', session_id=None)
body = {'approval_id': 'wrong' if case == 'mismatch' else pending.approval_id,
'decision': 'approve_task' if case == 'invalid_decision' else 'approve'}
with pytest.raises(HTTPException) as error:
await approve(request(owner, body), 'demo')
assert error.value.status_code == status
assert job['status'] == 'awaiting_approval'
async def test_allowed_skill_approval_is_single_action_and_cannot_be_reused(approval_route, monkeypatch):
approve, store, pending, job = approval_route
resumed = []
async def run(*args, **kwargs):
resumed.append((args, kwargs))
monkeypatch.setattr(routes, '_run_skill_test_job', run)
body = {'approval_id': pending.approval_id, 'decision': 'approve'}
assert await approve(request(body=body), 'demo') == {'ok': True, 'status': 'running', 'decision': 'approve'}
await asyncio.sleep(0)
assert len(resumed) == 1 and resumed[0][0][7] == 'alice'
grant = resumed[0][1]['exact_approval']
assert grant.pending is pending and grant.allow_remaining_actions is False
assert grant.grants_chat_session is False
assert store.peek(pending.approval_id) is None
messages = resumed[0][1]['messages']
tainted = [m for m in messages if m.get('metadata', {}).get('tool_gate_untrusted')]
assert len(tainted) == 2
assert any('untrusted result' in m['content'] for m in tainted)
with pytest.raises(HTTPException) as error:
await approve(request(body=body), 'demo')
assert error.value.status_code == 409
assert len(resumed) == 1
@pytest.mark.parametrize('scope,selected,skip,names,expected_scope', [
('all', None, False, ['draft', 'published', 'checked'], 'all'),
('drafts', None, False, ['draft'], 'unchecked'),
('all', ['published', 'draft', 'draft', 'builtin', 'missing'], False, ['published', 'draft'], 'selected'),
('all', None, True, ['draft', 'published'], 'all'),
])
def test_manual_audit_selection_preserves_published_and_builtin_rules(scope, selected, skip, names, expected_scope):
class Manager:
def load(self, owner=None):
assert owner == 'alice'
return [{'name': 'draft', 'status': 'draft'}, {'name': 'published', 'status': 'published'},
{'name': 'checked', 'audit_verdict': 'pass'}, {'name': 'builtin', 'source': 'builtin'}]
assert job_coordinator.select_audit_names(Manager(), 'alice', scope, selected, skip) == (names, expected_scope)