@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Covers:
|
|
4
4
|
- Per-agent home directory resolution (opus → agents/opus/, jarvis → agents/jarvis/)
|
|
5
|
-
-
|
|
5
|
+
- Default daemon port behavior under the profile-agnostic runtime
|
|
6
6
|
- Default (no-agent) mode keeps backward-compatible home and port
|
|
7
7
|
- SKCAPSTONE_AGENT env var propagation
|
|
8
8
|
- DaemonConfig accepts distinct homes and ports for simultaneous agents
|
|
@@ -13,7 +13,6 @@ Covers:
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
import json
|
|
17
16
|
import os
|
|
18
17
|
from pathlib import Path
|
|
19
18
|
from unittest.mock import MagicMock, patch
|
|
@@ -84,17 +83,19 @@ class TestResolveAgentHome:
|
|
|
84
83
|
|
|
85
84
|
|
|
86
85
|
class TestResolveAgentPort:
|
|
87
|
-
def
|
|
88
|
-
"""
|
|
86
|
+
def test_known_agent_uses_registered_default_port(self):
|
|
87
|
+
"""Known agents use the registered default daemon port."""
|
|
88
|
+
from skcapstone import AGENT_PORTS, DEFAULT_PORT
|
|
89
89
|
from skcapstone.cli.daemon import _resolve_agent_port
|
|
90
90
|
|
|
91
|
-
assert _resolve_agent_port("opus", None) ==
|
|
91
|
+
assert _resolve_agent_port("opus", None) == AGENT_PORTS["opus"] == DEFAULT_PORT
|
|
92
92
|
|
|
93
|
-
def
|
|
94
|
-
"""
|
|
93
|
+
def test_second_known_agent_uses_registered_default_port(self):
|
|
94
|
+
"""Jarvis also uses the registered default daemon port."""
|
|
95
|
+
from skcapstone import AGENT_PORTS, DEFAULT_PORT
|
|
95
96
|
from skcapstone.cli.daemon import _resolve_agent_port
|
|
96
97
|
|
|
97
|
-
assert _resolve_agent_port("jarvis", None) ==
|
|
98
|
+
assert _resolve_agent_port("jarvis", None) == AGENT_PORTS["jarvis"] == DEFAULT_PORT
|
|
98
99
|
|
|
99
100
|
def test_explicit_port_overrides_agent_default(self):
|
|
100
101
|
"""Explicit --port always wins over the agent default."""
|
|
@@ -103,11 +104,12 @@ class TestResolveAgentPort:
|
|
|
103
104
|
assert _resolve_agent_port("opus", 9999) == 9999
|
|
104
105
|
assert _resolve_agent_port("jarvis", 8000) == 8000
|
|
105
106
|
|
|
106
|
-
def
|
|
107
|
-
"""Single-agent / no-flag mode uses
|
|
107
|
+
def test_no_agent_defaults_to_default_port(self):
|
|
108
|
+
"""Single-agent / no-flag mode uses the package default port."""
|
|
109
|
+
from skcapstone import DEFAULT_PORT
|
|
108
110
|
from skcapstone.cli.daemon import _resolve_agent_port
|
|
109
111
|
|
|
110
|
-
assert _resolve_agent_port(None, None) ==
|
|
112
|
+
assert _resolve_agent_port(None, None) == DEFAULT_PORT
|
|
111
113
|
|
|
112
114
|
def test_unknown_agent_gets_next_port(self):
|
|
113
115
|
"""An agent not in AGENT_PORTS gets max(ports)+1."""
|
|
@@ -118,11 +120,11 @@ class TestResolveAgentPort:
|
|
|
118
120
|
result = _resolve_agent_port("brandnew", None)
|
|
119
121
|
assert result == expected
|
|
120
122
|
|
|
121
|
-
def
|
|
122
|
-
"""
|
|
123
|
+
def test_explicit_ports_can_differ_for_isolated_agents(self):
|
|
124
|
+
"""Simultaneous agent daemons can still isolate by explicit port."""
|
|
123
125
|
from skcapstone.cli.daemon import _resolve_agent_port
|
|
124
126
|
|
|
125
|
-
assert _resolve_agent_port("opus",
|
|
127
|
+
assert _resolve_agent_port("opus", 7777) != _resolve_agent_port("jarvis", 7778)
|
|
126
128
|
|
|
127
129
|
|
|
128
130
|
# ---------------------------------------------------------------------------
|
|
@@ -132,22 +134,22 @@ class TestResolveAgentPort:
|
|
|
132
134
|
|
|
133
135
|
class TestAgentPortsRegistry:
|
|
134
136
|
def test_opus_registered(self):
|
|
135
|
-
from skcapstone import AGENT_PORTS
|
|
137
|
+
from skcapstone import AGENT_PORTS, DEFAULT_PORT
|
|
136
138
|
|
|
137
139
|
assert "opus" in AGENT_PORTS
|
|
138
|
-
assert AGENT_PORTS["opus"] ==
|
|
140
|
+
assert AGENT_PORTS["opus"] == DEFAULT_PORT
|
|
139
141
|
|
|
140
142
|
def test_jarvis_registered(self):
|
|
141
|
-
from skcapstone import AGENT_PORTS
|
|
143
|
+
from skcapstone import AGENT_PORTS, DEFAULT_PORT
|
|
142
144
|
|
|
143
145
|
assert "jarvis" in AGENT_PORTS
|
|
144
|
-
assert AGENT_PORTS["jarvis"] ==
|
|
146
|
+
assert AGENT_PORTS["jarvis"] == DEFAULT_PORT
|
|
145
147
|
|
|
146
|
-
def
|
|
148
|
+
def test_all_ports_are_ints(self):
|
|
147
149
|
from skcapstone import AGENT_PORTS
|
|
148
150
|
|
|
149
|
-
|
|
150
|
-
assert
|
|
151
|
+
assert AGENT_PORTS
|
|
152
|
+
assert all(isinstance(port, int) for port in AGENT_PORTS.values())
|
|
151
153
|
|
|
152
154
|
|
|
153
155
|
# ---------------------------------------------------------------------------
|
|
@@ -220,8 +222,7 @@ class TestDaemonConfigMultiAgent:
|
|
|
220
222
|
|
|
221
223
|
assert opus_cfg.home != jarvis_cfg.home
|
|
222
224
|
assert opus_cfg.port != jarvis_cfg.port
|
|
223
|
-
|
|
224
|
-
assert jarvis_cfg.port == 7778
|
|
225
|
+
|
|
225
226
|
|
|
226
227
|
def test_log_files_are_in_respective_homes(self, tmp_path: Path):
|
|
227
228
|
"""Each agent's log file lives under its own home."""
|
|
@@ -244,24 +245,25 @@ class TestDaemonConfigMultiAgent:
|
|
|
244
245
|
|
|
245
246
|
|
|
246
247
|
class TestAgentHomeEnvVar:
|
|
247
|
-
def
|
|
248
|
-
"""SKCAPSTONE_AGENT
|
|
248
|
+
def test_env_var_keeps_shared_root_and_agent_home_resolves_subdir(self, monkeypatch):
|
|
249
|
+
"""SKCAPSTONE_AGENT keeps AGENT_HOME at root and agent_home() resolves the agent subdir."""
|
|
249
250
|
import importlib
|
|
250
251
|
|
|
251
252
|
monkeypatch.setenv("SKCAPSTONE_AGENT", "opus")
|
|
252
|
-
monkeypatch.setenv("
|
|
253
|
+
monkeypatch.setenv("SKCAPSTONE_HOME", "/tmp/sk")
|
|
253
254
|
|
|
254
255
|
import skcapstone as pkg
|
|
255
256
|
importlib.reload(pkg)
|
|
256
257
|
|
|
257
|
-
assert
|
|
258
|
+
assert pkg.AGENT_HOME == "/tmp/sk"
|
|
259
|
+
assert "agents/opus" in str(pkg.agent_home("opus")) or "agents\\opus" in str(pkg.agent_home("opus"))
|
|
258
260
|
|
|
259
261
|
def test_no_env_var_uses_root_directly(self, monkeypatch):
|
|
260
|
-
"""Without SKCAPSTONE_AGENT, AGENT_HOME
|
|
262
|
+
"""Without SKCAPSTONE_AGENT, AGENT_HOME stays at the shared root."""
|
|
261
263
|
import importlib
|
|
262
264
|
|
|
263
265
|
monkeypatch.delenv("SKCAPSTONE_AGENT", raising=False)
|
|
264
|
-
monkeypatch.setenv("
|
|
266
|
+
monkeypatch.setenv("SKCAPSTONE_HOME", "/tmp/sk")
|
|
265
267
|
|
|
266
268
|
import skcapstone as pkg
|
|
267
269
|
importlib.reload(pkg)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Tests for human-operator manifest linking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from skcapstone.operator_link import build_agent_manifest, discover_human_operator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_discover_human_operator_reads_capauth_profile(tmp_path: Path) -> None:
|
|
12
|
+
"""A CapAuth human profile is converted into operator metadata."""
|
|
13
|
+
capauth_home = tmp_path / ".capauth"
|
|
14
|
+
profile = capauth_home / "identity" / "profile.json"
|
|
15
|
+
profile.parent.mkdir(parents=True)
|
|
16
|
+
profile.write_text(
|
|
17
|
+
json.dumps(
|
|
18
|
+
{
|
|
19
|
+
"entity": {
|
|
20
|
+
"name": "Casey",
|
|
21
|
+
"entity_type": "human",
|
|
22
|
+
"email": "casey@example.com",
|
|
23
|
+
"handle": "casey@example.com",
|
|
24
|
+
},
|
|
25
|
+
"key_info": {
|
|
26
|
+
"fingerprint": "ABCDEF1234567890",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
),
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
operator = discover_human_operator(capauth_home)
|
|
34
|
+
|
|
35
|
+
assert operator == {
|
|
36
|
+
"name": "Casey",
|
|
37
|
+
"relationship": "human-operator",
|
|
38
|
+
"entity_type": "human",
|
|
39
|
+
"source": "capauth",
|
|
40
|
+
"email": "casey@example.com",
|
|
41
|
+
"handle": "casey@example.com",
|
|
42
|
+
"fingerprint": "ABCDEF1234567890",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_discover_human_operator_ignores_non_human_profile(tmp_path: Path) -> None:
|
|
47
|
+
"""AI CapAuth profiles are not treated as human operators."""
|
|
48
|
+
capauth_home = tmp_path / ".capauth"
|
|
49
|
+
profile = capauth_home / "identity" / "profile.json"
|
|
50
|
+
profile.parent.mkdir(parents=True)
|
|
51
|
+
profile.write_text(
|
|
52
|
+
json.dumps(
|
|
53
|
+
{
|
|
54
|
+
"entity": {
|
|
55
|
+
"name": "Jarvis",
|
|
56
|
+
"entity_type": "ai",
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
),
|
|
60
|
+
encoding="utf-8",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert discover_human_operator(capauth_home) is None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_build_agent_manifest_includes_operator_when_available() -> None:
|
|
67
|
+
"""Operator metadata is persisted directly in the manifest."""
|
|
68
|
+
manifest = build_agent_manifest(
|
|
69
|
+
"jarvis",
|
|
70
|
+
"0.6.0",
|
|
71
|
+
created_at="2026-01-01T00:00:00+00:00",
|
|
72
|
+
operator={"name": "Casey", "fingerprint": "FP123", "relationship": "human-operator"},
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert manifest["name"] == "jarvis"
|
|
76
|
+
assert manifest["entity_type"] == "ai-agent"
|
|
77
|
+
assert manifest["operator"]["name"] == "Casey"
|
|
78
|
+
assert manifest["operator"]["fingerprint"] == "FP123"
|
package/tests/test_runtime.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
7
8
|
from skcapstone.runtime import AgentRuntime
|
|
@@ -28,6 +29,26 @@ class TestAgentRuntime:
|
|
|
28
29
|
assert manifest.name == "test-agent"
|
|
29
30
|
assert manifest.last_awakened is not None
|
|
30
31
|
|
|
32
|
+
def test_awaken_prefers_identity_name_over_shared_config(self, tmp_path: Path):
|
|
33
|
+
"""Agent-local identity should beat a shared-root fallback config name."""
|
|
34
|
+
shared_root = tmp_path / ".skcapstone"
|
|
35
|
+
agent_home = shared_root / "agents" / "aster"
|
|
36
|
+
(agent_home / "identity").mkdir(parents=True)
|
|
37
|
+
(agent_home / "config").mkdir(parents=True)
|
|
38
|
+
|
|
39
|
+
(shared_root / "config").mkdir(parents=True)
|
|
40
|
+
(shared_root / "config" / "config.yaml").write_text("agent_name: Jarvis\n")
|
|
41
|
+
(agent_home / "manifest.json").write_text(json.dumps({"name": "aster"}))
|
|
42
|
+
(agent_home / "identity" / "identity.json").write_text(json.dumps({
|
|
43
|
+
"name": "Aster",
|
|
44
|
+
"fingerprint": "A" * 40,
|
|
45
|
+
"capauth_managed": True,
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
runtime = AgentRuntime(home=agent_home)
|
|
49
|
+
manifest = runtime.awaken()
|
|
50
|
+
assert manifest.name == "Aster"
|
|
51
|
+
|
|
31
52
|
def test_register_connector(self, initialized_agent_home: Path):
|
|
32
53
|
"""Registering a connector should persist it."""
|
|
33
54
|
runtime = AgentRuntime(home=initialized_agent_home)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Tests for the native SKCapstone session briefing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from click.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
from skcapstone.cli import main
|
|
12
|
+
from skcapstone.session_briefing import (
|
|
13
|
+
build_session_briefing,
|
|
14
|
+
format_session_briefing_text,
|
|
15
|
+
load_hammertime_briefing,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_load_hammertime_briefing_respects_disable_env(monkeypatch, tmp_path: Path) -> None:
|
|
20
|
+
"""It returns None when HammerTime briefing is explicitly disabled."""
|
|
21
|
+
monkeypatch.setenv("SK_INCLUDE_HAMMERTIME_BRIEFING", "0")
|
|
22
|
+
assert load_hammertime_briefing(root=tmp_path) is None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_load_hammertime_briefing_parses_json(monkeypatch, tmp_path: Path) -> None:
|
|
26
|
+
"""It parses JSON output from the HammerTime briefing script."""
|
|
27
|
+
script = tmp_path / "scripts" / "case-briefing.py"
|
|
28
|
+
script.parent.mkdir(parents=True)
|
|
29
|
+
script.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
|
30
|
+
|
|
31
|
+
payload = {"summary": {"queue_size": 2}, "alert_count": 1}
|
|
32
|
+
|
|
33
|
+
def fake_run(*args, **kwargs): # noqa: ANN002, ANN003
|
|
34
|
+
return subprocess.CompletedProcess(
|
|
35
|
+
args=args[0],
|
|
36
|
+
returncode=0,
|
|
37
|
+
stdout=json.dumps(payload),
|
|
38
|
+
stderr="",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
monkeypatch.delenv("SK_INCLUDE_HAMMERTIME_BRIEFING", raising=False)
|
|
42
|
+
monkeypatch.setattr("skcapstone.session_briefing.subprocess.run", fake_run)
|
|
43
|
+
|
|
44
|
+
assert load_hammertime_briefing(root=tmp_path) == payload
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_build_session_briefing_includes_skcapstone_and_hammertime(monkeypatch, tmp_path: Path) -> None:
|
|
48
|
+
"""It builds a combined payload for startup consumers."""
|
|
49
|
+
ctx = {"agent": {"name": "Aster"}, "memories": []}
|
|
50
|
+
briefing = {"summary": {"queue_size": 1}, "alert_count": 0}
|
|
51
|
+
|
|
52
|
+
monkeypatch.setattr("skcapstone.session_briefing.gather_context", lambda home, memory_limit=10: ctx)
|
|
53
|
+
monkeypatch.setattr(
|
|
54
|
+
"skcapstone.session_briefing.load_hammertime_briefing",
|
|
55
|
+
lambda python_bin=None: briefing,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
payload = build_session_briefing(tmp_path, memory_limit=3)
|
|
59
|
+
|
|
60
|
+
assert payload["agent_home"] == str(tmp_path)
|
|
61
|
+
assert payload["skcapstone_context"] == ctx
|
|
62
|
+
assert payload["hammertime_briefing"] == briefing
|
|
63
|
+
assert "generated_at" in payload
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_format_session_briefing_text_contains_hammertime_section() -> None:
|
|
67
|
+
"""It renders a readable summary including the HammerTime section."""
|
|
68
|
+
payload = {
|
|
69
|
+
"generated_at": "2026-04-09T00:00:00+00:00",
|
|
70
|
+
"agent_home": "/tmp/aster",
|
|
71
|
+
"skcapstone_context": {
|
|
72
|
+
"agent": {"name": "Aster", "is_conscious": True, "fingerprint": "abc123"},
|
|
73
|
+
"pillars": {},
|
|
74
|
+
"board": {"total": 0},
|
|
75
|
+
"memories": [],
|
|
76
|
+
"soul": {"active": None},
|
|
77
|
+
"mcp": {"available": False},
|
|
78
|
+
"gathered_at": "2026-04-09T00:00:00+00:00",
|
|
79
|
+
},
|
|
80
|
+
"hammertime_briefing": {
|
|
81
|
+
"alert_count": 1,
|
|
82
|
+
"summary": {"queue_size": 2},
|
|
83
|
+
"top_priority": {
|
|
84
|
+
"incident_id": "INC-001",
|
|
85
|
+
"problem_slug": "example-problem",
|
|
86
|
+
"action": "File claim of exemption",
|
|
87
|
+
"status": "in-progress",
|
|
88
|
+
},
|
|
89
|
+
"focus_items": [
|
|
90
|
+
{
|
|
91
|
+
"incident_id": "INC-001",
|
|
92
|
+
"action": "Review preferred filing",
|
|
93
|
+
"status": "in-progress",
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
output = format_session_briefing_text(payload)
|
|
100
|
+
|
|
101
|
+
assert "# SKCapstone Session Briefing" in output
|
|
102
|
+
assert "## hammertime briefing" in output
|
|
103
|
+
assert "INC-001" in output
|
|
104
|
+
assert "File claim of exemption" in output
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_session_briefing_cli_json(monkeypatch, tmp_path: Path) -> None:
|
|
108
|
+
"""The CLI exposes the combined payload as JSON."""
|
|
109
|
+
runner = CliRunner()
|
|
110
|
+
payload = {
|
|
111
|
+
"generated_at": "2026-04-09T00:00:00+00:00",
|
|
112
|
+
"agent_home": str(tmp_path),
|
|
113
|
+
"skcapstone_context": {"agent": {"name": "Aster"}},
|
|
114
|
+
"hammertime_briefing": {"summary": {"queue_size": 1}, "alert_count": 0},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
monkeypatch.setattr(
|
|
118
|
+
"skcapstone.session_briefing.build_session_briefing",
|
|
119
|
+
lambda home, memory_limit=10: payload,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
result = runner.invoke(
|
|
123
|
+
main,
|
|
124
|
+
["session", "briefing", "--home", str(tmp_path), "--format", "json"],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assert result.exit_code == 0
|
|
128
|
+
parsed = json.loads(result.output)
|
|
129
|
+
assert parsed["skcapstone_context"]["agent"]["name"] == "Aster"
|
|
130
|
+
assert parsed["hammertime_briefing"]["summary"]["queue_size"] == 1
|
|
@@ -121,6 +121,24 @@ class TestBuildGraph:
|
|
|
121
121
|
sync_edges = [e for e in graph.edges if e.edge_type == "sync"]
|
|
122
122
|
assert len(sync_edges) >= 1
|
|
123
123
|
|
|
124
|
+
def test_manifest_operator_creates_human_link(self, tmp_agent_home: Path):
|
|
125
|
+
"""Manifest operator metadata appears as an explicit trust relationship."""
|
|
126
|
+
_init_agent(tmp_agent_home, "operator-graph")
|
|
127
|
+
manifest_path = tmp_agent_home / "manifest.json"
|
|
128
|
+
manifest = json.loads(manifest_path.read_text())
|
|
129
|
+
manifest["operator"] = {
|
|
130
|
+
"name": "Casey",
|
|
131
|
+
"fingerprint": "FP1234567890",
|
|
132
|
+
"relationship": "human-operator",
|
|
133
|
+
"entity_type": "human",
|
|
134
|
+
}
|
|
135
|
+
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
136
|
+
|
|
137
|
+
graph = build_trust_graph(tmp_agent_home)
|
|
138
|
+
|
|
139
|
+
assert any(n.label == "Casey" for n in graph.nodes)
|
|
140
|
+
assert any(e.edge_type == "operator" and e.label == "human-operator" for e in graph.edges)
|
|
141
|
+
|
|
124
142
|
|
|
125
143
|
class TestFormatDot:
|
|
126
144
|
"""Tests for DOT format output."""
|