rapier-runtime 0.5.2__tar.gz → 0.5.3__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.
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/CHANGELOG.md +24 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/PKG-INFO +2 -2
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/docs/STATUS.md +35 -3
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/docs/mcp-server-scope.md +9 -1
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/pyproject.toml +4 -2
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/site/README.md +1 -1
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/site/index.html +1 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/mcp/server.py +72 -8
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/mcp/tools.py +51 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_mcp.py +11 -0
- rapier_runtime-0.5.3/tests/test_mcp_resources.py +98 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.claude/scripts/loom-deploy-nonprod.sh +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.claude/scripts/loom-deploy-prod.sh +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.claude/scripts/loom-env-check.sh +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.claude/scripts/loom-state-docs.sh +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.githooks/pre-commit +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.github/workflows/secret-scan.yml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.gitignore +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.gitleaks.toml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.loom/audits/smuggled-virtue.done +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/.loom/project.json +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/LICENSE +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/README.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/SECURITY.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/docs/chatlogs/.gitkeep +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/docs/chatlogs/202607301413.checkpoint.handoff-cut-0-5-0-to-pypi.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/docs/chatlogs/20260802-explainability-reminder.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/docs/threat-model.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/manifests/echo.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/manifests/sparring.frame.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/manifests/sparring.full.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/manifests/sparring.proposer.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/manifests/sparring.spar.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/site/coming-soon.html +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/site/rapier-logo.png +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/_json.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/cli.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/convergence.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/envelope.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/ledger.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/manifest.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/mcp/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/models.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/onboarding.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/pipeline.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/presets.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/secrets.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stage.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/echo.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/frame.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/proposer/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/proposer/phase_stages.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/proposer/phases.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/proposer/traceability_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/reconcile.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/_binding.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/_extract.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/anchored_fix.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/author.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/citation_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/compose.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/cross_review.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/definitiveness_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_bootstrap.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_llm_shim.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/PROVENANCE.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/cite_check.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/lib_llm.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/spar_cross_review.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/spar_definitiveness_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/spar_verify_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/verify_grounding.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/rider_record.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/service.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_cite_check_evidence_union.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_cli.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_convergence.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_envelope.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_extract_json.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_frame_stage.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_full_ceremony.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_manifest.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_models_vendors.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_onboarding.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_persistence_default.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_pipeline.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_policy.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_proposer_g2.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_proposer_stages.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_proposer_traceability_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_reconcile.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_resolver_stages.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_rider_record.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_secrets.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_single_vendor_report.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_vendor_policy_v4.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/tests/test_verify_service.py +0 -0
|
@@ -4,6 +4,30 @@ Notable changes to `rapier-runtime`. Earlier releases are summarized in
|
|
|
4
4
|
[`docs/STATUS.md`](docs/STATUS.md) and on the
|
|
5
5
|
[Releases](https://github.com/muddyone/rapier-runtime/releases) page.
|
|
6
6
|
|
|
7
|
+
## 0.5.3 — 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Added: saved runs as MCP resources
|
|
10
|
+
|
|
11
|
+
MCP clients can now browse saved runs instead of calling tools:
|
|
12
|
+
|
|
13
|
+
- `rapier://runs`: every saved run, newest first, with its kind, topic and
|
|
14
|
+
verdict (JSON).
|
|
15
|
+
- `rapier://runs/{run_id}`: a run's report, as the CLI printed it (plain text).
|
|
16
|
+
- `rapier://runs/{run_id}/record`: a run's full record, redacted when it was
|
|
17
|
+
saved (JSON).
|
|
18
|
+
|
|
19
|
+
Each saved run also appears in the client's resource list, titled by kind and
|
|
20
|
+
topic, including runs saved after the server started. The `list_runs` and
|
|
21
|
+
`get_run` tools still work.
|
|
22
|
+
|
|
23
|
+
### Fixed: `rapier mcp` failed on a fresh install
|
|
24
|
+
|
|
25
|
+
The `[mcp]` extra allowed any `mcp>=1.2`, so a new install got the mcp 2.x SDK,
|
|
26
|
+
which renamed the server API this version uses. `rapier mcp` then exited with a
|
|
27
|
+
message telling you to install the extra you already had. The extra is now
|
|
28
|
+
capped at `mcp<2`, and if a 2.x SDK is present anyway, `rapier mcp` says so and
|
|
29
|
+
gives the command that fixes it.
|
|
30
|
+
|
|
7
31
|
## 0.5.2 — 2026-10-16
|
|
8
32
|
|
|
9
33
|
### Fixed: the report no longer calls a same-vendor review independent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rapier-runtime
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: A code-orchestrated engine that runs the SPARRING method: grounded, cross-vendor adversarial review for AI-in-the-loop decisions.
|
|
5
5
|
Project-URL: Homepage, https://rapierruntime.com
|
|
6
6
|
Project-URL: Repository, https://github.com/muddyone/rapier-runtime
|
|
@@ -25,7 +25,7 @@ Requires-Dist: requests>=2.31
|
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
27
27
|
Provides-Extra: mcp
|
|
28
|
-
Requires-Dist: mcp
|
|
28
|
+
Requires-Dist: mcp<2,>=1.2; extra == 'mcp'
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
31
|
# Rapier Runtime
|
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
# Rapier — dev status
|
|
2
2
|
|
|
3
|
-
_Last updated: 2026-10-16 (tag v0.5.
|
|
3
|
+
_Last updated: 2026-10-16 (tag v0.5.2, 235 tests)._ A running "where we are /
|
|
4
4
|
what's next" so a new session can continue without reconstructing from git log.
|
|
5
5
|
|
|
6
6
|
## Done
|
|
7
|
+
- **RELEASED (2026-10-16) — `rapier-runtime` 0.5.2 on PyPI. The report stops
|
|
8
|
+
calling a same-vendor review independent.** Git tag `v0.5.2`;
|
|
9
|
+
https://pypi.org/project/rapier-runtime/0.5.2/. A live 0.5.1 run with only an
|
|
10
|
+
OpenAI key (author and reviewer both `gpt-6-sol`) showed three defects. The
|
|
11
|
+
report said "an independent, different-vendor reviewer" raised the objections
|
|
12
|
+
while its own trust section said the review was not independent. The progress
|
|
13
|
+
warning blamed a missing OpenAI key that was set. LaTeX arithmetic printed raw.
|
|
14
|
+
All three are fixed (`compose.py`, `cross_review.py`; the vendored reviewer is
|
|
15
|
+
untouched) and re-checked on a second live run. 235 tests. The same day the
|
|
16
|
+
repo gained the Loom secret-scan gate: the GitHub gitleaks workflow, the
|
|
17
|
+
pre-commit hook and `.gitleaks.toml`.
|
|
18
|
+
The live runs confirmed `gpt-6-sol` works, including the framing step at its
|
|
19
|
+
1,024-token limit. **OpenRouter confirmed end to end (2026-10-16)** with only
|
|
20
|
+
an OpenRouter key set: `frame` (1 call) and a full `spar` (8 calls) all went
|
|
21
|
+
to `openrouter` / `openai/gpt-6-sol` with no empty responses, and the 0.5.2
|
|
22
|
+
single-vendor wording held. Keys: 1Password, Sparring Framework vault, item
|
|
23
|
+
`OpenRouter` (field `rapier-runtime-test` for tests).
|
|
24
|
+
**Gemini default `gemini-3.5-flash-lite` confirmed live (2026-10-16):** one
|
|
25
|
+
`frame` call with only the stored Gemini key returned a complete answer.
|
|
26
|
+
**DeepSeek default `deepseek-flash` confirmed live (2026-10-16):** DeepSeek's
|
|
27
|
+
own model list for the key is `deepseek-flash` and `deepseek-v4-pro`, and one
|
|
28
|
+
`frame` call with only the DeepSeek key returned a complete answer. Key:
|
|
29
|
+
1Password, Sparring Framework vault, item `DeepSeek`.
|
|
30
|
+
Still never called live: the Groq and Together `openai/gpt-oss-120b`
|
|
31
|
+
defaults (no keys).
|
|
7
32
|
- **RELEASED (2026-10-16) — `rapier-runtime` 0.5.1 on PyPI. Default models for
|
|
8
33
|
five vendors replaced.** Git tag `v0.5.1`;
|
|
9
34
|
https://pypi.org/project/rapier-runtime/0.5.1/. OpenRouter's default
|
|
@@ -144,8 +169,15 @@ what's next" so a new session can continue without reconstructing from git log.
|
|
|
144
169
|
2. **Paper 2 (the Proposer)** stays parked until the engine is fully shipped.
|
|
145
170
|
Note: the Proposer study runs *on* this engine — Frame, seeded generation, and
|
|
146
171
|
the depth knob are the instrument it will exercise.
|
|
147
|
-
3.
|
|
148
|
-
|
|
172
|
+
3. ~~**Optional polish:** runs as MCP resources; a landing line for the front
|
|
173
|
+
door / `--depth` / `--seed`.~~ **DONE (2026-10-16).** The landing line is live.
|
|
174
|
+
Saved runs are MCP resources (`rapier://runs`, `rapier://runs/{run_id}`,
|
|
175
|
+
`rapier://runs/{run_id}/record`); the `list_runs` / `get_run` tools stay for
|
|
176
|
+
tool-only clients. Not yet released: ships with the next version, together
|
|
177
|
+
with the `mcp<2` cap that makes a fresh `pip install "rapier-runtime[mcp]"`
|
|
178
|
+
start again (mcp 2.0 renamed FastMCP).
|
|
179
|
+
4. **Port the MCP server to the mcp 2.x SDK** (`FastMCP` → `MCPServer`, per the
|
|
180
|
+
SDK's migration guide), then lift the `<2` cap.
|
|
149
181
|
|
|
150
182
|
_(M4 publish is **DONE** — v0.4.0 on PyPI + the full landing live; see Done above.
|
|
151
183
|
Loom submodule pin bumped to the v0.4.0 release commit `514df56`.)_
|
|
@@ -97,7 +97,15 @@ existing redaction on every tool result. No new secret surface — keys stay in
|
|
|
97
97
|
**THE RECORD** section naming the run directory, so a Desktop user can see the
|
|
98
98
|
audit trail exists and where — governance is only real if the record is visible.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
Saved runs are also MCP **resources** (2026-10-16): `rapier://runs` (index,
|
|
101
|
+
JSON), `rapier://runs/{run_id}` (the report, plain text) and
|
|
102
|
+
`rapier://runs/{run_id}/record` (the full redacted envelope, JSON). Each saved
|
|
103
|
+
run is listed as its own resource every time a client asks, including runs saved
|
|
104
|
+
after the server started. The `list_runs` / `get_run` tools remain for clients
|
|
105
|
+
that only call tools.
|
|
106
|
+
|
|
107
|
+
Follow-ups: port to the mcp 2.x SDK (`FastMCP` was renamed `MCPServer`); until
|
|
108
|
+
then the `[mcp]` extra is capped at `mcp<2`.
|
|
101
109
|
|
|
102
110
|
## Open decisions
|
|
103
111
|
1. `rapier mcp` subcommand (recommended) vs a separate `rapier-mcp` entry point.
|
|
@@ -9,7 +9,7 @@ build-backend = "hatchling.build"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "rapier-runtime"
|
|
12
|
-
version = "0.5.
|
|
12
|
+
version = "0.5.3"
|
|
13
13
|
description = "A code-orchestrated engine that runs the SPARRING method: grounded, cross-vendor adversarial review for AI-in-the-loop decisions."
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
requires-python = ">=3.10"
|
|
@@ -50,7 +50,9 @@ Paper = "https://doi.org/10.5281/zenodo.21210264"
|
|
|
50
50
|
dev = ["pytest>=8.0"]
|
|
51
51
|
# The MCP server (`rapier mcp`) — kept out of the core so the tiny runtime stays
|
|
52
52
|
# `requests` + `pyyaml`. Install with: pip install "rapier-runtime[mcp]".
|
|
53
|
-
|
|
53
|
+
# Capped below 2: mcp 2.0 renamed FastMCP (to MCPServer) and changed other APIs
|
|
54
|
+
# the server uses, so an uncapped install broke `rapier mcp`.
|
|
55
|
+
mcp = ["mcp>=1.2,<2"]
|
|
54
56
|
|
|
55
57
|
[project.scripts]
|
|
56
58
|
rapier = "rapier.cli:main"
|
|
@@ -11,7 +11,7 @@ fonts. `index.html` is **light-only** and serves the logo master
|
|
|
11
11
|
Kept as a rollback target; **not** what is deployed.
|
|
12
12
|
- **`index.html`** — the full MVP landing page. **This is what is live** at
|
|
13
13
|
rapierruntime.com (swapped in at launch, 2026-07-16; verified byte-identical
|
|
14
|
-
2026-
|
|
14
|
+
2026-10-16, after adding the quickstart line for `frame`, `--depth` and `--seed`).
|
|
15
15
|
|
|
16
16
|
## Status: LAUNCHED 2026-07-16 — the full landing is live
|
|
17
17
|
|
|
@@ -301,6 +301,7 @@ pip install <b>rapier-runtime</b>
|
|
|
301
301
|
rapier <span class="p">spar</span> --request <span class="s">"Should we drop Postgres for DynamoDB?"</span>
|
|
302
302
|
</div>
|
|
303
303
|
<p class="qs-note">Bring your own API keys — Rapier calls the AI vendors you configure, and nothing runs on our servers. Want the full four-phase ceremony instead of the resolver? <span class="gold">rapier sparring</span>.</p>
|
|
304
|
+
<p class="qs-note">Not sure if you have a decision or still a question? <span class="gold">rapier frame</span> tells you which, and which command fits: an open question needs options generated first (<span class="gold">sparring</span>); a well-argued decision can go straight to the challenge (<span class="gold">spar</span>). In <span class="gold">sparring</span>, <span class="gold">--depth shallow|standard|deep</span> sets how widely it explores options, and <span class="gold">--seed</span> adds one you already favor — it gets no head start and survives only on its merits.</p>
|
|
304
305
|
</div>
|
|
305
306
|
</section>
|
|
306
307
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
"""The ``rapier mcp`` stdio server — frame / proposer / spar / sparring / doctor as MCP tools
|
|
1
|
+
"""The ``rapier mcp`` stdio server — frame / proposer / spar / sparring / doctor as MCP tools,
|
|
2
|
+
and saved runs as MCP resources (``rapier://runs``, ``rapier://runs/{run_id}``,
|
|
3
|
+
``rapier://runs/{run_id}/record``).
|
|
2
4
|
|
|
3
5
|
Optional: requires the ``mcp`` extra (``pip install "rapier-runtime[mcp]"``). The
|
|
4
6
|
SDK is imported lazily inside :func:`build_server`, so importing this module (and
|
|
@@ -18,10 +20,15 @@ annotations`` — FastMCP evaluates each tool's signature, and the lazily-import
|
|
|
18
20
|
``Context`` annotation must be a real class object at definition time, not a
|
|
19
21
|
string it cannot resolve.
|
|
20
22
|
"""
|
|
23
|
+
import json
|
|
21
24
|
import os
|
|
22
25
|
import sys
|
|
23
26
|
from typing import Any, Callable
|
|
24
27
|
|
|
28
|
+
# Cap on runs listed individually in resources/list; every run stays readable by URI
|
|
29
|
+
# and is indexed in rapier://runs.
|
|
30
|
+
_MAX_LISTED_RUNS = 200
|
|
31
|
+
|
|
25
32
|
|
|
26
33
|
async def _run_with_progress(
|
|
27
34
|
fn: Callable[..., dict],
|
|
@@ -208,29 +215,86 @@ def build_server():
|
|
|
208
215
|
values) and whether cross-vendor review is available."""
|
|
209
216
|
return tools.doctor()
|
|
210
217
|
|
|
218
|
+
# Kept for clients that only call tools; the resources below are the browsable form.
|
|
211
219
|
@server.tool()
|
|
212
220
|
def list_runs() -> dict:
|
|
213
|
-
"""List persisted run ids
|
|
221
|
+
"""List persisted run ids. Clients that support resources can browse rapier://runs instead."""
|
|
214
222
|
return tools.list_runs(ledger_root)
|
|
215
223
|
|
|
216
224
|
@server.tool()
|
|
217
225
|
def get_run(run_id: str) -> dict:
|
|
218
|
-
"""Fetch a persisted run's report + verdict by id
|
|
226
|
+
"""Fetch a persisted run's report + verdict by id. Also readable as rapier://runs/{run_id}."""
|
|
219
227
|
return tools.get_run(ledger_root, run_id)
|
|
220
228
|
|
|
229
|
+
# ── saved runs as resources ──────────────────────────────────────────────
|
|
230
|
+
@server.resource("rapier://runs", name="runs", mime_type="application/json",
|
|
231
|
+
description="Every saved run, newest first: kind, topic, verdict, and "
|
|
232
|
+
"the URIs of its report and full record.")
|
|
233
|
+
def runs_index() -> str:
|
|
234
|
+
return json.dumps(tools.run_index(ledger_root), indent=2)
|
|
235
|
+
|
|
236
|
+
@server.resource("rapier://runs/{run_id}", name="run-report", mime_type="text/plain",
|
|
237
|
+
description="A saved run's report, as the CLI printed it.")
|
|
238
|
+
def run_report(run_id: str) -> str:
|
|
239
|
+
return tools.run_report(ledger_root, run_id)
|
|
240
|
+
|
|
241
|
+
@server.resource("rapier://runs/{run_id}/record", name="run-record",
|
|
242
|
+
mime_type="application/json",
|
|
243
|
+
description="A saved run's full record (redacted on write): request, "
|
|
244
|
+
"recommendation, trust rider, verdict, trace, meta.")
|
|
245
|
+
def run_record(run_id: str) -> str:
|
|
246
|
+
return json.dumps(tools.run_record(ledger_root, run_id), indent=2, default=str)
|
|
247
|
+
|
|
248
|
+
# FastMCP lists only resources registered at startup, so runs saved after the
|
|
249
|
+
# server started would never appear. Replace the list handler with one that adds
|
|
250
|
+
# each saved run's report as a concrete resource every time a client asks.
|
|
251
|
+
from mcp.types import Resource
|
|
252
|
+
|
|
253
|
+
async def _list_resources():
|
|
254
|
+
listed = await server.list_resources()
|
|
255
|
+
index = tools.run_index(ledger_root)
|
|
256
|
+
for run in (index.get("runs") or [])[:_MAX_LISTED_RUNS]:
|
|
257
|
+
listed.append(Resource(
|
|
258
|
+
uri=run["report_uri"],
|
|
259
|
+
name=run["run_id"],
|
|
260
|
+
title=f"{run['kind']}: {run['topic']}" if run["topic"] else run["run_id"],
|
|
261
|
+
description=f"verdict: {run['verdict']}" if run["verdict"] else None,
|
|
262
|
+
mimeType="text/plain",
|
|
263
|
+
))
|
|
264
|
+
return listed
|
|
265
|
+
|
|
266
|
+
server._mcp_server.list_resources()(_list_resources)
|
|
267
|
+
|
|
221
268
|
return server
|
|
222
269
|
|
|
223
270
|
|
|
271
|
+
def _installed_mcp_version() -> str | None:
|
|
272
|
+
try:
|
|
273
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
274
|
+
return version("mcp")
|
|
275
|
+
except PackageNotFoundError:
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
224
279
|
def serve() -> int:
|
|
225
280
|
"""Run the stdio MCP server. Returns non-zero with a hint if the SDK is absent."""
|
|
226
281
|
try:
|
|
227
282
|
server = build_server()
|
|
228
283
|
except ImportError:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
284
|
+
installed = _installed_mcp_version()
|
|
285
|
+
if installed and installed.split(".")[0].isdigit() and int(installed.split(".")[0]) >= 2:
|
|
286
|
+
print(
|
|
287
|
+
f"The MCP server needs the mcp SDK 1.x, but {installed} is installed\n"
|
|
288
|
+
"(mcp 2.0 renamed the server API this version uses). Install a 1.x SDK:\n"
|
|
289
|
+
' pip install "mcp>=1.2,<2"',
|
|
290
|
+
file=sys.stderr,
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
print(
|
|
294
|
+
'The MCP server needs the optional "mcp" extra:\n'
|
|
295
|
+
' pip install "rapier-runtime[mcp]"',
|
|
296
|
+
file=sys.stderr,
|
|
297
|
+
)
|
|
234
298
|
return 1
|
|
235
299
|
server.run() # stdio transport by default
|
|
236
300
|
return 0
|
|
@@ -162,6 +162,57 @@ def list_runs(ledger_root: str | None) -> dict[str, Any]:
|
|
|
162
162
|
return {"ok": True, "runs": runs, "ledger_root": ledger_root}
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
def _load_envelope(ledger_root: str | None, run_id: str) -> dict[str, Any]:
|
|
166
|
+
"""A run's persisted envelope (already redacted on write); ``LookupError`` if absent."""
|
|
167
|
+
if not ledger_root:
|
|
168
|
+
raise LookupError("run persistence is disabled (RAPIER_NO_PERSIST is set)")
|
|
169
|
+
if not _safe_run_id(run_id):
|
|
170
|
+
raise LookupError("invalid run id")
|
|
171
|
+
path = os.path.join(ledger_root, run_id, "envelope.json")
|
|
172
|
+
if not os.path.isfile(path):
|
|
173
|
+
raise LookupError(f"run '{run_id}' not found")
|
|
174
|
+
with open(path, encoding="utf-8") as fh:
|
|
175
|
+
return json.load(fh)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def run_index(ledger_root: str | None) -> dict[str, Any]:
|
|
179
|
+
"""Every persisted run with enough to choose one: kind, topic, verdict (newest first).
|
|
180
|
+
|
|
181
|
+
Backs the ``rapier://runs`` resource. A run id is ``<YYYYmmddHHMMSS>-<kind>``.
|
|
182
|
+
"""
|
|
183
|
+
listed = list_runs(ledger_root)
|
|
184
|
+
if not listed.get("ok"):
|
|
185
|
+
return listed
|
|
186
|
+
runs = []
|
|
187
|
+
for run_id in reversed(listed["runs"]):
|
|
188
|
+
try:
|
|
189
|
+
env = _load_envelope(ledger_root, run_id)
|
|
190
|
+
except (LookupError, ValueError, OSError):
|
|
191
|
+
continue # an in-progress or foreign dir has no envelope; it isn't a run yet
|
|
192
|
+
request = (env.get("request") or "").strip()
|
|
193
|
+
runs.append({
|
|
194
|
+
"run_id": run_id,
|
|
195
|
+
"kind": run_id.split("-", 1)[1] if "-" in run_id else "",
|
|
196
|
+
"topic": request.splitlines()[0][:120] if request else "",
|
|
197
|
+
"verdict": env.get("verdict"),
|
|
198
|
+
"report_uri": f"rapier://runs/{run_id}",
|
|
199
|
+
"record_uri": f"rapier://runs/{run_id}/record",
|
|
200
|
+
})
|
|
201
|
+
return {"ok": True, "ledger_root": ledger_root, "runs": runs}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def run_report(ledger_root: str | None, run_id: str) -> str:
|
|
205
|
+
"""A run's plain-text report, as the CLI printed it. ``LookupError`` if absent."""
|
|
206
|
+
env = _load_envelope(ledger_root, run_id)
|
|
207
|
+
meta = env.get("meta") or {}
|
|
208
|
+
return meta.get("report_md") or meta.get("report") or env.get("recommendation") or ""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def run_record(ledger_root: str | None, run_id: str) -> dict[str, Any]:
|
|
212
|
+
"""A run's full persisted record (the redacted envelope). ``LookupError`` if absent."""
|
|
213
|
+
return _load_envelope(ledger_root, run_id)
|
|
214
|
+
|
|
215
|
+
|
|
165
216
|
def get_run(ledger_root: str | None, run_id: str) -> dict[str, Any]:
|
|
166
217
|
"""Return a persisted run's report + verdict by id (from its envelope.json)."""
|
|
167
218
|
if not ledger_root:
|
|
@@ -169,3 +169,14 @@ def test_runs_disabled_without_ledger():
|
|
|
169
169
|
|
|
170
170
|
assert tools.list_runs(None)["ok"] is False
|
|
171
171
|
assert tools.get_run(None, "x")["ok"] is False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_serve_names_mcp_2_when_that_is_why_import_failed(monkeypatch, capsys):
|
|
175
|
+
def _raise():
|
|
176
|
+
raise ImportError("No module named 'mcp.server.fastmcp'")
|
|
177
|
+
|
|
178
|
+
monkeypatch.setattr(server, "build_server", _raise)
|
|
179
|
+
monkeypatch.setattr(server, "_installed_mcp_version", lambda: "2.3.0")
|
|
180
|
+
assert server.serve() == 1
|
|
181
|
+
err = capsys.readouterr().err
|
|
182
|
+
assert "2.3.0 is installed" in err and 'pip install "mcp>=1.2,<2"' in err
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Saved runs as MCP resources: rapier://runs, rapier://runs/{id}, rapier://runs/{id}/record.
|
|
2
|
+
|
|
3
|
+
The index/report/record logic is plain Python and always runs. The protocol test
|
|
4
|
+
needs the optional ``mcp`` extra and is skipped without it.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from rapier.mcp import tools
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _save_run(root, run_id, request="Should we ship?", verdict="PASS", report="REPORT TEXT"):
|
|
17
|
+
d = os.path.join(root, run_id)
|
|
18
|
+
os.makedirs(d)
|
|
19
|
+
with open(os.path.join(d, "envelope.json"), "w", encoding="utf-8") as fh:
|
|
20
|
+
json.dump({"request": request, "verdict": verdict,
|
|
21
|
+
"recommendation": "do it", "meta": {"report_md": report}}, fh)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def ledger(tmp_path):
|
|
26
|
+
root = str(tmp_path / "runs")
|
|
27
|
+
os.makedirs(root)
|
|
28
|
+
_save_run(root, "20261016095019-frame", request="Is this a question?\nsecond line",
|
|
29
|
+
verdict=None, report="FRAME REPORT")
|
|
30
|
+
_save_run(root, "20261016095044-spar", report="SPAR REPORT")
|
|
31
|
+
os.makedirs(os.path.join(root, "20261016100000-spar")) # in progress: no envelope yet
|
|
32
|
+
return root
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_index_lists_runs_newest_first_and_skips_unfinished(ledger):
|
|
36
|
+
out = tools.run_index(ledger)
|
|
37
|
+
assert out["ok"] is True
|
|
38
|
+
assert [r["run_id"] for r in out["runs"]] == ["20261016095044-spar", "20261016095019-frame"]
|
|
39
|
+
spar, frame = out["runs"]
|
|
40
|
+
assert spar["kind"] == "spar" and spar["verdict"] == "PASS"
|
|
41
|
+
assert frame["topic"] == "Is this a question?"
|
|
42
|
+
assert spar["report_uri"] == "rapier://runs/20261016095044-spar"
|
|
43
|
+
assert spar["record_uri"] == "rapier://runs/20261016095044-spar/record"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_index_when_persistence_is_off_or_nothing_saved(tmp_path):
|
|
47
|
+
assert tools.run_index(None)["ok"] is False
|
|
48
|
+
assert tools.run_index(str(tmp_path / "absent"))["runs"] == []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_report_and_record_read_the_saved_run(ledger):
|
|
52
|
+
assert tools.run_report(ledger, "20261016095044-spar") == "SPAR REPORT"
|
|
53
|
+
assert tools.run_record(ledger, "20261016095044-spar")["verdict"] == "PASS"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("bad", ["../etc", "a/b", "", "nope-spar"])
|
|
57
|
+
def test_report_rejects_traversal_and_unknown_ids(ledger, bad):
|
|
58
|
+
with pytest.raises(LookupError):
|
|
59
|
+
tools.run_report(ledger, bad)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_resources_over_the_protocol(ledger, monkeypatch):
|
|
63
|
+
pytest.importorskip("mcp.server.fastmcp")
|
|
64
|
+
anyio = pytest.importorskip("anyio")
|
|
65
|
+
from mcp.shared.memory import create_connected_server_and_client_session
|
|
66
|
+
|
|
67
|
+
from rapier.mcp.server import build_server
|
|
68
|
+
|
|
69
|
+
monkeypatch.delenv("RAPIER_NO_PERSIST", raising=False)
|
|
70
|
+
monkeypatch.setenv("RAPIER_MCP_LEDGER", ledger)
|
|
71
|
+
server = build_server()
|
|
72
|
+
|
|
73
|
+
async def go():
|
|
74
|
+
async with create_connected_server_and_client_session(server._mcp_server) as client:
|
|
75
|
+
listed = {str(r.uri): r for r in (await client.list_resources()).resources}
|
|
76
|
+
templates = {t.uriTemplate for t in
|
|
77
|
+
(await client.list_resource_templates()).resourceTemplates}
|
|
78
|
+
|
|
79
|
+
# A run saved after the server started still shows up in the next listing.
|
|
80
|
+
_save_run(ledger, "20261016110000-sparring", request="Later run")
|
|
81
|
+
relisted = {str(r.uri) for r in (await client.list_resources()).resources}
|
|
82
|
+
|
|
83
|
+
index = json.loads((await client.read_resource("rapier://runs")).contents[0].text)
|
|
84
|
+
report = (await client.read_resource("rapier://runs/20261016095044-spar")).contents[0]
|
|
85
|
+
record = (await client.read_resource(
|
|
86
|
+
"rapier://runs/20261016095044-spar/record")).contents[0]
|
|
87
|
+
return listed, templates, relisted, index, report, record
|
|
88
|
+
|
|
89
|
+
listed, templates, relisted, index, report, record = anyio.run(go)
|
|
90
|
+
|
|
91
|
+
assert "rapier://runs" in listed
|
|
92
|
+
assert listed["rapier://runs/20261016095044-spar"].mimeType == "text/plain"
|
|
93
|
+
assert "rapier://runs/20261016100000-spar" not in listed # unfinished run not listed
|
|
94
|
+
assert {"rapier://runs/{run_id}", "rapier://runs/{run_id}/record"} <= templates
|
|
95
|
+
assert "rapier://runs/20261016110000-sparring" in relisted
|
|
96
|
+
assert [r["run_id"] for r in index["runs"]][-1] == "20261016095019-frame"
|
|
97
|
+
assert report.text == "SPAR REPORT"
|
|
98
|
+
assert json.loads(record.text)["verdict"] == "PASS"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/docs/chatlogs/20260802-explainability-reminder.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/proposer/traceability_gate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/stages/resolver/definitiveness_gate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/spar_cross_review.py
RENAMED
|
File without changes
|
{rapier_runtime-0.5.2 → rapier_runtime-0.5.3}/src/rapier/verify/_vendor/spar_definitiveness_gate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|