test_recursive_context_policy.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. import json
  2. import tempfile
  3. import unittest
  4. from pydantic import ValidationError
  5. from cyber_agent.core.agent_mode import (
  6. RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY,
  7. policy_from_context,
  8. )
  9. from cyber_agent.core.runner import AgentRunner, RunConfig
  10. from cyber_agent.core.context_policy import (
  11. MAX_CONTEXT_REFS,
  12. MAX_CONTEXT_SNAPSHOTS_CHARS,
  13. MAX_ROOT_TASK_ANCHOR_CHARS,
  14. MAX_TASK_BRIEF_CHARS,
  15. ContextPolicyError,
  16. add_context_snapshot,
  17. build_child_context_access,
  18. canonical_json,
  19. context_chars,
  20. create_context_snapshot,
  21. get_authorized_context_snapshot,
  22. normalize_root_task_anchor,
  23. normalize_task_brief,
  24. persist_root_task_anchor,
  25. prune_context_access,
  26. replace_context_access,
  27. require_matching_root_task_anchor,
  28. require_root_task_anchor,
  29. root_task_anchor_hash,
  30. )
  31. from cyber_agent.core.task_protocol import (
  32. ContextRef,
  33. TaskBrief,
  34. initialize_task_progress,
  35. new_task_protocol,
  36. replace_task_brief,
  37. )
  38. from cyber_agent.tools.builtin.context import read_context_ref
  39. from cyber_agent.tools.builtin.subagent import _get_allowed_tools
  40. from cyber_agent.trace.models import Trace
  41. from cyber_agent.trace.run_api import CreateRequest
  42. from cyber_agent.trace.store import FileSystemTraceStore
  43. ROOT_ANCHOR = {
  44. "objective": "五层委托仍围绕同一个根任务",
  45. "completion_criteria": ["所有局部结论可追溯", "根结果通过独立验收"],
  46. "constraints": ["不得编造证据"],
  47. }
  48. def brief(level: int, **updates) -> TaskBrief:
  49. values = {
  50. "objective": f"完成第 {level} 层局部任务",
  51. "reason": f"第 {level - 1} 层需要该结果",
  52. "completion_criteria": [f"第 {level} 层结论可验收"],
  53. "expected_outputs": [f"第 {level} 层结论"],
  54. "parent_findings": [f"直接父级结论 {level - 1}"],
  55. "context": {"local_level": level},
  56. "constraints": [f"约束-{level}"],
  57. }
  58. values.update(updates)
  59. return TaskBrief.model_validate(values)
  60. def anchored_context(task_brief: TaskBrief | None = None) -> dict:
  61. context = {
  62. "agent_mode": "recursive",
  63. "agent_mode_revision": 3,
  64. "root_trace_id": "root",
  65. "agent_depth": 0,
  66. "task_protocol": new_task_protocol(task_brief),
  67. }
  68. anchor = persist_root_task_anchor(context, ROOT_ANCHOR)
  69. initialize_task_progress(
  70. context["task_protocol"],
  71. root_task_anchor_hash=context.get("root_task_anchor_hash"),
  72. )
  73. replace_context_access(
  74. context,
  75. [],
  76. root_task_anchor=anchor,
  77. task_brief=task_brief,
  78. )
  79. return context
  80. class RootAnchorAndTaskBriefTest(unittest.TestCase):
  81. def test_anchor_is_canonical_and_hash_stays_identical_for_five_levels(self):
  82. anchor = normalize_root_task_anchor({
  83. "constraints": ["不得编造证据", "不得编造证据"],
  84. "completion_criteria": ["所有局部结论可追溯", "根结果通过独立验收"],
  85. "objective": "五层委托仍围绕同一个根任务",
  86. })
  87. expected_json = canonical_json(anchor.model_dump(mode="json"))
  88. expected_hash = root_task_anchor_hash(anchor)
  89. for _ in range(6):
  90. context = {}
  91. persisted = persist_root_task_anchor(context, anchor)
  92. self.assertEqual(expected_json, canonical_json(persisted.model_dump(mode="json")))
  93. self.assertEqual(expected_hash, context["root_task_anchor_hash"])
  94. self.assertEqual(expected_json, canonical_json(require_root_task_anchor(context).model_dump(mode="json")))
  95. def test_anchor_rejects_tampering_and_enforces_real_4000_char_limit(self):
  96. context = {}
  97. persist_root_task_anchor(context, ROOT_ANCHOR)
  98. context["root_task_anchor"]["objective"] = "被篡改"
  99. with self.assertRaisesRegex(ContextPolicyError, "hash"):
  100. require_root_task_anchor(context)
  101. low, high = 1, MAX_ROOT_TASK_ANCHOR_CHARS * 2
  102. while low < high:
  103. middle = (low + high + 1) // 2
  104. candidate = {**ROOT_ANCHOR, "objective": "根" * middle}
  105. try:
  106. normalize_root_task_anchor(candidate)
  107. low = middle
  108. except ContextPolicyError:
  109. high = middle - 1
  110. largest = normalize_root_task_anchor({**ROOT_ANCHOR, "objective": "根" * low})
  111. self.assertLessEqual(
  112. context_chars(largest.model_dump(mode="json")),
  113. MAX_ROOT_TASK_ANCHOR_CHARS,
  114. )
  115. with self.assertRaisesRegex(ContextPolicyError, "4000"):
  116. normalize_root_task_anchor({**ROOT_ANCHOR, "objective": "根" * (low + 1)})
  117. def test_self_consistent_child_anchor_must_still_match_authoritative_root(self):
  118. root_context = {}
  119. child_context = {}
  120. persist_root_task_anchor(root_context, ROOT_ANCHOR)
  121. persist_root_task_anchor(child_context, {
  122. **ROOT_ANCHOR,
  123. "objective": "另一份自洽但非权威的根目标",
  124. })
  125. with self.assertRaisesRegex(ContextPolicyError, "does not match"):
  126. require_matching_root_task_anchor(root_context, child_context)
  127. def test_five_level_constraints_accumulate_without_other_context_drift(self):
  128. parent = None
  129. inherited_constraints = ROOT_ANCHOR["constraints"][:]
  130. for level in range(1, 6):
  131. current = normalize_task_brief(
  132. brief(level),
  133. parent_task_brief=parent,
  134. root_task_anchor=ROOT_ANCHOR,
  135. )
  136. inherited_constraints.append(f"约束-{level}")
  137. self.assertEqual(inherited_constraints, current.constraints)
  138. self.assertEqual([f"直接父级结论 {level - 1}"], current.parent_findings)
  139. self.assertEqual({"local_level": level}, current.context)
  140. self.assertEqual([], current.context_refs)
  141. parent = current
  142. def test_task_brief_version_preserves_old_normalized_content(self):
  143. first = normalize_task_brief(brief(1), root_task_anchor=ROOT_ANCHOR)
  144. state = new_task_protocol(first)
  145. second = normalize_task_brief(
  146. brief(1, objective="修订后的局部任务"),
  147. root_task_anchor=ROOT_ANCHOR,
  148. )
  149. replace_task_brief(state, second, effective_at_sequence=9)
  150. self.assertEqual(2, state["task_brief_version"])
  151. self.assertEqual(9, state["task_brief_effective_at_sequence"])
  152. self.assertEqual(first.model_dump(), state["task_brief_history"][0]["task_brief"])
  153. self.assertEqual(second.model_dump(), state["task_brief"])
  154. second.objective = "外部再次修改"
  155. self.assertEqual("修订后的局部任务", state["task_brief"]["objective"])
  156. def test_task_brief_uses_stable_json_and_enforces_16000_chars(self):
  157. escaped = normalize_task_brief(
  158. brief(1, context={"quote": "\"中文\\n", "ordered": {"b": 2, "a": 1}}),
  159. root_task_anchor=ROOT_ANCHOR,
  160. )
  161. encoded = canonical_json(escaped.model_dump(mode="json"))
  162. self.assertEqual(encoded, canonical_json(json.loads(encoded)))
  163. with self.assertRaisesRegex(ContextPolicyError, str(MAX_TASK_BRIEF_CHARS)):
  164. normalize_task_brief(
  165. brief(1, context={"payload": "资料" * MAX_TASK_BRIEF_CHARS}),
  166. root_task_anchor=ROOT_ANCHOR,
  167. )
  168. def test_create_request_uses_new_anchor_and_rejects_removed_field(self):
  169. request = CreateRequest(
  170. messages=[{"role": "user", "content": "开始"}],
  171. root_task_anchor=ROOT_ANCHOR,
  172. )
  173. self.assertEqual(ROOT_ANCHOR["objective"], request.root_task_anchor.objective)
  174. with self.assertRaisesRegex(ValidationError, "root_task_anchor"):
  175. CreateRequest(
  176. messages=[{"role": "user", "content": "开始"}],
  177. root_completion_criteria=["完成"],
  178. )
  179. class ContextReferencePolicyTest(unittest.TestCase):
  180. def test_root_child_only_gets_anchor_and_deeper_child_gets_parent_brief(self):
  181. root_context = anchored_context()
  182. level1 = normalize_task_brief(brief(1), root_task_anchor=ROOT_ANCHOR)
  183. child_access = build_child_context_access(
  184. parent_context=root_context,
  185. parent_trace_id="root",
  186. root_trace_id="root",
  187. uid="user-1",
  188. parent_task_state=root_context["task_protocol"],
  189. child_task_brief=level1,
  190. root_task_anchor=normalize_root_task_anchor(ROOT_ANCHOR),
  191. )
  192. self.assertEqual({}, child_access["snapshots"])
  193. level1_context = anchored_context(level1)
  194. level2 = normalize_task_brief(
  195. brief(2),
  196. parent_task_brief=level1,
  197. root_task_anchor=ROOT_ANCHOR,
  198. )
  199. grandchild_access = build_child_context_access(
  200. parent_context=level1_context,
  201. parent_trace_id="depth-1",
  202. root_trace_id="root",
  203. uid="user-1",
  204. parent_task_state=level1_context["task_protocol"],
  205. child_task_brief=level2,
  206. root_task_anchor=normalize_root_task_anchor(ROOT_ANCHOR),
  207. )
  208. snapshots = list(grandchild_access["snapshots"].values())
  209. self.assertEqual(1, len(snapshots))
  210. self.assertEqual("task_brief", snapshots[0]["kind"])
  211. self.assertEqual(level1.model_dump(mode="json"), snapshots[0]["content"])
  212. def test_reference_requires_exact_local_grant_version_tree_and_owner(self):
  213. context = anchored_context(brief(1))
  214. snapshot = create_context_snapshot(
  215. kind="reviewed_task_result",
  216. summary="已审核结果",
  217. source_trace_id="child",
  218. root_trace_id="root",
  219. uid="user-1",
  220. content={"task_report": {"summary": "真实结论"}},
  221. granted_at_sequence=7,
  222. )
  223. ref = add_context_snapshot(
  224. context,
  225. snapshot,
  226. root_task_anchor=ROOT_ANCHOR,
  227. task_brief=brief(1),
  228. )
  229. loaded = get_authorized_context_snapshot(
  230. context,
  231. ref_id=ref.ref_id,
  232. version=ref.version,
  233. root_trace_id="root",
  234. uid="user-1",
  235. )
  236. self.assertEqual("真实结论", loaded.content["task_report"]["summary"])
  237. for changes, message in (
  238. ({"version": "0" * 64}, "version"),
  239. ({"root_trace_id": "other-root"}, "another tree"),
  240. ({"uid": "user-2"}, "another tree"),
  241. ):
  242. values = {
  243. "ref_id": ref.ref_id,
  244. "version": ref.version,
  245. "root_trace_id": "root",
  246. "uid": "user-1",
  247. **changes,
  248. }
  249. with self.assertRaisesRegex(ContextPolicyError, message):
  250. get_authorized_context_snapshot(context, **values)
  251. with self.assertRaisesRegex(ContextPolicyError, "not authorized"):
  252. get_authorized_context_snapshot(
  253. context,
  254. ref_id="ctx_unknown",
  255. version="0" * 64,
  256. root_trace_id="root",
  257. uid="user-1",
  258. )
  259. context["context_access"]["snapshots"][ref.ref_id]["content"] = {
  260. "task_report": {"summary": "伪造结论"}
  261. }
  262. with self.assertRaisesRegex(ContextPolicyError, "version"):
  263. get_authorized_context_snapshot(
  264. context,
  265. ref_id=ref.ref_id,
  266. version=ref.version,
  267. root_trace_id="root",
  268. uid="user-1",
  269. )
  270. context = anchored_context(brief(1))
  271. ref = add_context_snapshot(
  272. context,
  273. snapshot,
  274. root_task_anchor=ROOT_ANCHOR,
  275. task_brief=brief(1),
  276. )
  277. context["context_access"]["snapshots"][ref.ref_id]["source_trace_id"] = "sibling"
  278. with self.assertRaisesRegex(ContextPolicyError, "ref_id"):
  279. get_authorized_context_snapshot(
  280. context,
  281. ref_id=ref.ref_id,
  282. version=ref.version,
  283. root_trace_id="root",
  284. uid="user-1",
  285. )
  286. context = anchored_context(brief(1))
  287. ref = add_context_snapshot(
  288. context,
  289. snapshot,
  290. root_task_anchor=ROOT_ANCHOR,
  291. task_brief=brief(1),
  292. )
  293. raw = context["context_access"]["snapshots"].pop(ref.ref_id)
  294. mismatched_key = "ctx_000000000000000000000000"
  295. context["context_access"]["snapshots"][mismatched_key] = raw
  296. with self.assertRaisesRegex(ContextPolicyError, "does not match its key"):
  297. get_authorized_context_snapshot(
  298. context,
  299. ref_id=mismatched_key,
  300. version=ref.version,
  301. root_trace_id="root",
  302. uid="user-1",
  303. )
  304. forwarded = brief(
  305. 2,
  306. context_refs=[ContextRef(ref_id=mismatched_key, version=ref.version)],
  307. )
  308. with self.assertRaisesRegex(ContextPolicyError, "does not match its key"):
  309. build_child_context_access(
  310. parent_context=context,
  311. parent_trace_id="depth-1",
  312. root_trace_id="root",
  313. uid="user-1",
  314. parent_task_state=context["task_protocol"],
  315. child_task_brief=forwarded,
  316. root_task_anchor=normalize_root_task_anchor(ROOT_ANCHOR),
  317. )
  318. def test_reference_must_be_forwarded_at_every_edge(self):
  319. parent_context = anchored_context(brief(1))
  320. ancestor_snapshot = create_context_snapshot(
  321. kind="reviewed_task_result",
  322. summary="祖先已审核结果",
  323. source_trace_id="ancestor-child",
  324. root_trace_id="root",
  325. uid="user-1",
  326. content={"task_review": {"reason": "已确认的完整结论"}},
  327. granted_at_sequence=4,
  328. )
  329. ancestor_ref = add_context_snapshot(
  330. parent_context,
  331. ancestor_snapshot,
  332. root_task_anchor=ROOT_ANCHOR,
  333. task_brief=brief(1),
  334. )
  335. forwarded_brief = normalize_task_brief(
  336. brief(2, context_refs=[ancestor_ref]),
  337. parent_task_brief=brief(1),
  338. root_task_anchor=ROOT_ANCHOR,
  339. )
  340. forwarded_access = build_child_context_access(
  341. parent_context=parent_context,
  342. parent_trace_id="depth-1",
  343. root_trace_id="root",
  344. uid="user-1",
  345. parent_task_state=parent_context["task_protocol"],
  346. child_task_brief=forwarded_brief,
  347. root_task_anchor=normalize_root_task_anchor(ROOT_ANCHOR),
  348. )
  349. self.assertIn(ancestor_ref.ref_id, forwarded_access["snapshots"])
  350. child_context = anchored_context(forwarded_brief)
  351. child_context["context_access"] = forwarded_access
  352. not_forwarded = normalize_task_brief(
  353. brief(3),
  354. parent_task_brief=forwarded_brief,
  355. root_task_anchor=ROOT_ANCHOR,
  356. )
  357. deeper_access = build_child_context_access(
  358. parent_context=child_context,
  359. parent_trace_id="depth-2",
  360. root_trace_id="root",
  361. uid="user-1",
  362. parent_task_state=child_context["task_protocol"],
  363. child_task_brief=not_forwarded,
  364. root_task_anchor=normalize_root_task_anchor(ROOT_ANCHOR),
  365. )
  366. self.assertNotIn(ancestor_ref.ref_id, deeper_access["snapshots"])
  367. def test_reference_limits_and_rewind_cleanup_are_enforced(self):
  368. context = anchored_context(brief(1))
  369. snapshots = [
  370. create_context_snapshot(
  371. kind="reviewed_task_result",
  372. summary=f"结果 {index}",
  373. source_trace_id=f"child-{index}",
  374. root_trace_id="root",
  375. uid="user-1",
  376. content={"index": index},
  377. granted_at_sequence=index,
  378. )
  379. for index in range(MAX_CONTEXT_REFS + 1)
  380. ]
  381. replace_context_access(
  382. context,
  383. snapshots[:MAX_CONTEXT_REFS],
  384. root_task_anchor=ROOT_ANCHOR,
  385. task_brief=brief(1),
  386. )
  387. self.assertEqual(
  388. MAX_CONTEXT_REFS,
  389. context["context_access"]["metrics"]["authorized_ref_count"],
  390. )
  391. with self.assertRaisesRegex(ContextPolicyError, str(MAX_CONTEXT_REFS)):
  392. replace_context_access(
  393. context,
  394. snapshots,
  395. root_task_anchor=ROOT_ANCHOR,
  396. task_brief=brief(1),
  397. )
  398. with self.assertRaisesRegex(ContextPolicyError, "16000"):
  399. create_context_snapshot(
  400. kind="reviewed_task_result",
  401. summary="超大结果",
  402. source_trace_id="oversized",
  403. root_trace_id="root",
  404. uid="user-1",
  405. content={"payload": "x" * 16_000},
  406. granted_at_sequence=1,
  407. )
  408. prune_context_access(context, 3)
  409. kept = context["context_access"]["snapshots"].values()
  410. self.assertTrue(all(item["granted_at_sequence"] <= 3 for item in kept))
  411. oversized = [
  412. create_context_snapshot(
  413. kind="reviewed_task_result",
  414. summary=f"大结果 {index}",
  415. source_trace_id=f"large-{index}",
  416. root_trace_id="root",
  417. uid="user-1",
  418. content={"payload": "x" * 13_000, "index": index},
  419. granted_at_sequence=index,
  420. )
  421. for index in range(5)
  422. ]
  423. self.assertGreater(
  424. sum(context_chars(item.content) for item in oversized),
  425. MAX_CONTEXT_SNAPSHOTS_CHARS,
  426. )
  427. with self.assertRaisesRegex(ContextPolicyError, str(MAX_CONTEXT_SNAPSHOTS_CHARS)):
  428. replace_context_access(
  429. context,
  430. oversized,
  431. root_task_anchor=ROOT_ANCHOR,
  432. task_brief=brief(1),
  433. )
  434. class ContextReferenceToolTest(unittest.IsolatedAsyncioTestCase):
  435. def test_legacy_and_recursive_revision_one_never_expose_context_tool(self):
  436. class Tools:
  437. names = [
  438. "agent",
  439. "read_context_ref",
  440. "submit_task_report",
  441. "review_task_result",
  442. ]
  443. def get_tool_names(self, current_url=None, groups=None):
  444. return list(self.names)
  445. def get_schemas(self, tool_names=None):
  446. selected = self.names if tool_names is None else tool_names
  447. return [{"type": "function", "function": {"name": name}}
  448. for name in selected]
  449. runner = AgentRunner(tool_registry=Tools(), llm_call=lambda **_kwargs: None)
  450. config = RunConfig()
  451. for context in (
  452. {"agent_mode": "legacy", "agent_mode_revision": 1},
  453. {"agent_mode": "recursive", "agent_mode_revision": 1},
  454. ):
  455. trace = Trace(trace_id="trace", mode="agent", task="task", context=context)
  456. names = {
  457. item["function"]["name"]
  458. for item in runner._get_runtime_tool_schemas(config, trace)
  459. }
  460. self.assertNotIn("read_context_ref", names)
  461. self.assertNotIn("submit_task_report", names)
  462. self.assertNotIn("review_task_result", names)
  463. async def test_read_tool_returns_only_local_authorized_snapshot(self):
  464. with tempfile.TemporaryDirectory() as directory:
  465. store = FileSystemTraceStore(directory)
  466. context = anchored_context(brief(1))
  467. snapshot = create_context_snapshot(
  468. kind="reviewed_task_result",
  469. summary="已审核证据",
  470. source_trace_id="child",
  471. root_trace_id="root",
  472. uid="user-1",
  473. content={"evidence": [{"fact": "授权原文"}]},
  474. granted_at_sequence=5,
  475. )
  476. ref = add_context_snapshot(
  477. context,
  478. snapshot,
  479. root_task_anchor=ROOT_ANCHOR,
  480. task_brief=brief(1),
  481. )
  482. await store.create_trace(Trace(
  483. trace_id="root",
  484. mode="agent",
  485. task="root",
  486. uid="user-1",
  487. context=context,
  488. ))
  489. result = await read_context_ref(
  490. ref_id=ref.ref_id,
  491. version=ref.version,
  492. context={"store": store, "trace_id": "root"},
  493. )
  494. payload = json.loads(result.output)
  495. self.assertEqual("授权原文", payload["content"]["evidence"][0]["fact"])
  496. denied = await read_context_ref(
  497. ref_id="ctx_unknown",
  498. version="0" * 64,
  499. context={"store": store, "trace_id": "root"},
  500. )
  501. self.assertIn("not authorized", denied.error)
  502. async def test_read_and_validator_reject_child_anchor_that_differs_from_root(self):
  503. with tempfile.TemporaryDirectory() as directory:
  504. store = FileSystemTraceStore(directory)
  505. root_context = anchored_context()
  506. child_context = anchored_context(brief(1))
  507. persist_root_task_anchor(child_context, {
  508. **ROOT_ANCHOR,
  509. "objective": "非权威但自洽的根目标",
  510. })
  511. child_context["root_trace_id"] = "root"
  512. child_context["agent_depth"] = 1
  513. await store.create_trace(Trace(
  514. trace_id="root",
  515. mode="agent",
  516. task="root",
  517. uid="user-1",
  518. context=root_context,
  519. ))
  520. await store.create_trace(Trace(
  521. trace_id="child",
  522. mode="agent",
  523. task="child",
  524. parent_trace_id="root",
  525. uid="user-1",
  526. context=child_context,
  527. ))
  528. denied = await read_context_ref(
  529. ref_id="ctx_000000000000000000000000",
  530. version="0" * 64,
  531. context={"store": store, "trace_id": "child"},
  532. )
  533. self.assertIn("does not match", denied.error)
  534. async def must_not_call(**_kwargs):
  535. raise AssertionError("mismatched child must fail before Validator LLM")
  536. runner = AgentRunner(trace_store=store, llm_call=must_not_call)
  537. with self.assertRaisesRegex(ContextPolicyError, "does not match"):
  538. await runner.validate_recursive_trace(
  539. "child",
  540. scope="task",
  541. )
  542. def test_depth_five_keeps_read_permission_but_loses_agent(self):
  543. class Tools:
  544. @staticmethod
  545. def get_tool_names():
  546. return [
  547. "agent",
  548. "read_context_ref",
  549. "submit_task_report",
  550. "evaluate",
  551. "bash_command",
  552. ]
  553. class Runner:
  554. tools = Tools()
  555. trace_context = {
  556. "agent_mode": "recursive",
  557. "agent_mode_revision": 2,
  558. }
  559. policy = policy_from_context(trace_context)
  560. context = {
  561. "runner": Runner(),
  562. RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY: [
  563. "agent",
  564. "read_context_ref",
  565. "submit_task_report",
  566. ],
  567. }
  568. depth_four = _get_allowed_tools(context, 4, policy)
  569. depth_five = _get_allowed_tools(context, 5, policy)
  570. self.assertIn("agent", depth_four)
  571. self.assertIn("read_context_ref", depth_four)
  572. self.assertNotIn("agent", depth_five)
  573. self.assertIn("read_context_ref", depth_five)
  574. self.assertNotIn("evaluate", depth_five)
  575. self.assertNotIn("bash_command", depth_five)
  576. context[RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY].remove("read_context_ref")
  577. self.assertNotIn("read_context_ref", _get_allowed_tools(context, 4, policy))
  578. self.assertNotIn("read_context_ref", _get_allowed_tools(context, 5, policy))
  579. if __name__ == "__main__":
  580. unittest.main()