feat(tests): J10 journey — agents call each other through the MCP server with their own keys (#2349) - #2809
Conversation
…ver with their own keys (#2349) The journey harness for J10, "my agents can call each other, and I can see what they said". Two ephemeral agents; every call is made the way a real playbook makes it — through the MCP server with the caller's own agent-scoped key, read from its container because no API returns it — so the harness crosses the same checkAgentAccess gate a real agent does. Credential-free on every PR: the permitted call lands on the callee attributed to the caller with a collaboration activity on the caller (IA-01, AC-01); a call with no edge is refused with a reason naming both agents and nothing runs (P-02); a stopped callee answers 503 "Agent is not running" within seconds and leaves no row (IA-03); a fan-out is refused past 50 at the tool and the backend and lands as one batch on one agent (IA-02); a loop stops at its budget; deleting the callee leaves no dangling edge (L-03). On a keyed stack the callee's real answer is read back from its execution record and every fan-out subtask completes. Three strict=True xfails carry the findings, each with its own issue: no chain-depth guard on agent-to-agent chat chains (#2806); a refused call is audited as a successful tool call (#2807); run_agent_loop never runs the permission gate (Abilityai/trinity-enterprise#628). The backend routes' owner- equivalence for agent keys is a ruling, Abilityai/trinity-enterprise#629, with no public reproducer. Journey conftest gains the primitives: create_agent_and_wait (lifted out of journey_agent), agent_mcp_key (Docker SDK, by the trinity.agent-name label, extracting exactly one variable), McpSession (httpx JSON-RPC over the server's streamable-HTTP transport, reprs never carry headers), permission-edge and read-back helpers, and a module-scoped concurrently provisioned agent pair. Catalog: J10 flips to tier journey-smoke (the smoke lane runs the directory wholesale) with built: false, J03-style. IA-03's A2A sentence is corrected — the inbound route never answers 409. Registry entry, regenerated JOURNEYS.md, Testing pointers in four feature flows, two learnings entries, and the diff-scoped CSO report. Journey Impact: extends: J10 Fixes #2349 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/review ReportBranch: 0 critical. 4 informational. This is careful work — most of what I went looking for was already reasoned about in a docstring. Findings below are small, and two of them are about a guarantee the harness states but does not check. Scope checkIntent (#2349): a journey harness proving a two-agent round trip, the permission boundary, loop/budget guards and an honest failure on a dead callee — with findings shipped as Plan completion vs the 8 acceptance criteria
Informational findings[I1] @pytest.fixture()
def pair(journey_client, journey_agent_pair):
"""The pair with a clean slate: no permission edges either way, both running."""def clear_edges(client, source: str) -> None:
for target in permitted_agents(client, source):
client.delete(f"/api/agents/{source}/permissions/{target}") # status discardedEvery other helper in this file asserts its call (
So the expensive design choice rests on a reset that is not checked. A DELETE that 404s or 500s leaks an edge into the next test, and the failure direction — while safe (the permission test goes red) — goes red blaming the product for a harness leak, which is the one failure mode a journey harness must not have. Suggestion: one line at the end of [I2] The model gate asks about the harness host, not about the agent (Confidence: 8/10) _HAS_MODEL_KEY = bool(os.getenv("ANTHROPIC_API_KEY")) and os.getenv("ANTHROPIC_API_KEY") != "placeholder"
needs_model = pytest.mark.skipif(not _HAS_MODEL_KEY, reason="journey needs a real provider key")
Suggestion: treat a registered subscription as satisfying the gate ( [I3] The SSE branch parses unguarded (Confidence: 6/10) msgs = [json.loads(line[6:]) for line in resp.text.splitlines() if line.startswith("data: ")]
mine = [m for m in msgs if m.get("id") == body["id"]]A Suggestion: skip unparseable lines and non-dict messages rather than letting them throw. [I4] Drift fails the test (safe direction) but reports it as a product regression. The mitigating half is already there and is the right instinct: the test asserts the refusal names the cap ( Suggestion: none required — noting it so the next person to raise the cap knows two places move. Clean categories — with the line that proves it
Summary
No commits pushed to this branch — it is not mine, and there were no critical findings to auto-fix. One note for sequencing rather than for this PR: #2806's xfail is the flip condition named in that issue's own AC #4, so whoever takes #2806 needs this PR merged first, or has to stack on it. Worth knowing before either is scheduled. |
… the SSE parser skips bad frames (#2349) Review of #2809, findings I1 and I3. `clear_edges` now asserts every revoke and the empty edge set afterwards, so a leaked edge blames the harness reset rather than the product in a later permission test — the property the module-scoped pair's order-independence rests on. `McpSession._post` skips a `data:` frame that is not JSON or not an object instead of raising from inside the transport, so a malformed frame surfaces as the caller's named failure. No assertion changed; J10 rerun on a live stack: 9 passed, 3 xfailed. I2 (the model gate reads the pytest host's environment, so keyed journeys skip invisibly on a subscription-authenticated stack) predates this PR — J03 carries the same gate — and is filed as #2812. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks for the pass — all four findings check out. Dispositions:
No assertion changed. J10 rerun on a live stack after the fixes: 9 passed, 3 xfailed (96 s). And yes on sequencing — #2806's flip condition needs this merged first; the follow-ups are ordered that way. |
|
Resolve by running |
Resolves the append-only ledger conflict in docs/memory/learnings.md by keeping both sides in landing order: dev's three 2026-09-15 entries (#2795, #2794 ×2) first, then this branch's two (#2349). tests/registry.json auto-merged; no migration files were incoming. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dolho
left a comment
There was a problem hiding this comment.
Re-review — head 7143c1a1 — APPROVE
Checked the dispositions against the code, not the comment:
- I1 —
0e32cef8f:clear_edgesnow asserts each revoke answers 200/204 and thatpermitted_agents()is empty afterwards, both with messages that say harness reset, so a leaked edge can no longer fail a later permission test blaming the product. Exactly the one line I asked for, plus the per-call check. ✅ - I3 — same commit: the SSE loop skips non-JSON and non-dict frames; the caller's "no message" path is the one that fires. ✅
- I2 — filed as #2812 (verified OPEN) rather than fixed here. Agreed with the reasoning: gating on "the instance has a subscription" would trade an invisible skip for a false failure on a fresh agent still on the platform key, so the shared tier helper is the right home. ✅
- I4 — no change, as suggested. ✅
The origin/dev merge (7143c1a1) resolved only learnings.md and tests/registry.json by hand; no conflict markers on the head; src/ still untouched against the merge-base, which is AC #7 held structurally. All 20 checks green including journey-smoke (5m49s) and the six-shard unit matrix with a clean regression diff. Fixes #2349 present, so the promoter picks it up on merge.
Sequencing reminder for whoever merges: #2806's own AC #4 is the strict-xfail in this file flipping, so this lands before #2806 is picked up.
|
Resolve by merging |
Summary
tests/journeys/test_j10_agent_calls_agent_journey.py): two ephemeral agents, every call made through the MCP server with the caller's own agent-scoped key, read from its container — the harness crosses the realcheckAgentAccessgate. Credential-free on every PR: attribution (IA-01, AC-01), refusal with a reason (P-02), stopped callee fails fast and row-less (IA-03), fan-out capped at both layers and landing as one batch (IA-02), loop stops at its budget, delete cascade (L-03). On a keyed stack: the callee's real answer read back from its execution record, and all 12 fan-out subtasks completed.success: true(bug(mcp): a denied chat_with_agent / fan_out call is audited as a successful tool call #2807),run_agent_loopskips the permission gate (abilityai/trinity-enterprise#628). The backend routes' owner-equivalence for agent keys is filed as a ruling (abilityai/trinity-enterprise#629) with no public reproducer, per the plan-gate decision.tier: journey-smoke,built: false; IA-03's false A2A 409 claim corrected; registry entry; regenerated JOURNEYS.md; Testing pointers in four feature flows; two learnings entries; diff-scoped CSO report (0 findings introduced, 4 pre-existing tracked).Fixes #2349
Journey Impact
Journey Impact: extends: J10
Type of Change
Changes
tests/journeys/test_j10_agent_calls_agent_journey.py(new, 12 tests)tests/journeys/conftest.py—create_agent_and_wait,agent_mcp_key,McpSession, edge / executions / activities helpers,journey_agent_pair,pair,mcp_as_callertests/journeys/catalog.yaml,tests/registry.json,docs/testing/JOURNEYS.md(regenerated),docs/testing/orchestration-invariant-catalog.md(IA-03)docs/memory/feature-flows/{agent-to-agent-collaboration,agent-permissions,fan-out,run-agent-loop}.md,docs/memory/learnings.mddocs/security-reports/cso-diff-2026-09-15-2349-j10-journey.{json,md}Test Plan
cd tests && TRINITY_API_URL=http://localhost:8000 pytest journeys/ -v -rsxX --timeout=300→ 11 passed, 4 xfailed (2 m 17 s); J10 alone → 9 passed, 3 xfailed (89 s);--runxfailon the two behavioural xfails shows the audit rowsuccess: Trueand the loop started on the sibling with no edgetests/unit/test_2338_journey_catalog.py,test_2337_invariant_namespace.py,test_2339_testing_docs_consolidated.py,test_ent614_source_agent_attribution.py→ 189 passed;tests/lint_root_test_placement.pyOK;scripts/ci/generate_journeys_md.py --checkin syncjourney-smokeon this PR runs the credential-free subset (the keyed tests skip with the allowlisted reason) — expected: 12 collected in J10, 10 pass / 3 xfail / 2 skip across the tierNeeds the Docker socket of the host running the stack (the agent key lives only in the caller's container env);
journey-smoke.ymlboots the stack on the runner, so that holds there.🤖 Generated with Claude Code