初版
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""PreToolUse guard:人格鎖驗證 + 跨人格資料隔離(唯一的強制執行點)。
|
||||
|
||||
擋下的情形:
|
||||
* 讀寫非「本 session 當前人格」的人格目錄(含 Read/Write/Edit/Glob/Grep/Bash)
|
||||
* guest(persona-guest sub agent)寫入任何人格檔案,或換讀別的人格
|
||||
* CLI 帶假的 --session(冒用其他程序身分)
|
||||
* 目標人格的鎖屬於其他還活著的程序
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
try:
|
||||
decision, reason = pl.guard_decide(event)
|
||||
except Exception as exc: # guard 自己壞掉不該擋住整個 session
|
||||
print(f"persona guard 內部錯誤:{exc}", file=sys.stderr)
|
||||
return 0
|
||||
if decision != "deny":
|
||||
return 0
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": f"[jsc-persona 隔離] {reason}",
|
||||
}
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py\"",
|
||||
"timeout": 20,
|
||||
"statusMessage": "載入人格執行環境…"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/prompt_submit.py\"",
|
||||
"timeout": 20,
|
||||
"statusMessage": "回想記憶與情緒…"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Read|Write|Edit|MultiEdit|NotebookEdit|Glob|Grep|LS|Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/guard.py\"",
|
||||
"timeout": 15,
|
||||
"statusMessage": "檢查人格隔離…"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/turn_end.py\"",
|
||||
"timeout": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent_stop.py\"",
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/session_end.py\"",
|
||||
"timeout": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""UserPromptSubmit:每輪注入「人格 + 情緒 + 記憶 + 關係」上下文,並記錄原始逐字。
|
||||
|
||||
短期記憶(memory/short-term.jsonl)由 skill 做完語意分析後才寫入(有主題/實體/
|
||||
顯著度/情緒變化);這裡只寫 journal 原始逐字,避免同一句話被記兩次。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
prompt = event.get("prompt") or ""
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if not host or not pl.persona_exists(host):
|
||||
return 0
|
||||
|
||||
pl.heartbeat_lock(host, session_id)
|
||||
for guest, info in (data.get("guests") or {}).items():
|
||||
if pl.persona_exists(guest):
|
||||
pl.add_guest_lease(guest, session_id, info.get("room", ""), host)
|
||||
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "utterance", "role": "user", "text": prompt[:4000],
|
||||
})
|
||||
|
||||
context = pl.turn_context(host, session_id, prompt)
|
||||
hint = (
|
||||
"\n回覆前請:①做語意分析(意圖/主題/實體/情感)②依十二情緒更新狀態"
|
||||
" ③以人格語氣回覆 ④用 `persona.py remember` 寫回短期記憶。"
|
||||
"\n情緒與記憶指令請參考 /jsc-persona:persona-chat。"
|
||||
)
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "UserPromptSubmit",
|
||||
"additionalContext": context + hint,
|
||||
},
|
||||
"suppressOutput": True,
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SessionEnd:釋放人格鎖與所有 guest 租約,人格才能被下一個程序載入。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if host and pl.persona_exists(host):
|
||||
state = pl.decay_emotion(pl.load_emotion(host))
|
||||
pl.write_json(pl.emotion_path(host), state)
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "session-end", "session": session_id[:8],
|
||||
"mood": pl.mood(state),
|
||||
})
|
||||
released = pl.unbind_session(session_id)
|
||||
pl.gc_runtime()
|
||||
out: dict = {"suppressOutput": True}
|
||||
if released["host"]:
|
||||
out["systemMessage"] = (
|
||||
f"[jsc-persona] 已釋放 `{released['host']}` 的載入鎖"
|
||||
+ (f",guest:{released['guests']}" if released["guests"] else "")
|
||||
)
|
||||
print(json.dumps(out, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SessionStart:清理死鎖、續租、並把 PERSONA_SESSION 與人格清單注入上下文。
|
||||
|
||||
session_id 只有 hook 拿得到,所以這裡把它注入上下文;後續 skill 呼叫 CLI 時
|
||||
必須帶 `--session <這個值>`,guard hook 會驗證,程序無法冒用別人的身分。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
source = event.get("source") or "startup"
|
||||
cwd = event.get("cwd")
|
||||
|
||||
pl.gc_runtime()
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
|
||||
lines = [
|
||||
"<persona-runtime>",
|
||||
f"PERSONA_SESSION={session_id}",
|
||||
f"人格倉庫:{pl.persona_home()}",
|
||||
"規則:",
|
||||
" 1. 呼叫 persona.py 一律帶 `--session " + session_id + "`(值不符會被 hook 拒絕)。",
|
||||
" 2. 一個程序只能載入一個人格;要與別的人格對話請用 /jsc-persona:persona-invite。",
|
||||
" 3. 禁止直接讀寫非當前人格的目錄,hook 會擋下(跨人格資料隔離)。",
|
||||
]
|
||||
|
||||
if host and pl.persona_exists(host):
|
||||
# resume / compact / clear:把鎖續租回來,並帶回人格狀態
|
||||
try:
|
||||
pl.acquire_lock(host, session_id, cwd=cwd)
|
||||
lines.append(f"已接續人格 `{host}`(session 恢復:{source})。")
|
||||
lines.append(pl.turn_context(host, session_id))
|
||||
except pl.LockError as exc:
|
||||
lines.append(f"⚠ 無法接續人格 `{host}`:{exc}")
|
||||
else:
|
||||
personas = pl.list_personas()
|
||||
if personas:
|
||||
lines.append("尚未載入人格。可用人格:")
|
||||
for slug in personas:
|
||||
status = pl.lock_status(slug)
|
||||
mark = "🔒" if status["locked"] else "🔓"
|
||||
lines.append(f" - {mark} `{slug}` {pl.identity_brief(slug)}")
|
||||
lines.append("載入方式:/jsc-persona:persona-chat <slug>")
|
||||
else:
|
||||
lines.append("尚無任何人格,可用 /jsc-persona:persona-create 建立。")
|
||||
lines.append("</persona-runtime>")
|
||||
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "\n".join(lines),
|
||||
},
|
||||
"suppressOutput": True,
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SubagentStop:guest sub agent 結束時解除它的 pin(下次啟動要重新 first-touch)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
agent_id = event.get("agent_id")
|
||||
agent_type = str(event.get("agent_type") or "")
|
||||
if not agent_id or "persona-guest" not in agent_type:
|
||||
return 0
|
||||
data = pl.load_session(session_id)
|
||||
pins = data.get("pins") or {}
|
||||
slug = pins.pop(agent_id, None)
|
||||
if slug is not None:
|
||||
pl.save_session(session_id, data)
|
||||
info = (data.get("guests") or {}).get(slug) or {}
|
||||
if info.get("room"):
|
||||
pl.add_guest_lease(slug, session_id, info["room"], data.get("host") or "")
|
||||
print(json.dumps({"suppressOutput": True}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Stop:情緒隨時間衰減、續租、記錄逐字、必要時提醒固化記憶。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||
import persona_lib as pl # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
session_id = event.get("session_id") or "unknown"
|
||||
data = pl.load_session(session_id)
|
||||
host = data.get("host")
|
||||
if not host or not pl.persona_exists(host):
|
||||
return 0
|
||||
|
||||
pl.heartbeat_lock(host, session_id)
|
||||
state = pl.decay_emotion(pl.load_emotion(host))
|
||||
pl.write_json(pl.emotion_path(host), state)
|
||||
|
||||
message = event.get("last_assistant_message") or ""
|
||||
if message:
|
||||
pl.append_jsonl(pl.journal_path(host), {
|
||||
"ts": pl.iso(), "kind": "utterance", "role": "persona", "text": message[:4000],
|
||||
"mood": pl.mood(state),
|
||||
})
|
||||
|
||||
pending = len(pl.read_jsonl(pl.short_term_path(host)))
|
||||
out: dict = {"suppressOutput": True}
|
||||
if pending >= pl.CONSOLIDATE_THRESHOLD:
|
||||
out["systemMessage"] = (
|
||||
f"[jsc-persona] `{host}` 的短期記憶已 {pending} 筆,"
|
||||
"建議執行 /jsc-persona:persona-memory 固化為長期記憶。"
|
||||
)
|
||||
print(json.dumps(out, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user