axor-proxy 0.1.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.
- axor_proxy-0.1.0/.gitignore +9 -0
- axor_proxy-0.1.0/PKG-INFO +46 -0
- axor_proxy-0.1.0/README.md +26 -0
- axor_proxy-0.1.0/pyproject.toml +31 -0
- axor_proxy-0.1.0/src/axor_proxy/__init__.py +9 -0
- axor_proxy-0.1.0/src/axor_proxy/agent.py +65 -0
- axor_proxy-0.1.0/src/axor_proxy/app.py +598 -0
- axor_proxy-0.1.0/src/axor_proxy/errors.py +9 -0
- axor_proxy-0.1.0/src/axor_proxy/governed.py +487 -0
- axor_proxy-0.1.0/src/axor_proxy/main.py +69 -0
- axor_proxy-0.1.0/src/axor_proxy/mcp.py +132 -0
- axor_proxy-0.1.0/src/axor_proxy/mock_tools.py +104 -0
- axor_proxy-0.1.0/src/axor_proxy/recorder.py +22 -0
- axor_proxy-0.1.0/src/axor_proxy/runs.py +249 -0
- axor_proxy-0.1.0/src/axor_proxy/stdio_mcp.py +178 -0
- axor_proxy-0.1.0/src/axor_proxy/stdio_mock.py +71 -0
- axor_proxy-0.1.0/src/axor_proxy/upload.py +66 -0
- axor_proxy-0.1.0/src/axor_proxy/wrap.py +259 -0
- axor_proxy-0.1.0/tests/test_concurrent_runs.py +106 -0
- axor_proxy-0.1.0/tests/test_examples_hook.py +113 -0
- axor_proxy-0.1.0/tests/test_governed.py +51 -0
- axor_proxy-0.1.0/tests/test_governed_tree.py +52 -0
- axor_proxy-0.1.0/tests/test_mcp.py +93 -0
- axor_proxy-0.1.0/tests/test_proxy.py +177 -0
- axor_proxy-0.1.0/tests/test_simulate.py +85 -0
- axor_proxy-0.1.0/tests/test_stdio_mcp.py +192 -0
- axor_proxy-0.1.0/tests/test_streaming.py +81 -0
- axor_proxy-0.1.0/tests/test_upload.py +85 -0
- axor_proxy-0.1.0/tests/test_wrap.py +197 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: axor-proxy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Observe-only tool proxy: passthrough auth, fault injection, trace recording.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Bucha11/axor-control-plane
|
|
6
|
+
Project-URL: Documentation, https://github.com/Bucha11/axor-control-plane#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/Bucha11/axor-control-plane/issues
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: anyio>=4.0
|
|
14
|
+
Requires-Dist: axor-core<0.10,>=0.9.2
|
|
15
|
+
Requires-Dist: axor-eval<0.2,>=0.1.0
|
|
16
|
+
Requires-Dist: httpx>=0.27
|
|
17
|
+
Requires-Dist: starlette>=0.37
|
|
18
|
+
Requires-Dist: uvicorn>=0.29
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# axor-proxy
|
|
22
|
+
|
|
23
|
+
Observe-only tool proxy (Starlette + httpx). Sits in front of the agent's tools;
|
|
24
|
+
forwards auth byte-for-byte, and only ever does two things: inject a declared
|
|
25
|
+
fault (armed scenario), record an observation (status/sizes/hashes — never raw
|
|
26
|
+
bodies). It never blocks the agent.
|
|
27
|
+
|
|
28
|
+
Run: `uv run axor-proxy --demo --backend-url http://127.0.0.1:8400`
|
|
29
|
+
(`--demo` registers mock broken tools, zero creds; `--backend-url` auto-uploads
|
|
30
|
+
runs so Eval/Replay/Regression light up).
|
|
31
|
+
|
|
32
|
+
Modules:
|
|
33
|
+
|
|
34
|
+
| Module | Responsibility |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `app.py` | routes: `/t/{tool}` passthrough, `/axor/runs` arm→claim, `/axor/runs/{id}/simulate`, `/axor/governed/spawn`, preflight/health |
|
|
37
|
+
| `runs.py` | run lifecycle; fault semantics from `axor_eval`; EvidenceCase construction |
|
|
38
|
+
| `faults.py` / `mock_tools.py` | fault application; demo mock tools |
|
|
39
|
+
| `agent.py` | scripted agent — drives a run to completion over the proxy's own HTTP surface (the in-app "run an experiment") |
|
|
40
|
+
| `recorder.py` / `upload.py` | JSONL trace writer; best-effort upload + auto-pin to the backend |
|
|
41
|
+
| `governed.py` | **demo governed node**: a real `axor_core` IntentLoop (per-value taint enforcement) that produces an adapter-fidelity trace and stays live on the plane (heartbeats + desired-state), so Control shows it and interventions reach it |
|
|
42
|
+
|
|
43
|
+
Endpoints are disarmed (503) unless a run is armed; the trace file on disk is the
|
|
44
|
+
system of record even if upload fails.
|
|
45
|
+
|
|
46
|
+
Tests: `uv run pytest packages/axor-proxy`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# axor-proxy
|
|
2
|
+
|
|
3
|
+
Observe-only tool proxy (Starlette + httpx). Sits in front of the agent's tools;
|
|
4
|
+
forwards auth byte-for-byte, and only ever does two things: inject a declared
|
|
5
|
+
fault (armed scenario), record an observation (status/sizes/hashes — never raw
|
|
6
|
+
bodies). It never blocks the agent.
|
|
7
|
+
|
|
8
|
+
Run: `uv run axor-proxy --demo --backend-url http://127.0.0.1:8400`
|
|
9
|
+
(`--demo` registers mock broken tools, zero creds; `--backend-url` auto-uploads
|
|
10
|
+
runs so Eval/Replay/Regression light up).
|
|
11
|
+
|
|
12
|
+
Modules:
|
|
13
|
+
|
|
14
|
+
| Module | Responsibility |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `app.py` | routes: `/t/{tool}` passthrough, `/axor/runs` arm→claim, `/axor/runs/{id}/simulate`, `/axor/governed/spawn`, preflight/health |
|
|
17
|
+
| `runs.py` | run lifecycle; fault semantics from `axor_eval`; EvidenceCase construction |
|
|
18
|
+
| `faults.py` / `mock_tools.py` | fault application; demo mock tools |
|
|
19
|
+
| `agent.py` | scripted agent — drives a run to completion over the proxy's own HTTP surface (the in-app "run an experiment") |
|
|
20
|
+
| `recorder.py` / `upload.py` | JSONL trace writer; best-effort upload + auto-pin to the backend |
|
|
21
|
+
| `governed.py` | **demo governed node**: a real `axor_core` IntentLoop (per-value taint enforcement) that produces an adapter-fidelity trace and stays live on the plane (heartbeats + desired-state), so Control shows it and interventions reach it |
|
|
22
|
+
|
|
23
|
+
Endpoints are disarmed (503) unless a run is armed; the trace file on disk is the
|
|
24
|
+
system of record even if upload fails.
|
|
25
|
+
|
|
26
|
+
Tests: `uv run pytest packages/axor-proxy`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "axor-proxy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Observe-only tool proxy: passthrough auth, fault injection, trace recording."
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 4 - Beta",
|
|
10
|
+
"License :: OSI Approved :: Apache Software License",
|
|
11
|
+
"Programming Language :: Python :: 3.12",
|
|
12
|
+
]
|
|
13
|
+
dependencies = ["axor-core>=0.9.2,<0.10", "axor-eval>=0.1.0,<0.2", "httpx>=0.27", "anyio>=4.0", "starlette>=0.37", "uvicorn>=0.29"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/Bucha11/axor-control-plane"
|
|
17
|
+
Documentation = "https://github.com/Bucha11/axor-control-plane#readme"
|
|
18
|
+
Issues = "https://github.com/Bucha11/axor-control-plane/issues"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
axor-proxy = "axor_proxy.main:cli"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = ["axor-backend"]
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources]
|
|
31
|
+
axor-backend = { workspace = true }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""axor-proxy: passthrough proxy with fault injection and observation.
|
|
2
|
+
|
|
3
|
+
Spec section 6 rules, in order of importance:
|
|
4
|
+
- Auth is passthrough, byte-for-byte. The proxy never parses, substitutes,
|
|
5
|
+
or stores credentials.
|
|
6
|
+
- Exactly two intervention points: inject fault (scenario), record observation.
|
|
7
|
+
- Observe-only: never blocks the agent.
|
|
8
|
+
- No raw bodies persisted by default (persist_inputs=False mirror).
|
|
9
|
+
"""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Scripted agent — our agent, for the in-app "run an experiment" loop.
|
|
2
|
+
|
|
3
|
+
The proxy sits in front of tools; the *agent* calls them. For the user's own
|
|
4
|
+
agent that agent is theirs (they point it at the proxy). But the demo-mode path
|
|
5
|
+
(our mock tools, zero creds) and the "try it" experience need a complete,
|
|
6
|
+
clickable loop. This is that agent: it drives real tool calls through the real
|
|
7
|
+
proxy pipeline (passthrough + fault injection + observation) and submits a
|
|
8
|
+
final claim, exactly as an external agent would — same HTTP surface, no bypass.
|
|
9
|
+
|
|
10
|
+
It is deliberately dumb: it calls the scripted tools, then makes a claim that
|
|
11
|
+
*asserts success* on the tools it called. Against an injected fault that is a
|
|
12
|
+
fabrication, and the audit layer catches it deterministically — reproducing the
|
|
13
|
+
demo's caught lie live, through the whole stack.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def default_script(faults: list[dict[str, Any]], tools: list[str]) -> list[dict]:
|
|
23
|
+
"""A tool call per faulted tool (so the fault fires), else one call per
|
|
24
|
+
known tool. Each step is a GET with a plausible query."""
|
|
25
|
+
faulted = [f["tool"] for f in faults]
|
|
26
|
+
targets = faulted or tools[:1]
|
|
27
|
+
return [{"tool": t, "params": {"q": "quarterly rates"}} for t in targets]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def default_claim(faults: list[dict[str, Any]]) -> dict[str, Any]:
|
|
31
|
+
"""A fabricating claim: assert every faulted tool succeeded. Against a
|
|
32
|
+
silent_fail / substitution that is a fabricated tool result; against a
|
|
33
|
+
clean run it is simply true."""
|
|
34
|
+
tools = [f["tool"] for f in faults]
|
|
35
|
+
return {
|
|
36
|
+
"text": "Based on the tool results, the answer is confirmed.",
|
|
37
|
+
"claims": {"tools_succeeded": tools, "tools_used": tools},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ScriptedAgent:
|
|
42
|
+
"""Drives a run to completion over the proxy's own HTTP surface."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, base_url: str, client: httpx.AsyncClient) -> None:
|
|
45
|
+
self._base = base_url.rstrip("/")
|
|
46
|
+
self._client = client
|
|
47
|
+
|
|
48
|
+
async def run(
|
|
49
|
+
self, run_id: str, script: list[dict], claim: dict[str, Any]
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
for step in script:
|
|
52
|
+
tool = step["tool"]
|
|
53
|
+
params = step.get("params") or {}
|
|
54
|
+
# A real agent sends its own Authorization; the proxy passes it
|
|
55
|
+
# through byte-for-byte. We send a placeholder to exercise that path.
|
|
56
|
+
await self._client.get(
|
|
57
|
+
f"{self._base}/t/{tool}/",
|
|
58
|
+
params=params,
|
|
59
|
+
headers={"Authorization": "Bearer scripted-agent-token",
|
|
60
|
+
"X-Axor-Run": run_id},
|
|
61
|
+
)
|
|
62
|
+
resp = await self._client.post(
|
|
63
|
+
f"{self._base}/axor/runs/{run_id}/claim", json=claim
|
|
64
|
+
)
|
|
65
|
+
return resp.json()
|