Skip to content

Commit f43d512

Browse files
committed
Add P0 agent harness: tool governance, audit sinks, MCP, skills, checkpointing, and streaming
- Implement FilePolicy with glob-based deny rules for pre-dispatch tool blocking - Add ToolRegistry rate limiting with sliding-window quota enforcement - Introduce WebhookAuditSink with HMAC signing and event filtering - Add MCPToolAdapter for remote MCP tool registration via HTTP - Implement skill loader for SKILL.md discovery and prompt injection - Add SQLite checkpoint store for run resume and crash recovery - Support streaming LLM responses with on_chunk callbacks - Include cost tracking, cancel token, subagent budget inheritance, and DPoP replay cache - Add error hint system with actionable messages on all error classes - Write 8 acceptance flows covering policy-as-code, audit chain, cancel, cost, errors, MCP, skills, webhooks - Write 14 integration tests for all new subsystems
1 parent 3ff7f47 commit f43d512

39 files changed

Lines changed: 6138 additions & 24 deletions

docs/acceptance.md

Lines changed: 214 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,219 @@
11
# Acceptance Coverage
22

3-
TeaAgent acceptance tests live under `tests/acceptance/` and verify user-facing workflows rather than isolated primitives.
4-
5-
## Covered
6-
7-
- Daily CLI read-only workflow: `agent preflight`, `agent run`, `agent show`, audit persistence, and run-level audit summary.
8-
- CLI prompt approval workflow: destructive call pauses, `agent resume` auto-approves the pending call id, writes the file, and reports audit summary.
9-
- Daily TUI workflow: chat mode, memory injection, progress streaming, agent answer persistence in session history.
10-
- TUI prompt approval workflow: approval prompt, destructive write, final run payload, and audit summary.
11-
- MCP client compatibility flow: bearer auth, session lifecycle, `tools/list`, `tools/call`, and session close.
12-
- A2A federation flow: remote discovery, partial endpoint failure, capability routing, delegation, context forwarding, and agent trace metadata.
13-
- Managed runtime flow: tool metadata context construction, workspace/request context forwarding, persisted managed task audit events, and result trace metadata.
14-
- Long-running worker flow: background worker start, list, show, log tail, and stop lifecycle.
15-
- Workspace edit flow: hash-anchored read/edit, git status, test command execution, git diff inspection, and final diff summary.
16-
- Live provider conformance flow: provider checks are skipped unless an explicit environment gate is set, preventing accidental live API calls in CI.
17-
- Hosted-provider smoke flow: `model smoke --live-env-var` skips live adapter calls unless CI explicitly sets the gate to `1`.
3+
TeaAgent acceptance tests live under `tests/acceptance/` and verify user-facing
4+
workflows rather than isolated primitives. Integration tests live under
5+
`tests/integration/` and verify cross-component interactions.
6+
7+
Run acceptance tests:
8+
9+
```bash
10+
python3 -m pytest tests/acceptance
11+
```
12+
13+
Run integration tests:
14+
15+
```bash
16+
python3 -m pytest tests/integration
17+
```
18+
19+
---
20+
21+
## Acceptance Flows (tests/acceptance/)
22+
23+
### Original Flows
24+
25+
| File | Story | Key assertions |
26+
|---|---|---|
27+
| `test_daily_cli.py` | Daily CLI read-only workflow | `agent preflight`, `agent run`, `agent show`, audit persistence, run-level audit summary |
28+
| `test_daily_tui.py` | Daily TUI workflow | chat mode, memory injection, progress streaming, agent answer persistence in session history |
29+
| `test_mcp_client_flow.py` | MCP client compatibility | bearer auth, session lifecycle, `tools/list`, `tools/call`, session close |
30+
| `test_a2a_federation_flow.py` | A2A federation | remote discovery, partial endpoint failure, capability routing, delegation, context forwarding, agent trace metadata |
31+
| `test_managed_runtime_flow.py` | Managed runtime | tool metadata context construction, workspace/request context forwarding, persisted managed task audit events, result trace metadata |
32+
| `test_ultrawork_flow.py` | Long-running worker | background worker start, list, show, log tail, and stop lifecycle |
33+
| `test_workspace_edit_flow.py` | Workspace edit | hash-anchored read/edit, git status, test command execution, git diff inspection, final diff summary |
34+
| `test_live_provider_conformance_flow.py` | Live provider conformance | provider checks skipped unless explicit env gate set, preventing accidental live API calls in CI |
35+
| `test_model_smoke_gating_flow.py` | Hosted-provider smoke | `model smoke --live-env-var` skips live adapter calls unless CI explicitly sets the gate |
36+
37+
### New Flows — Added in this Sprint
38+
39+
#### Cost & Token Tracking
40+
41+
**File:** `test_cost_tracking_flow.py`
42+
43+
**Story (AC-NEW-7):** *As a user, I want to see live token usage and estimated
44+
cost after a run so that I can track spending and tune my budget limits.*
45+
46+
| Assertion | Details |
47+
|---|---|
48+
| `RunResult.cost_cents >= 0.0` | Present on all terminal statuses |
49+
| `RunResult.input_tokens` accumulated | Summed across all LLM calls in the run |
50+
| `RunResult.output_tokens` accumulated | Summed across all LLM calls in the run |
51+
| `run_completed` audit event carries cost fields | `cost_cents`, `input_tokens`, `output_tokens` |
52+
53+
---
54+
55+
#### Graceful Cancel
56+
57+
**File:** `test_cancel_flow.py`
58+
59+
**Story (AC-NEW-5):** *As a developer, I want to interrupt a running agent using
60+
a cancel token so that long-running tasks can be stopped cleanly without
61+
corrupting state.*
62+
63+
| Assertion | Details |
64+
|---|---|
65+
| `cancel_token.set()` stops the run | Status is `failed:system` |
66+
| `run_started` still in audit log | No corruption on cancel |
67+
| Cancel token set from a different thread | Thread-safe via `threading.Event` |
68+
| Un-set cancel token runs normally | No performance penalty when not cancelled |
69+
| `ChatAgentConfig.cancel_token` wired end-to-end | Flows from config → runner |
70+
71+
---
72+
73+
#### Remote MCP Tool Consumption
74+
75+
**File:** `test_remote_mcp_consumption_flow.py`
76+
77+
**Story (AC-NEW-9):** *As a platform engineer, I want to connect TeaAgent to a
78+
remote MCP server and have its tools appear in the tool registry, with approval
79+
rules applied uniformly.*
80+
81+
| Assertion | Details |
82+
|---|---|
83+
| All remote tools registered | Names match MCP `tools/list` response |
84+
| Annotations inferred from MCP hints | `readOnlyHint`, `destructiveHint` |
85+
| `name_prefix` filter works | Only matching tools are registered |
86+
| `ToolRateLimit` applied uniformly | All remote tools share the same quota |
87+
| Remote tool callable via `ToolRegistry.execute` | Proxied through `MCPHTTPClient` |
88+
89+
---
90+
91+
#### Skill Discovery and Injection
92+
93+
**File:** `test_skill_install_flow.py`
94+
95+
**Story (AC-NEW-10):** *As a platform engineer, I want to install skills into
96+
`.opencode/skill/<name>/SKILL.md` and have them automatically injected into the
97+
agent system prompt.*
98+
99+
| Assertion | Details |
100+
|---|---|
101+
| Skill content in system prompt | After `run_chat_agent` with skill installed |
102+
| Multiple skills all present | All discovered skills injected |
103+
| No `Skills:` section when empty | Clean prompt when no skills installed |
104+
| Project skill overrides user skill | First-wins deduplication by name |
105+
| Skill reaches `ModelDecisionEngine` system prompt | End-to-end via stub adapter assertion |
106+
107+
---
108+
109+
#### Policy-as-Code Deny Rules
110+
111+
**File:** `test_policy_as_code_flow.py`
112+
113+
**Story (AC-NEW-14):** *As a security lead, I want a `policy.yaml` deny-rule
114+
that blocks specific tool calls regardless of permission mode.*
115+
116+
| Assertion | Details |
117+
|---|---|
118+
| `policy.yaml` loaded from workspace root | `load_file_policy(root)` finds it |
119+
| Matching rule blocks the tool call | Runner status is `failed:*` |
120+
| Non-matching tools not affected | Read-only tools pass through |
121+
| Rules fire in `danger-full-access` mode | File policy is independent of `PermissionMode` |
122+
| `argument_pattern` + `tool_pattern` combined | Glob matching on both axes |
123+
124+
---
125+
126+
#### Webhook Audit Event Delivery
127+
128+
**File:** `test_webhook_audit_flow.py`
129+
130+
**Story (AC-NEW-12):** *As a fleet operator, I want to subscribe to audit events
131+
via webhook so my SIEM receives every important event in real time.*
132+
133+
| Assertion | Details |
134+
|---|---|
135+
| `run_started` and `run_completed` delivered | During a full `run_chat_agent` call |
136+
| HMAC-SHA256 signature verifiable | `X-TeaAgent-Signature-256` header |
137+
| Event filter restricts delivery | Only whitelisted `event_type` values sent |
138+
| Webhook failure does not abort run | Unreachable endpoint silently ignored |
139+
140+
---
141+
142+
#### Error Remediation Hints
143+
144+
**File:** `test_error_remediation_flow.py`
145+
146+
**Story (AC-NEW-2):** *As a user with a misconfigured environment, I want error
147+
messages to include actionable remediation hints.*
148+
149+
| Assertion | Details |
150+
|---|---|
151+
| `BudgetExceededError` has hint about limits | Default hint mentions `max_iterations` |
152+
| `ToolPermissionError` has hint about mode | Default hint mentions `allow`/`permission` |
153+
| `ToolExecutionError` has workspace hint | Default hint mentions workspace/command |
154+
| `RunCancelledError` has resume hint | Default hint mentions `agent resume` |
155+
| Hints appear in `str()` output | `` separator injected by `__str__` |
156+
| Custom hints override defaults | `hint=` kwarg takes precedence |
157+
158+
---
159+
160+
#### Audit Log Integrity
161+
162+
**File:** `test_audit_chain_integrity_flow.py`
163+
164+
**Story (AC-NEW-13):** *As a security lead, I want the audit JSONL log to be
165+
verifiable so that tampering is detectable.*
166+
167+
| Assertion | Details |
168+
|---|---|
169+
| Every JSONL line is valid JSON | Parseable individually |
170+
| Event IDs are unique within a run | No duplicates across events |
171+
| Sensitive values redacted in persisted log | `content` argument not in raw file |
172+
| Disk log matches in-memory events | Event types in same order |
173+
| Audit files have restricted permissions | Not world-readable (mode ≤ 0o600) |
174+
175+
---
176+
177+
## Integration Tests (tests/integration/)
178+
179+
| File | Coverage |
180+
|---|---|
181+
| `test_runner_cost_tracking.py` | `RunResult` cost fields + audit event fields (IT-1) |
182+
| `test_cancel_token.py` | Cancel token — pre-cancel, mid-run, thread-safe (IT-2) |
183+
| `test_tool_rate_limit.py` | Sliding-window quota, concurrency safety, expiry (IT-3) |
184+
| `test_mcp_tool_adapter.py` | `register_mcp_tools` discovery, annotations, filter (IT-4) |
185+
| `test_skill_loader.py` | `load_skills` discovery, dedup, cap, prompt injection (IT-5) |
186+
| `test_audit_sink_isolation.py` | Crashing sink does not propagate or block other sinks (IT-6) |
187+
| `test_file_policy.py` | `DenyRule` matching, `FilePolicy.assert_allowed`, runner wiring (IT-7) |
188+
| `test_webhook_sink.py` | HTTP delivery, HMAC, filter, failure suppression (IT-8) |
189+
| `test_error_hints.py` | All error classes have default hints, `str()` rendering (IT-9) |
190+
| `test_subagent_budget_inheritance.py` | Subagent depth limit, error dict, registry guard (IT-10) |
191+
| `test_run_resume_checkpoint.py` | Checkpoint save on tool completion, pending-approval, SQLite round-trip, obs replay (IT-11) |
192+
| `test_destructive_approval_lifecycle.py` | Pause → approve → resume; deny path; auto-approve handler; read-only block (IT-12) |
193+
| `test_streaming_tool_calls.py` | `on_chunk` callbacks, audit events, token accumulation (IT-13) |
194+
| `test_schema_migration_live.py` | Migration ordering, idempotency, data survival, version tracking (IT-14) |
195+
| `test_dpop_replay_concurrency.py` | Concurrent JTI consume: exactly one success; expiry reset (IT-15) |
196+
197+
---
18198

19199
## Next User Stories
20200

21-
- Define the next product-facing acceptance story beyond safe provider gates, such as release packaging smoke or IDE command flow.
201+
The following stories are specified and prioritised but do not yet have
202+
acceptance tests. Each maps to a Review gap analysis recommendation.
203+
204+
| ID | Story | Priority |
205+
|---|---|---|
206+
| AC-NEW-1 | `teaagent init` first-time wizard | P2 |
207+
| AC-NEW-3 | Workspace profile (`.teaagent/profile.toml`) | P2 |
208+
| AC-NEW-4 | Interactive diff preview + inline y/n approval | P0 |
209+
| AC-NEW-6 | `teaagent run undo <run_id>` workspace rollback | P2 |
210+
| AC-NEW-8 | Desktop/webhook notification on ultrawork completion | P2 |
211+
| AC-NEW-11 | A2A delegation carries `traceparent` header | P1 |
212+
| AC-NEW-15 | Configurable PII redaction categories | P2 |
213+
| AC-NEW-16 | HTTPS_PROXY / CA bundle / mTLS "just works" | P2 |
214+
| AC-NEW-17 | `teaagent upgrade` schema migration dry-run preview | P2 |
215+
| AC-NEW-18 | Graceful degradation when disk fills mid-run | P2 |
216+
| AC-NEW-19 | `teaagent run export / import` | P2 |
217+
| AC-NEW-20 | Local model (Ollama/vLLM) with full governance | P2 |
218+
| AC-NEW-21 | `teaagent eval run <suite>` with HTML report | P2 |
219+
| AC-NEW-22 | `teaagent benchmark` latency/cost regression tracking | P2 |

teaagent/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
execute_code_mode,
2626
)
2727
from teaagent.context import ContextCompactor
28+
from teaagent.errors import RunCancelledError
2829
from teaagent.eval import EvalCase, EvalReport, run_eval
30+
from teaagent.file_policy import DenyRule, FilePolicy, load_file_policy
2931
from teaagent.graph_rag import GraphEdge, KnowledgeGraph, graph_retrieve
3032
from teaagent.graphqlite_production import (
3133
GraphQLitePersistentStore,
@@ -68,6 +70,7 @@
6870
from teaagent.mcp_client import MCPClientError, MCPHTTPClient
6971
from teaagent.mcp_http import build_mcp_http_server, serve_mcp_http
7072
from teaagent.mcp_server import handle_mcp_request, serve_mcp_stdio
73+
from teaagent.mcp_tool_adapter import register_mcp_tools
7174
from teaagent.memory import MemoryCatalog, MemoryEntry
7275
from teaagent.model_routing import ModelRoute, classify_task, route_model
7376
from teaagent.oauth21 import (
@@ -111,6 +114,7 @@
111114
FinalAnswer,
112115
ToolRequest,
113116
)
117+
from teaagent.skill_loader import SkillContent, load_skills, skills_to_prompt_section
114118
from teaagent.skill_review import SkillReviewResult, review_skill
115119
from teaagent.stateless_mcp import (
116120
StatelessMCPRequest,
@@ -129,9 +133,10 @@
129133
configure_metrics,
130134
configure_telemetry,
131135
)
132-
from teaagent.tools import ToolAnnotations, ToolRegistry
136+
from teaagent.tools import ToolAnnotations, ToolRateLimit, ToolRegistry
133137
from teaagent.trace import TraceRecorder
134138
from teaagent.ultrawork import UltraworkStore, WorkerRecord
139+
from teaagent.webhook_sink import WebhookAuditSink
135140
from teaagent.workspace_tools import (
136141
WorkspaceToolConfig,
137142
build_workspace_tool_registry,
@@ -154,7 +159,9 @@
154159
'ContextCompactor',
155160
'DPoPValidationResult',
156161
'Decision',
162+
'DenyRule',
157163
'Document',
164+
'FilePolicy',
158165
'EvalCase',
159166
'EvalReport',
160167
'FinalAnswer',
@@ -186,6 +193,7 @@
186193
'LLMResponseFormatError',
187194
'MCPClientError',
188195
'MCPHTTPClient',
196+
'register_mcp_tools',
189197
'MemoryCatalog',
190198
'MemoryEntry',
191199
'MetricSnapshot',
@@ -210,22 +218,29 @@
210218
'ProviderProfile',
211219
'ReadinessReport',
212220
'RunBudget',
221+
'RunCancelledError',
213222
'RunStore',
214223
'RunSummary',
215224
'SQLiteOAuthStore',
225+
'SkillContent',
216226
'SkillReviewResult',
227+
'load_file_policy',
228+
'load_skills',
229+
'skills_to_prompt_section',
217230
'StatelessMCPRequest',
218231
'StatelessMCPResponse',
219232
'TelemetryConfig',
220233
'TelemetryNotAvailable',
221234
'ToolAnnotations',
235+
'ToolRateLimit',
222236
'ToolRegistry',
223237
'ToolRequest',
224238
'TraceRecorder',
225239
'TracingHTTPTransport',
226240
'UltraworkStore',
227241
'UnsafeCodeError',
228242
'WorkerRecord',
243+
'WebhookAuditSink',
229244
'WorkspaceToolConfig',
230245
'agentic_retrieve',
231246
'assemble_agent_prompt',

teaagent/audit.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import contextlib
34
import json
45
import re
56
import threading
@@ -100,7 +101,8 @@ def record(self, event_type: str, run_id: str, **payload: Any) -> AuditEvent:
100101
secure_audit_file(self.path)
101102
sinks = list(self._sinks)
102103
for sink in sinks:
103-
sink(event)
104+
with contextlib.suppress(Exception):
105+
sink(event)
104106
return event
105107

106108

@@ -169,7 +171,9 @@ def redact_tool_result_value(key: str, value: Any) -> Any:
169171

170172

171173
def redact_audit_value(key: str, value: Any) -> Any:
172-
if is_sensitive_key(key):
174+
# Only redact string / bytes values by key sensitivity.
175+
# Numeric, bool, and None values are telemetry data and are never sensitive.
176+
if is_sensitive_key(key) and isinstance(value, (str, bytes)):
173177
return AUDIT_REDACTED
174178
if isinstance(value, dict):
175179
return {

0 commit comments

Comments
 (0)