@smilintux/skcapstone 0.6.2 → 0.6.4
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.
- package/.github/workflows/publish.yml +1 -1
- package/CLAUDE.md +17 -0
- package/docs/CUSTOM_AGENT.md +40 -28
- package/docs/SOUL_SWAPPER.md +5 -5
- package/docs/hammertime-audit.md +402 -0
- package/openclaw-plugin/src/index.ts +2 -1
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/scripts/install.sh +126 -1
- package/scripts/model-fallback-monitor.sh +4 -2
- package/scripts/refresh-anthropic-token.sh +9 -3
- package/scripts/release.sh +98 -0
- package/scripts/session-to-memory.py +1 -1
- package/scripts/sk-agent-picker.sh +237 -0
- package/scripts/telegram-catchup-all.sh +2 -1
- package/scripts/watch-anthropic-token.sh +12 -17
- package/src/skcapstone/__init__.py +34 -2
- package/src/skcapstone/cli/__init__.py +3 -1
- package/src/skcapstone/cli/_common.py +1 -0
- package/src/skcapstone/cli/context_cmd.py +16 -4
- package/src/skcapstone/cli/daemon.py +2 -1
- package/src/skcapstone/cli/joule_cmd.py +7 -3
- package/src/skcapstone/cli/memory.py +4 -2
- package/src/skcapstone/cli/register_cmd.py +19 -3
- package/src/skcapstone/cli/session.py +25 -0
- package/src/skcapstone/cli/setup.py +96 -30
- package/src/skcapstone/cli/soul.py +3 -3
- package/src/skcapstone/context_loader.py +9 -0
- package/src/skcapstone/coordination.py +9 -2
- package/src/skcapstone/daemon.py +22 -12
- package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
- package/src/skcapstone/defaults/claude/settings.json +74 -0
- package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
- package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
- package/src/skcapstone/defaults/unhinged.json +13 -0
- package/src/skcapstone/discovery.py +5 -5
- package/src/skcapstone/doctor.py +4 -2
- package/src/skcapstone/dreaming.py +3 -1
- package/src/skcapstone/fuse_mount.py +3 -1
- package/src/skcapstone/housekeeping.py +3 -3
- package/src/skcapstone/install_wizard.py +131 -0
- package/src/skcapstone/mcp_launcher.py +14 -1
- package/src/skcapstone/mcp_server.py +6 -21
- package/src/skcapstone/mcp_tools/notification_tools.py +3 -1
- package/src/skcapstone/memory_engine.py +10 -3
- package/src/skcapstone/migrate_multi_agent.py +7 -6
- package/src/skcapstone/notifications.py +6 -2
- package/src/skcapstone/onboard.py +19 -8
- package/src/skcapstone/operator_link.py +164 -0
- package/src/skcapstone/pillars/consciousness.py +2 -1
- package/src/skcapstone/pillars/identity.py +51 -7
- package/src/skcapstone/pillars/memory.py +9 -3
- package/src/skcapstone/runtime.py +13 -3
- package/src/skcapstone/service_health.py +23 -10
- package/src/skcapstone/session_briefing.py +108 -0
- package/src/skcapstone/trust_graph.py +40 -5
- package/src/skcapstone/unified_search.py +11 -2
- package/systemd/skcapstone.service +4 -6
- package/systemd/skcapstone@.service +7 -8
- package/systemd/skcomm-heartbeat.service +5 -2
- package/tests/conftest.py +21 -0
- package/tests/test_agent_home_scaffold.py +34 -0
- package/tests/test_backup.py +2 -1
- package/tests/test_mcp_server.py +78 -33
- package/tests/test_multi_agent.py +31 -29
- package/tests/test_operator_link.py +78 -0
- package/tests/test_runtime.py +21 -0
- package/tests/test_session_briefing.py +130 -0
- package/tests/test_trust_graph.py +18 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Session briefing helpers for SKCapstone startup flows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .context_loader import format_text, gather_context
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_HAMMERTIME_ROOT = Path("/mnt/cloud/onedrive/projects/DAVE AI/hammerTime")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_hammertime_root() -> Path:
|
|
20
|
+
"""Resolve the HammerTime workspace root."""
|
|
21
|
+
return Path(os.environ.get("HAMMERTIME_ROOT", DEFAULT_HAMMERTIME_ROOT)).expanduser()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_hammertime_briefing(
|
|
25
|
+
*,
|
|
26
|
+
python_bin: str | None = None,
|
|
27
|
+
root: Path | None = None,
|
|
28
|
+
) -> dict[str, Any] | None:
|
|
29
|
+
"""Load the HammerTime case briefing if the repo is available."""
|
|
30
|
+
if os.environ.get("SK_INCLUDE_HAMMERTIME_BRIEFING", "1") == "0":
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
hammer_root = root or _resolve_hammertime_root()
|
|
34
|
+
script = hammer_root / "scripts" / "case-briefing.py"
|
|
35
|
+
if not script.exists():
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
completed = subprocess.run(
|
|
40
|
+
[python_bin or sys.executable, str(script)],
|
|
41
|
+
check=True,
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
)
|
|
45
|
+
except (OSError, subprocess.CalledProcessError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
payload = json.loads(completed.stdout)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
return None
|
|
52
|
+
return payload if isinstance(payload, dict) else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_session_briefing(
|
|
56
|
+
home: Path,
|
|
57
|
+
*,
|
|
58
|
+
memory_limit: int = 10,
|
|
59
|
+
python_bin: str | None = None,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Build a native session briefing payload."""
|
|
62
|
+
return {
|
|
63
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
64
|
+
"agent_home": str(home),
|
|
65
|
+
"skcapstone_context": gather_context(home, memory_limit=memory_limit),
|
|
66
|
+
"hammertime_briefing": load_hammertime_briefing(python_bin=python_bin),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def format_session_briefing_text(payload: dict[str, Any]) -> str:
|
|
71
|
+
"""Render a human-readable session briefing."""
|
|
72
|
+
lines = [
|
|
73
|
+
"# SKCapstone Session Briefing",
|
|
74
|
+
"",
|
|
75
|
+
f"generated_at={payload.get('generated_at')}",
|
|
76
|
+
f"agent_home={payload.get('agent_home')}",
|
|
77
|
+
"",
|
|
78
|
+
"## skcapstone context",
|
|
79
|
+
format_text(payload["skcapstone_context"]).rstrip(),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
briefing = payload.get("hammertime_briefing")
|
|
83
|
+
if briefing:
|
|
84
|
+
top = briefing.get("top_priority") or {}
|
|
85
|
+
lines.extend(
|
|
86
|
+
[
|
|
87
|
+
"",
|
|
88
|
+
"## hammertime briefing",
|
|
89
|
+
f"- alert_count: {briefing.get('alert_count', 0)}",
|
|
90
|
+
f"- queue_size: {briefing.get('summary', {}).get('queue_size', 0)}",
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
if top:
|
|
94
|
+
lines.extend(
|
|
95
|
+
[
|
|
96
|
+
f"- do_this_now_incident: {top.get('incident_id')} ({top.get('problem_slug')})",
|
|
97
|
+
f"- do_this_now_action: {top.get('action')}",
|
|
98
|
+
f"- do_this_now_status: {top.get('status')}",
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
for item in (briefing.get("focus_items") or [])[:3]:
|
|
102
|
+
lines.append(
|
|
103
|
+
f"- focus: {item.get('incident_id')} -> {item.get('action')} [{item.get('status')}]"
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
lines.extend(["", "## hammertime briefing", "- unavailable"])
|
|
107
|
+
|
|
108
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
@@ -117,6 +117,8 @@ def build_trust_graph(home: Path) -> TrustGraph:
|
|
|
117
117
|
|
|
118
118
|
def _add_self_node(home: Path, graph: TrustGraph) -> None:
|
|
119
119
|
"""Add the local agent as the central node."""
|
|
120
|
+
manifest_data: dict[str, Any] = {}
|
|
121
|
+
|
|
120
122
|
identity_file = home / "identity" / "identity.json"
|
|
121
123
|
if identity_file.exists():
|
|
122
124
|
try:
|
|
@@ -130,20 +132,53 @@ def _add_self_node(home: Path, graph: TrustGraph) -> None:
|
|
|
130
132
|
fingerprint=data.get("fingerprint"),
|
|
131
133
|
metadata={"capauth_managed": data.get("capauth_managed", False)},
|
|
132
134
|
))
|
|
133
|
-
return
|
|
134
135
|
except (json.JSONDecodeError, OSError):
|
|
135
136
|
pass
|
|
136
137
|
|
|
137
138
|
manifest = home / "manifest.json"
|
|
138
139
|
if manifest.exists():
|
|
139
140
|
try:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
manifest_data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
142
|
+
if not graph.nodes:
|
|
143
|
+
name = manifest_data.get("name", "self")
|
|
144
|
+
graph.agent_name = name
|
|
145
|
+
graph.add_node(TrustNode(id=name, label=name, node_type="agent"))
|
|
144
146
|
except (json.JSONDecodeError, OSError):
|
|
145
147
|
pass
|
|
146
148
|
|
|
149
|
+
_add_operator_edge(manifest_data, graph)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _add_operator_edge(manifest_data: dict[str, Any], graph: TrustGraph) -> None:
|
|
153
|
+
"""Add an explicit human-operator relationship from manifest metadata."""
|
|
154
|
+
operator = manifest_data.get("operator")
|
|
155
|
+
if not isinstance(operator, dict):
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
name = str(operator.get("name", "")).strip()
|
|
159
|
+
if not name or not graph.agent_name:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
node_id = str(operator.get("fingerprint", "")).strip() or f"operator:{name}"
|
|
163
|
+
graph.add_node(TrustNode(
|
|
164
|
+
id=node_id,
|
|
165
|
+
label=name,
|
|
166
|
+
node_type="peer",
|
|
167
|
+
fingerprint=str(operator.get("fingerprint", "")).strip() or None,
|
|
168
|
+
metadata={
|
|
169
|
+
"relationship": operator.get("relationship", "human-operator"),
|
|
170
|
+
"entity_type": operator.get("entity_type", "human"),
|
|
171
|
+
"source": operator.get("source", "manifest"),
|
|
172
|
+
},
|
|
173
|
+
))
|
|
174
|
+
graph.add_edge(TrustEdge(
|
|
175
|
+
source=graph.agent_name,
|
|
176
|
+
target=node_id,
|
|
177
|
+
edge_type="operator",
|
|
178
|
+
label=operator.get("relationship", "human-operator"),
|
|
179
|
+
strength=1.0,
|
|
180
|
+
))
|
|
181
|
+
|
|
147
182
|
|
|
148
183
|
def _add_token_edges(home: Path, graph: TrustGraph) -> None:
|
|
149
184
|
"""Add edges from capability token issuance (issuer trusts subject)."""
|
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
|
+
import os
|
|
18
19
|
import re
|
|
19
20
|
from dataclasses import dataclass, field
|
|
20
21
|
from datetime import datetime, timezone
|
|
@@ -140,8 +141,16 @@ def _search_memories(
|
|
|
140
141
|
List of SearchResult objects from the memory store.
|
|
141
142
|
"""
|
|
142
143
|
results: list[SearchResult] = []
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
from . import active_agent_name
|
|
145
|
+
|
|
146
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT") or active_agent_name()
|
|
147
|
+
if home.parent.name == "agents":
|
|
148
|
+
# home is already an agent-specific dir (e.g. ~/.skcapstone/agents/lumina)
|
|
149
|
+
mem_dir = home / "memory"
|
|
150
|
+
elif agent_name:
|
|
151
|
+
mem_dir = home / "agents" / agent_name / "memory"
|
|
152
|
+
else:
|
|
153
|
+
mem_dir = home / "memory"
|
|
145
154
|
if not mem_dir.exists():
|
|
146
155
|
return results
|
|
147
156
|
|
|
@@ -16,20 +16,18 @@ MemoryMax=4G
|
|
|
16
16
|
# Keep Ollama models warm for 5 minutes between requests
|
|
17
17
|
Environment=PYTHONUNBUFFERED=1
|
|
18
18
|
Environment=OLLAMA_KEEP_ALIVE=5m
|
|
19
|
+
Environment=SKAGENT=lumina
|
|
19
20
|
Environment=SKCAPSTONE_AGENT=lumina
|
|
21
|
+
Environment=SKMEMORY_AGENT=lumina
|
|
20
22
|
# Journal logging
|
|
21
23
|
StandardOutput=journal
|
|
22
24
|
StandardError=journal
|
|
23
25
|
SyslogIdentifier=skcapstone
|
|
24
26
|
|
|
25
|
-
# Security hardening
|
|
27
|
+
# Security hardening (relaxed — ProtectHome=read-only breaks if any
|
|
28
|
+
# ReadWritePaths dir is missing on the host)
|
|
26
29
|
NoNewPrivileges=true
|
|
27
|
-
ProtectSystem=strict
|
|
28
|
-
ProtectHome=read-only
|
|
29
|
-
ReadWritePaths=%h/.skcapstone %h/.skenv %h/.capauth %h/.cloud9 %h/.skcomm %h/.skchat
|
|
30
30
|
PrivateTmp=true
|
|
31
|
-
ProtectKernelTunables=true
|
|
32
|
-
ProtectControlGroups=true
|
|
33
31
|
|
|
34
32
|
[Install]
|
|
35
33
|
WantedBy=default.target
|
|
@@ -20,8 +20,8 @@ Wants=network-online.target
|
|
|
20
20
|
|
|
21
21
|
[Service]
|
|
22
22
|
Type=notify
|
|
23
|
-
ExecStart
|
|
24
|
-
ExecStop
|
|
23
|
+
ExecStart=%h/.skenv/bin/skcapstone daemon start --agent %i --foreground
|
|
24
|
+
ExecStop=%h/.skenv/bin/skcapstone daemon stop --agent %i
|
|
25
25
|
ExecReload=/bin/kill -HUP $MAINPID
|
|
26
26
|
Restart=on-failure
|
|
27
27
|
RestartSec=10
|
|
@@ -31,20 +31,19 @@ WatchdogSec=300
|
|
|
31
31
|
# resolve the correct per-agent home even before the CLI flag is processed.
|
|
32
32
|
Environment=PYTHONUNBUFFERED=1
|
|
33
33
|
Environment=OLLAMA_KEEP_ALIVE=5m
|
|
34
|
+
Environment=SKAGENT=%i
|
|
34
35
|
Environment=SKCAPSTONE_AGENT=%i
|
|
36
|
+
Environment=SKMEMORY_AGENT=%i
|
|
37
|
+
Environment=PATH=%h/.skenv/bin:/usr/local/bin:/usr/bin:/bin
|
|
35
38
|
# Journal logging — logs appear under skcapstone@<instance>
|
|
36
39
|
StandardOutput=journal
|
|
37
40
|
StandardError=journal
|
|
38
41
|
SyslogIdentifier=skcapstone@%i
|
|
39
42
|
|
|
40
|
-
# Security hardening (
|
|
43
|
+
# Security hardening (relaxed — ProtectHome=read-only breaks if any
|
|
44
|
+
# ReadWritePaths dir is missing on the host)
|
|
41
45
|
NoNewPrivileges=true
|
|
42
|
-
ProtectSystem=strict
|
|
43
|
-
ProtectHome=read-only
|
|
44
|
-
ReadWritePaths=%h/.skcapstone %h/.skmemory %h/.capauth %h/.cloud9 %h/.skcomm %h/.skchat
|
|
45
46
|
PrivateTmp=true
|
|
46
|
-
ProtectKernelTunables=true
|
|
47
|
-
ProtectControlGroups=true
|
|
48
47
|
|
|
49
48
|
[Install]
|
|
50
49
|
WantedBy=default.target
|
|
@@ -5,8 +5,8 @@ After=network-online.target
|
|
|
5
5
|
|
|
6
6
|
[Service]
|
|
7
7
|
Type=oneshot
|
|
8
|
-
ExecStart
|
|
9
|
-
ExecStart
|
|
8
|
+
ExecStart=%h/.skenv/bin/skcomm heartbeat --no-emit
|
|
9
|
+
ExecStart=%h/.skenv/bin/skcomm heartbeat
|
|
10
10
|
Nice=19
|
|
11
11
|
|
|
12
12
|
NoNewPrivileges=true
|
|
@@ -16,3 +16,6 @@ ReadWritePaths=%h/.skcapstone %h/.skcomm
|
|
|
16
16
|
PrivateTmp=true
|
|
17
17
|
|
|
18
18
|
Environment=PYTHONUNBUFFERED=1
|
|
19
|
+
Environment=SKAGENT=lumina
|
|
20
|
+
Environment=SKCAPSTONE_AGENT=lumina
|
|
21
|
+
Environment=PATH=%h/.skenv/bin:/usr/local/bin:/usr/bin:/bin
|
package/tests/conftest.py
CHANGED
|
@@ -19,6 +19,27 @@ from pathlib import Path
|
|
|
19
19
|
import pytest
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
@pytest.fixture(autouse=True)
|
|
23
|
+
def _isolate_agent_env(monkeypatch):
|
|
24
|
+
"""Prevent host SKCAPSTONE_AGENT / SKMEMORY_AGENT from leaking into unit tests.
|
|
25
|
+
|
|
26
|
+
The profile-aware runtime reads both the env var and the module-level
|
|
27
|
+
skcapstone.SKCAPSTONE_AGENT (set at import time). We clear both so that
|
|
28
|
+
_memory_dir() falls back to the flat "home/memory" layout expected by
|
|
29
|
+
tests that use the tmp_agent_home fixture.
|
|
30
|
+
Tests that need a specific agent should override explicitly via monkeypatch.
|
|
31
|
+
"""
|
|
32
|
+
monkeypatch.delenv("SKCAPSTONE_AGENT", raising=False)
|
|
33
|
+
monkeypatch.delenv("SKMEMORY_AGENT", raising=False)
|
|
34
|
+
import skcapstone
|
|
35
|
+
monkeypatch.setattr(skcapstone, "SKCAPSTONE_AGENT", "")
|
|
36
|
+
# _detect_active_agent() scans ~/.skcapstone/agents/ even when the env var
|
|
37
|
+
# is cleared, returning a real agent name that routes memory writes to the
|
|
38
|
+
# wrong directory. Stub it out so tests using tmp directories get the flat
|
|
39
|
+
# "home/memory" layout they expect.
|
|
40
|
+
monkeypatch.setattr(skcapstone, "_detect_active_agent", lambda root=None: None)
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
@pytest.fixture
|
|
23
44
|
def tmp_agent_home(tmp_path: Path) -> Path:
|
|
24
45
|
"""Provide a temporary agent home directory for testing."""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Tests for fresh agent-home scaffolding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from skcapstone.migrate_multi_agent import create_agent_home
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCreateAgentHome:
|
|
13
|
+
def test_manifest_includes_human_operator_when_available(self, tmp_path: Path):
|
|
14
|
+
"""New agent homes persist the linked human operator in manifest.json."""
|
|
15
|
+
with patch(
|
|
16
|
+
"skcapstone.migrate_multi_agent.discover_human_operator",
|
|
17
|
+
return_value={"name": "Casey", "fingerprint": "FP123", "relationship": "human-operator"},
|
|
18
|
+
):
|
|
19
|
+
result = create_agent_home(tmp_path, "teddy")
|
|
20
|
+
|
|
21
|
+
manifest = json.loads((tmp_path / "agents" / "teddy" / "manifest.json").read_text())
|
|
22
|
+
assert result["agent_name"] == "teddy"
|
|
23
|
+
assert manifest["name"] == "teddy"
|
|
24
|
+
assert manifest["operator"]["name"] == "Casey"
|
|
25
|
+
assert manifest["operator"]["fingerprint"] == "FP123"
|
|
26
|
+
|
|
27
|
+
def test_manifest_omits_operator_when_none_available(self, tmp_path: Path):
|
|
28
|
+
"""New agent homes still create a valid manifest when no operator exists yet."""
|
|
29
|
+
with patch("skcapstone.migrate_multi_agent.discover_human_operator", return_value=None):
|
|
30
|
+
create_agent_home(tmp_path, "lumina")
|
|
31
|
+
|
|
32
|
+
manifest = json.loads((tmp_path / "agents" / "lumina" / "manifest.json").read_text())
|
|
33
|
+
assert manifest["name"] == "lumina"
|
|
34
|
+
assert "operator" not in manifest
|
package/tests/test_backup.py
CHANGED
|
@@ -237,7 +237,8 @@ class TestBackupManifest:
|
|
|
237
237
|
def test_manifest_defaults(self) -> None:
|
|
238
238
|
"""Manifest has sensible defaults."""
|
|
239
239
|
m = BackupManifest()
|
|
240
|
-
|
|
240
|
+
import skcapstone
|
|
241
|
+
assert m.version == skcapstone.__version__
|
|
241
242
|
assert m.files == {}
|
|
242
243
|
assert m.total_size == 0
|
|
243
244
|
|
package/tests/test_mcp_server.py
CHANGED
|
@@ -76,14 +76,15 @@ class TestToolListing:
|
|
|
76
76
|
async def test_list_tools_returns_all(self):
|
|
77
77
|
"""list_tools returns all registered tools."""
|
|
78
78
|
tools = await list_tools()
|
|
79
|
-
assert len(tools) ==
|
|
79
|
+
assert len(tools) == 122
|
|
80
80
|
|
|
81
81
|
@pytest.mark.asyncio
|
|
82
82
|
async def test_tool_names(self):
|
|
83
83
|
"""All required tool names are registered."""
|
|
84
84
|
tools = await list_tools()
|
|
85
85
|
names = {t.name for t in tools}
|
|
86
|
-
|
|
86
|
+
# Verify all known core tools are registered; exact set may grow with new modules.
|
|
87
|
+
core_expected = {
|
|
87
88
|
"agent_status",
|
|
88
89
|
"memory_store",
|
|
89
90
|
"memory_search",
|
|
@@ -98,7 +99,6 @@ class TestToolListing:
|
|
|
98
99
|
"coord_create",
|
|
99
100
|
"ritual",
|
|
100
101
|
"soul_show",
|
|
101
|
-
"journal_write",
|
|
102
102
|
"journal_read",
|
|
103
103
|
"anchor_show",
|
|
104
104
|
"germination",
|
|
@@ -122,48 +122,93 @@ class TestToolListing:
|
|
|
122
122
|
"skchat_inbox",
|
|
123
123
|
"skchat_group_create",
|
|
124
124
|
"skchat_group_send",
|
|
125
|
-
# Heartbeat
|
|
126
125
|
"heartbeat_pulse",
|
|
127
126
|
"heartbeat_peers",
|
|
128
127
|
"heartbeat_health",
|
|
129
128
|
"heartbeat_find_capable",
|
|
130
|
-
# File transfer
|
|
131
129
|
"file_send",
|
|
132
130
|
"file_receive",
|
|
133
131
|
"file_list",
|
|
134
132
|
"file_status",
|
|
135
|
-
# Pub/sub
|
|
136
133
|
"pubsub_publish",
|
|
137
134
|
"pubsub_subscribe",
|
|
138
135
|
"pubsub_poll",
|
|
139
136
|
"pubsub_topics",
|
|
140
|
-
# Memory fortress
|
|
141
137
|
"fortress_verify",
|
|
142
138
|
"fortress_seal_existing",
|
|
143
139
|
"fortress_status",
|
|
144
|
-
# Memory promoter
|
|
145
140
|
"promoter_sweep",
|
|
146
141
|
"promoter_history",
|
|
147
|
-
# KMS
|
|
148
142
|
"kms_status",
|
|
149
143
|
"kms_list_keys",
|
|
150
144
|
"kms_rotate",
|
|
151
|
-
# SKSeed (Logic Kernel)
|
|
152
145
|
"skseed_collide",
|
|
153
146
|
"skseed_audit",
|
|
154
147
|
"skseed_philosopher",
|
|
155
148
|
"skseed_truth_check",
|
|
156
149
|
"skseed_alignment",
|
|
157
|
-
# Model Router
|
|
158
150
|
"model_route",
|
|
159
|
-
# Consciousness
|
|
160
151
|
"consciousness_status",
|
|
161
152
|
"consciousness_test",
|
|
162
|
-
# Notifications & pub/sub stats
|
|
163
153
|
"send_notification",
|
|
164
154
|
"pubsub_stats",
|
|
155
|
+
# Newer tools added post-v0.3
|
|
156
|
+
"brain_first_check",
|
|
157
|
+
"capauth_secret_get",
|
|
158
|
+
"capauth_status",
|
|
159
|
+
"capauth_verify",
|
|
160
|
+
"chat_history",
|
|
161
|
+
"chat_send",
|
|
162
|
+
"comm_notify",
|
|
163
|
+
"comm_status",
|
|
164
|
+
"deploy_status",
|
|
165
|
+
"did_identity_card",
|
|
166
|
+
"did_policy",
|
|
167
|
+
"did_publish",
|
|
168
|
+
"did_show",
|
|
169
|
+
"did_verify_peer",
|
|
170
|
+
"emotion_trend",
|
|
171
|
+
"gtd_capture",
|
|
172
|
+
"gtd_clarify",
|
|
173
|
+
"gtd_done",
|
|
174
|
+
"gtd_inbox",
|
|
175
|
+
"gtd_move",
|
|
176
|
+
"gtd_next",
|
|
177
|
+
"gtd_projects",
|
|
178
|
+
"gtd_review",
|
|
179
|
+
"gtd_status",
|
|
180
|
+
"gtd_waiting",
|
|
181
|
+
"itil_cab_vote",
|
|
182
|
+
"itil_change_propose",
|
|
183
|
+
"itil_change_update",
|
|
184
|
+
"itil_incident_create",
|
|
185
|
+
"itil_incident_list",
|
|
186
|
+
"itil_incident_update",
|
|
187
|
+
"itil_kedb_search",
|
|
188
|
+
"itil_problem_create",
|
|
189
|
+
"itil_problem_update",
|
|
190
|
+
"itil_status",
|
|
191
|
+
"run_ansible_playbook",
|
|
192
|
+
"security_audit_log",
|
|
193
|
+
"security_status",
|
|
194
|
+
"skstacks_secret_get",
|
|
195
|
+
"skstacks_secret_set",
|
|
196
|
+
"soul_registry_publish",
|
|
197
|
+
"soul_registry_search",
|
|
198
|
+
"telegram_catchup",
|
|
199
|
+
"telegram_chats",
|
|
200
|
+
"telegram_import",
|
|
201
|
+
"telegram_import_api",
|
|
202
|
+
"telegram_poll",
|
|
203
|
+
"telegram_send",
|
|
204
|
+
"telegram_setup",
|
|
205
|
+
"telegram_soul_swap",
|
|
206
|
+
"trust_febs",
|
|
207
|
+
"trust_rehydrate",
|
|
208
|
+
"trust_status",
|
|
209
|
+
"version_check",
|
|
165
210
|
}
|
|
166
|
-
assert names
|
|
211
|
+
assert core_expected.issubset(names)
|
|
167
212
|
|
|
168
213
|
@pytest.mark.asyncio
|
|
169
214
|
async def test_tool_schemas_valid(self):
|
|
@@ -709,7 +754,7 @@ class TestSKChatTools:
|
|
|
709
754
|
@pytest.mark.asyncio
|
|
710
755
|
async def test_skchat_send_requires_params(self):
|
|
711
756
|
"""skchat_send without recipient/message returns error."""
|
|
712
|
-
with patch("skcapstone.
|
|
757
|
+
with patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:test@local"):
|
|
713
758
|
result = await call_tool("skchat_send", {})
|
|
714
759
|
parsed = _extract_json(result)
|
|
715
760
|
assert "error" in parsed
|
|
@@ -717,7 +762,7 @@ class TestSKChatTools:
|
|
|
717
762
|
@pytest.mark.asyncio
|
|
718
763
|
async def test_skchat_send_requires_message(self):
|
|
719
764
|
"""skchat_send with only recipient returns error."""
|
|
720
|
-
with patch("skcapstone.
|
|
765
|
+
with patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:test@local"):
|
|
721
766
|
result = await call_tool("skchat_send", {"recipient": "lumina"})
|
|
722
767
|
parsed = _extract_json(result)
|
|
723
768
|
assert "error" in parsed
|
|
@@ -734,8 +779,8 @@ class TestSKChatTools:
|
|
|
734
779
|
})()
|
|
735
780
|
|
|
736
781
|
with (
|
|
737
|
-
patch("skcapstone.
|
|
738
|
-
patch("skcapstone.
|
|
782
|
+
patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"),
|
|
783
|
+
patch("skcapstone.mcp_server._resolve_recipient", return_value="capauth:lumina@local"),
|
|
739
784
|
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
740
785
|
):
|
|
741
786
|
result = await call_tool(
|
|
@@ -760,8 +805,8 @@ class TestSKChatTools:
|
|
|
760
805
|
mock_messenger = type("M", (), {"send": lambda self, **kw: capture_send(**kw)})()
|
|
761
806
|
|
|
762
807
|
with (
|
|
763
|
-
patch("skcapstone.
|
|
764
|
-
patch("skcapstone.
|
|
808
|
+
patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"),
|
|
809
|
+
patch("skcapstone.mcp_server._resolve_recipient", return_value="capauth:jarvis@local"),
|
|
765
810
|
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
766
811
|
):
|
|
767
812
|
result = await call_tool(
|
|
@@ -797,7 +842,7 @@ class TestSKChatTools:
|
|
|
797
842
|
})()
|
|
798
843
|
|
|
799
844
|
with (
|
|
800
|
-
patch("skcapstone.
|
|
845
|
+
patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"),
|
|
801
846
|
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
802
847
|
):
|
|
803
848
|
result = await call_tool("skchat_inbox", {})
|
|
@@ -830,7 +875,7 @@ class TestSKChatTools:
|
|
|
830
875
|
})()
|
|
831
876
|
|
|
832
877
|
with (
|
|
833
|
-
patch("skcapstone.
|
|
878
|
+
patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"),
|
|
834
879
|
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
835
880
|
):
|
|
836
881
|
result = await call_tool("skchat_inbox", {"limit": 10})
|
|
@@ -850,7 +895,7 @@ class TestSKChatTools:
|
|
|
850
895
|
})()
|
|
851
896
|
|
|
852
897
|
with (
|
|
853
|
-
patch("skcapstone.
|
|
898
|
+
patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"),
|
|
854
899
|
patch("skchat.agent_comm.AgentMessenger.from_identity", return_value=mock_messenger),
|
|
855
900
|
):
|
|
856
901
|
result = await call_tool("skchat_inbox", {"message_type": "finding"})
|
|
@@ -861,7 +906,7 @@ class TestSKChatTools:
|
|
|
861
906
|
@pytest.mark.asyncio
|
|
862
907
|
async def test_skchat_group_create_requires_name(self):
|
|
863
908
|
"""skchat_group_create without name returns error."""
|
|
864
|
-
with patch("skcapstone.
|
|
909
|
+
with patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"):
|
|
865
910
|
result = await call_tool("skchat_group_create", {})
|
|
866
911
|
parsed = _extract_json(result)
|
|
867
912
|
assert "error" in parsed
|
|
@@ -874,8 +919,8 @@ class TestSKChatTools:
|
|
|
874
919
|
})()
|
|
875
920
|
|
|
876
921
|
with (
|
|
877
|
-
patch("skcapstone.
|
|
878
|
-
patch("skcapstone.
|
|
922
|
+
patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"),
|
|
923
|
+
patch("skcapstone.mcp_server._get_skchat_history", return_value=mock_history),
|
|
879
924
|
):
|
|
880
925
|
result = await call_tool(
|
|
881
926
|
"skchat_group_create",
|
|
@@ -895,9 +940,9 @@ class TestSKChatTools:
|
|
|
895
940
|
})()
|
|
896
941
|
|
|
897
942
|
with (
|
|
898
|
-
patch("skcapstone.
|
|
899
|
-
patch("skcapstone.
|
|
900
|
-
patch("skcapstone.
|
|
943
|
+
patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"),
|
|
944
|
+
patch("skcapstone.mcp_server._get_skchat_history", return_value=mock_history),
|
|
945
|
+
patch("skcapstone.mcp_server._resolve_recipient", side_effect=lambda n: f"capauth:{n}@local"),
|
|
901
946
|
):
|
|
902
947
|
result = await call_tool(
|
|
903
948
|
"skchat_group_create",
|
|
@@ -922,7 +967,7 @@ class TestSKChatTools:
|
|
|
922
967
|
"get_thread": lambda self, gid: None,
|
|
923
968
|
})()
|
|
924
969
|
|
|
925
|
-
with patch("skcapstone.
|
|
970
|
+
with patch("skcapstone.mcp_server._get_skchat_history", return_value=mock_history):
|
|
926
971
|
result = await call_tool(
|
|
927
972
|
"skchat_group_send",
|
|
928
973
|
{"group_id": "nonexistent", "message": "Hello"},
|
|
@@ -938,7 +983,7 @@ class TestSKChatTools:
|
|
|
938
983
|
"get_thread": lambda self, gid: {"title": "Just a thread"},
|
|
939
984
|
})()
|
|
940
985
|
|
|
941
|
-
with patch("skcapstone.
|
|
986
|
+
with patch("skcapstone.mcp_server._get_skchat_history", return_value=mock_history):
|
|
942
987
|
result = await call_tool(
|
|
943
988
|
"skchat_group_send",
|
|
944
989
|
{"group_id": "thread-123", "message": "Hello"},
|
|
@@ -982,8 +1027,8 @@ class TestSKChatTools:
|
|
|
982
1027
|
})()
|
|
983
1028
|
|
|
984
1029
|
with (
|
|
985
|
-
patch("skcapstone.
|
|
986
|
-
patch("skcapstone.
|
|
1030
|
+
patch("skcapstone.mcp_server._get_skchat_identity", return_value="capauth:opus@local"),
|
|
1031
|
+
patch("skcapstone.mcp_server._get_skchat_history", return_value=mock_history),
|
|
987
1032
|
):
|
|
988
1033
|
result = await call_tool(
|
|
989
1034
|
"skchat_group_send",
|