test_wp09_approval_consumption_postgres.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. from __future__ import annotations
  2. import hashlib
  3. import json
  4. import os
  5. import uuid
  6. from concurrent.futures import ThreadPoolExecutor
  7. from threading import Barrier
  8. from typing import Any
  9. import pytest
  10. from sqlalchemy import create_engine, text
  11. pytestmark = pytest.mark.integration
  12. def _database_url() -> str:
  13. value = os.getenv("TEST_DATABASE_URL")
  14. if not value:
  15. pytest.skip("TEST_DATABASE_URL is required")
  16. return value
  17. def _seed_http_runtime_fixture(engine) -> dict[str, Any]:
  18. """Commit only the precise persisted scope consumed by the HTTP runtime."""
  19. suffix = uuid.uuid4().hex[:12]
  20. ids = {
  21. name: str(uuid.uuid4())
  22. for name in ("workflow", "version", "agent", "grant", "route")
  23. }
  24. fixture = {
  25. **ids,
  26. "tenant": f"wp09_http_{suffix}",
  27. "domain": str(uuid.uuid4()),
  28. "code": f"wp09-http-{suffix}",
  29. "control_incident": None,
  30. "task_uids": [],
  31. }
  32. with engine.begin() as connection:
  33. users = [
  34. str(row[0])
  35. for row in connection.execute(text(
  36. "SELECT id::text FROM public.users WHERE status='active' ORDER BY id LIMIT 2"
  37. ))
  38. ]
  39. if len(users) < 2:
  40. pytest.skip("requires two existing test users for independent review")
  41. fixture["owner"], fixture["reviewer"] = users
  42. connection.execute(text("""
  43. INSERT INTO public.governance_workflows
  44. (uid,code,name,status,current_version,created_by)
  45. VALUES (CAST(:workflow AS uuid),:code,'wp09 http','published',1,
  46. CAST(:owner AS uuid))
  47. """), fixture)
  48. connection.execute(text("""
  49. INSERT INTO public.governance_workflow_versions
  50. (uid,workflow_uid,version,status,definition,created_by,published_by,published_at)
  51. VALUES (CAST(:version AS uuid),CAST(:workflow AS uuid),1,'published','{}',
  52. CAST(:owner AS uuid),CAST(:owner AS uuid),clock_timestamp())
  53. """), fixture)
  54. connection.execute(text("""
  55. UPDATE public.governance_workflows
  56. SET active_version_uid=CAST(:version AS uuid)
  57. WHERE uid=CAST(:workflow AS uuid)
  58. """), fixture)
  59. connection.execute(text("""
  60. INSERT INTO public.governed_agents
  61. (uid,code,name,purpose,owner_uid,machine_subject,business_domain_uids,
  62. environments,autonomy_level,prompt_policy,status,created_by,updated_by)
  63. VALUES (CAST(:agent AS uuid),:code,'wp09 http','test',CAST(:owner AS uuid),
  64. :machine_subject,CAST(:domains AS jsonb),'["test"]',
  65. 'approval_execution','{}','active',CAST(:owner AS uuid),
  66. CAST(:owner AS uuid))
  67. """), {
  68. **fixture,
  69. "machine_subject": f"wp09-http-{suffix}",
  70. "domains": json.dumps([fixture["domain"]]),
  71. })
  72. connection.execute(text("""
  73. INSERT INTO public.agent_tool_grants
  74. (uid,agent_uid,interface_type,tool_name,action,business_domain_uid,
  75. environment,risk_level,requires_approval,status,created_by)
  76. VALUES (CAST(:grant AS uuid),CAST(:agent AS uuid),'mcp','knowledge.search',
  77. 'execute',CAST(:domain AS uuid),'test','high',true,'active',
  78. CAST(:owner AS uuid))
  79. """), fixture)
  80. connection.execute(text("""
  81. INSERT INTO public.model_gateway_routes
  82. (route_id,tenant_id,principal_id,business_domain_uid,environment,provider,
  83. model,prompt_version,generation,canary_status)
  84. VALUES (:route,:tenant,CAST(:owner AS uuid),CAST(:domain AS uuid),'test',
  85. 'controlled','model','prompt','gen','approved')
  86. """), fixture)
  87. connection.execute(text("""
  88. INSERT INTO public.agent_runtime_budgets
  89. (tenant_id,token_remaining,cost_remaining_micros,tool_remaining,
  90. time_remaining_ms,concurrency_remaining)
  91. VALUES (:tenant,10,10,3,100,3);
  92. INSERT INTO public.agent_runtime_model_budgets(tenant_id,model,remaining)
  93. VALUES (:tenant,'model',3)
  94. """), fixture)
  95. return fixture
  96. def _seed_http_approval(engine, fixture: dict[str, Any], input_text: str) -> str:
  97. task_uid = str(uuid.uuid4())
  98. input_hash = hashlib.sha256(input_text.strip().encode()).hexdigest()
  99. context = json.dumps({
  100. "agent_uid": fixture["agent"],
  101. "principal_id": fixture["owner"],
  102. "business_domain_uid": fixture["domain"],
  103. "environment": "test",
  104. "tool_name": "knowledge.search",
  105. "action": "execute",
  106. "request_digest": input_hash,
  107. "automatic_execution_allowed": False,
  108. })
  109. with engine.begin() as connection:
  110. connection.execute(text("""
  111. INSERT INTO public.governance_tasks
  112. (uid,task_code,workflow_uid,workflow_version,task_type,subject_type,
  113. subject_uid,source_type,source_uid,title,description,business_domain_uid,
  114. priority,status,assignee_uid,due_at,route_snapshot,context,created_by,updated_by)
  115. VALUES (CAST(:task AS uuid),:task_code,CAST(:workflow AS uuid),1,'high_risk',
  116. 'agent',:agent,'wp09',:task,'wp09 http','wp09 http',:domain,
  117. 'critical','approved',CAST(:owner AS uuid),
  118. clock_timestamp()+interval '5 minutes','{}',CAST(:context AS jsonb),
  119. CAST(:owner AS uuid),CAST(:owner AS uuid))
  120. """), {**fixture, "task": task_uid, "task_code": f"WP09-HTTP-{uuid.uuid4().hex[:12]}", "context": context})
  121. connection.execute(text("""
  122. INSERT INTO public.governance_task_reviews(uid,task_uid,reviewer_uid,decision,reason)
  123. VALUES (CAST(:uid AS uuid),CAST(:task AS uuid),CAST(:reviewer AS uuid),
  124. 'approve','wp09 http')
  125. """), {**fixture, "task": task_uid, "uid": str(uuid.uuid4())})
  126. fixture["task_uids"].append(task_uid)
  127. return task_uid
  128. def _runtime_payload(grant_uid: str, approval_task_uid: str, *, key: str, input_text: str) -> dict[str, Any]:
  129. return {
  130. "grant_uid": grant_uid,
  131. "idempotency_key": key,
  132. "input_text": input_text,
  133. "evidence_refs": [],
  134. "estimated_tokens": 1,
  135. "estimated_cost_micros": "1",
  136. "requested_time_ms": 1,
  137. "approval_task_uid": approval_task_uid,
  138. }
  139. def _cleanup_http_runtime_fixture(engine, fixture: dict[str, Any]) -> None:
  140. with engine.begin() as connection:
  141. connection.execute(text("""
  142. DELETE FROM public.agent_runtime_control_claims WHERE tenant_id=:tenant;
  143. DELETE FROM public.agent_runtime_control_events WHERE tenant_id=:tenant;
  144. DELETE FROM public.agent_runtime_settlement_audits WHERE tenant_id=:tenant;
  145. DELETE FROM public.agent_invocation_audits WHERE tenant_id=:tenant;
  146. DELETE FROM public.agent_runtime_reservations WHERE tenant_id=:tenant;
  147. DELETE FROM public.agent_runtime_model_budgets WHERE tenant_id=:tenant;
  148. DELETE FROM public.agent_runtime_budgets WHERE tenant_id=:tenant;
  149. DELETE FROM public.agent_generation_canaries WHERE tenant_id=:tenant;
  150. DELETE FROM public.agent_runtime_states WHERE tenant_id=:tenant;
  151. DELETE FROM public.data_incidents WHERE uid=CAST(:control_incident AS uuid);
  152. DELETE FROM public.governance_task_reviews
  153. WHERE task_uid = ANY(CAST(:task_uids AS uuid[]));
  154. DELETE FROM public.governance_tasks WHERE uid = ANY(CAST(:task_uids AS uuid[]));
  155. DELETE FROM public.agent_governance_events WHERE agent_uid=CAST(:agent AS uuid);
  156. DELETE FROM public.agent_runtime_control_claims WHERE agent_uid=CAST(:agent AS uuid);
  157. DELETE FROM public.agent_credentials WHERE agent_uid=CAST(:agent AS uuid);
  158. DELETE FROM public.model_gateway_routes WHERE route_id=:route;
  159. DELETE FROM public.agent_tool_grants WHERE uid=CAST(:grant AS uuid);
  160. DELETE FROM public.governed_agents WHERE uid=CAST(:agent AS uuid);
  161. UPDATE public.governance_workflows SET active_version_uid=NULL
  162. WHERE uid=CAST(:workflow AS uuid);
  163. DELETE FROM public.governance_workflow_versions WHERE uid=CAST(:version AS uuid);
  164. DELETE FROM public.governance_workflows WHERE uid=CAST(:workflow AS uuid)
  165. """), fixture)
  166. # Deliberately use a new connection: no assertion may be satisfied only by
  167. # the cleanup transaction's uncommitted view.
  168. with engine.connect() as observer:
  169. remaining = observer.execute(text("""
  170. SELECT
  171. (SELECT count(*) FROM public.agent_runtime_control_claims WHERE tenant_id=:tenant) +
  172. (SELECT count(*) FROM public.agent_runtime_control_events WHERE tenant_id=:tenant) +
  173. (SELECT count(*) FROM public.agent_runtime_settlement_audits WHERE tenant_id=:tenant) +
  174. (SELECT count(*) FROM public.agent_invocation_audits WHERE tenant_id=:tenant) +
  175. (SELECT count(*) FROM public.agent_runtime_reservations WHERE tenant_id=:tenant) +
  176. (SELECT count(*) FROM public.agent_runtime_model_budgets WHERE tenant_id=:tenant) +
  177. (SELECT count(*) FROM public.agent_runtime_budgets WHERE tenant_id=:tenant) +
  178. (SELECT count(*) FROM public.agent_generation_canaries WHERE tenant_id=:tenant) +
  179. (SELECT count(*) FROM public.agent_runtime_states WHERE tenant_id=:tenant) +
  180. (SELECT count(*) FROM public.data_incidents WHERE uid=CAST(:control_incident AS uuid)) +
  181. (SELECT count(*) FROM public.governance_task_reviews WHERE task_uid=ANY(CAST(:task_uids AS uuid[]))) +
  182. (SELECT count(*) FROM public.governance_tasks WHERE uid=ANY(CAST(:task_uids AS uuid[]))) +
  183. (SELECT count(*) FROM public.agent_credentials WHERE agent_uid=CAST(:agent AS uuid)) +
  184. (SELECT count(*) FROM public.agent_governance_events WHERE agent_uid=CAST(:agent AS uuid)) +
  185. (SELECT count(*) FROM public.model_gateway_routes WHERE route_id=:route) +
  186. (SELECT count(*) FROM public.agent_tool_grants WHERE uid=CAST(:grant AS uuid)) +
  187. (SELECT count(*) FROM public.governed_agents WHERE uid=CAST(:agent AS uuid)) +
  188. (SELECT count(*) FROM public.governance_workflow_versions WHERE uid=CAST(:version AS uuid)) +
  189. (SELECT count(*) FROM public.governance_workflows WHERE uid=CAST(:workflow AS uuid))
  190. """), fixture).scalar_one()
  191. assert remaining == 0
  192. def test_wp09_runtime_http_commits_once_across_new_app_restart_and_rolls_back(monkeypatch):
  193. """Exercise the real Flask route, credential service and PostgreSQL boundary."""
  194. database_url = _database_url()
  195. engine = create_engine(database_url, pool_pre_ping=True)
  196. fixture = _seed_http_runtime_fixture(engine)
  197. first_task = _seed_http_approval(engine, fixture, "approved-runtime-input")
  198. try:
  199. monkeypatch.setenv("DATABASE_URL", database_url)
  200. monkeypatch.setattr(
  201. "app.core.system.permissions.authenticate_request",
  202. lambda: {"id": fixture["owner"], "sub": fixture["owner"], "roles": ["editor"]},
  203. )
  204. from app import create_app, db
  205. from app.api.knowledge_base.agent_governance_routes import _service
  206. first_app = create_app()
  207. first_app.config.update(TESTING=True)
  208. with first_app.app_context():
  209. credential = _service().issue_credential(
  210. fixture["agent"], {"ttl_seconds": 60}, actor_uid=fixture["owner"]
  211. )
  212. headers = {
  213. "Authorization": "Bearer integration-test",
  214. "X-Agent-Credential": credential["token"],
  215. }
  216. payload = _runtime_payload(
  217. fixture["grant"], first_task, key="restart-once", input_text="approved-runtime-input"
  218. )
  219. response = first_app.test_client().post(
  220. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  221. json=payload,
  222. headers=headers,
  223. )
  224. assert response.status_code == 201
  225. assert response.headers["Cache-Control"] == "no-store"
  226. assert response.get_json()["data"]["replay"] is False
  227. with engine.connect() as observer:
  228. assert observer.execute(text("""
  229. SELECT token_remaining,cost_remaining_micros,tool_remaining,
  230. time_remaining_ms,concurrency_remaining
  231. FROM public.agent_runtime_budgets WHERE tenant_id=:tenant
  232. """), fixture).one() == (9, 9, 2, 99, 2)
  233. assert observer.execute(text("""
  234. SELECT count(*) FROM public.agent_invocation_audits WHERE tenant_id=:tenant
  235. """), fixture).scalar_one() == 1
  236. assert observer.execute(text("""
  237. SELECT status FROM public.governance_tasks WHERE uid=CAST(:task AS uuid)
  238. """), {"task": first_task}).scalar_one() == "closed"
  239. with first_app.app_context():
  240. db.session.remove()
  241. del first_app
  242. restarted_app = create_app()
  243. restarted_app.config.update(TESTING=True)
  244. replay = restarted_app.test_client().post(
  245. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  246. json=payload,
  247. headers=headers,
  248. )
  249. assert replay.status_code == 201
  250. assert replay.headers["Cache-Control"] == "no-store"
  251. assert replay.get_json()["data"]["replay"] is True
  252. with engine.connect() as observer:
  253. assert observer.execute(text("""
  254. SELECT token_remaining,cost_remaining_micros,tool_remaining,
  255. time_remaining_ms,concurrency_remaining
  256. FROM public.agent_runtime_budgets WHERE tenant_id=:tenant
  257. """), fixture).one() == (9, 9, 2, 99, 2)
  258. assert observer.execute(text("""
  259. SELECT count(*) FROM public.agent_invocation_audits WHERE tenant_id=:tenant
  260. """), fixture).scalar_one() == 1
  261. assert observer.execute(text("""
  262. SELECT status FROM public.governance_tasks WHERE uid=CAST(:task AS uuid)
  263. """), {"task": first_task}).scalar_one() == "closed"
  264. changed = restarted_app.test_client().post(
  265. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  266. json={**payload, "input_text": "changed-runtime-input"},
  267. headers=headers,
  268. )
  269. assert changed.status_code == 409
  270. assert changed.headers["Cache-Control"] == "no-store"
  271. rollback_task = _seed_http_approval(engine, fixture, "rollback-runtime-input")
  272. rollback_payload = _runtime_payload(
  273. fixture["grant"], rollback_task, key="rollback-once", input_text="rollback-runtime-input"
  274. )
  275. original_commit = db.session.commit
  276. def fail_commit():
  277. raise RuntimeError("synthetic commit failure")
  278. monkeypatch.setattr(db.session, "commit", fail_commit)
  279. rejected = restarted_app.test_client().post(
  280. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  281. json=rollback_payload,
  282. headers=headers,
  283. )
  284. assert rejected.status_code == 409
  285. assert rejected.headers["Cache-Control"] == "no-store"
  286. with engine.connect() as observer:
  287. assert observer.execute(text("""
  288. SELECT count(*) FROM public.agent_invocation_audits WHERE tenant_id=:tenant
  289. """), fixture).scalar_one() == 1
  290. assert observer.execute(text("""
  291. SELECT status FROM public.governance_tasks WHERE uid=CAST(:task AS uuid)
  292. """), {"task": rollback_task}).scalar_one() == "approved"
  293. assert observer.execute(text("""
  294. SELECT token_remaining,cost_remaining_micros,tool_remaining,
  295. time_remaining_ms,concurrency_remaining
  296. FROM public.agent_runtime_budgets WHERE tenant_id=:tenant
  297. """), fixture).one() == (9, 9, 2, 99, 2)
  298. monkeypatch.setattr(db.session, "commit", original_commit)
  299. reused = restarted_app.test_client().post(
  300. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  301. json=rollback_payload,
  302. headers=headers,
  303. )
  304. assert reused.status_code == 201
  305. assert reused.get_json()["data"]["replay"] is False
  306. finally:
  307. _cleanup_http_runtime_fixture(engine, fixture)
  308. engine.dispose()
  309. def test_wp09_runtime_settlement_is_fenced_replay_safe_and_cross_session(monkeypatch):
  310. """498 settles the durable reservation exactly once on the real HTTP path."""
  311. database_url = _database_url()
  312. engine = create_engine(database_url, pool_pre_ping=True)
  313. fixture = _seed_http_runtime_fixture(engine)
  314. task_uid = _seed_http_approval(engine, fixture, "settlement-input")
  315. try:
  316. monkeypatch.setenv("DATABASE_URL", database_url)
  317. monkeypatch.setattr(
  318. "app.core.system.permissions.authenticate_request",
  319. lambda: {"id": fixture["owner"], "sub": fixture["owner"], "roles": ["editor"]},
  320. )
  321. from app import create_app, db
  322. from app.api.knowledge_base.agent_governance_routes import _service
  323. first_app = create_app()
  324. first_app.config.update(TESTING=True)
  325. with first_app.app_context():
  326. credential = _service().issue_credential(
  327. fixture["agent"], {"ttl_seconds": 60}, actor_uid=fixture["owner"]
  328. )
  329. headers = {"Authorization": "Bearer integration-test", "X-Agent-Credential": credential["token"]}
  330. authorize = first_app.test_client().post(
  331. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  332. json=_runtime_payload(fixture["grant"], task_uid, key="settle-once", input_text="settlement-input"),
  333. headers=headers,
  334. )
  335. assert authorize.status_code == 201
  336. fence = authorize.get_json()["data"]["lease_fence"]
  337. settle_url = f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations/settle-once/settle"
  338. settle_payload = {
  339. "lease_fence": fence, "outcome": "success", "actual_tokens": 0,
  340. "actual_cost_micros": 0, "actual_tools": 0, "actual_time_ms": 0,
  341. }
  342. settled = first_app.test_client().post(settle_url, json=settle_payload, headers=headers)
  343. assert settled.status_code == 201
  344. assert settled.headers["Cache-Control"] == "no-store"
  345. assert settled.get_json()["data"]["replay"] is False
  346. with engine.connect() as observer:
  347. assert observer.execute(text("""
  348. SELECT token_remaining,cost_remaining_micros,tool_remaining,time_remaining_ms,concurrency_remaining
  349. FROM public.agent_runtime_budgets WHERE tenant_id=:tenant
  350. """), fixture).one() == (10, 10, 3, 100, 3)
  351. assert observer.execute(text("SELECT count(*) FROM public.agent_runtime_settlement_audits WHERE tenant_id=:tenant"), fixture).scalar_one() == 1
  352. with first_app.app_context():
  353. db.session.remove()
  354. del first_app
  355. restarted_app = create_app()
  356. restarted_app.config.update(TESTING=True)
  357. replay = restarted_app.test_client().post(settle_url, json=settle_payload, headers=headers)
  358. assert replay.status_code == 201
  359. assert replay.get_json()["data"]["replay"] is True
  360. changed = restarted_app.test_client().post(
  361. settle_url, json={**settle_payload, "actual_tokens": 1}, headers=headers
  362. )
  363. assert changed.status_code == 409
  364. assert changed.headers["Cache-Control"] == "no-store"
  365. with engine.connect() as observer:
  366. assert observer.execute(text("SELECT count(*) FROM public.agent_runtime_settlement_audits WHERE tenant_id=:tenant"), fixture).scalar_one() == 1
  367. expired_task = _seed_http_approval(engine, fixture, "expired-settlement-input")
  368. expired_key = "settle-expired"
  369. expired_auth = restarted_app.test_client().post(
  370. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  371. json=_runtime_payload(fixture["grant"], expired_task, key=expired_key, input_text="expired-settlement-input"),
  372. headers=headers,
  373. )
  374. assert expired_auth.status_code == 201
  375. with engine.begin() as writer:
  376. writer.execute(text("""
  377. UPDATE public.agent_runtime_reservations
  378. SET lease_expires_at=clock_timestamp()-interval '1 second'
  379. WHERE tenant_id=:tenant AND idempotency_key=:key
  380. """), {**fixture, "key": expired_key})
  381. expired_url = f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations/{expired_key}/settle"
  382. late = restarted_app.test_client().post(
  383. expired_url, json={**settle_payload, "lease_fence": expired_auth.get_json()["data"]["lease_fence"]}, headers=headers
  384. )
  385. assert late.status_code == 409
  386. assert late.headers["Cache-Control"] == "no-store"
  387. with engine.connect() as observer:
  388. assert observer.execute(text("""
  389. SELECT status,settlement FROM public.agent_runtime_reservations
  390. WHERE tenant_id=:tenant AND idempotency_key=:key
  391. """), {**fixture, "key": expired_key}).one() == ("failed", "lease_expired")
  392. assert observer.execute(text("""
  393. SELECT token_remaining,cost_remaining_micros,tool_remaining,time_remaining_ms,concurrency_remaining
  394. FROM public.agent_runtime_budgets WHERE tenant_id=:tenant
  395. """), fixture).one() == (10, 10, 3, 100, 3)
  396. rollback_task = _seed_http_approval(engine, fixture, "settlement-rollback-input")
  397. rollback_key = "settle-rollback"
  398. rollback_auth = restarted_app.test_client().post(
  399. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  400. json=_runtime_payload(fixture["grant"], rollback_task, key=rollback_key, input_text="settlement-rollback-input"),
  401. headers=headers,
  402. )
  403. assert rollback_auth.status_code == 201
  404. rollback_url = f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations/{rollback_key}/settle"
  405. rollback_payload = {**settle_payload, "lease_fence": rollback_auth.get_json()["data"]["lease_fence"]}
  406. original_commit = db.session.commit
  407. monkeypatch.setattr(db.session, "commit", lambda: (_ for _ in ()).throw(RuntimeError("synthetic commit")))
  408. failed_commit = restarted_app.test_client().post(rollback_url, json=rollback_payload, headers=headers)
  409. assert failed_commit.status_code == 409
  410. with engine.connect() as observer:
  411. assert observer.execute(text("""
  412. SELECT status FROM public.agent_runtime_reservations WHERE tenant_id=:tenant AND idempotency_key=:key
  413. """), {**fixture, "key": rollback_key}).scalar_one() == "reserved"
  414. assert observer.execute(text("""
  415. SELECT count(*) FROM public.agent_runtime_settlement_audits WHERE tenant_id=:tenant AND idempotency_key=:key
  416. """), {**fixture, "key": rollback_key}).scalar_one() == 0
  417. monkeypatch.setattr(db.session, "commit", original_commit)
  418. overflow = restarted_app.test_client().post(
  419. rollback_url, json={**rollback_payload, "actual_tokens": 2}, headers=headers
  420. )
  421. assert overflow.status_code == 409
  422. settled_after_rollback = restarted_app.test_client().post(rollback_url, json=rollback_payload, headers=headers)
  423. assert settled_after_rollback.status_code == 201
  424. concurrent_task = _seed_http_approval(engine, fixture, "settlement-concurrent-input")
  425. concurrent_key = "settle-concurrent"
  426. concurrent_auth = restarted_app.test_client().post(
  427. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  428. json=_runtime_payload(fixture["grant"], concurrent_task, key=concurrent_key, input_text="settlement-concurrent-input"),
  429. headers=headers,
  430. )
  431. assert concurrent_auth.status_code == 201
  432. concurrent_payload = json.dumps({
  433. "agent_uid": fixture["agent"], "principal_id": fixture["owner"],
  434. "worker_id": f"agent-runtime-http:{fixture['owner']}", "idempotency_key": concurrent_key,
  435. "lease_fence": concurrent_auth.get_json()["data"]["lease_fence"], "outcome": "cancelled",
  436. "actual_tokens": 0, "actual_cost_micros": 0, "actual_tools": 0, "actual_time_ms": 0,
  437. })
  438. barrier = Barrier(2)
  439. def settle_from_independent_connection():
  440. with engine.begin() as connection:
  441. connection.execute(text("SET LOCAL ROLE dataops_agent_runtime"))
  442. barrier.wait(timeout=10)
  443. return connection.execute(text("""
  444. SELECT public.agent_runtime_settle(CAST(:payload AS jsonb))
  445. """), {"payload": concurrent_payload}).scalar_one()
  446. with ThreadPoolExecutor(max_workers=2) as pool:
  447. first, second = list(pool.map(lambda _unused: settle_from_independent_connection(), range(2)))
  448. assert sorted((first["replay"], second["replay"])) == [False, True]
  449. finally:
  450. _cleanup_http_runtime_fixture(engine, fixture)
  451. engine.dispose()
  452. def test_wp09_runtime_rejects_approval_agent_and_principal_tampering(monkeypatch):
  453. database_url = _database_url()
  454. engine = create_engine(database_url, pool_pre_ping=True)
  455. fixture = _seed_http_runtime_fixture(engine)
  456. agent_tampered = _seed_http_approval(engine, fixture, "agent-tamper-input")
  457. principal_tampered = _seed_http_approval(engine, fixture, "principal-tamper-input")
  458. try:
  459. with engine.begin() as connection:
  460. connection.execute(text("""
  461. UPDATE public.governance_tasks SET context=jsonb_set(context,'{agent_uid}',to_jsonb(CAST(:wrong AS text)))
  462. WHERE uid=CAST(:task AS uuid)
  463. """), {"task": agent_tampered, "wrong": str(uuid.uuid4())})
  464. connection.execute(text("""
  465. UPDATE public.governance_tasks SET context=jsonb_set(context,'{principal_id}',to_jsonb(CAST(:wrong AS text)))
  466. WHERE uid=CAST(:task AS uuid)
  467. """), {"task": principal_tampered, "wrong": str(uuid.uuid4())})
  468. monkeypatch.setenv("DATABASE_URL", database_url)
  469. monkeypatch.setattr(
  470. "app.core.system.permissions.authenticate_request",
  471. lambda: {"id": fixture["owner"], "sub": fixture["owner"], "roles": ["editor"]},
  472. )
  473. from app import create_app
  474. from app.api.knowledge_base.agent_governance_routes import _service
  475. app = create_app()
  476. app.config.update(TESTING=True)
  477. with app.app_context():
  478. credential = _service().issue_credential(
  479. fixture["agent"], {"ttl_seconds": 60}, actor_uid=fixture["owner"]
  480. )
  481. headers = {"Authorization": "Bearer integration-test", "X-Agent-Credential": credential["token"]}
  482. for task, value, key in (
  483. (agent_tampered, "agent-tamper-input", "tampered-agent"),
  484. (principal_tampered, "principal-tamper-input", "tampered-principal"),
  485. ):
  486. response = app.test_client().post(
  487. f"/api/knowledge/agents/{fixture['agent']}/runtime/invocations",
  488. json=_runtime_payload(fixture["grant"], task, key=key, input_text=value), headers=headers,
  489. )
  490. assert response.status_code == 409
  491. assert response.headers["Cache-Control"] == "no-store"
  492. with engine.connect() as observer:
  493. assert observer.execute(text("SELECT count(*) FROM public.agent_invocation_audits WHERE tenant_id=:tenant"), fixture).scalar_one() == 0
  494. assert observer.execute(text("SELECT count(*) FROM public.governance_tasks WHERE uid=ANY(CAST(:tasks AS uuid[])) AND status='approved'"), {"tasks": [agent_tampered, principal_tampered]}).scalar_one() == 2
  495. finally:
  496. _cleanup_http_runtime_fixture(engine, fixture)
  497. engine.dispose()
  498. def test_wp09_control_approvals_are_operation_bound_one_shot_and_replay_safe(monkeypatch):
  499. """499 binds recovery/promotion/rollback to separate locked work-center tasks."""
  500. engine = create_engine(_database_url(), pool_pre_ping=True)
  501. fixture = _seed_http_runtime_fixture(engine)
  502. incident = str(uuid.uuid4())
  503. fixture["control_incident"] = incident
  504. fixed = {"dataset_version": "fixture-v1", "dataset_digest": "a" * 64,
  505. "metrics_digest": "b" * 64, "threshold_policy": "default-v1"}
  506. def control_task(operation, digest, fence, from_state, to_state, *, incident_uid="", generation="g1"):
  507. task = str(uuid.uuid4())
  508. context = {
  509. "tenant_id": fixture["tenant"], "agent_uid": fixture["agent"],
  510. "principal_id": fixture["owner"], "business_domain_uid": fixture["domain"],
  511. "environment": "test", "control_operation": operation,
  512. "request_digest": digest, "expected_fence": str(fence), "incident_uid": incident_uid,
  513. "issued_at": "2026-08-13T00:00:00+00:00",
  514. "from_state": from_state, "to_state": to_state, "generation": generation,
  515. "from_generation": generation, "to_generation": generation, **fixed,
  516. }
  517. with engine.begin() as connection:
  518. connection.execute(text("""
  519. INSERT INTO public.governance_tasks(uid,task_code,workflow_uid,workflow_version,task_type,
  520. subject_type,subject_uid,source_type,source_uid,title,description,business_domain_uid,
  521. priority,status,assignee_uid,due_at,route_snapshot,context,created_by,updated_by)
  522. VALUES(CAST(:task AS uuid),:code,CAST(:workflow AS uuid),1,'high_risk','agent',:agent,
  523. 'wp09-control',:task,'wp09 control','wp09 control',:domain,'critical','approved',
  524. CAST(:owner AS uuid),clock_timestamp()+interval '5 minutes','{}',CAST(:context AS jsonb),
  525. CAST(:owner AS uuid),CAST(:owner AS uuid))
  526. """), {**fixture, "task": task, "code": f"WP09-CONTROL-{uuid.uuid4().hex[:10]}", "context": json.dumps(context)})
  527. connection.execute(text("""
  528. INSERT INTO public.governance_task_reviews(uid,task_uid,reviewer_uid,decision,reason)
  529. VALUES(CAST(:uid AS uuid),CAST(:task AS uuid),CAST(:reviewer AS uuid),'approve','control')
  530. """), {**fixture, "task": task, "uid": str(uuid.uuid4())})
  531. fixture["task_uids"].append(task)
  532. return task, context
  533. monkeypatch.setenv("DATABASE_URL", _database_url())
  534. current_actor = {"id": fixture["owner"]}
  535. monkeypatch.setattr(
  536. "app.core.system.permissions.authenticate_request",
  537. lambda: {"id": current_actor["id"], "sub": current_actor["id"], "roles": ["editor"]},
  538. )
  539. from app import create_app, db
  540. from app.api.knowledge_base.agent_governance_routes import (
  541. _runtime_service,
  542. _service,
  543. )
  544. app = create_app()
  545. app.config.update(TESTING=True)
  546. with app.app_context():
  547. credential = _service().issue_credential(
  548. fixture["agent"], {"ttl_seconds": 60}, actor_uid=fixture["owner"]
  549. )
  550. headers = {"Authorization": "Bearer integration-test", "X-Agent-Credential": credential["token"]}
  551. def invoke(payload):
  552. response = app.test_client().post(
  553. f"/api/knowledge/agents/{fixture['agent']}/runtime/control",
  554. json={key: value for key, value in payload.items() if key not in {"agent_uid", "principal_id"}},
  555. headers=headers,
  556. )
  557. if response.status_code != 201:
  558. raise RuntimeError(response.get_json())
  559. return response.get_json()["data"]
  560. try:
  561. with engine.begin() as connection:
  562. connection.execute(text("""
  563. INSERT INTO public.data_incidents(uid,code,dedup_key,title,severity,status,owner_uid,
  564. escalation_level,first_detected_at,last_observed_at,created_by)
  565. VALUES(CAST(:incident AS uuid),:code,:digest,'wp09','critical','open',CAST(:owner AS uuid),1,
  566. clock_timestamp(),clock_timestamp(),CAST(:owner AS uuid));
  567. INSERT INTO public.agent_runtime_states(tenant_id,state,incident_ref,lease_fence)
  568. VALUES(:tenant,'paused',:incident,0);
  569. INSERT INTO public.agent_generation_canaries(tenant_id,generation,status,lease_fence)
  570. VALUES(:tenant,'g1','pending',0)
  571. """), {**fixture, "incident": incident, "code": f"WP09-CONTROL-{uuid.uuid4().hex[:10]}", "digest": hashlib.sha256(incident.encode()).hexdigest()})
  572. recover_task, recover_context = control_task("recover", "c" * 64, 0, "paused", "active", incident_uid=incident)
  573. recover = {**{key: value for key, value in recover_context.items() if key != "control_operation"}, "operation": "recover", "approval_task_uid": recover_task, "idempotency_key": "recover-1", "expected_fence": 0}
  574. # A persisted, credential-bound claim expired by the DB clock cannot be used late.
  575. with app.app_context():
  576. stored_credential = _service().repository.get_credential(credential["jti"])
  577. expired_claim = _runtime_service().repository.issue_control_claim({
  578. "claim_uid": str(uuid.uuid4()), "credential_uid": stored_credential["uid"],
  579. "credential_token_digest": hashlib.sha256(credential["token"].encode()).hexdigest(),
  580. "nonce": str(uuid.uuid4()), "control_payload": recover,
  581. })
  582. db.session.commit()
  583. with engine.begin() as connection:
  584. connection.execute(text("""
  585. UPDATE public.agent_runtime_control_claims
  586. SET expires_at=clock_timestamp()-interval '1 second'
  587. WHERE claim_uid=CAST(:claim AS uuid)
  588. """), {"claim": expired_claim})
  589. with app.app_context(), pytest.raises(RuntimeError):
  590. _runtime_service().repository.control_claimed(recover, expired_claim)
  591. with app.app_context():
  592. db.session.rollback()
  593. assert invoke(recover)["replay"] is False
  594. # Discard the first Flask application/session before replaying its signed credential.
  595. app = create_app()
  596. app.config.update(TESTING=True)
  597. assert invoke(recover)["replay"] is True
  598. with engine.connect() as observer:
  599. assert observer.execute(text("SELECT state FROM public.agent_runtime_states WHERE tenant_id=:tenant"), fixture).scalar_one() == "active"
  600. assert observer.execute(text("SELECT status FROM public.governance_tasks WHERE uid=CAST(:task AS uuid)"), {"task": recover_task}).scalar_one() == "closed"
  601. reused = {**recover, "operation": "canary_promote", "idempotency_key": "cross-operation"}
  602. with pytest.raises(Exception): # noqa: B017 - PostgreSQL emits a driver-specific DBAPI error.
  603. invoke(reused)
  604. promote_task, promote_context = control_task("canary_promote", "d" * 64, 0, "pending", "approved")
  605. promote = {**{key: value for key, value in promote_context.items() if key != "control_operation"}, "operation": "canary_promote", "approval_task_uid": promote_task, "idempotency_key": "promote-1", "expected_fence": 0}
  606. for attack in (
  607. {"tenant_id": "other-tenant"},
  608. {"incident_uid": str(uuid.uuid4())}, {"dataset_digest": "e" * 64},
  609. {"metrics_digest": "e" * 64}, {"generation": "other-generation"},
  610. ):
  611. with pytest.raises(Exception): # noqa: B017 - PostgreSQL emits a driver-specific DBAPI error.
  612. invoke({**promote, **attack, "idempotency_key": f"attack-{uuid.uuid4().hex[:8]}"})
  613. current_actor["id"] = fixture["reviewer"]
  614. with pytest.raises(Exception): # noqa: B017 - Flask returns a stable no-store rejection.
  615. invoke(promote)
  616. current_actor["id"] = fixture["owner"]
  617. with engine.connect() as observer:
  618. assert observer.execute(text("SELECT status FROM public.governance_tasks WHERE uid=CAST(:task AS uuid)"), {"task": promote_task}).scalar_one() == "approved"
  619. assert observer.execute(text("SELECT status FROM public.agent_generation_canaries WHERE tenant_id=:tenant AND generation='g1'"), fixture).scalar_one() == "pending"
  620. assert invoke(promote)["lease_fence"] == 1
  621. wrong_credential = app.test_client().post(
  622. f"/api/knowledge/agents/{fixture['agent']}/runtime/control",
  623. json={key: value for key, value in promote.items() if key not in {"agent_uid", "principal_id"}},
  624. headers={**headers, "X-Agent-Credential": headers["X-Agent-Credential"] + "x"},
  625. )
  626. assert wrong_credential.status_code == 403
  627. assert wrong_credential.headers["Cache-Control"] == "no-store"
  628. changed_metrics = {**promote, "metrics_digest": "e" * 64}
  629. with pytest.raises(Exception): # noqa: B017 - PostgreSQL emits a driver-specific DBAPI error.
  630. invoke(changed_metrics)
  631. rollback_task, rollback_context = control_task("canary_rollback", "f" * 64, 1, "approved", "rolled_back")
  632. rollback = {**{key: value for key, value in rollback_context.items() if key != "control_operation"}, "operation": "canary_rollback", "approval_task_uid": rollback_task, "idempotency_key": "rollback-1", "expected_fence": 1}
  633. assert invoke(rollback)["lease_fence"] == 2
  634. concurrent_task, concurrent_context = control_task("canary_promote", "1" * 64, 0, "pending", "approved", generation="g2")
  635. concurrent = {**{key: value for key, value in concurrent_context.items() if key != "control_operation"}, "operation": "canary_promote", "approval_task_uid": concurrent_task, "idempotency_key": "promote-concurrent", "expected_fence": 0}
  636. with engine.begin() as connection:
  637. connection.execute(text("""
  638. INSERT INTO public.agent_generation_canaries(tenant_id,generation,status,lease_fence)
  639. VALUES(:tenant,'g2','pending',0)
  640. """), fixture)
  641. barrier = Barrier(2)
  642. def concurrent_invoke():
  643. local_app = create_app()
  644. local_app.config.update(TESTING=True)
  645. barrier.wait(timeout=10)
  646. response = local_app.test_client().post(
  647. f"/api/knowledge/agents/{fixture['agent']}/runtime/control",
  648. json={key: value for key, value in concurrent.items() if key not in {"agent_uid", "principal_id"}},
  649. headers=headers,
  650. )
  651. assert response.status_code == 201, response.get_json()
  652. return response.get_json()["data"]
  653. with ThreadPoolExecutor(max_workers=2) as pool:
  654. first, second = list(pool.map(lambda _unused: concurrent_invoke(), range(2)))
  655. assert sorted((first["replay"], second["replay"])) == [False, True]
  656. with engine.connect() as observer:
  657. assert observer.execute(text("SELECT status,lease_fence FROM public.agent_generation_canaries WHERE tenant_id=:tenant AND generation='g1'"), fixture).one() == ("rolled_back", 2)
  658. assert observer.execute(text("SELECT count(*) FROM public.agent_runtime_control_events WHERE tenant_id=:tenant"), fixture).scalar_one() == 4
  659. finally:
  660. _cleanup_http_runtime_fixture(engine, fixture)
  661. engine.dispose()
  662. def test_wp09_approved_high_risk_task_is_consumed_once_and_exact_replay_is_free():
  663. url = os.getenv("TEST_DATABASE_URL")
  664. if not url:
  665. pytest.skip("TEST_DATABASE_URL is required")
  666. engine = create_engine(url)
  667. suffix = uuid.uuid4().hex[:12]
  668. tenant = f"wp09_{suffix}"
  669. ids = {name: str(uuid.uuid4()) for name in ("workflow", "version", "agent", "grant", "route", "task")}
  670. try:
  671. c = engine.connect()
  672. transaction = c.begin()
  673. try:
  674. users = [str(row[0]) for row in c.execute(text("SELECT id::text FROM public.users WHERE status='active' ORDER BY id LIMIT 2"))]
  675. if len(users) < 2:
  676. pytest.skip("requires two existing test users for independent review")
  677. owner, reviewer = users
  678. domain = str(uuid.uuid4())
  679. digest = hashlib.sha256(b"approved-runtime-input").hexdigest()
  680. c.execute(text("INSERT INTO public.governance_workflows(uid,code,name,status,current_version,created_by) VALUES(CAST(:id AS uuid),:code,'wp09','published',1,CAST(:owner AS uuid))"), {"id": ids["workflow"], "code": f"wp09-{suffix}", "owner": owner})
  681. c.execute(text("INSERT INTO public.governance_workflow_versions(uid,workflow_uid,version,status,definition,created_by,published_by,published_at) VALUES(CAST(:id AS uuid),CAST(:workflow AS uuid),1,'published','{}',CAST(:owner AS uuid),CAST(:owner AS uuid),clock_timestamp())"), {"id": ids["version"], "workflow": ids["workflow"], "owner": owner})
  682. c.execute(text("UPDATE public.governance_workflows SET active_version_uid=CAST(:version AS uuid) WHERE uid=CAST(:workflow AS uuid)"), {"workflow": ids["workflow"], "version": ids["version"]})
  683. c.execute(text("INSERT INTO public.governed_agents(uid,code,name,purpose,owner_uid,machine_subject,business_domain_uids,environments,autonomy_level,prompt_policy,status,created_by,updated_by) VALUES(CAST(:id AS uuid),:code,'wp09','test',CAST(:owner AS uuid),:subject,CAST(:domains AS jsonb),'[\"test\"]','approval_execution','{}','active',CAST(:owner AS uuid),CAST(:owner AS uuid))"), {"id": ids["agent"], "code": f"wp09-{suffix}", "owner": owner, "subject": f"wp09-{suffix}", "domains": json.dumps([domain])})
  684. c.execute(text("INSERT INTO public.agent_tool_grants(uid,agent_uid,interface_type,tool_name,action,business_domain_uid,environment,risk_level,requires_approval,status,created_by) VALUES(CAST(:id AS uuid),CAST(:agent AS uuid),'mcp','knowledge.search','execute',CAST(:domain AS uuid),'test','high',true,'active',CAST(:owner AS uuid))"), {"id": ids["grant"], "agent": ids["agent"], "domain": domain, "owner": owner})
  685. c.execute(text("INSERT INTO public.model_gateway_routes(route_id,tenant_id,principal_id,business_domain_uid,environment,provider,model,prompt_version,generation,canary_status) VALUES(:id,:tenant,CAST(:owner AS uuid),CAST(:domain AS uuid),'test','controlled','model','prompt','gen','approved')"), {"id": ids["route"], "tenant": tenant, "owner": owner, "domain": domain})
  686. c.execute(text("INSERT INTO public.agent_runtime_budgets(tenant_id,token_remaining,cost_remaining_micros,tool_remaining,time_remaining_ms,concurrency_remaining) VALUES(:tenant,10,10,2,100,2); INSERT INTO public.agent_runtime_model_budgets(tenant_id,model,remaining) VALUES(:tenant,'model',2)"), {"tenant": tenant})
  687. context = json.dumps({"agent_uid": ids["agent"], "principal_id": owner, "business_domain_uid": domain, "environment": "test", "tool_name": "knowledge.search", "action": "execute", "request_digest": digest, "automatic_execution_allowed": False})
  688. c.execute(text("INSERT INTO public.governance_tasks(uid,task_code,workflow_uid,workflow_version,task_type,subject_type,subject_uid,source_type,source_uid,title,description,business_domain_uid,priority,status,assignee_uid,due_at,route_snapshot,context,created_by,updated_by) VALUES(CAST(:id AS uuid),:code,CAST(:workflow AS uuid),1,'high_risk','agent',:agent,'wp09',:agent,'wp09','wp09',:domain,'critical','approved',CAST(:owner AS uuid),clock_timestamp()+interval '5 minutes','{}',CAST(:context AS jsonb),CAST(:owner AS uuid),CAST(:owner AS uuid))"), {"id": ids["task"], "code": f"WP09-{suffix}", "workflow": ids["workflow"], "agent": ids["agent"], "domain": domain, "owner": owner, "context": context})
  689. c.execute(text("INSERT INTO public.governance_task_reviews(uid,task_uid,reviewer_uid,decision,reason) VALUES(CAST(:id AS uuid),CAST(:task AS uuid),CAST(:reviewer AS uuid),'approve','wp09')"), {"id": str(uuid.uuid4()), "task": ids["task"], "reviewer": reviewer})
  690. incident = str(uuid.uuid4())
  691. c.execute(text("INSERT INTO public.data_incidents(uid,code,dedup_key,title,severity,status,owner_uid,escalation_level,first_detected_at,last_observed_at,created_by) VALUES(CAST(:id AS uuid),:code,:dedup,'wp09','critical','open',CAST(:owner AS uuid),1,clock_timestamp(),clock_timestamp(),CAST(:owner AS uuid)); INSERT INTO public.agent_runtime_states(tenant_id,state,incident_ref,lease_fence) VALUES(:tenant,'paused',CAST(:incident AS text),0); INSERT INTO public.agent_generation_canaries(tenant_id,generation,status,lease_fence) VALUES(:tenant,'gen','pending',0)"), {"id": incident, "code": f"INC-{suffix}", "dedup": hashlib.sha256((tenant + '-incident').encode()).hexdigest(), "owner": owner, "tenant": tenant, "incident": incident})
  692. with pytest.raises(Exception), c.begin_nested(): # noqa: B017 - Legacy direct gateway emits driver-specific error.
  693. c.execute(text("SELECT public.agent_runtime_transition(:tenant,'active',NULL,CAST(:task AS uuid),0)"), {"tenant": tenant, "task": ids["task"]})
  694. with pytest.raises(Exception), c.begin_nested(): # noqa: B017 - Legacy direct gateway emits driver-specific error.
  695. c.execute(text("SELECT public.agent_runtime_canary_transition(:tenant,'gen','approved',CAST(:task AS uuid),0,CAST(:metrics AS jsonb),'engineering-fixture-v1')"), {"tenant": tenant, "task": ids["task"], "metrics": '{"recall":1.0}'})
  696. payload = {"agent_uid":ids["agent"],"grant_uid":ids["grant"],"tenant_id":tenant,"principal_id":owner,"business_domain_uid":domain,"environment":"test","provider":"controlled","model":"model","prompt_version":"prompt","generation":"gen","interface_type":"mcp","tool_name":"knowledge.search","action":"execute","risk_level":"high","idempotency_key":"once","request_digest":hashlib.sha256(b"approved-runtime-input|[]").hexdigest(),"input_hash":digest,"evidence_digests":[],"estimated_tokens":"1","estimated_cost_micros":"1","requested_time_ms":"1","approval_task_uid":ids["task"]}
  697. transaction.commit()
  698. barrier = Barrier(2)
  699. def invoke():
  700. with engine.begin() as worker:
  701. barrier.wait(timeout=10)
  702. return worker.execute(text("SELECT public.agent_runtime_authorize(CAST(:payload AS jsonb))"), {"payload": json.dumps(payload)}).scalar_one()
  703. with ThreadPoolExecutor(max_workers=2) as pool:
  704. first, replay = list(pool.map(lambda _unused: invoke(), range(2)))
  705. assert sum(item["replay"] is False for item in (first, replay)) == 1
  706. assert c.execute(text("SELECT status FROM public.governance_tasks WHERE uid=CAST(:id AS uuid)"), {"id": ids["task"]}).scalar_one() == "closed"
  707. changed = {**payload, "input_hash": "b" * 64, "request_digest": "b" * 64}
  708. with pytest.raises(Exception): # noqa: B017 - PostgreSQL emits a driver-specific DBAPI error.
  709. c.execute(text("SELECT public.agent_runtime_authorize(CAST(:payload AS jsonb))"), {"payload": json.dumps(changed)})
  710. c.commit()
  711. finally:
  712. if transaction.is_active:
  713. transaction.rollback()
  714. c.close()
  715. finally:
  716. with engine.begin() as c:
  717. c.execute(text("DELETE FROM public.agent_invocation_audits WHERE tenant_id=:tenant; DELETE FROM public.agent_runtime_reservations WHERE tenant_id=:tenant; DELETE FROM public.agent_generation_canaries WHERE tenant_id=:tenant; DELETE FROM public.agent_runtime_states WHERE tenant_id=:tenant; DELETE FROM public.agent_runtime_budgets WHERE tenant_id=:tenant; DELETE FROM public.governance_task_reviews WHERE task_uid=CAST(:task AS uuid); DELETE FROM public.governance_tasks WHERE uid=CAST(:task AS uuid); DELETE FROM public.data_incidents WHERE code=:incident_code; DELETE FROM public.model_gateway_routes WHERE tenant_id=:tenant; DELETE FROM public.agent_tool_grants WHERE uid=CAST(:grant AS uuid); DELETE FROM public.governed_agents WHERE uid=CAST(:agent AS uuid); UPDATE public.governance_workflows SET active_version_uid=NULL WHERE uid=CAST(:workflow AS uuid); DELETE FROM public.governance_workflow_versions WHERE uid=CAST(:version AS uuid); DELETE FROM public.governance_workflows WHERE uid=CAST(:workflow AS uuid)"), {**ids, "tenant": tenant, "incident_code": f"INC-{suffix}"})
  718. engine.dispose()
  719. def test_wp09_persistent_pause_degrade_and_late_fence_are_database_enforced():
  720. url = os.getenv("TEST_DATABASE_URL")
  721. if not url:
  722. pytest.skip("TEST_DATABASE_URL is required")
  723. engine = create_engine(url)
  724. tenant, incident = f"wp09_{uuid.uuid4().hex[:12]}", str(uuid.uuid4())
  725. try:
  726. with engine.begin() as c:
  727. owner = str(c.execute(text("SELECT id::text FROM public.users WHERE status='active' LIMIT 1")).scalar_one())
  728. c.execute(text("INSERT INTO public.data_incidents(uid,code,dedup_key,title,severity,status,owner_uid,escalation_level,first_detected_at,last_observed_at,created_by) VALUES(CAST(:id AS uuid),:code,:dedup,'wp09','critical','open',CAST(:owner AS uuid),1,clock_timestamp(),clock_timestamp(),CAST(:owner AS uuid))"), {"id": incident, "code": f"WP09-{tenant[-12:]}", "dedup": hashlib.sha256(tenant.encode()).hexdigest(), "owner": owner})
  729. c.execute(text("INSERT INTO public.agent_runtime_states(tenant_id,state,lease_fence) VALUES(:tenant,'active',0); INSERT INTO public.agent_generation_canaries(tenant_id,generation,status,lease_fence) VALUES(:tenant,'g1','pending',0)"), {"tenant": tenant})
  730. with pytest.raises(Exception), c.begin_nested(): # noqa: B017 - Legacy direct gateway emits driver-specific error.
  731. c.execute(text("SELECT public.agent_runtime_transition(:tenant,'degraded',CAST(:incident AS uuid),NULL,0)"), {"tenant": tenant, "incident": incident})
  732. with pytest.raises(Exception), c.begin_nested(): # noqa: B017 - Legacy direct gateway emits driver-specific error.
  733. c.execute(text("SELECT public.agent_runtime_transition(:tenant,'paused',CAST(:incident AS uuid),NULL,1)"), {"tenant": tenant, "incident": incident})
  734. with pytest.raises(Exception): # noqa: B017 - PostgreSQL emits a driver-specific DBAPI error.
  735. c.execute(text("SELECT public.agent_runtime_transition(:tenant,'paused',CAST(:incident AS uuid),NULL,1)"), {"tenant": tenant, "incident": incident})
  736. with pytest.raises(Exception): # noqa: B017 - PostgreSQL emits a driver-specific DBAPI error.
  737. c.execute(text("SELECT public.agent_runtime_transition(:tenant,'active',NULL,NULL,2)"), {"tenant": tenant})
  738. with pytest.raises(Exception): # noqa: B017 - PostgreSQL emits a driver-specific DBAPI error.
  739. c.execute(text("SELECT public.agent_runtime_canary_transition(:tenant,'g1','approved',NULL,0,'{}','fixture-v1')"), {"tenant": tenant})
  740. finally:
  741. with engine.begin() as c:
  742. c.execute(text("DELETE FROM public.agent_generation_canaries WHERE tenant_id=:tenant; DELETE FROM public.agent_runtime_states WHERE tenant_id=:tenant; DELETE FROM public.data_incidents WHERE uid=CAST(:incident AS uuid)"), {"tenant": tenant, "incident": incident})
  743. engine.dispose()