agent-tokenops 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {agent_tokenops-0.2.1/src/agent_tokenops.egg-info → agent_tokenops-0.3.0}/PKG-INFO +23 -22
  2. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/README.md +20 -21
  3. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/pyproject.toml +15 -4
  4. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0/src/agent_tokenops.egg-info}/PKG-INFO +23 -22
  5. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/agent_tokenops.egg-info/SOURCES.txt +4 -0
  6. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/agent_tokenops.egg-info/requires.txt +3 -0
  7. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/__init__.py +1 -1
  8. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/__init__.py +17 -2
  9. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/client.py +69 -32
  10. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/config.py +32 -11
  11. agent_tokenops-0.3.0/src/tokenops/control/dev_plane.py +70 -0
  12. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/http_store.py +12 -3
  13. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/ledger.py +230 -27
  14. agent_tokenops-0.3.0/src/tokenops/control/ledger_backend.py +277 -0
  15. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/models.py +11 -1
  16. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/run.py +18 -3
  17. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/store.py +17 -3
  18. agent_tokenops-0.3.0/src/tokenops/demo.py +194 -0
  19. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_control_plane_app.py +16 -11
  20. agent_tokenops-0.3.0/tests/test_control_plane_client.py +65 -0
  21. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_demo.py +9 -8
  22. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_governance_config_cache.py +0 -4
  23. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_ledger.py +40 -0
  24. agent_tokenops-0.3.0/tests/test_ledger_backend_contract.py +142 -0
  25. agent_tokenops-0.3.0/tests/test_ledger_backend_mode.py +163 -0
  26. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_policies_wrap_integration.py +4 -0
  27. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_tokenops_run.py +22 -14
  28. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_trajectory_hint.py +12 -1
  29. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_trajectory_hint_e2e.py +10 -1
  30. agent_tokenops-0.2.1/src/tokenops/demo.py +0 -113
  31. agent_tokenops-0.2.1/tests/test_control_plane_client.py +0 -88
  32. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/LICENSE.txt +0 -0
  33. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/setup.cfg +0 -0
  34. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/agent_tokenops.egg-info/dependency_links.txt +0 -0
  35. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/agent_tokenops.egg-info/top_level.txt +0 -0
  36. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/adapters/__init__.py +0 -0
  37. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/adapters/langchain.py +0 -0
  38. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/config/__init__.py +0 -0
  39. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/config/default.yaml +0 -0
  40. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/config/loader.py +0 -0
  41. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/attribution.py +0 -0
  42. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/boundary.py +0 -0
  43. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/context.py +0 -0
  44. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/core.py +0 -0
  45. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/crossing.py +0 -0
  46. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/engine.py +0 -0
  47. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/governance_cache.py +0 -0
  48. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/http.py +0 -0
  49. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/instrument.py +0 -0
  50. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/integration.py +0 -0
  51. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/__init__.py +0 -0
  52. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/_util.py +0 -0
  53. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/concurrency_cap.py +0 -0
  54. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/context_compaction.py +0 -0
  55. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/cost_budget.py +0 -0
  56. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/cost_guard.py +0 -0
  57. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/output_runaway.py +0 -0
  58. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/pre_call_worst_case.py +0 -0
  59. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/progress_guard.py +0 -0
  60. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/step_cap.py +0 -0
  61. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/tool_fix.py +0 -0
  62. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/tool_output_cap.py +0 -0
  63. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/policies/trajectory_hint.py +0 -0
  64. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/pricing.py +0 -0
  65. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/propagate.py +0 -0
  66. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/request_context.py +0 -0
  67. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/__init__.py +0 -0
  68. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/compress.py +0 -0
  69. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/enqueue.py +0 -0
  70. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/gates.py +0 -0
  71. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/hint.py +0 -0
  72. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/scope.py +0 -0
  73. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/serialize.py +0 -0
  74. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/control/trajectory/worker.py +0 -0
  75. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/env.py +0 -0
  76. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/__init__.py +0 -0
  77. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/anthropic.py +0 -0
  78. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/factory.py +0 -0
  79. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/openai.py +0 -0
  80. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/providers/types.py +0 -0
  81. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/server/__init__.py +0 -0
  82. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/server/__main__.py +0 -0
  83. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/server/app.py +0 -0
  84. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/__init__.py +0 -0
  85. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/app.py +0 -0
  86. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/run_detail.py +0 -0
  87. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/store_client.py +0 -0
  88. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/theme.py +0 -0
  89. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/views/__init__.py +0 -0
  90. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/views/admin.py +0 -0
  91. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/src/tokenops/ui/views/dashboard.py +0 -0
  92. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_a2a_health.py +0 -0
  93. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_actuators.py +0 -0
  94. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_apply.py +0 -0
  95. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_attribution.py +0 -0
  96. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_attribution_ledger_policies_e2e.py +0 -0
  97. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_boundary.py +0 -0
  98. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_chronicle_boundary.py +0 -0
  99. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_concurrency_cap.py +0 -0
  100. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_config.py +0 -0
  101. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_context_compaction.py +0 -0
  102. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_cost_budget.py +0 -0
  103. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_cost_guard.py +0 -0
  104. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_cross_process_budget_gating.py +0 -0
  105. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_crossing_hook.py +0 -0
  106. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_governance_per_agent.py +0 -0
  107. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_integration.py +0 -0
  108. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_llm_boundary_precall.py +0 -0
  109. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_output_runaway.py +0 -0
  110. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_policy_coordination.py +0 -0
  111. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_pre_call_worst_case.py +0 -0
  112. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_preview_mode.py +0 -0
  113. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_pricing.py +0 -0
  114. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_progress_guard.py +0 -0
  115. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_propagate.py +0 -0
  116. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_reason_encoding.py +0 -0
  117. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_server_enforcement.py +0 -0
  118. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_step_cap.py +0 -0
  119. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_store.py +0 -0
  120. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_thread_safety.py +0 -0
  121. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_tool_fix.py +0 -0
  122. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_tool_output_cap.py +0 -0
  123. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_wrap_adapters.py +0 -0
  124. {agent_tokenops-0.2.1 → agent_tokenops-0.3.0}/tests/test_wrap_complete_chronicle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-tokenops
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: TokenOps control plane and SDK for run-aware agent token governance
5
5
  Author: Susheem Koul, Tisha Chawla
6
6
  License-Expression: MIT
@@ -38,6 +38,8 @@ Requires-Dist: ruff>=0.8; extra == "dev"
38
38
  Requires-Dist: mypy>=1.13; extra == "dev"
39
39
  Requires-Dist: pre-commit>=4.0; extra == "dev"
40
40
  Requires-Dist: types-PyYAML>=6.0; extra == "dev"
41
+ Provides-Extra: contract
42
+ Requires-Dist: agentplane-control-plane>=0.2.0; extra == "contract"
41
43
  Provides-Extra: examples
42
44
  Requires-Dist: ddgs>=9.0; extra == "examples"
43
45
  Requires-Dist: langchain-core>=0.3; extra == "examples"
@@ -100,7 +102,10 @@ add your own.
100
102
 
101
103
  ## 🚀 Quickstart
102
104
 
103
- Requires Python 3.10+.
105
+ Requires Python 3.10+ and a running control plane — TokenOps has no ledger of its
106
+ own, so every agent (even a single process) governs against one. Zero-setup taste:
107
+ `python -m tokenops.demo` launches a throwaway plane for you automatically. For your
108
+ own agent, start one first (see [Quickdeploy](#-quickdeploy) below), then:
104
109
 
105
110
  ### 1. Put it in your agent
106
111
 
@@ -123,14 +128,16 @@ Anywhere else (Cursor, Copilot, ...), paste this:
123
128
  <details>
124
129
  <summary><b>Manual</b>, about ten lines</summary>
125
130
 
126
- Wrap your model call once, then hand the wrapped version to your agent.
131
+ Wrap your model call once, then hand the wrapped version to your agent. Needs
132
+ `CONTROL_PLANE_URL` (or `TOKENOPS_URL`) pointing at a running control plane —
133
+ see [Quickdeploy](#-quickdeploy).
127
134
 
128
135
  ```python
129
136
  from tokenops import ControlPlaneClient, tokenops_run
130
137
  from tokenops.control import Halt, wrap_complete
131
138
  from tokenops.providers import complete
132
139
 
133
- client = ControlPlaneClient.from_env()
140
+ client = ControlPlaneClient.from_env() # raises if CONTROL_PLANE_URL isn't set
134
141
 
135
142
  with tokenops_run(client=client, service="my-agent", intent="research",
136
143
  provider="openai", model="gpt-4o") as bound:
@@ -178,9 +185,9 @@ run is out, even from another process.
178
185
  ## 🐳 Quickdeploy
179
186
 
180
187
  > [!TIP]
181
- > The control plane (`python -m tokenops.server`) shares one budget across
182
- > processes and powers the dashboard. A single-process agent doesn't need it
183
- > running at all.
188
+ > Every agent needs a control plane running — TokenOps has no ledger of its own.
189
+ > A single-process agent still needs one, just not the multi-process sharing this
190
+ > section is about. `python -m tokenops.server` powers the dashboard too.
184
191
 
185
192
  One command, plane + dashboard:
186
193
 
@@ -231,8 +238,8 @@ export TOKENOPS_URL=http://localhost:7700
231
238
  export TOKENOPS_DB=tokenops.db # plane and every agent read the same file
232
239
  ```
233
240
 
234
- > `TOKENOPS_EMBEDDED=1` overrides `TOKENOPS_URL`. Leave it unset here, or each
235
- > process silently falls back to its own local ledger and gets the full budget.
241
+ > tokenops has no ledger of its own — every process governs against the plane at
242
+ > `TOKENOPS_URL`. There is no local-fallback env var to accidentally leave unset.
236
243
 
237
244
  PyPI name is `agent-tokenops`; the import is `tokenops`. Extras:
238
245
  `pip install "agent-tokenops[examples]"` for the LangChain benches,
@@ -289,7 +296,7 @@ flowchart LR
289
296
  | **Control plane** (`python -m tokenops.server`) | `POST /v1/runs`, shared SQLite, Admin/Dashboard | Agent loops, LLM calls, tools |
290
297
  | **SDK (in agents)** | `tokenops_run`, `wrap_complete`, ledger/policies, Chronicle crossing hook | Ad-hoc run IDs; mounting `/v1/runs` when `TOKENOPS_URL` is set |
291
298
 
292
- Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay.
299
+ Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay, accepted at [REALM](https://realm-workshop.github.io/) (Research on Agent Language Models), a workshop at EMNLP 2026.
293
300
 
294
301
  </details>
295
302
 
@@ -298,22 +305,15 @@ Chronicle records decision boundaries; TokenOps attaches as the cost/governance
298
305
 
299
306
  | Variable | Purpose |
300
307
  |---|---|
301
- | `TOKENOPS_URL` | Remote plane base URL (e.g. `http://localhost:7700`) → HTTP `register_run` |
302
- | `TOKENOPS_EMBEDDED` | Set to `1` to force in-process `Store` (tests / single-process) |
303
- | `TOKENOPS_DB` | SQLite path shared by plane + agents |
308
+ | `TOKENOPS_URL` (or `CONTROL_PLANE_URL`) | The control plane's base URL (e.g. `http://localhost:8800`) — **required** |
304
309
  | `TOKENOPS_CONFIG` | YAML for governance seed (core: `src/tokenops/config/default.yaml`) |
305
310
 
306
- `TOKENOPS_URL` also accepts the aliases `CONTROL_PLANE_URL` and
311
+ tokenops has no ledger or run registry of its own — `ControlPlaneClient.from_env()`
312
+ raises if `TOKENOPS_URL`/`CONTROL_PLANE_URL` isn't set, rather than silently falling
313
+ back to anything local. Every process talks to the same plane, so they share one
314
+ budget by construction. `TOKENOPS_URL` also accepts the alias
307
315
  `TOKENOPS_CONTROL_PLANE_URL`.
308
316
 
309
- Production / multi-process: set `TOKENOPS_URL`; agents must **not** mount `/v1/runs`. Tests: `TOKENOPS_EMBEDDED=1` (or omit URL).
310
-
311
- > **Precedence.** `ControlPlaneClient.from_env` takes the HTTP path only when a
312
- > URL is set **and** `TOKENOPS_EMBEDDED` is not `1`. Setting both falls back to a
313
- > local SQLite file with no warning, and every process then gets its own full
314
- > budget. Check with
315
- > `print("embedded" if client.embedded else client.url)`.
316
-
317
317
  </details>
318
318
 
319
319
  <details>
@@ -372,6 +372,7 @@ tests/ # unit + e2e
372
372
  [LinkedIn](https://www.linkedin.com/posts/microsoft-developers_who-spent-all-the-tokens-tokenops-gives-activity-7499191980715982848-224b) and [X](https://x.com/msdev/status/2093425027500978292).
373
373
  - **[Who spent all the tokens? Real-time, run-scoped cost control for AI agents](https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/)**: *Command Line*, a Microsoft publication.
374
374
  - **[FinOps for AI Agents: Who Spent All the Tokens?](https://www.youtube.com/watch?v=GJX19pNhmSw)**: talk at the **AI Engineer World's Fair**, San Francisco.
375
+ - **Selected to participate in the [GitHub Secure Open Source Fund](https://github.com/open-source/github-secure-open-source-fund)** (Session 5), GitHub Security Lab's open source security program.
375
376
 
376
377
  ## 🛟 Support
377
378
 
@@ -53,7 +53,10 @@ add your own.
53
53
 
54
54
  ## 🚀 Quickstart
55
55
 
56
- Requires Python 3.10+.
56
+ Requires Python 3.10+ and a running control plane — TokenOps has no ledger of its
57
+ own, so every agent (even a single process) governs against one. Zero-setup taste:
58
+ `python -m tokenops.demo` launches a throwaway plane for you automatically. For your
59
+ own agent, start one first (see [Quickdeploy](#-quickdeploy) below), then:
57
60
 
58
61
  ### 1. Put it in your agent
59
62
 
@@ -76,14 +79,16 @@ Anywhere else (Cursor, Copilot, ...), paste this:
76
79
  <details>
77
80
  <summary><b>Manual</b>, about ten lines</summary>
78
81
 
79
- Wrap your model call once, then hand the wrapped version to your agent.
82
+ Wrap your model call once, then hand the wrapped version to your agent. Needs
83
+ `CONTROL_PLANE_URL` (or `TOKENOPS_URL`) pointing at a running control plane —
84
+ see [Quickdeploy](#-quickdeploy).
80
85
 
81
86
  ```python
82
87
  from tokenops import ControlPlaneClient, tokenops_run
83
88
  from tokenops.control import Halt, wrap_complete
84
89
  from tokenops.providers import complete
85
90
 
86
- client = ControlPlaneClient.from_env()
91
+ client = ControlPlaneClient.from_env() # raises if CONTROL_PLANE_URL isn't set
87
92
 
88
93
  with tokenops_run(client=client, service="my-agent", intent="research",
89
94
  provider="openai", model="gpt-4o") as bound:
@@ -131,9 +136,9 @@ run is out, even from another process.
131
136
  ## 🐳 Quickdeploy
132
137
 
133
138
  > [!TIP]
134
- > The control plane (`python -m tokenops.server`) shares one budget across
135
- > processes and powers the dashboard. A single-process agent doesn't need it
136
- > running at all.
139
+ > Every agent needs a control plane running — TokenOps has no ledger of its own.
140
+ > A single-process agent still needs one, just not the multi-process sharing this
141
+ > section is about. `python -m tokenops.server` powers the dashboard too.
137
142
 
138
143
  One command, plane + dashboard:
139
144
 
@@ -184,8 +189,8 @@ export TOKENOPS_URL=http://localhost:7700
184
189
  export TOKENOPS_DB=tokenops.db # plane and every agent read the same file
185
190
  ```
186
191
 
187
- > `TOKENOPS_EMBEDDED=1` overrides `TOKENOPS_URL`. Leave it unset here, or each
188
- > process silently falls back to its own local ledger and gets the full budget.
192
+ > tokenops has no ledger of its own — every process governs against the plane at
193
+ > `TOKENOPS_URL`. There is no local-fallback env var to accidentally leave unset.
189
194
 
190
195
  PyPI name is `agent-tokenops`; the import is `tokenops`. Extras:
191
196
  `pip install "agent-tokenops[examples]"` for the LangChain benches,
@@ -242,7 +247,7 @@ flowchart LR
242
247
  | **Control plane** (`python -m tokenops.server`) | `POST /v1/runs`, shared SQLite, Admin/Dashboard | Agent loops, LLM calls, tools |
243
248
  | **SDK (in agents)** | `tokenops_run`, `wrap_complete`, ledger/policies, Chronicle crossing hook | Ad-hoc run IDs; mounting `/v1/runs` when `TOKENOPS_URL` is set |
244
249
 
245
- Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay.
250
+ Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay, accepted at [REALM](https://realm-workshop.github.io/) (Research on Agent Language Models), a workshop at EMNLP 2026.
246
251
 
247
252
  </details>
248
253
 
@@ -251,22 +256,15 @@ Chronicle records decision boundaries; TokenOps attaches as the cost/governance
251
256
 
252
257
  | Variable | Purpose |
253
258
  |---|---|
254
- | `TOKENOPS_URL` | Remote plane base URL (e.g. `http://localhost:7700`) → HTTP `register_run` |
255
- | `TOKENOPS_EMBEDDED` | Set to `1` to force in-process `Store` (tests / single-process) |
256
- | `TOKENOPS_DB` | SQLite path shared by plane + agents |
259
+ | `TOKENOPS_URL` (or `CONTROL_PLANE_URL`) | The control plane's base URL (e.g. `http://localhost:8800`) — **required** |
257
260
  | `TOKENOPS_CONFIG` | YAML for governance seed (core: `src/tokenops/config/default.yaml`) |
258
261
 
259
- `TOKENOPS_URL` also accepts the aliases `CONTROL_PLANE_URL` and
262
+ tokenops has no ledger or run registry of its own — `ControlPlaneClient.from_env()`
263
+ raises if `TOKENOPS_URL`/`CONTROL_PLANE_URL` isn't set, rather than silently falling
264
+ back to anything local. Every process talks to the same plane, so they share one
265
+ budget by construction. `TOKENOPS_URL` also accepts the alias
260
266
  `TOKENOPS_CONTROL_PLANE_URL`.
261
267
 
262
- Production / multi-process: set `TOKENOPS_URL`; agents must **not** mount `/v1/runs`. Tests: `TOKENOPS_EMBEDDED=1` (or omit URL).
263
-
264
- > **Precedence.** `ControlPlaneClient.from_env` takes the HTTP path only when a
265
- > URL is set **and** `TOKENOPS_EMBEDDED` is not `1`. Setting both falls back to a
266
- > local SQLite file with no warning, and every process then gets its own full
267
- > budget. Check with
268
- > `print("embedded" if client.embedded else client.url)`.
269
-
270
268
  </details>
271
269
 
272
270
  <details>
@@ -325,6 +323,7 @@ tests/ # unit + e2e
325
323
  [LinkedIn](https://www.linkedin.com/posts/microsoft-developers_who-spent-all-the-tokens-tokenops-gives-activity-7499191980715982848-224b) and [X](https://x.com/msdev/status/2093425027500978292).
326
324
  - **[Who spent all the tokens? Real-time, run-scoped cost control for AI agents](https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/)**: *Command Line*, a Microsoft publication.
327
325
  - **[FinOps for AI Agents: Who Spent All the Tokens?](https://www.youtube.com/watch?v=GJX19pNhmSw)**: talk at the **AI Engineer World's Fair**, San Francisco.
326
+ - **Selected to participate in the [GitHub Secure Open Source Fund](https://github.com/open-source/github-secure-open-source-fund)** (Session 5), GitHub Security Lab's open source security program.
328
327
 
329
328
  ## 🛟 Support
330
329
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-tokenops"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "TokenOps control plane and SDK for run-aware agent token governance"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -56,6 +56,14 @@ dev = [
56
56
  "pre-commit>=4.0",
57
57
  "types-PyYAML>=6.0",
58
58
  ]
59
+ # The verified-fake contract suite + e2e run against a real control plane in-process.
60
+ # Not in `dev` because the 0.2.0 line is not on PyPI yet — the tests that need it
61
+ # `pytest.importorskip("control_plane")`, so `[dev]` alone stays green. To run them:
62
+ # pip install -e ".[dev,contract]" # once 0.2.0 is released, or
63
+ # pip install "agentplane-control-plane @ git+https://github.com/theagentplane/control-plane@main"
64
+ contract = [
65
+ "agentplane-control-plane>=0.2.0",
66
+ ]
59
67
  examples = [
60
68
  "ddgs>=9.0",
61
69
  "langchain-core>=0.3",
@@ -74,10 +82,13 @@ testpaths = ["tests"]
74
82
  pythonpath = ["src", "."]
75
83
  asyncio_mode = "strict"
76
84
  markers = [
77
- "e2e: example / bench end-to-end tests (not run in default CI)",
78
- "live: requires API keys or vendored frameworks",
85
+ "e2e: example / bench end-to-end tests — real servers, real Governor/policy",
86
+ " enforcement, faked model+network only. Part of the default suite (see",
87
+ " addopts) precisely so real run+policy regressions are caught.",
88
+ "live: requires real API keys or vendored frameworks — self-skips without them,",
89
+ " excluded from the default run since it needs secrets to do anything.",
79
90
  ]
80
- addopts = "-m 'not e2e and not live'"
91
+ addopts = "-m 'not live'"
81
92
 
82
93
  [tool.ruff]
83
94
  target-version = "py310"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-tokenops
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: TokenOps control plane and SDK for run-aware agent token governance
5
5
  Author: Susheem Koul, Tisha Chawla
6
6
  License-Expression: MIT
@@ -38,6 +38,8 @@ Requires-Dist: ruff>=0.8; extra == "dev"
38
38
  Requires-Dist: mypy>=1.13; extra == "dev"
39
39
  Requires-Dist: pre-commit>=4.0; extra == "dev"
40
40
  Requires-Dist: types-PyYAML>=6.0; extra == "dev"
41
+ Provides-Extra: contract
42
+ Requires-Dist: agentplane-control-plane>=0.2.0; extra == "contract"
41
43
  Provides-Extra: examples
42
44
  Requires-Dist: ddgs>=9.0; extra == "examples"
43
45
  Requires-Dist: langchain-core>=0.3; extra == "examples"
@@ -100,7 +102,10 @@ add your own.
100
102
 
101
103
  ## 🚀 Quickstart
102
104
 
103
- Requires Python 3.10+.
105
+ Requires Python 3.10+ and a running control plane — TokenOps has no ledger of its
106
+ own, so every agent (even a single process) governs against one. Zero-setup taste:
107
+ `python -m tokenops.demo` launches a throwaway plane for you automatically. For your
108
+ own agent, start one first (see [Quickdeploy](#-quickdeploy) below), then:
104
109
 
105
110
  ### 1. Put it in your agent
106
111
 
@@ -123,14 +128,16 @@ Anywhere else (Cursor, Copilot, ...), paste this:
123
128
  <details>
124
129
  <summary><b>Manual</b>, about ten lines</summary>
125
130
 
126
- Wrap your model call once, then hand the wrapped version to your agent.
131
+ Wrap your model call once, then hand the wrapped version to your agent. Needs
132
+ `CONTROL_PLANE_URL` (or `TOKENOPS_URL`) pointing at a running control plane —
133
+ see [Quickdeploy](#-quickdeploy).
127
134
 
128
135
  ```python
129
136
  from tokenops import ControlPlaneClient, tokenops_run
130
137
  from tokenops.control import Halt, wrap_complete
131
138
  from tokenops.providers import complete
132
139
 
133
- client = ControlPlaneClient.from_env()
140
+ client = ControlPlaneClient.from_env() # raises if CONTROL_PLANE_URL isn't set
134
141
 
135
142
  with tokenops_run(client=client, service="my-agent", intent="research",
136
143
  provider="openai", model="gpt-4o") as bound:
@@ -178,9 +185,9 @@ run is out, even from another process.
178
185
  ## 🐳 Quickdeploy
179
186
 
180
187
  > [!TIP]
181
- > The control plane (`python -m tokenops.server`) shares one budget across
182
- > processes and powers the dashboard. A single-process agent doesn't need it
183
- > running at all.
188
+ > Every agent needs a control plane running — TokenOps has no ledger of its own.
189
+ > A single-process agent still needs one, just not the multi-process sharing this
190
+ > section is about. `python -m tokenops.server` powers the dashboard too.
184
191
 
185
192
  One command, plane + dashboard:
186
193
 
@@ -231,8 +238,8 @@ export TOKENOPS_URL=http://localhost:7700
231
238
  export TOKENOPS_DB=tokenops.db # plane and every agent read the same file
232
239
  ```
233
240
 
234
- > `TOKENOPS_EMBEDDED=1` overrides `TOKENOPS_URL`. Leave it unset here, or each
235
- > process silently falls back to its own local ledger and gets the full budget.
241
+ > tokenops has no ledger of its own — every process governs against the plane at
242
+ > `TOKENOPS_URL`. There is no local-fallback env var to accidentally leave unset.
236
243
 
237
244
  PyPI name is `agent-tokenops`; the import is `tokenops`. Extras:
238
245
  `pip install "agent-tokenops[examples]"` for the LangChain benches,
@@ -289,7 +296,7 @@ flowchart LR
289
296
  | **Control plane** (`python -m tokenops.server`) | `POST /v1/runs`, shared SQLite, Admin/Dashboard | Agent loops, LLM calls, tools |
290
297
  | **SDK (in agents)** | `tokenops_run`, `wrap_complete`, ledger/policies, Chronicle crossing hook | Ad-hoc run IDs; mounting `/v1/runs` when `TOKENOPS_URL` is set |
291
298
 
292
- Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay.
299
+ Chronicle records decision boundaries; TokenOps attaches as the cost/governance observer on live crossings. See [Chronicle](https://github.com/theagentplane/chronicle) for record-and-replay, accepted at [REALM](https://realm-workshop.github.io/) (Research on Agent Language Models), a workshop at EMNLP 2026.
293
300
 
294
301
  </details>
295
302
 
@@ -298,22 +305,15 @@ Chronicle records decision boundaries; TokenOps attaches as the cost/governance
298
305
 
299
306
  | Variable | Purpose |
300
307
  |---|---|
301
- | `TOKENOPS_URL` | Remote plane base URL (e.g. `http://localhost:7700`) → HTTP `register_run` |
302
- | `TOKENOPS_EMBEDDED` | Set to `1` to force in-process `Store` (tests / single-process) |
303
- | `TOKENOPS_DB` | SQLite path shared by plane + agents |
308
+ | `TOKENOPS_URL` (or `CONTROL_PLANE_URL`) | The control plane's base URL (e.g. `http://localhost:8800`) — **required** |
304
309
  | `TOKENOPS_CONFIG` | YAML for governance seed (core: `src/tokenops/config/default.yaml`) |
305
310
 
306
- `TOKENOPS_URL` also accepts the aliases `CONTROL_PLANE_URL` and
311
+ tokenops has no ledger or run registry of its own — `ControlPlaneClient.from_env()`
312
+ raises if `TOKENOPS_URL`/`CONTROL_PLANE_URL` isn't set, rather than silently falling
313
+ back to anything local. Every process talks to the same plane, so they share one
314
+ budget by construction. `TOKENOPS_URL` also accepts the alias
307
315
  `TOKENOPS_CONTROL_PLANE_URL`.
308
316
 
309
- Production / multi-process: set `TOKENOPS_URL`; agents must **not** mount `/v1/runs`. Tests: `TOKENOPS_EMBEDDED=1` (or omit URL).
310
-
311
- > **Precedence.** `ControlPlaneClient.from_env` takes the HTTP path only when a
312
- > URL is set **and** `TOKENOPS_EMBEDDED` is not `1`. Setting both falls back to a
313
- > local SQLite file with no warning, and every process then gets its own full
314
- > budget. Check with
315
- > `print("embedded" if client.embedded else client.url)`.
316
-
317
317
  </details>
318
318
 
319
319
  <details>
@@ -372,6 +372,7 @@ tests/ # unit + e2e
372
372
  [LinkedIn](https://www.linkedin.com/posts/microsoft-developers_who-spent-all-the-tokens-tokenops-gives-activity-7499191980715982848-224b) and [X](https://x.com/msdev/status/2093425027500978292).
373
373
  - **[Who spent all the tokens? Real-time, run-scoped cost control for AI agents](https://commandline.microsoft.com/tokenops-real-time-run-scoped-cost-control-ai-agents/)**: *Command Line*, a Microsoft publication.
374
374
  - **[FinOps for AI Agents: Who Spent All the Tokens?](https://www.youtube.com/watch?v=GJX19pNhmSw)**: talk at the **AI Engineer World's Fair**, San Francisco.
375
+ - **Selected to participate in the [GitHub Secure Open Source Fund](https://github.com/open-source/github-secure-open-source-fund)** (Session 5), GitHub Security Lab's open source security program.
375
376
 
376
377
  ## 🛟 Support
377
378
 
@@ -22,6 +22,7 @@ src/tokenops/control/config.py
22
22
  src/tokenops/control/context.py
23
23
  src/tokenops/control/core.py
24
24
  src/tokenops/control/crossing.py
25
+ src/tokenops/control/dev_plane.py
25
26
  src/tokenops/control/engine.py
26
27
  src/tokenops/control/governance_cache.py
27
28
  src/tokenops/control/http.py
@@ -29,6 +30,7 @@ src/tokenops/control/http_store.py
29
30
  src/tokenops/control/instrument.py
30
31
  src/tokenops/control/integration.py
31
32
  src/tokenops/control/ledger.py
33
+ src/tokenops/control/ledger_backend.py
32
34
  src/tokenops/control/models.py
33
35
  src/tokenops/control/pricing.py
34
36
  src/tokenops/control/propagate.py
@@ -93,6 +95,8 @@ tests/test_governance_config_cache.py
93
95
  tests/test_governance_per_agent.py
94
96
  tests/test_integration.py
95
97
  tests/test_ledger.py
98
+ tests/test_ledger_backend_contract.py
99
+ tests/test_ledger_backend_mode.py
96
100
  tests/test_llm_boundary_precall.py
97
101
  tests/test_output_runaway.py
98
102
  tests/test_policies_wrap_integration.py
@@ -8,6 +8,9 @@ httpx>=0.27
8
8
  fastapi>=0.115
9
9
  uvicorn>=0.32
10
10
 
11
+ [contract]
12
+ agentplane-control-plane>=0.2.0
13
+
11
14
  [dev]
12
15
  pytest>=8.0
13
16
  pytest-asyncio>=0.24
@@ -4,7 +4,7 @@ from tokenops.env import load_env
4
4
 
5
5
  load_env()
6
6
 
7
- __version__ = "0.2.1"
7
+ __version__ = "0.3.0"
8
8
 
9
9
 
10
10
  def init() -> None:
@@ -62,7 +62,15 @@ from tokenops.control.integration import (
62
62
  wrap_complete,
63
63
  wrap_stream,
64
64
  )
65
- from tokenops.control.ledger import Budget, Ledger, RunState, segment_key
65
+ from tokenops.control.ledger import Budget, Ledger, LocalRunState, segment_key
66
+ from tokenops.control.ledger_backend import (
67
+ AggregateState,
68
+ ApplyResult,
69
+ HttpLedgerBackend,
70
+ LedgerBackend,
71
+ LedgerEvent,
72
+ PrecheckRequest,
73
+ )
66
74
 
67
75
  # Process-wide: re-attach after every reset_session (Chronicle clears on_crossing).
68
76
  install_crossing_hook()
@@ -104,8 +112,15 @@ __all__ = [
104
112
  # ledger
105
113
  "Budget",
106
114
  "Ledger",
107
- "RunState",
115
+ "LocalRunState",
108
116
  "segment_key",
117
+ # ledger backend (remote-only)
118
+ "LedgerBackend",
119
+ "HttpLedgerBackend",
120
+ "LedgerEvent",
121
+ "PrecheckRequest",
122
+ "AggregateState",
123
+ "ApplyResult",
109
124
  # harness
110
125
  "Governor",
111
126
  "RaiseControls",
@@ -1,13 +1,18 @@
1
1
  """SDK client for the TokenOps control plane.
2
2
 
3
- Prefer :class:`ControlPlaneClient` over posting ``/v1/runs`` at an agent URL.
4
- When ``TOKENOPS_URL`` is set, registration (and future plane APIs) go over HTTP.
5
- When ``TOKENOPS_EMBEDDED=1`` or no URL is set, the client uses an in-process
6
- :class:`~tokenops.control.store.Store` (shared SQLite via ``TOKENOPS_DB``).
7
-
8
- Agents should talk to the plane through this client (§6) — do not construct
9
- ``Store(TOKENOPS_DB)`` on the happy path. ``require_store()`` is an escape hatch
10
- for ledger / dashboard rows until those APIs are fully remote.
3
+ ``ControlPlaneClient`` is the only supported way a live agent reaches the plane —
4
+ ``from_env()`` requires ``CONTROL_PLANE_URL`` / ``TOKENOPS_URL`` and raises if neither
5
+ is set. There is no embedded / local-file fallback: tokenops has no ledger of its own
6
+ (tokenops#118, the remote-only rewrite) — every spend/inflight/halt read or write goes
7
+ to the control plane over HTTP via :meth:`backend` (a
8
+ :class:`~tokenops.control.ledger_backend.LedgerBackend`), and registration/governance
9
+ config/dashboard rows go through :class:`~tokenops.control.http_store.HttpStore`. Both
10
+ are HTTP-only; neither ever opens a local SQLite file.
11
+
12
+ The ``store=`` constructor kwarg is a explicit, visible test-only escape hatch (dependency
13
+ injection for unit/integration tests that want to exercise policy logic against a local
14
+ :class:`~tokenops.control.store.Store` without a running plane) — it is never reachable
15
+ from ``from_env()``, so no environment misconfiguration can silently select it.
11
16
  """
12
17
 
13
18
  from __future__ import annotations
@@ -17,8 +22,11 @@ import os
17
22
  from collections.abc import Mapping
18
23
  from typing import Any, cast
19
24
 
25
+ import httpx
26
+
20
27
  from tokenops.control.http import post_run, post_run_sync
21
28
  from tokenops.control.http_store import HttpStore
29
+ from tokenops.control.ledger_backend import HttpLedgerBackend, LedgerBackend
22
30
  from tokenops.control.models import (
23
31
  GovernanceMode,
24
32
  RunAlreadyRegisteredError,
@@ -46,7 +54,7 @@ def _mode_value(mode: GovernanceMode | str | None) -> GovernanceMode:
46
54
 
47
55
 
48
56
  class ControlPlaneClient:
49
- """Talk to a remote control plane or an embedded Store."""
57
+ """Talk to the remote control plane over HTTP (or, in tests only, a local Store)."""
50
58
 
51
59
  def __init__(
52
60
  self,
@@ -54,36 +62,43 @@ class ControlPlaneClient:
54
62
  url: str | None = None,
55
63
  store: Store | None = None,
56
64
  timeout: float = 30.0,
65
+ client: httpx.Client | None = None,
57
66
  ) -> None:
58
67
  if bool(url) == bool(store):
59
68
  raise ValueError("exactly one of url or store is required")
60
69
  self._url = url.rstrip("/") if url else None
61
70
  self._store = store
62
71
  self._hybrid_store: Any = None
72
+ self._backend: LedgerBackend | None = None
63
73
  self._timeout = timeout
74
+ self._client = client # optional injected httpx.Client — tests only
64
75
 
65
76
  @classmethod
66
77
  def from_env(cls, *, timeout: float = 30.0) -> ControlPlaneClient:
67
- """Build a client from ``TOKENOPS_URL`` / ``TOKENOPS_EMBEDDED`` / ``TOKENOPS_DB``.
78
+ """Build a client from ``CONTROL_PLANE_URL`` / ``TOKENOPS_URL``.
68
79
 
69
- * ``TOKENOPS_URL`` or ``CONTROL_PLANE_URL`` set and ``TOKENOPS_EMBEDDED`` not ``1``
70
- → HTTP to the plane (no local SQLite).
71
- * otherwise → embedded :class:`Store` at ``TOKENOPS_DB``.
80
+ Raises if neither is set — tokenops has no embedded / local-file ledger to fall
81
+ back to (tokenops#118). Point this at a running control plane: ``control-plane
82
+ serve`` locally, or the Docker image, for anything other than tests that pass
83
+ ``store=`` explicitly.
72
84
  """
73
85
  from tokenops.control.crossing import install_crossing_hook
74
86
 
75
87
  install_crossing_hook()
76
- embedded = os.environ.get("TOKENOPS_EMBEDDED", "").strip() == "1"
77
88
  url = (
78
89
  os.environ.get("CONTROL_PLANE_URL")
79
90
  or os.environ.get("TOKENOPS_URL")
80
91
  or os.environ.get("TOKENOPS_CONTROL_PLANE_URL")
81
92
  or ""
82
93
  ).strip()
83
- if url and not embedded:
84
- return cls(url=url, timeout=timeout)
85
- db = os.environ.get("TOKENOPS_DB", "tokenops.db")
86
- return cls(store=Store(db), timeout=timeout)
94
+ if not url:
95
+ raise RuntimeError(
96
+ "CONTROL_PLANE_URL (or TOKENOPS_URL) is required — tokenops has no "
97
+ "embedded/local ledger; point it at a running control plane. Run "
98
+ "`control-plane serve` locally (see the agentplane-control-plane "
99
+ "package) or use its Docker image, then set CONTROL_PLANE_URL to it."
100
+ )
101
+ return cls(url=url, timeout=timeout)
87
102
 
88
103
  @property
89
104
  def url(self) -> str | None:
@@ -91,29 +106,51 @@ class ControlPlaneClient:
91
106
 
92
107
  @property
93
108
  def embedded(self) -> bool:
109
+ """``True`` only for the explicit test-only ``store=`` constructor path.
110
+
111
+ Never ``True`` for a client built by :meth:`from_env` — there is no
112
+ environment variable that selects a local ledger.
113
+ """
94
114
  return self._store is not None
95
115
 
96
116
  @property
97
117
  def store(self) -> Store | None:
98
- """Embedded registration Store, or ``None`` when registration is remote.
99
-
100
- Prefer :meth:`governance_config_for`, :meth:`resolve_run`, and
101
- :meth:`require_store` over using this directly. Escape hatch only (§6).
102
- """
118
+ """The explicit test-only local Store, or ``None`` in the (only production)
119
+ remote mode. Prefer :meth:`backend`, :meth:`governance_config_for`, and
120
+ :meth:`resolve_run` over using this directly."""
103
121
  return self._store
104
122
 
123
+ @property
124
+ def backend(self) -> LedgerBackend:
125
+ """The :class:`~tokenops.control.ledger_backend.LedgerBackend` for this client
126
+ — every spend/inflight/halt read (``precheck``) and write (``events:batch``)
127
+ goes through this. Only valid in remote mode (``from_env()`` always is)."""
128
+ if self._url is None:
129
+ raise RuntimeError(
130
+ "no LedgerBackend for a store=-constructed (test-only) ControlPlaneClient"
131
+ )
132
+ if self._backend is None:
133
+ key = os.environ.get("CONTROL_PLANE_API_KEY") or os.environ.get("TOKENOPS_API_KEY")
134
+ self._backend = HttpLedgerBackend(
135
+ self._url, api_key=key, timeout=self._timeout, client=self._client
136
+ )
137
+ return self._backend
138
+
105
139
  def require_store(self) -> Store:
106
- """Backing store for ledger / config / dashboard rows.
140
+ """Registration / governance-config / dashboard-row transport.
107
141
 
108
- Embedded mode returns the in-process SQLite Store. Remote mode returns
109
- :class:`HttpStore` — never a local DB file.
142
+ The test-only ``store=`` path returns that Store. Remote mode (``from_env()``)
143
+ returns :class:`HttpStore` — HTTP only, never a local DB file. Ledger accounting
144
+ (spend/inflight/halt) does **not** go through this — see :meth:`backend`.
110
145
  """
111
146
  if self._store is not None:
112
147
  return self._store
113
148
  if self._hybrid_store is None:
114
149
  assert self._url is not None
115
150
  key = os.environ.get("CONTROL_PLANE_API_KEY") or os.environ.get("TOKENOPS_API_KEY")
116
- self._hybrid_store = HttpStore(self._url, api_key=key, timeout=self._timeout)
151
+ self._hybrid_store = HttpStore(
152
+ self._url, api_key=key, timeout=self._timeout, client=self._client
153
+ )
117
154
  return cast(Store, self._hybrid_store)
118
155
 
119
156
  def register_run(
@@ -203,9 +240,9 @@ class ControlPlaneClient:
203
240
  def should_mount_run_registration() -> bool:
204
241
  """Whether an agent app should expose ``POST /v1/runs``.
205
242
 
206
- When ``TOKENOPS_URL`` points at a standalone plane (and embedded mode is off),
207
- registration is centralized on the plane — agents must not mount the route.
243
+ Always ``False`` — registration is centralized on the plane (tokenops#118); there
244
+ is no embedded mode left for an agent to self-host it under. Kept (rather than
245
+ deleted outright) so existing ``if should_mount_run_registration(): ...`` call
246
+ sites keep working unchanged while they're cleaned up.
208
247
  """
209
- if os.environ.get("TOKENOPS_EMBEDDED", "").strip() == "1":
210
- return True
211
- return not (os.environ.get("TOKENOPS_URL") or "").strip()
248
+ return False