queue-aiops 0.9.4__tar.gz → 0.10.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.
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/.claude-plugin/plugin.json +1 -1
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/.mcp.json +1 -1
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/CHANGELOG.md +12 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/PKG-INFO +1 -1
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/RELEASE_NOTES.md +3 -0
- queue_aiops-0.10.0/mcp_server/_annotations.py +66 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/server.py +5 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/openclaw.plugin.json +1 -1
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/package.json +1 -1
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/pyproject.toml +1 -1
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/server.json +2 -2
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/skills/queue-aiops/references/agent-guardrails.md +1 -1
- queue_aiops-0.10.0/tests/test_tool_annotations.py +128 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/uv.lock +1 -1
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/.github/workflows/mcp-publish.yml +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/.github/workflows/publish.yml +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/.gitignore +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/Dockerfile +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/LICENSE +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/README.md +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/SECURITY.md +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/docs/VERIFICATION.md +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/glama.json +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/__init__.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/_shared.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/tools/__init__.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/tools/analysis.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/tools/overview.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/tools/rabbit_reads.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/tools/redis_reads.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/tools/undo.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/mcp_server/tools/writes.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/__init__.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/__init__.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/_common.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/_root.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/analyze.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/doctor.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/init.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/overview.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/rabbit_cmds.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/redis_cmds.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/secret.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/cli/undo.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/config.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/connection.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/doctor.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/__init__.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/audit.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/budget.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/decorators.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/outcome.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/paths.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/patterns.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/policy.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/sanitize.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/governance/undo.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/ops/__init__.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/ops/_util.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/ops/analysis.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/ops/overview.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/ops/rabbit_reads.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/ops/redis_reads.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/ops/writes.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/platform.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/queue_aiops/secretstore.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/skills/queue-aiops/SKILL.md +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/skills/queue-aiops/references/capabilities.md +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/skills/queue-aiops/references/cli-reference.md +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/skills/queue-aiops/references/setup-guide.md +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/smithery.yaml +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/conftest.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/fakes.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_analysis.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_cli_audited.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_cli_governed_exit.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_cli_rabbit_writes.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_cli_reads.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_cli_secret.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_cli_writes.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_connection.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_doctor.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_gov_audit.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_gov_decorators.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_gov_patterns.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_gov_policy.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_governance_persistence.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_init.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_int_fidelity.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_mcp_reads.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_optional_fields.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_platform.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_reads.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_secretstore.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_self_kill.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_self_lockout.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_smoke.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_undo_executor.py +0 -0
- {queue_aiops-0.9.4 → queue_aiops-0.10.0}/tests/test_writes.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "queue-aiops",
|
|
4
4
|
"displayName": "Queue AIops",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.10.0",
|
|
6
6
|
"description": "Governed redis + rabbitmq ops: memory/latency/backlog/churn RCA, policies. 28 tools.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Wei Zhou",
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.10.0 — 2026-10-15
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- MCP tool annotations derived from each tool's governance `risk_level`, so
|
|
7
|
+
clients can gate approval from `tools/list` alone. `low` tools advertise
|
|
8
|
+
`readOnlyHint: true`; every other tool advertises `readOnlyHint: false` and
|
|
9
|
+
`destructiveHint: true`. The `medium` tier includes deletes and hard
|
|
10
|
+
power-offs, so a tool is never advertised as non-destructive on the strength
|
|
11
|
+
of its tier. `idempotentHint` only repeats an explicit declaration. The exact
|
|
12
|
+
level is published in `_meta` under `io.github.AIops-tools/riskLevel`. These
|
|
13
|
+
are hints for the client; audit and undo are unchanged.
|
|
14
|
+
|
|
3
15
|
## v0.9.4 — 2026-09-16
|
|
4
16
|
|
|
5
17
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: queue-aiops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Governed AI-ops for redis + rabbitmq: memory-pressure/latency/queue-backlog/connection-churn RCAs, slowlog and SCAN-budgeted big-key sampling, queues/policies/nodes, and governed writes (config set, client kill, purge/delete queue, policies) with a built-in governance harness (audit, budget, undo, risk tiers)
|
|
5
5
|
Author-email: Wei Zhou <zhouwei008@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Release notes — queue-aiops 0.4.0
|
|
2
2
|
|
|
3
|
+
> Historical — this file covers the 0.4.0 release only and is not updated.
|
|
4
|
+
> For every release since, see [CHANGELOG.md](CHANGELOG.md) and the GitHub releases.
|
|
5
|
+
|
|
3
6
|
Previous release: 0.3.0.
|
|
4
7
|
|
|
5
8
|
## BREAKING — the authorization layer is removed
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Derive MCP tool annotations from the governance risk level.
|
|
2
|
+
|
|
3
|
+
Every tool already declares ``risk_level`` on ``@governed_tool``; this module
|
|
4
|
+
publishes it to MCP clients as the standard ``ToolAnnotations`` hints plus the
|
|
5
|
+
exact level under ``_meta``, so a client can gate approval without reading our
|
|
6
|
+
docstrings.
|
|
7
|
+
|
|
8
|
+
Only the safe direction is ever asserted:
|
|
9
|
+
|
|
10
|
+
- ``low`` → ``readOnlyHint: true``. Backed by the smoke-test invariant that
|
|
11
|
+
``risk_level == "low"`` exactly when the docstring says ``[READ]``.
|
|
12
|
+
- anything else → ``readOnlyHint: false, destructiveHint: true``. The
|
|
13
|
+
``medium`` tier is NOT non-destructive — it holds deletes, hard power-offs,
|
|
14
|
+
state kills and stat resets — so claiming ``destructiveHint: false`` from the
|
|
15
|
+
tier would tell a client it may skip approval for a delete. Over-warning
|
|
16
|
+
costs a confirmation prompt; under-warning costs data.
|
|
17
|
+
- ``idempotentHint`` only echoes an explicit ``@governed_tool(idempotent=True)``
|
|
18
|
+
declaration; it is never inferred.
|
|
19
|
+
- ``openWorldHint`` is left unset (spec default ``true``): every tool talks to
|
|
20
|
+
an external system.
|
|
21
|
+
|
|
22
|
+
This file is identical across the AIops line; keep it that way.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from mcp.types import ToolAnnotations
|
|
28
|
+
|
|
29
|
+
RISK_META_KEY = "io.github.AIops-tools/riskLevel"
|
|
30
|
+
RISK_LEVELS = frozenset({"low", "medium", "high", "critical"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def annotations_for(risk_level: str, idempotent: bool) -> ToolAnnotations:
|
|
34
|
+
"""Return the MCP hints that a tool of ``risk_level`` may honestly claim."""
|
|
35
|
+
if risk_level not in RISK_LEVELS:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"unknown risk_level {risk_level!r}; expected one of {sorted(RISK_LEVELS)}"
|
|
38
|
+
)
|
|
39
|
+
if risk_level == "low":
|
|
40
|
+
return ToolAnnotations(read_only_hint=True)
|
|
41
|
+
return ToolAnnotations(
|
|
42
|
+
read_only_hint=False,
|
|
43
|
+
destructive_hint=True,
|
|
44
|
+
idempotent_hint=idempotent,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def apply_risk_annotations(server: Any) -> None:
|
|
49
|
+
"""Replace every registered tool with a copy carrying its risk annotations.
|
|
50
|
+
|
|
51
|
+
Fails fast on a tool without ``@governed_tool`` metadata: an unannotated
|
|
52
|
+
tool would be advertised with the spec defaults, i.e. as a destructive
|
|
53
|
+
write with no stated risk level — wrong for a read, silent for a write.
|
|
54
|
+
"""
|
|
55
|
+
tools = server._tool_manager._tools # same private registry undo.py relies on
|
|
56
|
+
for name, tool in list(tools.items()):
|
|
57
|
+
risk_level = getattr(tool.fn, "_risk_level", None)
|
|
58
|
+
if risk_level is None:
|
|
59
|
+
raise RuntimeError(f"tool {name!r} has no @governed_tool risk_level")
|
|
60
|
+
idempotent = bool(getattr(tool.fn, "_idempotent", False))
|
|
61
|
+
tools[name] = tool.model_copy(
|
|
62
|
+
update={
|
|
63
|
+
"annotations": annotations_for(risk_level, idempotent),
|
|
64
|
+
"meta": {**(tool.meta or {}), RISK_META_KEY: risk_level},
|
|
65
|
+
}
|
|
66
|
+
)
|
|
@@ -17,6 +17,7 @@ License: MIT
|
|
|
17
17
|
import logging
|
|
18
18
|
import os
|
|
19
19
|
|
|
20
|
+
from mcp_server._annotations import apply_risk_annotations
|
|
20
21
|
from mcp_server._shared import _safe_error, mcp, tool_errors
|
|
21
22
|
|
|
22
23
|
# Importing the tool modules registers every @mcp.tool() onto the shared
|
|
@@ -30,6 +31,10 @@ from mcp_server.tools import ( # noqa: F401 — side effects
|
|
|
30
31
|
writes,
|
|
31
32
|
)
|
|
32
33
|
|
|
34
|
+
# Publish each tool's governance risk level as MCP annotations. Runs after
|
|
35
|
+
# every tool module has registered, so no tool is advertised unannotated.
|
|
36
|
+
apply_risk_annotations(mcp)
|
|
37
|
+
|
|
33
38
|
__all__ = ["mcp", "main", "_safe_error", "tool_errors"]
|
|
34
39
|
|
|
35
40
|
logger = logging.getLogger(__name__)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queue-aiops"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "Governed AI-ops for redis + rabbitmq: memory-pressure/latency/queue-backlog/connection-churn RCAs, slowlog and SCAN-budgeted big-key sampling, queues/policies/nodes, and governed writes (config set, client kill, purge/delete queue, policies) with a built-in governance harness (audit, budget, undo, risk tiers)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/AIops-tools/Queue-AIops",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.10.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "queue-aiops",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.10.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -48,7 +48,7 @@ because fragmentation is checked before big keys. Do not let the model treat `fi
|
|
|
48
48
|
as the headline — make it weigh every finding's `evidence` and say which one it acted on.
|
|
49
49
|
(Earlier versions of this file claimed these lists were ranked worst-first. They never were.)
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
Recommended setup for a local model (adapt it to your agent client):
|
|
52
52
|
|
|
53
53
|
```text
|
|
54
54
|
You operate a RabbitMQ broker or a Redis instance through the queue-aiops MCP
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Invariants for the MCP annotations derived from ``risk_level``.
|
|
2
|
+
|
|
3
|
+
Asserted on ``list_tools()`` — the payload a client actually receives — not on
|
|
4
|
+
the internal registry, so a regression in how annotations reach the wire fails
|
|
5
|
+
here too. This file is identical across the AIops line.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import re
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from mcp.server import MCPServer
|
|
14
|
+
|
|
15
|
+
import mcp_server.server as server
|
|
16
|
+
from mcp_server._annotations import (
|
|
17
|
+
RISK_META_KEY,
|
|
18
|
+
annotations_for,
|
|
19
|
+
apply_risk_annotations,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(scope="module")
|
|
24
|
+
def listed():
|
|
25
|
+
return asyncio.run(server.mcp.list_tools())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(scope="module")
|
|
29
|
+
def registry():
|
|
30
|
+
return server.mcp._tool_manager._tools
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_every_tool_is_annotated(listed):
|
|
34
|
+
assert listed, "no tools listed"
|
|
35
|
+
missing = [t.name for t in listed if t.annotations is None or not t.meta]
|
|
36
|
+
assert not missing, f"tools advertised without annotations: {missing}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_meta_carries_the_declared_risk_level(listed, registry):
|
|
40
|
+
wrong = [t.name for t in listed if t.meta.get(RISK_META_KEY) != registry[t.name].fn._risk_level]
|
|
41
|
+
assert not wrong, f"_meta risk level differs from @governed_tool: {wrong}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_read_only_hint_true_exactly_for_low_risk(listed):
|
|
45
|
+
wrong = [
|
|
46
|
+
t.name for t in listed if t.annotations.read_only_hint != (t.meta[RISK_META_KEY] == "low")
|
|
47
|
+
]
|
|
48
|
+
assert not wrong, f"readOnlyHint disagrees with risk_level: {wrong}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_no_write_claims_to_be_non_destructive(listed):
|
|
52
|
+
# medium holds deletes and hard power-offs; the tier cannot prove safety.
|
|
53
|
+
wrong = [
|
|
54
|
+
t.name
|
|
55
|
+
for t in listed
|
|
56
|
+
if not t.annotations.read_only_hint and t.annotations.destructive_hint is not True
|
|
57
|
+
]
|
|
58
|
+
assert not wrong, f"writes advertised as non-destructive: {wrong}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_idempotent_hint_only_echoes_an_explicit_declaration(listed, registry):
|
|
62
|
+
wrong = [
|
|
63
|
+
t.name
|
|
64
|
+
for t in listed
|
|
65
|
+
if t.annotations.idempotent_hint and not registry[t.name].fn._idempotent
|
|
66
|
+
]
|
|
67
|
+
assert not wrong, f"idempotentHint inferred without a declaration: {wrong}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_open_world_hint_is_never_narrowed(listed):
|
|
71
|
+
assert all(t.annotations.open_world_hint in (None, True) for t in listed)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize("level", ["low", "medium", "high", "critical"])
|
|
75
|
+
def test_mapping_per_level(level):
|
|
76
|
+
hints = annotations_for(level, idempotent=False)
|
|
77
|
+
if level == "low":
|
|
78
|
+
assert hints.read_only_hint is True
|
|
79
|
+
assert hints.destructive_hint is None
|
|
80
|
+
else:
|
|
81
|
+
assert hints.read_only_hint is False
|
|
82
|
+
assert hints.destructive_hint is True
|
|
83
|
+
assert hints.idempotent_hint is False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_declared_idempotent_write_is_echoed():
|
|
87
|
+
assert annotations_for("medium", idempotent=True).idempotent_hint is True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_unknown_risk_level_is_rejected():
|
|
91
|
+
with pytest.raises(ValueError, match="unknown risk_level"):
|
|
92
|
+
annotations_for("Low", idempotent=False)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_ungoverned_tool_fails_fast():
|
|
96
|
+
fake_tool = SimpleNamespace(fn=lambda: None, meta=None)
|
|
97
|
+
fake = SimpleNamespace(_tool_manager=SimpleNamespace(_tools={"rogue": fake_tool}))
|
|
98
|
+
with pytest.raises(RuntimeError, match="rogue"):
|
|
99
|
+
apply_risk_annotations(fake)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_meta_key_is_the_published_contract():
|
|
103
|
+
# Clients match on this literal; it must be a valid, non-reserved _meta key.
|
|
104
|
+
assert RISK_META_KEY == "io.github.AIops-tools/riskLevel"
|
|
105
|
+
prefix, name = RISK_META_KEY.split("/")
|
|
106
|
+
label = re.compile(r"^[A-Za-z]([A-Za-z0-9-]*[A-Za-z0-9])?$")
|
|
107
|
+
labels = prefix.split(".")
|
|
108
|
+
assert all(label.match(part) for part in labels)
|
|
109
|
+
assert not {"mcp", "modelcontextprotocol"} & {part.lower() for part in labels}
|
|
110
|
+
assert re.match(r"^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$", name)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_existing_meta_survives_on_a_real_tool():
|
|
114
|
+
fresh = MCPServer("annotations-test")
|
|
115
|
+
|
|
116
|
+
def probe() -> str:
|
|
117
|
+
return "ok"
|
|
118
|
+
|
|
119
|
+
probe._risk_level = "medium"
|
|
120
|
+
probe._idempotent = True
|
|
121
|
+
fresh.tool(meta={"x": 1})(probe)
|
|
122
|
+
apply_risk_annotations(fresh)
|
|
123
|
+
|
|
124
|
+
(listed_tool,) = asyncio.run(fresh.list_tools())
|
|
125
|
+
assert listed_tool.meta == {"x": 1, RISK_META_KEY: "medium"}
|
|
126
|
+
assert listed_tool.annotations.destructive_hint is True
|
|
127
|
+
assert listed_tool.annotations.idempotent_hint is True
|
|
128
|
+
assert fresh._tool_manager._tools["probe"].fn is probe
|
|
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
|
|
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
|
|
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
|