rapier-runtime 0.5.2__tar.gz → 0.6.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.
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/CHANGELOG.md +39 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/PKG-INFO +2 -2
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/STATUS.md +45 -3
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/mcp-server-scope.md +11 -1
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/pyproject.toml +4 -2
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/site/README.md +1 -1
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/site/index.html +1 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/mcp/server.py +99 -20
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/mcp/tools.py +51 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_mcp.py +39 -6
- rapier_runtime-0.6.0/tests/test_mcp_resources.py +98 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.claude/scripts/loom-deploy-nonprod.sh +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.claude/scripts/loom-deploy-prod.sh +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.claude/scripts/loom-env-check.sh +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.claude/scripts/loom-state-docs.sh +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.githooks/pre-commit +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.github/workflows/secret-scan.yml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.gitignore +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.gitleaks.toml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.loom/audits/smuggled-virtue.done +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/.loom/project.json +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/LICENSE +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/README.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/SECURITY.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/chatlogs/.gitkeep +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/chatlogs/202607301413.checkpoint.handoff-cut-0-5-0-to-pypi.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/chatlogs/20260802-explainability-reminder.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/docs/threat-model.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/echo.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/sparring.frame.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/sparring.full.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/sparring.proposer.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/manifests/sparring.spar.yaml +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/site/coming-soon.html +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/site/rapier-logo.png +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/_json.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/cli.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/convergence.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/envelope.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/ledger.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/manifest.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/mcp/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/models.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/onboarding.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/pipeline.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/presets.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/secrets.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stage.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/echo.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/frame.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/proposer/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/proposer/phase_stages.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/proposer/phases.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/proposer/traceability_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/reconcile.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/_binding.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/_extract.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/anchored_fix.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/author.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/citation_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/compose.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/cross_review.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/stages/resolver/definitiveness_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/__init__.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_bootstrap.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_llm_shim.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/PROVENANCE.md +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/cite_check.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/lib_llm.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/spar_cross_review.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/spar_definitiveness_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/spar_verify_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/_vendor/verify_grounding.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/rider_record.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/src/rapier/verify/service.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_cite_check_evidence_union.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_cli.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_convergence.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_envelope.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_extract_json.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_frame_stage.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_full_ceremony.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_manifest.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_models_vendors.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_onboarding.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_persistence_default.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_pipeline.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_policy.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_proposer_g2.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_proposer_stages.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_proposer_traceability_gate.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_reconcile.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_resolver_stages.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_rider_record.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_secrets.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_single_vendor_report.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_vendor_policy_v4.py +0 -0
- {rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/tests/test_verify_service.py +0 -0
|
@@ -4,6 +4,45 @@ 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.6.0 — 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Changed: the MCP server now runs on the mcp 2.x SDK
|
|
10
|
+
|
|
11
|
+
The `[mcp]` extra now requires `mcp>=2.3,<3` (it was `>=1.2,<2`), and the
|
|
12
|
+
server is built on the SDK's `MCPServer`. Tools, their results and the run
|
|
13
|
+
resources behave as before. Two visible differences:
|
|
14
|
+
|
|
15
|
+
- **Progress shows the stage.** Each stage's progress update now carries the
|
|
16
|
+
stage name as its message. mcp 2.x sends log messages only to clients that
|
|
17
|
+
ask for them, so the progress message is what every client will show.
|
|
18
|
+
- **`pip install -U "rapier-runtime[mcp]"` upgrades the SDK.** An environment
|
|
19
|
+
pinned to mcp 1.x will be moved to 2.x. If a 1.x SDK is installed anyway,
|
|
20
|
+
`rapier mcp` says so and gives the command that fixes it.
|
|
21
|
+
|
|
22
|
+
## 0.5.3 — 2026-10-16
|
|
23
|
+
|
|
24
|
+
### Added: saved runs as MCP resources
|
|
25
|
+
|
|
26
|
+
MCP clients can now browse saved runs instead of calling tools:
|
|
27
|
+
|
|
28
|
+
- `rapier://runs`: every saved run, newest first, with its kind, topic and
|
|
29
|
+
verdict (JSON).
|
|
30
|
+
- `rapier://runs/{run_id}`: a run's report, as the CLI printed it (plain text).
|
|
31
|
+
- `rapier://runs/{run_id}/record`: a run's full record, redacted when it was
|
|
32
|
+
saved (JSON).
|
|
33
|
+
|
|
34
|
+
Each saved run also appears in the client's resource list, titled by kind and
|
|
35
|
+
topic, including runs saved after the server started. The `list_runs` and
|
|
36
|
+
`get_run` tools still work.
|
|
37
|
+
|
|
38
|
+
### Fixed: `rapier mcp` failed on a fresh install
|
|
39
|
+
|
|
40
|
+
The `[mcp]` extra allowed any `mcp>=1.2`, so a new install got the mcp 2.x SDK,
|
|
41
|
+
which renamed the server API this version uses. `rapier mcp` then exited with a
|
|
42
|
+
message telling you to install the extra you already had. The extra is now
|
|
43
|
+
capped at `mcp<2`, and if a 2.x SDK is present anyway, `rapier mcp` says so and
|
|
44
|
+
gives the command that fixes it.
|
|
45
|
+
|
|
7
46
|
## 0.5.2 — 2026-10-16
|
|
8
47
|
|
|
9
48
|
### 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.
|
|
3
|
+
Version: 0.6.0
|
|
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<3,>=2.3; extra == 'mcp'
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
31
|
# Rapier Runtime
|
|
@@ -1,9 +1,43 @@
|
|
|
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.3, 249 tests with the mcp SDK)._ 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.3 on PyPI. Saved runs as MCP
|
|
8
|
+
resources, and `rapier mcp` starts again on a fresh install.** Git tag
|
|
9
|
+
`v0.5.3`; https://pypi.org/project/rapier-runtime/0.5.3/. Resources:
|
|
10
|
+
`rapier://runs`, `rapier://runs/{run_id}`, `rapier://runs/{run_id}/record`;
|
|
11
|
+
each saved run is listed individually, including runs saved after the server
|
|
12
|
+
started. The fix: `[mcp]` allowed any `mcp>=1.2`, so new installs got mcp
|
|
13
|
+
2.3.0, which renamed FastMCP, and 0.5.2's `rapier mcp` failed with a message
|
|
14
|
+
pointing at the wrong cause. Capped at `mcp<2`. Verified from PyPI in a clean
|
|
15
|
+
environment: the install resolved mcp 1.30.0 and the server listed its runs.
|
|
16
|
+
- **RELEASED (2026-10-16) — `rapier-runtime` 0.5.2 on PyPI. The report stops
|
|
17
|
+
calling a same-vendor review independent.** Git tag `v0.5.2`;
|
|
18
|
+
https://pypi.org/project/rapier-runtime/0.5.2/. A live 0.5.1 run with only an
|
|
19
|
+
OpenAI key (author and reviewer both `gpt-6-sol`) showed three defects. The
|
|
20
|
+
report said "an independent, different-vendor reviewer" raised the objections
|
|
21
|
+
while its own trust section said the review was not independent. The progress
|
|
22
|
+
warning blamed a missing OpenAI key that was set. LaTeX arithmetic printed raw.
|
|
23
|
+
All three are fixed (`compose.py`, `cross_review.py`; the vendored reviewer is
|
|
24
|
+
untouched) and re-checked on a second live run. 235 tests. The same day the
|
|
25
|
+
repo gained the Loom secret-scan gate: the GitHub gitleaks workflow, the
|
|
26
|
+
pre-commit hook and `.gitleaks.toml`.
|
|
27
|
+
The live runs confirmed `gpt-6-sol` works, including the framing step at its
|
|
28
|
+
1,024-token limit. **OpenRouter confirmed end to end (2026-10-16)** with only
|
|
29
|
+
an OpenRouter key set: `frame` (1 call) and a full `spar` (8 calls) all went
|
|
30
|
+
to `openrouter` / `openai/gpt-6-sol` with no empty responses, and the 0.5.2
|
|
31
|
+
single-vendor wording held. Keys: 1Password, Sparring Framework vault, item
|
|
32
|
+
`OpenRouter` (field `rapier-runtime-test` for tests).
|
|
33
|
+
**Gemini default `gemini-3.5-flash-lite` confirmed live (2026-10-16):** one
|
|
34
|
+
`frame` call with only the stored Gemini key returned a complete answer.
|
|
35
|
+
**DeepSeek default `deepseek-flash` confirmed live (2026-10-16):** DeepSeek's
|
|
36
|
+
own model list for the key is `deepseek-flash` and `deepseek-v4-pro`, and one
|
|
37
|
+
`frame` call with only the DeepSeek key returned a complete answer. Key:
|
|
38
|
+
1Password, Sparring Framework vault, item `DeepSeek`.
|
|
39
|
+
Still never called live: the Groq and Together `openai/gpt-oss-120b`
|
|
40
|
+
defaults (no keys).
|
|
7
41
|
- **RELEASED (2026-10-16) — `rapier-runtime` 0.5.1 on PyPI. Default models for
|
|
8
42
|
five vendors replaced.** Git tag `v0.5.1`;
|
|
9
43
|
https://pypi.org/project/rapier-runtime/0.5.1/. OpenRouter's default
|
|
@@ -144,8 +178,16 @@ what's next" so a new session can continue without reconstructing from git log.
|
|
|
144
178
|
2. **Paper 2 (the Proposer)** stays parked until the engine is fully shipped.
|
|
145
179
|
Note: the Proposer study runs *on* this engine — Frame, seeded generation, and
|
|
146
180
|
the depth knob are the instrument it will exercise.
|
|
147
|
-
3.
|
|
148
|
-
|
|
181
|
+
3. ~~**Optional polish:** runs as MCP resources; a landing line for the front
|
|
182
|
+
door / `--depth` / `--seed`.~~ **DONE (2026-10-16).** The landing line is live.
|
|
183
|
+
Saved runs are MCP resources (`rapier://runs`, `rapier://runs/{run_id}`,
|
|
184
|
+
`rapier://runs/{run_id}/record`); the `list_runs` / `get_run` tools stay for
|
|
185
|
+
tool-only clients. Released in 0.5.3.
|
|
186
|
+
4. ~~**Port the MCP server to the mcp 2.x SDK.**~~ **DONE (2026-10-16), not yet
|
|
187
|
+
released.** `MCPServer` replaces `FastMCP`; the `[mcp]` extra is now
|
|
188
|
+
`mcp>=2.3,<3`. 250 tests pass on mcp 2.3.0, and a built wheel was checked over
|
|
189
|
+
real stdio: tools, resources, a live `frame` call with progress, and the new
|
|
190
|
+
run appearing in the next listing. Ships with the next release.
|
|
149
191
|
|
|
150
192
|
_(M4 publish is **DONE** — v0.4.0 on PyPI + the full landing live; see Done above.
|
|
151
193
|
Loom submodule pin bumped to the v0.4.0 release commit `514df56`.)_
|
|
@@ -97,7 +97,17 @@ 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
|
+
The server is built on the mcp **2.x** SDK (`MCPServer`; ported 2026-10-16), and
|
|
108
|
+
the `[mcp]` extra requires `mcp>=2.3,<3`. Stage progress is sent as the
|
|
109
|
+
`notifications/progress` message, because 2.x delivers log messages only to
|
|
110
|
+
clients that opt in.
|
|
101
111
|
|
|
102
112
|
## Open decisions
|
|
103
113
|
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.
|
|
12
|
+
version = "0.6.0"
|
|
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
|
+
# Built on the mcp 2.x SDK (MCPServer). Capped below the next major, because 2.0
|
|
54
|
+
# renamed the server API and an uncapped >=1.2 broke `rapier mcp` (0.5.2).
|
|
55
|
+
mcp = ["mcp>=2.3,<3"]
|
|
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
|
|
@@ -13,15 +15,22 @@ minutes, so the tools **stream per-stage progress** to the client: the engine's
|
|
|
13
15
|
run-log lines are bridged from the worker thread to MCP ``info`` + ``report_progress``
|
|
14
16
|
notifications, so the client shows life instead of a hang.
|
|
15
17
|
|
|
18
|
+
Built on the mcp 2.x SDK (``MCPServer``; it was ``FastMCP`` in 1.x).
|
|
19
|
+
|
|
16
20
|
NOTE: this module deliberately does *not* use ``from __future__ import
|
|
17
|
-
annotations`` —
|
|
21
|
+
annotations`` — MCPServer evaluates each tool's signature, and the lazily-imported
|
|
18
22
|
``Context`` annotation must be a real class object at definition time, not a
|
|
19
23
|
string it cannot resolve.
|
|
20
24
|
"""
|
|
25
|
+
import json
|
|
21
26
|
import os
|
|
22
27
|
import sys
|
|
23
28
|
from typing import Any, Callable
|
|
24
29
|
|
|
30
|
+
# Cap on runs listed individually in resources/list; every run stays readable by URI
|
|
31
|
+
# and is indexed in rapier://runs.
|
|
32
|
+
_MAX_LISTED_RUNS = 200
|
|
33
|
+
|
|
25
34
|
|
|
26
35
|
async def _run_with_progress(
|
|
27
36
|
fn: Callable[..., dict],
|
|
@@ -32,8 +41,13 @@ async def _run_with_progress(
|
|
|
32
41
|
**kwargs: Any,
|
|
33
42
|
) -> dict:
|
|
34
43
|
"""Run a blocking tool ``fn(log=…, cancel=…, ledger_root=…, **kwargs)`` in a
|
|
35
|
-
worker thread while streaming its log lines to the MCP client
|
|
36
|
-
|
|
44
|
+
worker thread while streaming its log lines to the MCP client, with
|
|
45
|
+
cooperative cancellation and an optional timeout.
|
|
46
|
+
|
|
47
|
+
Every line goes out as an ``info`` log message, and each ``stage:`` line also
|
|
48
|
+
ticks progress with that line as the progress message. The mcp 2.x SDK
|
|
49
|
+
deprecates the logging helpers and delivers them only to clients that opt in,
|
|
50
|
+
so the progress message is what keeps every client showing the stage.
|
|
37
51
|
|
|
38
52
|
``ctx`` may be ``None`` (a unit test with no session), in which case the run
|
|
39
53
|
proceeds silently. On client cancellation or timeout the cancel flag is set,
|
|
@@ -69,11 +83,14 @@ async def _run_with_progress(
|
|
|
69
83
|
if msg is sentinel:
|
|
70
84
|
return
|
|
71
85
|
if ctx is not None:
|
|
72
|
-
|
|
86
|
+
try:
|
|
87
|
+
await ctx.info(msg)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass # logging is best-effort; never fail a run over it
|
|
73
90
|
if msg.startswith("stage:"):
|
|
74
91
|
done += 1
|
|
75
92
|
try:
|
|
76
|
-
await ctx.report_progress(done, total)
|
|
93
|
+
await ctx.report_progress(done, total, message=msg)
|
|
77
94
|
except Exception:
|
|
78
95
|
pass # progress is best-effort; never fail a run over it
|
|
79
96
|
|
|
@@ -99,22 +116,26 @@ async def _run_with_progress(
|
|
|
99
116
|
|
|
100
117
|
|
|
101
118
|
def build_server():
|
|
102
|
-
"""Construct the
|
|
119
|
+
"""Construct the MCP server with Rapier's tools and run resources.
|
|
103
120
|
|
|
104
121
|
Raises ``ImportError`` if the ``mcp`` extra is not installed (handled by
|
|
105
122
|
:func:`serve`).
|
|
106
123
|
"""
|
|
107
|
-
|
|
124
|
+
import warnings
|
|
125
|
+
|
|
126
|
+
from mcp.server.mcpserver import Context, MCPServer # optional dependency
|
|
127
|
+
from mcp.shared.exceptions import MCPDeprecationWarning
|
|
128
|
+
from mcp.types import Resource
|
|
108
129
|
|
|
109
130
|
from .. import __version__
|
|
110
131
|
from ..presets import load_preset
|
|
111
132
|
from . import tools
|
|
112
133
|
|
|
113
|
-
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
# ctx.info is deprecated (SEP-2577) but still reaches clients that opt in to
|
|
135
|
+
# logs; keep sending it without the SDK warning on the server's stderr. The
|
|
136
|
+
# warning is raised inside the SDK, so match its text, not the calling module.
|
|
137
|
+
warnings.filterwarnings("ignore", message="The logging capability is deprecated",
|
|
138
|
+
category=MCPDeprecationWarning)
|
|
118
139
|
# Run persistence (governance default: ON). Ceremonies are written to a
|
|
119
140
|
# durable run dir and become retrievable via list_runs / get_run. Precedence:
|
|
120
141
|
# RAPIER_NO_PERSIST opts out entirely; else RAPIER_MCP_LEDGER pins the location
|
|
@@ -126,6 +147,26 @@ def build_server():
|
|
|
126
147
|
else:
|
|
127
148
|
ledger_root = os.environ.get("RAPIER_MCP_LEDGER") or default_runs_root()
|
|
128
149
|
|
|
150
|
+
class _RapierServer(MCPServer):
|
|
151
|
+
async def list_resources(self):
|
|
152
|
+
# MCPServer lists only resources registered at startup, so runs saved
|
|
153
|
+
# after the server started would never appear. Add each saved run's
|
|
154
|
+
# report as a concrete resource every time a client asks.
|
|
155
|
+
listed = await super().list_resources()
|
|
156
|
+
index = tools.run_index(ledger_root)
|
|
157
|
+
for run in (index.get("runs") or [])[:_MAX_LISTED_RUNS]:
|
|
158
|
+
listed.append(Resource(
|
|
159
|
+
uri=run["report_uri"],
|
|
160
|
+
name=run["run_id"],
|
|
161
|
+
title=f"{run['kind']}: {run['topic']}" if run["topic"] else run["run_id"],
|
|
162
|
+
description=f"verdict: {run['verdict']}" if run["verdict"] else None,
|
|
163
|
+
mime_type="text/plain",
|
|
164
|
+
))
|
|
165
|
+
return listed
|
|
166
|
+
|
|
167
|
+
# The version goes in the initialize handshake; without it 2.x reports "".
|
|
168
|
+
server = _RapierServer("rapier", version=__version__)
|
|
169
|
+
|
|
129
170
|
def _stage_total(name: str, settle: int = 0, verify: str = "gate",
|
|
130
171
|
seed: list | None = None, depth: str = "standard") -> int:
|
|
131
172
|
try:
|
|
@@ -208,29 +249,67 @@ def build_server():
|
|
|
208
249
|
values) and whether cross-vendor review is available."""
|
|
209
250
|
return tools.doctor()
|
|
210
251
|
|
|
252
|
+
# Kept for clients that only call tools; the resources below are the browsable form.
|
|
211
253
|
@server.tool()
|
|
212
254
|
def list_runs() -> dict:
|
|
213
|
-
"""List persisted run ids
|
|
255
|
+
"""List persisted run ids. Clients that support resources can browse rapier://runs instead."""
|
|
214
256
|
return tools.list_runs(ledger_root)
|
|
215
257
|
|
|
216
258
|
@server.tool()
|
|
217
259
|
def get_run(run_id: str) -> dict:
|
|
218
|
-
"""Fetch a persisted run's report + verdict by id
|
|
260
|
+
"""Fetch a persisted run's report + verdict by id. Also readable as rapier://runs/{run_id}."""
|
|
219
261
|
return tools.get_run(ledger_root, run_id)
|
|
220
262
|
|
|
263
|
+
# ── saved runs as resources ──────────────────────────────────────────────
|
|
264
|
+
@server.resource("rapier://runs", name="runs", mime_type="application/json",
|
|
265
|
+
description="Every saved run, newest first: kind, topic, verdict, and "
|
|
266
|
+
"the URIs of its report and full record.")
|
|
267
|
+
def runs_index() -> str:
|
|
268
|
+
return json.dumps(tools.run_index(ledger_root), indent=2)
|
|
269
|
+
|
|
270
|
+
@server.resource("rapier://runs/{run_id}", name="run-report", mime_type="text/plain",
|
|
271
|
+
description="A saved run's report, as the CLI printed it.")
|
|
272
|
+
def run_report(run_id: str) -> str:
|
|
273
|
+
return tools.run_report(ledger_root, run_id)
|
|
274
|
+
|
|
275
|
+
@server.resource("rapier://runs/{run_id}/record", name="run-record",
|
|
276
|
+
mime_type="application/json",
|
|
277
|
+
description="A saved run's full record (redacted on write): request, "
|
|
278
|
+
"recommendation, trust rider, verdict, trace, meta.")
|
|
279
|
+
def run_record(run_id: str) -> str:
|
|
280
|
+
return json.dumps(tools.run_record(ledger_root, run_id), indent=2, default=str)
|
|
281
|
+
|
|
221
282
|
return server
|
|
222
283
|
|
|
223
284
|
|
|
285
|
+
def _installed_mcp_version() -> str | None:
|
|
286
|
+
try:
|
|
287
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
288
|
+
return version("mcp")
|
|
289
|
+
except PackageNotFoundError:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
224
293
|
def serve() -> int:
|
|
225
294
|
"""Run the stdio MCP server. Returns non-zero with a hint if the SDK is absent."""
|
|
226
295
|
try:
|
|
227
296
|
server = build_server()
|
|
228
297
|
except ImportError:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
298
|
+
installed = _installed_mcp_version()
|
|
299
|
+
major = installed.split(".")[0] if installed else ""
|
|
300
|
+
if major.isdigit() and int(major) != 2:
|
|
301
|
+
print(
|
|
302
|
+
f"The MCP server needs the mcp SDK 2.x, but {installed} is installed\n"
|
|
303
|
+
"(its server API differs). Install a 2.x SDK:\n"
|
|
304
|
+
' pip install "mcp>=2.3,<3"',
|
|
305
|
+
file=sys.stderr,
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
print(
|
|
309
|
+
'The MCP server needs the optional "mcp" extra:\n'
|
|
310
|
+
' pip install "rapier-runtime[mcp]"',
|
|
311
|
+
file=sys.stderr,
|
|
312
|
+
)
|
|
234
313
|
return 1
|
|
235
314
|
server.run() # stdio transport by default
|
|
236
315
|
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:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""MCP-0 — tool logic (no SDK needed) + graceful behaviour when the extra is absent.
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The MCPServer wiring itself is exercised only when the optional ``mcp`` extra is
|
|
4
4
|
installed; these tests lock in the parts that must hold regardless.
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
@@ -66,8 +66,8 @@ def test_run_with_progress_streams_stage_progress():
|
|
|
66
66
|
async def info(self, m):
|
|
67
67
|
self.infos.append(m)
|
|
68
68
|
|
|
69
|
-
async def report_progress(self, done, total=None):
|
|
70
|
-
self.progress.append((done, total))
|
|
69
|
+
async def report_progress(self, done, total=None, message=None):
|
|
70
|
+
self.progress.append((done, total, message))
|
|
71
71
|
|
|
72
72
|
def _fn(log=None, **kw):
|
|
73
73
|
log("stage: author (transform)")
|
|
@@ -78,7 +78,9 @@ def test_run_with_progress_streams_stage_progress():
|
|
|
78
78
|
ctx = _Ctx()
|
|
79
79
|
out = anyio.run(server._run_with_progress, _fn, ctx, 2)
|
|
80
80
|
assert out == {"ok": True, "report_md": "R"}
|
|
81
|
-
|
|
81
|
+
# one tick per 'stage:' line, carrying that line as the progress message
|
|
82
|
+
assert ctx.progress == [(1, 2, "stage: author (transform)"),
|
|
83
|
+
(2, 2, "stage: compose (transform)")]
|
|
82
84
|
assert any("substitution" in m for m in ctx.infos) # every line streamed as info
|
|
83
85
|
|
|
84
86
|
|
|
@@ -111,9 +113,19 @@ def test_build_server_advertises_rapier_version_not_sdk():
|
|
|
111
113
|
pytest.importorskip("mcp")
|
|
112
114
|
from rapier import __version__
|
|
113
115
|
|
|
116
|
+
anyio = pytest.importorskip("anyio")
|
|
117
|
+
from mcp import Client
|
|
118
|
+
|
|
114
119
|
srv = server.build_server()
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
|
|
121
|
+
async def go():
|
|
122
|
+
async with Client(srv) as client:
|
|
123
|
+
return client.server_info
|
|
124
|
+
|
|
125
|
+
# The initialize handshake must report rapier's name and version (2.x
|
|
126
|
+
# reports "" when a server passes none).
|
|
127
|
+
info = anyio.run(go)
|
|
128
|
+
assert (info.name, info.version) == ("rapier", __version__)
|
|
117
129
|
|
|
118
130
|
|
|
119
131
|
# --- MCP-2: timeout + ledger-run access ---
|
|
@@ -169,3 +181,24 @@ def test_runs_disabled_without_ledger():
|
|
|
169
181
|
|
|
170
182
|
assert tools.list_runs(None)["ok"] is False
|
|
171
183
|
assert tools.get_run(None, "x")["ok"] is False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_serve_names_an_old_mcp_sdk_when_that_is_why_import_failed(monkeypatch, capsys):
|
|
187
|
+
def _raise():
|
|
188
|
+
raise ImportError("No module named 'mcp.server.mcpserver'")
|
|
189
|
+
|
|
190
|
+
monkeypatch.setattr(server, "build_server", _raise)
|
|
191
|
+
monkeypatch.setattr(server, "_installed_mcp_version", lambda: "1.30.0")
|
|
192
|
+
assert server.serve() == 1
|
|
193
|
+
err = capsys.readouterr().err
|
|
194
|
+
assert "1.30.0 is installed" in err and 'pip install "mcp>=2.3,<3"' in err
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_serve_without_sdk_points_at_the_extra(monkeypatch, capsys):
|
|
198
|
+
def _raise():
|
|
199
|
+
raise ImportError("No module named 'mcp'")
|
|
200
|
+
|
|
201
|
+
monkeypatch.setattr(server, "build_server", _raise)
|
|
202
|
+
monkeypatch.setattr(server, "_installed_mcp_version", lambda: None)
|
|
203
|
+
assert server.serve() == 1
|
|
204
|
+
assert 'pip install "rapier-runtime[mcp]"' in capsys.readouterr().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.mcpserver")
|
|
64
|
+
anyio = pytest.importorskip("anyio")
|
|
65
|
+
from mcp import Client
|
|
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 Client(server) as client:
|
|
75
|
+
listed = {str(r.uri): r for r in (await client.list_resources()).resources}
|
|
76
|
+
templates = {t.uri_template for t in
|
|
77
|
+
(await client.list_resource_templates()).resource_templates}
|
|
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"].mime_type == "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.6.0}/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.6.0}/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.6.0}/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.6.0}/src/rapier/verify/_vendor/spar_cross_review.py
RENAMED
|
File without changes
|
{rapier_runtime-0.5.2 → rapier_runtime-0.6.0}/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
|