* 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
22 KiB
Skills evaluation and audit architecture
The Skills backend separates HTTP transport, model evaluation, agent execution,
audit orchestration and publication policy. services/skills/ coordinates those
responsibilities; services/memory/skills.py remains the canonical persistence
and lifecycle owner. The extraction preserves the existing HTTP and Python
contracts. PR #6711 tracks
this boundary change under issue #6710.
Responsibilities and dependency boundaries
| Module | Responsibility |
|---|---|
routes/skills_routes.py |
FastAPI request models, endpoint declarations, authentication, ownership, HTTP errors, exact approval validation and Python compatibility bindings |
services/skills/job_coordinator.py |
Transient job creation, continuation after approval validation, candidate selection, public polling projections and cancellation requests |
services/skills/agent_runner.py |
Manual execution, audit arms, agent loop integration, transcripts, authority propagation, approval pauses and same-task stream closure |
services/skills/audit_workflow.py |
Per-skill audit sequencing, self-edit retries, teacher escalation, sequential batches and scheduled execution |
services/skills/evaluation.py |
Model grading, verdict parsing, necessity and retrieval checks, efficiency fields and markdown improvement generation |
services/skills/publication.py |
Confidence and publishing policy, duplicate rules, batch finalization and markdown updates through SkillsManager |
services/skills/model_selection.py |
Owner-scoped worker/teacher resolution, utility fallback, endpoint selection and credential lookup |
services/skills/prompts.py |
Shared task construction and explicitly untrusted skill context |
services/skills/dependencies.py |
Frozen AuditDependencies containing callable bindings and transient registry references |
services/memory/skills.py |
Canonical skill identity, owner isolation, durable storage, indexing, retrieval and lifecycle state |
The package initializer identifies the domain without adding a service facade.
Evaluation does not schedule jobs or persist skills. Publication computes policy
and applies updates through the supplied SkillsManager. Services do not import
routes.skills_routes. The existing preferences adapter in publication.py
still imports routes.prefs_routes; moving that adapter is a separate change.
flowchart TD
HTTP[Authenticated HTTP requests] --> Routes[skills_routes]
Builtins[builtin_actions callers] --> Routes
Routes --> Jobs[job_coordinator]
Routes --> Models[model_selection]
Routes --> Evaluation[evaluation]
Routes --> Runner[agent_runner]
Routes --> Workflow[audit_workflow]
Jobs -->|injected calls| Runner
Jobs -->|injected calls| Workflow
Workflow -->|injected calls| Runner
Workflow -->|injected calls| Evaluation
Workflow -->|injected calls| Publication[publication]
Runner -->|injected calls| Evaluation
Runner --> Runtime[Agent runtime and authority enforcement]
Runner --> Prompts[prompts and untrusted context]
Jobs --> Prompts
Workflow --> Prompts
Routes --> Manager[Canonical SkillsManager]
Runner --> Manager
Workflow --> Manager
Publication --> Manager
Publication --> Preferences[Existing preferences adapter]
Models --> Resolver[Endpoint resolver and owner-scoped credentials]
Arrows include injected calls, rather than only Python imports. The workflow
imports SkillAuditUnavailable from the runner. Route wrappers construct
AuditDependencies on entry from the current route bindings, so existing
callers and monkeypatches reach the same collaborators without rebinding shared
service globals.
The manual test registry belongs to agent_runner.py; the audit registry belongs
to audit_workflow.py. Routes expose aliases to those dictionaries. Registries
hold process-local job state and are not durable skill stores. All durable
metadata, confidence, markdown and publication changes use SkillsManager.
Published-only retrieval and local exclusion of remote-only skills stay in
the manager/index boundary. Service loggers use their module names; the route
module retains its own logger. Root handlers supply the existing log output.
Execution and publication lifecycle
- The HTTP handler authenticates the caller, verifies ownership and validates the request. Manual model choices retain registered-endpoint validation and owner-scoped credential lookup.
- Job coordination creates the owner-keyed transient record and launches the execution coroutine. Replacing a manual test retires its pending approval. Polling exposes only the existing public fields, excluding credentials, private transcripts, continuation state and asyncio tasks.
- The runner uses the originating request authority or the consumed sealed
approval authority. It captures the transcript and closes the async stream
with
aclosinginside the task that opened it, including early approval pauses, cancellation and exceptions. - Manual approval pauses return to the HTTP validation boundary. Continuation reconstructs context from the original untrusted skill and accumulated untrusted transcript. Manual completion records audit metadata and adjusts confidence (pass .95, needs_work .6, fail .4) without publishing.
- An automated audit checks deterministic duplicates, advisory necessity and, when indicated, retrieval precision. It runs skill and no-skill arms on the same task, grades the result, then may self-edit and retry. An optional distinct teacher rewrites the skill; the worker performs the retry.
- Publication applies owner preferences, confidence thresholds, necessity, utility and duplicate rules through SkillsManager. A normal sequential batch finalizes its processed results. Cancellation or infrastructure unavailability skips batch finalization and clears task/current state.
A clean audit pass sets confidence to .95, a self-edit pass to .85 and a teacher rewrite pass to .8. Continued failure becomes draft at .35. Unknown and inconclusive verdicts preserve publication state. Automatic publishing requires an enabled owner policy, a passing verdict at or above the configured threshold, and no necessity, worse-baseline or duplicate blocker. The default threshold is .85; owner values are clamped to [0, 1]. Markdown edits pin the existing skill name so usage and audit sidecars remain attached to the same identity.
Per-skill duplicate blocking and batch duplicate finalization have distinct selection rules and must not be consolidated without a behavior proposal. Batch finalization excludes builtins and binned records, changes only processed skills, protects approval-required results, and bins duplicate losers. Duplicate similarity uses the existing .38 threshold and name-suffix grouping.
Batches run sequentially per owner, retain current/done/results and timestamps, and cap progress logs at 1,000 entries. Ordinary exceptions record inconclusive skill audits. Infrastructure failures stop the batch without inventing a skill verdict. Scheduled execution skips an active owner job, selects existing lifecycle candidates by oldest audit, caps the batch (default eight), and marks model phases as background work. These services retain the existing scheduling and cancellation semantics.
Security and approval invariants
HTTP authentication and owner verification remain at the transport boundary. Manager reads and writes receive the owner. Ownerless builtins retain their existing visibility; an authenticated user cannot access an ownerless ordinary skill. Existing ownerless deployment behavior is preserved.
The /test-approval handler validates the pending job, approval ID, decision,
owner and empty session binding, then destructively consumes the sealed approval
with allow_continuation=False before calling the coordinator. Stale, mismatched
or reused approvals fail with 409. The consumed grant authorizes only its exact
action. Services do not manufacture approval authority or relax process identity,
workspace confinement or request-authority enforcement in the agent runtime.
Skill markdown and resumed transcripts remain explicitly untrusted context, never trusted system instructions. Audit arms suppress saved skill injection. Unattended runs deny encountered approval records instead of leaving reusable grants. An initial approval-required audit result preserves the skill's publication and confidence and directs the user to manual approval. Retry handling has an inherited gap: an approval pause after a rewrite can still lead to demotion. Same-task stream closure preserves the originating ContextVar cleanup, but manual cancellation can retain private continuation state. These are separate hardening items, not guarantees supplied by the extraction.
Model selection and evaluation
Audit resolution uses an explicit endpoint/model pair when supplied, looking up
credentials among enabled owner-visible endpoints. A model-only choice uses the
existing owner-scoped model resolver. Otherwise it resolves the utility model,
including the resolver's default behavior, then takes the first configured
utility fallback if the primary result is incomplete. A missing worker raises
ValueError. Served-model normalization keeps an exact match, then a basename
match, then the first served ID; discovery exceptions keep the resolved choice.
An enabled teacher resolves separately with the same owner. Its credentials are not borrowed from the worker. Teacher resolution failure logs the existing warning and leaves the teacher absent. Manual tests retain their HTTP endpoint validation and registered fallback credentials.
Owner reasoning effort is resolved for each evaluation or rewrite call. Workload flags, credentials, retries, prompts and permissive parsing remain unchanged:
| Phase | Token budget | Timeout in seconds |
|---|---|---|
| Run review, up to two attempts | 32768 | 180 |
| Necessity | 8192 | 120 |
| Retrieval precision | 4096 | 90 |
| Markdown improvement | 16384 | 180 |
Review parsing retains think-block stripping, trailing-comma recovery, last usable verdict selection, prose fallback, bounded fields and unknown outcomes. These model outputs are advisory and do not grant execution authority.
Public HTTP contract
All paths below have prefix /api/skills. Successful status defaults to 200.
Request models and endpoint declaration order remain fixed. A frozen
OpenAPI and declaration-order fixture records
the baseline contract independently of the extracted implementation.
| Method and suffix | Successful response | Authorization and errors |
|---|---|---|
| GET empty | skills,count |
Owner-filtered load |
GET /index |
index,count |
Owner-scoped eligible index |
GET /slash-catalog |
skills,count |
Owner-scoped index; sorted slash entries |
GET /builtin |
builtin,count and optional error |
Existing shared capability text |
GET /builtin/{name} |
name,text,default,is_overridden |
404 missing tool; 500 import failure |
PUT /builtin/{name} |
ok,name,is_overridden |
Admin; 404 unknown, 400 missing text |
DELETE /builtin/{name} |
ok,name,is_overridden |
Admin |
POST /import-from-url |
ok,skill,files |
Admin and current owner; 400 import, 502 HTTP, 500 other failure |
POST /add |
ok,deduped,skill |
Current owner passed to manager; unchanged old/new request fields |
POST /{skill_id}/invoke |
ok,type,name,command,message |
Eligible owner index only; 404 unavailable/source missing |
GET /{skill_id} |
Full skill dictionary | Owner load; 404 missing |
GET /{skill_id}/markdown |
name,markdown |
Owner verification; 404 missing/source missing |
POST /{skill_id}/test |
ok,status,skill,model |
Owner verification, HTTP request authority, registered fallback credentials; 400 missing/invalid model |
POST /{skill_id}/test-approval |
ok,status,decision |
User approval request, owner verification, pending ID/owner/empty session match; 400 invalid body/decision, 409 stale/mismatch/consumed |
GET /{skill_id}/test-status |
status or status,task,model,log,verdict,approval |
Owner-keyed lookup; server continuation/credentials omitted |
POST /audit-all |
ok,status,total plus running done,model or empty results,log |
Owner-keyed singleton; 400 model resolution |
GET /audit-all/status |
status or job progress/result/log/timestamps |
Owner-keyed lookup; task and credentials omitted |
POST /audit-all/cancel |
ok,status |
Owner-keyed lookup and cancellation |
POST /{skill_id}/markdown |
ok,name |
Owner verification; 400 missing/parse, 404 missing, 500 update |
PUT /{skill_id} |
ok |
Owner verification and scoped manager update; 404 missing |
DELETE /{skill_id} |
ok |
Owner verification and scoped manager deletion; 404 missing |
POST /search |
skills,query,count |
Owner-filtered input; 400 empty query |
Strict owner verification allows an ownerless builtin but denies an ownerless ordinary skill for an authenticated user. The existing ownerless deployment behavior remains. Global auth middleware is unchanged.
Python compatibility and frozen contracts
app.py imports setup_skills_routes. src/builtin_actions.py uses the existing
route helpers and audit registry. Route aliases and wrappers preserve the original
28 inspectable interfaces, SkillAuditUnavailable, registry identity and patchable
collaborators. Lazy model/agent imports and route-level effort injection keep
existing offline test seams effective.
The task and message callbacks are bound through AuditDependencies at service
entry. Patches to route-level test tasks and test/baseline messages reach manual
execution, approval continuation, both audit arms, worker retries and scheduled
audits. The _skill_baseline_task compatibility alias delegates to the current
route test-task builder. Execution continues to reuse one task for both arms,
as in the original implementation; it does not independently derive a baseline
task through that alias.
tests/fixtures/skills_api_baseline.json records declaration order and structural
signatures captured from pre-extraction revision
86b886c611338282af6d4a717246ea4e256f58df. Signature records retain parameter
names, order, kinds, defaults, default factories and annotations, including
Pydantic constraint metadata. They compare typing origins and arguments instead
of Python-version-dependent annotation representations. The expanded records also
pin field validation aliases, alias priority, discriminators, validate_default,
rebuilt field annotations and model configuration. Field order remains visible
through the constructor parameter sequence. Only the known builtin factories
list, tuple, dict, set and frozenset are supported. Custom factories,
including closures and factories taking validated data, fail closed without being
executed. Unsupported metadata/configuration values also raise rather than using
unstable representations. This is a structural contract for supported interfaces,
not proof of behavioral equivalence for arbitrary validators or Python code.
The expanded fixture was derived again from the original revision above in a detached scratch worktree, with the updated comparator copied in. Python 3.11 and 3.12 produced byte-identical snapshots. The previous parameter/return records, all 28 interface names, all 22 endpoint declarations and the original OpenAPI digest were checked unchanged before accepting the new field records.
tests/fixtures/skills_openapi_baseline.json is the same baseline's complete
Skills-router OpenAPI document, generated with a standalone FastAPI application.
Sorted keys, ASCII escaping, two-space indentation and a final newline make the
fixture deterministic. Tests require exact canonical equality and print a unified
diff on mismatch. The redundant digest field and assertions have been removed;
they provided no independent compatibility guarantee. The fixture pins model
schemas and constraints. CI reads committed fixtures and needs no Git history
for these tests. Baseline updates require an intentional compatibility decision, rather
than regenerating fixtures from a failing implementation.
The narrow .gitleaks.toml exception remains necessary for the historical public
digest in earlier fixture and documentation revisions. It applies to the exact digest,
the generic-api-key rule and the two named paths. Removing a current copy cannot
remove findings from published history. Keep the exception unless the pinned
full-history scan proves it unnecessary; do not rewrite history to remove it.
The checksum-verified 8.30.1 scan without this exception still reports both
historical copies in commit a547c4d8; retaining the exception passes the full
PR/dev history scan.
Reconsider it after squash merge only after verifying the resulting reachable
history with the pinned full-history scan.
The ReDoS source guard scans services/skills/evaluation.py, the canonical
evaluation parser module, as well as the retained route and memory-extractor
locations. Verdict regex runtime tests import the canonical module directly.
The source guard must reject reintroducing the lazy think-block expression;
the ordinary-input and adversarial-flood runtime tests remain in place. An isolated
source mutation reproduced the old guard's false green for the evaluation module.
The corrected guard rejected the vulnerable expression in each of the three
locations and passed again after restoring the temporary copies. The mutated
regex was never executed, and production parser behavior was not changed.
Reproducible validation
From the repository root, use a virtual environment with requirements-dev.txt
installed and the repository's frontend dependencies available. The examples use
.venv/bin/python; repeat the contract tests with a Python 3.12 environment as
well as Python 3.11. Record interpreter/dependency versions with results.
.venv/bin/python -m pytest -q \
tests/test_skills_audit_characterization.py tests/test_skills_audit_services.py
.venv/bin/python -m compileall -q app.py core routes src services scripts tests
.venv/bin/python -c 'import routes.skills_routes; import services.skills.agent_runner; import services.skills.audit_workflow'
.venv/bin/python scripts/build_frontend.py --check
node scripts/check_frontend.mjs
node --test tests/skillsApproval.test.mjs tests/frontendArchitecture.test.mjs
git diff --check
The focused Skills, authority and approval selection is reproducible with:
.venv/bin/python - <<'PYTHON'
from pathlib import Path
import subprocess
import sys
files = set()
for path in Path("tests").rglob("test_*.py"):
text = path.read_text()
if ("skill" in path.name or "approval" in path.name
or "utility_reasoning" in path.name or "skills_routes" in text
or "review_endpoint_credentials" in path.name):
files.add(str(path))
files.update([
"tests/test_request_authority.py",
"tests/test_endpoint_registered_authority.py",
"tests/test_redos_verdict_continuation.py",
"tests/test_redos_residual_python.py",
])
subprocess.run([sys.executable, "-m", "pytest", "-q", "-rs",
"--durations=15", *sorted(files)], check=True)
PYTHON
The original selection contained 729 cases. Additional characterization cases
increase that count without removing the original coverage. For the full suite,
remove a development APP_PORT and put the environment's tools on PATH:
env -u APP_PORT PATH="$PWD/.venv/bin:$PATH" \
PLAYWRIGHT_BROWSERS_PATH="$PWD/.cache/ms-playwright" \
.venv/bin/python -m pytest -q -rs --durations=25
Run the checksum-verified Gitleaks binary pinned in
.github/workflows/secret-scan.yml on full reachable history, including the PR
head and fetched official dev. The current workflow pins 8.30.1. Its CI command
is gitleaks git --no-banner --redact --verbose . with a full-history checkout.
A local checkout can restrict refs to the relevant histories with
--log-opts='HEAD upstream/dev'; do not restrict the commit range to just the
latest patch. Scan uncommitted fixtures too before committing.
Historical evidence and validation limits
The initial extraction validation at a547c4d8 reported 42 characterization and
service cases, 729 focused cases, 13 relevant Node cases and 17 isolated uvicorn
HTTP checks passing. The historical repository-wide run reported 13,959 passed,
60 skipped and three failures. These counts describe the extraction validation,
not the current revision. The HTTP checks used isolated state and authentication
and included second-owner denial, but exercised neither a configured live model
nor an external tool action.
The two containment failures reproduced on unchanged baseline 86b886c6 with
extra Failed to create stream fd: No such file or directory stderr output:
test_namespace_handshake_closes_model_stdintest_execution_environment_cannot_replace_probed_bwrap[False]
test_legacy_cleanup_against_a_private_real_tmux_server failed under the original
full-suite load and passed isolated reruns. Its cause was not fully diagnosed.
These historical failures must be distinguished from new regressions; successful
CI and maintainer review remain integration gates. Earlier AST comparisons and
the maintainer review
provide historical extraction evidence, not an independent review of later fixes.
Known limitations and follow-up boundaries
Issue #6635 remains open: manual tests without an appropriate workspace can still fail to execute their proposed action. Stream cleanup and exact approvals are preserved, but do not supply a missing workspace.
Jobs remain process-local and do not survive restarts. Durable jobs, moving the preferences adapter below transport, consolidating storage under the domain package, or changing duplicate/publication policies need separate designs. The canonical manager, skill format/lifecycle modules, provider implementations, agent authority enforcement and frontend contracts remain their existing owners.
The second-review correction report records the restored prompt seams, independently expanded constructor baseline, actual validation results and a separate prioritized plan for the six inherited P2 failure paths. Model-controlled lifecycle fields and preference-load failures require maintainer action before automatic rewrite/publication deployment.