@smilintux/skcapstone 0.6.2 → 0.6.3
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 +1 -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
|
@@ -59,7 +59,9 @@ def _store_notification_memory(title: str, body: str, urgency: str) -> None:
|
|
|
59
59
|
if not home.exists():
|
|
60
60
|
return
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
from . import active_agent_name
|
|
63
|
+
|
|
64
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT") or active_agent_name()
|
|
63
65
|
notif_dir = home / "agents" / agent_name / "skcomm" / "notifications"
|
|
64
66
|
notif_dir.mkdir(parents=True, exist_ok=True)
|
|
65
67
|
|
|
@@ -89,7 +91,9 @@ def _store_click_event(action: str, detail: str) -> None:
|
|
|
89
91
|
if not home.exists():
|
|
90
92
|
return
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
from . import active_agent_name
|
|
95
|
+
|
|
96
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT") or active_agent_name()
|
|
93
97
|
notif_dir = home / "agents" / agent_name / "skcomm" / "notifications"
|
|
94
98
|
notif_dir.mkdir(parents=True, exist_ok=True)
|
|
95
99
|
|
|
@@ -93,6 +93,7 @@ def _step_identity(home_path: Path, name: str, email: str | None) -> tuple[str,
|
|
|
93
93
|
from .pillars.memory import initialize_memory
|
|
94
94
|
from .pillars.sync import initialize_sync
|
|
95
95
|
from .models import AgentConfig, SyncConfig
|
|
96
|
+
from .operator_link import build_agent_manifest, discover_human_operator
|
|
96
97
|
from .soul import SoulManager
|
|
97
98
|
|
|
98
99
|
with Status(" Generating PGP identity…", console=console, spinner="dots") as s:
|
|
@@ -158,12 +159,11 @@ def _step_identity(home_path: Path, name: str, email: str | None) -> tuple[str,
|
|
|
158
159
|
for d in skeleton_dirs:
|
|
159
160
|
d.mkdir(parents=True, exist_ok=True)
|
|
160
161
|
|
|
161
|
-
manifest =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
162
|
+
manifest = build_agent_manifest(
|
|
163
|
+
name,
|
|
164
|
+
__version__,
|
|
165
|
+
operator=discover_human_operator(),
|
|
166
|
+
)
|
|
167
167
|
(home_path / "manifest.json").write_text(
|
|
168
168
|
json.dumps(manifest, indent=2), encoding="utf-8"
|
|
169
169
|
)
|
|
@@ -1652,7 +1652,7 @@ def run_onboard(home: Optional[str] = None) -> None:
|
|
|
1652
1652
|
# -----------------------------------------------------------------------
|
|
1653
1653
|
# Write global CLAUDE.md and register Claude Code hooks
|
|
1654
1654
|
# -----------------------------------------------------------------------
|
|
1655
|
-
# Write global CLAUDE.md
|
|
1655
|
+
# Write global CLAUDE.md from bundled skeleton template
|
|
1656
1656
|
try:
|
|
1657
1657
|
from .cli.setup import _write_global_claude_md
|
|
1658
1658
|
_write_global_claude_md(home_path, name)
|
|
@@ -1660,7 +1660,18 @@ def run_onboard(home: Optional[str] = None) -> None:
|
|
|
1660
1660
|
except Exception as exc:
|
|
1661
1661
|
_warn(f"Could not write CLAUDE.md: {exc}")
|
|
1662
1662
|
|
|
1663
|
-
#
|
|
1663
|
+
# Write ~/.claude/settings.json with SK hooks (merge with existing)
|
|
1664
|
+
try:
|
|
1665
|
+
from .cli.setup import _write_claude_settings
|
|
1666
|
+
settings_path = _write_claude_settings(merge=True)
|
|
1667
|
+
if settings_path:
|
|
1668
|
+
_ok(f"~/.claude/settings.json updated ({settings_path})")
|
|
1669
|
+
else:
|
|
1670
|
+
_info("claude settings: skipped (skmemory not installed or template missing)")
|
|
1671
|
+
except Exception as exc:
|
|
1672
|
+
_warn(f"Could not write claude settings: {exc}")
|
|
1673
|
+
|
|
1674
|
+
# Register Claude Code hooks via skmemory (adds any hooks not yet in settings.json)
|
|
1664
1675
|
try:
|
|
1665
1676
|
from skmemory.register import register_hooks
|
|
1666
1677
|
actions = register_hooks()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Human-operator link helpers for manifests and identity attestations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def discover_human_operator(capauth_home: Path | None = None) -> dict[str, str] | None:
|
|
12
|
+
"""Return the active human operator from the local CapAuth profile.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
capauth_home: Optional CapAuth home directory. Defaults to ``~/.capauth``.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
A compact operator mapping, or ``None`` if no human profile is available.
|
|
19
|
+
"""
|
|
20
|
+
base = Path(capauth_home).expanduser() if capauth_home else _resolve_operator_home()
|
|
21
|
+
profile_path = base / "identity" / "profile.json"
|
|
22
|
+
if not profile_path.exists():
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
data = json.loads(profile_path.read_text(encoding="utf-8"))
|
|
27
|
+
except (json.JSONDecodeError, OSError):
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
entity = data.get("entity", {})
|
|
31
|
+
key_info = data.get("key_info", {})
|
|
32
|
+
entity_type = str(entity.get("entity_type", "")).lower()
|
|
33
|
+
if entity_type not in {"human", "entitytype.human"}:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
name = entity.get("name", "").strip()
|
|
37
|
+
if not name:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
operator = {
|
|
41
|
+
"name": name,
|
|
42
|
+
"relationship": "human-operator",
|
|
43
|
+
"entity_type": "human",
|
|
44
|
+
"source": "capauth",
|
|
45
|
+
}
|
|
46
|
+
if entity.get("email"):
|
|
47
|
+
operator["email"] = entity["email"]
|
|
48
|
+
if entity.get("handle"):
|
|
49
|
+
operator["handle"] = entity["handle"]
|
|
50
|
+
if key_info.get("fingerprint"):
|
|
51
|
+
operator["fingerprint"] = key_info["fingerprint"]
|
|
52
|
+
return operator
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_agent_manifest(
|
|
56
|
+
name: str,
|
|
57
|
+
version: str,
|
|
58
|
+
*,
|
|
59
|
+
created_at: str | None = None,
|
|
60
|
+
connectors: list[str] | None = None,
|
|
61
|
+
operator: dict[str, str] | None = None,
|
|
62
|
+
entity_type: str = "ai-agent",
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
"""Build a standard manifest for a sovereign agent."""
|
|
65
|
+
manifest: dict[str, Any] = {
|
|
66
|
+
"name": name,
|
|
67
|
+
"version": version,
|
|
68
|
+
"entity_type": entity_type,
|
|
69
|
+
"created_at": created_at or datetime.now(timezone.utc).isoformat(),
|
|
70
|
+
"connectors": connectors or [],
|
|
71
|
+
}
|
|
72
|
+
if operator:
|
|
73
|
+
manifest["operator"] = operator
|
|
74
|
+
return manifest
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def create_operator_attestation(
|
|
78
|
+
agent_name: str,
|
|
79
|
+
agent_fingerprint: str,
|
|
80
|
+
agent_public_key_path: Path,
|
|
81
|
+
output_dir: Path,
|
|
82
|
+
*,
|
|
83
|
+
capauth_home: Path | None = None,
|
|
84
|
+
) -> dict[str, Any] | None:
|
|
85
|
+
"""Create a signed attestation linking a human operator to an agent key.
|
|
86
|
+
|
|
87
|
+
The operator remains distinct from the agent identity. This produces a
|
|
88
|
+
signed claim that the human operator vouches for the agent fingerprint.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
agent_name: Agent display name.
|
|
92
|
+
agent_fingerprint: Agent PGP fingerprint.
|
|
93
|
+
agent_public_key_path: Path to the agent public key armor.
|
|
94
|
+
output_dir: Directory where the attestation JSON should be written.
|
|
95
|
+
capauth_home: Optional CapAuth home for the human operator.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The attestation mapping, or ``None`` if no human operator profile is
|
|
99
|
+
available or signing failed.
|
|
100
|
+
"""
|
|
101
|
+
base = Path(capauth_home).expanduser() if capauth_home else _resolve_operator_home()
|
|
102
|
+
profile_path = base / "identity" / "profile.json"
|
|
103
|
+
private_key_path = base / "identity" / "private.asc"
|
|
104
|
+
public_key_path = base / "identity" / "public.asc"
|
|
105
|
+
if not profile_path.exists() or not private_key_path.exists() or not public_key_path.exists():
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
from capauth.crypto import get_backend # type: ignore[import-untyped]
|
|
110
|
+
from capauth.profile import load_profile # type: ignore[import-untyped]
|
|
111
|
+
except ImportError:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
profile = load_profile(base_dir=base)
|
|
116
|
+
except Exception:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
entity_type = str(profile.entity.entity_type).lower()
|
|
120
|
+
if entity_type not in {"human", "entitytype.human"}:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
payload = {
|
|
125
|
+
"agent_name": agent_name,
|
|
126
|
+
"agent_fingerprint": agent_fingerprint,
|
|
127
|
+
"agent_public_key_path": str(agent_public_key_path),
|
|
128
|
+
"relationship": "human-operator",
|
|
129
|
+
"operator_name": profile.entity.name,
|
|
130
|
+
"operator_email": profile.entity.email,
|
|
131
|
+
"operator_handle": profile.entity.handle,
|
|
132
|
+
"operator_fingerprint": profile.key_info.fingerprint,
|
|
133
|
+
"signed_at": datetime.now(timezone.utc).isoformat(),
|
|
134
|
+
}
|
|
135
|
+
payload_bytes = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
|
|
136
|
+
private_armor = private_key_path.read_text(encoding="utf-8")
|
|
137
|
+
operator_public_armor = public_key_path.read_text(encoding="utf-8")
|
|
138
|
+
backend = get_backend(profile.crypto_backend)
|
|
139
|
+
signature = backend.sign(payload_bytes, private_armor, "")
|
|
140
|
+
except Exception:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
attestation = {
|
|
144
|
+
"payload": payload,
|
|
145
|
+
"signature": signature,
|
|
146
|
+
"operator_public_key_path": str(public_key_path),
|
|
147
|
+
"operator_public_key_armor": operator_public_armor,
|
|
148
|
+
}
|
|
149
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
(output_dir / "operator-attestation.json").write_text(
|
|
151
|
+
json.dumps(attestation, indent=2),
|
|
152
|
+
encoding="utf-8",
|
|
153
|
+
)
|
|
154
|
+
return attestation
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _resolve_operator_home() -> Path:
|
|
158
|
+
"""Resolve the human operator's CapAuth home."""
|
|
159
|
+
try:
|
|
160
|
+
from capauth import resolve_capauth_home # type: ignore[import-untyped]
|
|
161
|
+
|
|
162
|
+
return resolve_capauth_home()
|
|
163
|
+
except Exception:
|
|
164
|
+
return Path.home() / ".skcapstone" / "capauth"
|
|
@@ -15,6 +15,7 @@ import os
|
|
|
15
15
|
from datetime import datetime, timezone
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
+
from .. import active_agent_name
|
|
18
19
|
from ..models import ConsciousnessState, PillarStatus
|
|
19
20
|
|
|
20
21
|
|
|
@@ -27,7 +28,7 @@ def initialize_consciousness(home: Path) -> ConsciousnessState:
|
|
|
27
28
|
Returns:
|
|
28
29
|
ConsciousnessState with current status.
|
|
29
30
|
"""
|
|
30
|
-
agent_name = os.environ.get("SKCAPSTONE_AGENT"
|
|
31
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT") or active_agent_name() or ""
|
|
31
32
|
# home may be the agent dir (~/.skcapstone/agents/jarvis/) or the
|
|
32
33
|
# shared root (~/.skcapstone/). Check for skwhisper/ directly first.
|
|
33
34
|
whisper_dir = home / "skwhisper"
|
|
@@ -9,16 +9,22 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
-
import subprocess
|
|
13
12
|
from datetime import datetime, timezone
|
|
13
|
+
from shutil import copyfile
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Optional
|
|
16
16
|
|
|
17
17
|
from ..models import IdentityState, PillarStatus
|
|
18
|
+
from ..operator_link import create_operator_attestation
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger("skcapstone.identity")
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _capauth_home(home: Path) -> Path:
|
|
24
|
+
"""Return the agent-local CapAuth home for an SKCapstone agent."""
|
|
25
|
+
return home / "capauth"
|
|
26
|
+
|
|
27
|
+
|
|
22
28
|
def generate_identity(
|
|
23
29
|
home: Path,
|
|
24
30
|
name: str,
|
|
@@ -47,10 +53,14 @@ def generate_identity(
|
|
|
47
53
|
status=PillarStatus.DEGRADED,
|
|
48
54
|
)
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
capauth_home = _capauth_home(home)
|
|
57
|
+
capauth_state = _try_init_capauth(name, state.email, identity_dir, capauth_home)
|
|
51
58
|
if capauth_state is not None:
|
|
52
59
|
state.fingerprint = capauth_state.fingerprint
|
|
53
60
|
state.key_path = capauth_state.key_path
|
|
61
|
+
state.name = capauth_state.name
|
|
62
|
+
state.email = capauth_state.email
|
|
63
|
+
state.created_at = capauth_state.created_at
|
|
54
64
|
state.status = PillarStatus.ACTIVE
|
|
55
65
|
else:
|
|
56
66
|
state.fingerprint = _generate_placeholder_fingerprint(name)
|
|
@@ -63,18 +73,39 @@ def generate_identity(
|
|
|
63
73
|
"created_at": state.created_at.isoformat() if state.created_at else None,
|
|
64
74
|
"capauth_managed": state.status == PillarStatus.ACTIVE,
|
|
65
75
|
}
|
|
76
|
+
if state.key_path is not None:
|
|
77
|
+
identity_manifest["public_key_path"] = str(state.key_path)
|
|
78
|
+
if state.status == PillarStatus.ACTIVE:
|
|
79
|
+
identity_manifest["capauth_home"] = str(capauth_home)
|
|
80
|
+
|
|
81
|
+
attestation = create_operator_attestation(
|
|
82
|
+
agent_name=state.name or name,
|
|
83
|
+
agent_fingerprint=state.fingerprint or "",
|
|
84
|
+
agent_public_key_path=state.key_path or (capauth_home / "identity" / "public.asc"),
|
|
85
|
+
output_dir=identity_dir,
|
|
86
|
+
)
|
|
87
|
+
if attestation is not None:
|
|
88
|
+
payload = attestation.get("payload", {})
|
|
89
|
+
identity_manifest["operator_attestation_path"] = str(
|
|
90
|
+
identity_dir / "operator-attestation.json"
|
|
91
|
+
)
|
|
92
|
+
identity_manifest["operator_attested_by"] = payload.get("operator_fingerprint")
|
|
93
|
+
|
|
66
94
|
(identity_dir / "identity.json").write_text(json.dumps(identity_manifest, indent=2), encoding="utf-8")
|
|
67
95
|
|
|
68
96
|
return state
|
|
69
97
|
|
|
70
98
|
|
|
71
99
|
def _try_init_capauth(
|
|
72
|
-
name: str,
|
|
100
|
+
name: str,
|
|
101
|
+
email: str,
|
|
102
|
+
identity_dir: Path,
|
|
103
|
+
capauth_home: Path,
|
|
73
104
|
) -> Optional[IdentityState]:
|
|
74
105
|
"""Try to create or load a real CapAuth identity.
|
|
75
106
|
|
|
76
107
|
Attempts (in order):
|
|
77
|
-
1. Load an existing CapAuth profile from
|
|
108
|
+
1. Load an existing CapAuth profile from the agent-local CapAuth home
|
|
78
109
|
2. Create a new profile via capauth.profile.init_profile()
|
|
79
110
|
3. Fall back to legacy capauth.keys.generate_keypair()
|
|
80
111
|
|
|
@@ -90,12 +121,17 @@ def _try_init_capauth(
|
|
|
90
121
|
try:
|
|
91
122
|
from capauth.profile import load_profile # type: ignore[import-untyped]
|
|
92
123
|
|
|
93
|
-
profile = load_profile()
|
|
124
|
+
profile = load_profile(base_dir=capauth_home)
|
|
125
|
+
key_path = Path(profile.key_info.public_key_path)
|
|
126
|
+
legacy_key_path = identity_dir / "agent.pub"
|
|
127
|
+
if key_path.exists() and not legacy_key_path.exists():
|
|
128
|
+
copyfile(key_path, legacy_key_path)
|
|
94
129
|
return IdentityState(
|
|
95
130
|
fingerprint=profile.key_info.fingerprint,
|
|
96
|
-
key_path=
|
|
131
|
+
key_path=key_path,
|
|
97
132
|
name=profile.entity.name,
|
|
98
133
|
email=profile.entity.email,
|
|
134
|
+
created_at=profile.key_info.created,
|
|
99
135
|
status=PillarStatus.ACTIVE,
|
|
100
136
|
)
|
|
101
137
|
except ImportError:
|
|
@@ -105,18 +141,26 @@ def _try_init_capauth(
|
|
|
105
141
|
|
|
106
142
|
# No existing profile — try creating one
|
|
107
143
|
try:
|
|
144
|
+
from capauth.models import EntityType # type: ignore[import-untyped]
|
|
108
145
|
from capauth.profile import init_profile # type: ignore[import-untyped]
|
|
109
146
|
|
|
110
147
|
profile = init_profile(
|
|
111
148
|
name=name,
|
|
112
149
|
email=email,
|
|
113
150
|
passphrase="",
|
|
151
|
+
entity_type=EntityType.AI,
|
|
152
|
+
base_dir=capauth_home,
|
|
114
153
|
)
|
|
154
|
+
key_path = Path(profile.key_info.public_key_path)
|
|
155
|
+
legacy_key_path = identity_dir / "agent.pub"
|
|
156
|
+
if key_path.exists():
|
|
157
|
+
copyfile(key_path, legacy_key_path)
|
|
115
158
|
return IdentityState(
|
|
116
159
|
fingerprint=profile.key_info.fingerprint,
|
|
117
|
-
key_path=
|
|
160
|
+
key_path=key_path,
|
|
118
161
|
name=profile.entity.name,
|
|
119
162
|
email=profile.entity.email,
|
|
163
|
+
created_at=profile.key_info.created,
|
|
120
164
|
status=PillarStatus.ACTIVE,
|
|
121
165
|
)
|
|
122
166
|
except Exception as exc:
|
|
@@ -11,9 +11,11 @@ store/search/recall/list/gc capabilities. The optional external
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import os
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Optional
|
|
16
17
|
|
|
18
|
+
from .. import active_agent_name
|
|
17
19
|
from ..models import MemoryLayer, MemoryState, PillarStatus
|
|
18
20
|
|
|
19
21
|
|
|
@@ -31,9 +33,13 @@ def initialize_memory(home: Path, memory_home: Optional[Path] = None) -> MemoryS
|
|
|
31
33
|
Returns:
|
|
32
34
|
MemoryState after initialization.
|
|
33
35
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT") or active_agent_name()
|
|
37
|
+
if home.parent.name == "agents":
|
|
38
|
+
memory_dir = home / "memory"
|
|
39
|
+
elif agent_name:
|
|
40
|
+
memory_dir = home / "agents" / agent_name / "memory"
|
|
41
|
+
else:
|
|
42
|
+
memory_dir = home / "memory"
|
|
37
43
|
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
38
44
|
|
|
39
45
|
for layer in MemoryLayer:
|
|
@@ -94,10 +94,14 @@ class AgentRuntime:
|
|
|
94
94
|
logger.info("Awakening agent from %s (shared: %s)", self.home, self.shared_root)
|
|
95
95
|
|
|
96
96
|
manifest_file = self.home / "manifest.json"
|
|
97
|
+
manifest_name_loaded = False
|
|
97
98
|
if manifest_file.exists():
|
|
98
99
|
try:
|
|
99
100
|
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
100
|
-
|
|
101
|
+
manifest_name = data.get("name")
|
|
102
|
+
if manifest_name:
|
|
103
|
+
self.manifest.name = manifest_name
|
|
104
|
+
manifest_name_loaded = True
|
|
101
105
|
if data.get("created_at"):
|
|
102
106
|
self.manifest.created_at = datetime.fromisoformat(data["created_at"])
|
|
103
107
|
connectors_data = data.get("connectors", [])
|
|
@@ -105,8 +109,6 @@ class AgentRuntime:
|
|
|
105
109
|
except (json.JSONDecodeError, ValueError) as exc:
|
|
106
110
|
logger.warning("Failed to load manifest: %s", exc)
|
|
107
111
|
|
|
108
|
-
self.manifest.name = self.config.agent_name
|
|
109
|
-
|
|
110
112
|
# Discover pillars from per-agent home
|
|
111
113
|
pillars = discover_all(self.home, shared_root=self.shared_root)
|
|
112
114
|
self.manifest.identity = pillars["identity"]
|
|
@@ -117,6 +119,14 @@ class AgentRuntime:
|
|
|
117
119
|
self.manifest.sync = pillars["sync"]
|
|
118
120
|
self.manifest.skills = pillars["skills"]
|
|
119
121
|
|
|
122
|
+
if (
|
|
123
|
+
self.manifest.identity.name
|
|
124
|
+
and self.manifest.identity.status == PillarStatus.ACTIVE
|
|
125
|
+
):
|
|
126
|
+
self.manifest.name = self.manifest.identity.name
|
|
127
|
+
elif not manifest_name_loaded and self.config.agent_name:
|
|
128
|
+
self.manifest.name = self.config.agent_name
|
|
129
|
+
|
|
120
130
|
self.manifest.last_awakened = datetime.now(timezone.utc)
|
|
121
131
|
self._awakened = True
|
|
122
132
|
|
|
@@ -32,6 +32,10 @@ logger = logging.getLogger("skcapstone.service_health")
|
|
|
32
32
|
# Default timeout per service check (seconds).
|
|
33
33
|
CHECK_TIMEOUT = 3
|
|
34
34
|
|
|
35
|
+
# Hostname tag for multi-machine dedup — prevents Syncthing conflicts when
|
|
36
|
+
# multiple daemons write to the same ITIL incident files.
|
|
37
|
+
_HOSTNAME = socket.gethostname()
|
|
38
|
+
|
|
35
39
|
|
|
36
40
|
# ---------------------------------------------------------------------------
|
|
37
41
|
# Individual service checks
|
|
@@ -218,18 +222,23 @@ def _create_incident_for_down_service(service_result: dict[str, Any]) -> None:
|
|
|
218
222
|
from .itil import ITILManager
|
|
219
223
|
|
|
220
224
|
svc_name = service_result["name"]
|
|
225
|
+
error_info = service_result.get("error") or "unreachable"
|
|
221
226
|
mgr = ITILManager(os.path.expanduser(SHARED_ROOT))
|
|
222
227
|
|
|
223
228
|
# Dedup: skip if there's already an open incident for this service
|
|
224
229
|
existing = mgr.find_open_incident_for_service(svc_name)
|
|
225
230
|
if existing:
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
231
|
+
# Only add a "still down" note if this host hasn't noted it recently
|
|
232
|
+
last_notes = [e.get("note", "") for e in (existing.timeline or [])[-3:]]
|
|
233
|
+
host_tag = f"[{_HOSTNAME}]"
|
|
234
|
+
if any(host_tag in n and "still down" in n for n in last_notes):
|
|
235
|
+
logger.debug("Skipping duplicate down note for %s from %s", svc_name, _HOSTNAME)
|
|
236
|
+
else:
|
|
237
|
+
mgr.update_incident(
|
|
238
|
+
existing.id, "service_health",
|
|
239
|
+
note=f"[{_HOSTNAME}] Service {svc_name} still down: {error_info}",
|
|
240
|
+
)
|
|
230
241
|
return
|
|
231
|
-
|
|
232
|
-
error_info = service_result.get("error") or "unreachable"
|
|
233
242
|
mgr.create_incident(
|
|
234
243
|
title=f"{svc_name} down",
|
|
235
244
|
severity="sev3",
|
|
@@ -267,10 +276,14 @@ def _auto_resolve_recovered_service(service_result: dict[str, Any]) -> None:
|
|
|
267
276
|
logger.info("Auto-resolved sev4 incident %s for recovered service %s",
|
|
268
277
|
existing.id, svc_name)
|
|
269
278
|
else:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
)
|
|
279
|
+
# Skip if this host already noted recovery recently
|
|
280
|
+
last_notes = [e.get("note", "") for e in (existing.timeline or [])[-3:]]
|
|
281
|
+
host_tag = f"[{_HOSTNAME}]"
|
|
282
|
+
if not any(host_tag in n and "back up" in n for n in last_notes):
|
|
283
|
+
mgr.update_incident(
|
|
284
|
+
existing.id, "service_health",
|
|
285
|
+
note=f"[{_HOSTNAME}] Service {svc_name} appears to be back up",
|
|
286
|
+
)
|
|
274
287
|
except Exception as exc:
|
|
275
288
|
logger.debug("Failed to auto-resolve incident for %s: %s",
|
|
276
289
|
service_result.get("name"), exc)
|
|
@@ -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"
|