@rubytech/create-maxy-code 0.1.81 → 0.1.82
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/package.json +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-revoke.test.sh +130 -0
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +19 -17
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +34 -0
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +8 -16
- package/payload/platform/plugins/admin/skills/access-manager/SKILL.md +2 -2
- package/payload/platform/plugins/admin/skills/access-manager/references/operations.md +10 -25
- package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +14 -14
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +5 -5
- package/payload/platform/plugins/docs/references/admin-session.md +4 -4
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +20 -14
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/server/public/assets/{admin-Bn3SKHdv.js → admin-Dl273yTS.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-VMeuB5OP.js → architectureDiagram-Q4EWVU46-Iyf6H93s.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-B7mq0Me0.js → blockDiagram-DXYQGD6D-e7l5Mhun.js} +1 -1
- package/payload/server/public/assets/{brand-CZQNuuLo.css → brand-CTMGYFDd.css} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Btu6ejD1.js → c4Diagram-AHTNJAMY-bDICnX2_.js} +1 -1
- package/payload/server/public/assets/channel-CkgyfA91.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-DAL_d_Xa.js → chunk-336JU56O-CbnUj19H.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-DPD9ot_N.js → chunk-426QAEUC-DxGLqjw5.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-BexM9T-f.js → chunk-4TB4RGXK-CzvvHuMJ.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-EwDdji3T.js → chunk-5FUZZQ4R-DxqHPDro.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-BP9uoVws.js → chunk-5PVQY5BW-BA6nMu9F.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-DSFmUMEW.js → chunk-EDXVE4YY-gUSTktie.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-IVAy2Qk9.js → chunk-ENJZ2VHE-C-wW17kd.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-DkUupPox.js → chunk-ICPOFSXX-zLc9NsO5.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-BPnzam8G.js → chunk-OYMX7WX6-Bt9RJ_rb.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-xYNchOVT.js → chunk-U2HBQHQK-BAM6MO6g.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-C1V_V8v8.js → chunk-X2U36JSP-DbOwgg1R.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-CUibW87w.js → chunk-YZCP3GAM-BXUfYLeM.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-HkKx4xBt.js → chunk-ZZ45TVLE-BBIHnMjI.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-PyvJekA9.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-6hFNhiTb.js +1 -0
- package/payload/server/public/assets/clone-CFIJrRLl.js +1 -0
- package/payload/server/public/assets/{dagre-DIaJ9oMM.js → dagre-Bv8aHCaJ.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-Oyox5ACd.js → dagre-KV5264BT-BZ0q7dDA.js} +1 -1
- package/payload/server/public/assets/data-BgADjmk1.js +1 -0
- package/payload/server/public/assets/{device-url-actions-BjDrEVoY.js → device-url-actions-CK6x8-FQ.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-DoGhkzBd.js → diagram-5BDNPKRD-Bbzy2KoW.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-Dc3Ix-Dh.js → diagram-G4DWMVQ6-CadVHWrV.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-CFM9_Ggj.js → diagram-MMDJMWI5-DwJ6fVMv.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-nMWgzxC3.js → diagram-TYMM5635-Bh8d1vg-.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-DcQWiGgF.js → erDiagram-SMLLAGMA-B1oTY2ob.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CcX5waal.js → flowDiagram-DWJPFMVM-BZU76iLs.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BTQWoD8W.js → ganttDiagram-T4ZO3ILL-PjhJx62i.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BHZcRznS.js → gitGraphDiagram-UUTBAWPF-DLrxzJYi.js} +1 -1
- package/payload/server/public/assets/graph-B1_erzSo.js +1 -0
- package/payload/server/public/assets/{graph-labels-CqsRzbp0.js → graph-labels-CpFvcba2.js} +1 -1
- package/payload/server/public/assets/{graphlib-BpUb9z9y.js → graphlib-D2ExKXiT.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CBoiO8yF.js → infoDiagram-42DDH7IO-Cn0r9nCK.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-mTDI_0As.js → ishikawaDiagram-UXIWVN3A-Ed8ANWEq.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CcuU_0rp.js → journeyDiagram-VCZTEJTY-DF1gG775.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BWA7Do5Y.js → kanban-definition-6JOO6SKY-D8VcaKAp.js} +1 -1
- package/payload/server/public/assets/{line-BlGptbDR.js → line-CgMZIaVM.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-hslUiqJ-.js → mermaid-parser.core-ez9lChuL.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-DWU_5Faf.js → mermaid.core-BiVW_Lka.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-B3_sQr-2.js → mindmap-definition-QFDTVHPH-C-BlLBbo.js} +1 -1
- package/payload/server/public/assets/{page-eQPbgLXP.js → page-BQCQwUG5.js} +1 -1
- package/payload/server/public/assets/{page-lPEIoiHs.js → page-DStDaJgc.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-02u2dI6Z.js → pieDiagram-DEJITSTG-BK0-LxVl.js} +1 -1
- package/payload/server/public/assets/{public-j7U67c6E.js → public-DMk9cZKl.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BfgiMQiy.js → quadrantDiagram-34T5L4WZ-vKAQHu-t.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-JKZoibIp.js → requirementDiagram-MS252O5E-Df9sXR35.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-N1iaY19u.js → sankeyDiagram-XADWPNL6-DG6xirA4.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-DjU3oQ0_.js → sequenceDiagram-FGHM5R23-B1a38KHX.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BuFcldiV.js → stateDiagram-FHFEXIEX-CRiIDR4O.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-C8cVmi_o.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-3vztYjFm.js → timeline-definition-GMOUNBTQ-CkBOE7Y8.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Zdv9WVqJ.js → vennDiagram-DHZGUBPP-CwKBydYc.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CkTAZI7f.js → wardleyDiagram-NUSXRM2D-BWXYfzIr.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CgMCxpms.js → xychartDiagram-5P7HB3ND-D-4v5Dy_.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +5 -5
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/public/assets/channel-C_bSLH4h.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-B6oW43xg.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D0237d4h.js +0 -1
- package/payload/server/public/assets/clone-AMRCmnen.js +0 -1
- package/payload/server/public/assets/data-Mm3pTL1b.js +0 -1
- package/payload/server/public/assets/graph-Cd4jsDi3.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-rZLYem3J.js +0 -1
- /package/payload/server/public/assets/{brand-BnMilSyt.js → brand-dPgPNs7n.js} +0 -0
package/package.json
CHANGED
package/payload/platform/plugins/admin/hooks/__tests__/pre-tool-use-memory-write-revoke.test.sh
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Task 213 regression test for the memory-write / memory-update revoke
|
|
3
|
+
# block in pre-tool-use.sh. The block sits at the top of the admin branch
|
|
4
|
+
# and ensures any admin tool_use targeting the two writers exits 2 with
|
|
5
|
+
# stderr explaining the recorder ownership, and emits one structured
|
|
6
|
+
# `admin-tool-revoke` line through `/api/admin/log-ingest`.
|
|
7
|
+
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
HOOK="$(cd "$(dirname "$0")/.." && pwd)/pre-tool-use.sh"
|
|
11
|
+
if [[ ! -x "$HOOK" ]]; then
|
|
12
|
+
echo "FAIL: $HOOK not executable" >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
TMPFILES=()
|
|
17
|
+
LISTENER_PIDS=()
|
|
18
|
+
cleanup() {
|
|
19
|
+
for pid in "${LISTENER_PIDS[@]:-}"; do
|
|
20
|
+
[[ -n "$pid" ]] && kill "$pid" 2>/dev/null || true
|
|
21
|
+
[[ -n "$pid" ]] && wait "$pid" 2>/dev/null || true
|
|
22
|
+
done
|
|
23
|
+
for f in "${TMPFILES[@]:-}"; do
|
|
24
|
+
[[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
|
|
25
|
+
done
|
|
26
|
+
}
|
|
27
|
+
trap cleanup EXIT
|
|
28
|
+
|
|
29
|
+
PASS=0
|
|
30
|
+
FAIL=0
|
|
31
|
+
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
|
32
|
+
fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
|
33
|
+
|
|
34
|
+
start_listener() {
|
|
35
|
+
REQ_LOG=$(mktemp); TMPFILES+=("$REQ_LOG")
|
|
36
|
+
LISTENER_PORT=$((39500 + RANDOM % 100))
|
|
37
|
+
python3 - "$LISTENER_PORT" "$REQ_LOG" <<'PY' &
|
|
38
|
+
import sys, http.server
|
|
39
|
+
port = int(sys.argv[1]); log = sys.argv[2]
|
|
40
|
+
class H(http.server.BaseHTTPRequestHandler):
|
|
41
|
+
def log_message(self, *a, **k): pass
|
|
42
|
+
def do_POST(self):
|
|
43
|
+
n = int(self.headers.get('Content-Length','0') or 0)
|
|
44
|
+
body = self.rfile.read(n).decode('utf-8','replace')
|
|
45
|
+
with open(log,'a',encoding='utf-8') as f:
|
|
46
|
+
f.write(self.path + '\t' + body + '\n')
|
|
47
|
+
self.send_response(200); self.end_headers(); self.wfile.write(b'{}')
|
|
48
|
+
http.server.HTTPServer(('127.0.0.1', port), H).serve_forever()
|
|
49
|
+
PY
|
|
50
|
+
LISTENER_PIDS+=("$!")
|
|
51
|
+
for _ in $(seq 1 20); do
|
|
52
|
+
curl -sS --max-time 1 -X POST "http://127.0.0.1:${LISTENER_PORT}/ping" -d '{}' >/dev/null 2>&1 && break
|
|
53
|
+
sleep 0.1
|
|
54
|
+
done
|
|
55
|
+
: > "$REQ_LOG"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
run_with_tool() {
|
|
59
|
+
local tool="$1"
|
|
60
|
+
local sid="$2"
|
|
61
|
+
local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
|
|
62
|
+
local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
|
|
63
|
+
python3 -c '
|
|
64
|
+
import json, sys
|
|
65
|
+
print(json.dumps({
|
|
66
|
+
"hook_event_name": "PreToolUse",
|
|
67
|
+
"session_id": sys.argv[1],
|
|
68
|
+
"tool_name": sys.argv[2],
|
|
69
|
+
"tool_input": {"name":"Smalleys","accountId":"acct-x","scope":"shared"},
|
|
70
|
+
}, separators=(",", ":")))
|
|
71
|
+
' "$sid" "$tool" | \
|
|
72
|
+
MAXY_UI_INTERNAL_PORT="$LISTENER_PORT" \
|
|
73
|
+
bash "$HOOK" admin >"$stdout_file" 2>"$stderr_file"
|
|
74
|
+
HOOK_RC=$?
|
|
75
|
+
HOOK_STDERR=$(cat "$stderr_file")
|
|
76
|
+
sleep 0.1 # let listener flush
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
start_listener
|
|
80
|
+
|
|
81
|
+
# --- memory-write rejected ---------------------------------------------
|
|
82
|
+
: > "$REQ_LOG"
|
|
83
|
+
run_with_tool "mcp__plugin_memory_memory__memory-write" "sess-abc"
|
|
84
|
+
[[ "$HOOK_RC" -eq 2 ]] || fail "memory-write expected exit 2, got $HOOK_RC"
|
|
85
|
+
echo "$HOOK_STDERR" | grep -qF "admin agent does not write to the graph" \
|
|
86
|
+
|| fail "memory-write stderr missing rationale, got: $HOOK_STDERR"
|
|
87
|
+
if grep -qE '/api/admin/log-ingest .*"tag":\s*"admin-tool-revoke".*sessionId=sess-abc.*tool=mcp__plugin_memory_memory__memory-write.*source=permission-hook' "$REQ_LOG"; then
|
|
88
|
+
pass "memory-write rejection emits admin-tool-revoke with sessionId, tool, source"
|
|
89
|
+
else
|
|
90
|
+
fail "memory-write rejection did not emit admin-tool-revoke; log: $(cat "$REQ_LOG")"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# --- memory-update rejected --------------------------------------------
|
|
94
|
+
: > "$REQ_LOG"
|
|
95
|
+
run_with_tool "mcp__plugin_memory_memory__memory-update" "sess-xyz"
|
|
96
|
+
[[ "$HOOK_RC" -eq 2 ]] || fail "memory-update expected exit 2, got $HOOK_RC"
|
|
97
|
+
if grep -qE '/api/admin/log-ingest .*"tag":\s*"admin-tool-revoke".*sessionId=sess-xyz.*tool=mcp__plugin_memory_memory__memory-update.*source=permission-hook' "$REQ_LOG"; then
|
|
98
|
+
pass "memory-update rejection emits admin-tool-revoke with sessionId, tool, source"
|
|
99
|
+
else
|
|
100
|
+
fail "memory-update rejection did not emit admin-tool-revoke; log: $(cat "$REQ_LOG")"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# --- memory-search (allowed read tool) NOT rejected --------------------
|
|
104
|
+
: > "$REQ_LOG"
|
|
105
|
+
run_with_tool "mcp__plugin_memory_memory__memory-search" "sess-read"
|
|
106
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "memory-search expected exit 0, got $HOOK_RC stderr: $HOOK_STDERR"
|
|
107
|
+
if grep -qE '"tag":\s*"admin-tool-revoke"' "$REQ_LOG"; then
|
|
108
|
+
fail "memory-search must NOT emit admin-tool-revoke; log: $(cat "$REQ_LOG")"
|
|
109
|
+
else
|
|
110
|
+
pass "memory-search passes through; no admin-tool-revoke emitted"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# --- the old mcp__memory__ namespace is NOT matched -------------------
|
|
114
|
+
# The revoke block targets the current runtime namespace literally; the
|
|
115
|
+
# old namespace was never the surface admin saw, so matching it would
|
|
116
|
+
# be a regression.
|
|
117
|
+
: > "$REQ_LOG"
|
|
118
|
+
run_with_tool "mcp__memory__memory-write" "sess-old-ns"
|
|
119
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "pre-209 namespace expected exit 0, got $HOOK_RC"
|
|
120
|
+
if grep -qE '"tag":\s*"admin-tool-revoke"' "$REQ_LOG"; then
|
|
121
|
+
fail "pre-209 namespace must NOT emit admin-tool-revoke"
|
|
122
|
+
else
|
|
123
|
+
pass "pre-209 mcp__memory__memory-write passes through (block is namespace-literal)"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# --- Summary ------------------------------------------------------------
|
|
127
|
+
echo "---"
|
|
128
|
+
echo "PASSED: $PASS FAILED: $FAIL"
|
|
129
|
+
[[ "$FAIL" -eq 0 ]] || exit 1
|
|
130
|
+
exit 0
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
# is the FILLED database-operator paragraph — the four-sentence
|
|
15
15
|
# template with `{schema}` replaced by `schema-base.md` (plus the
|
|
16
16
|
# brand-declared vertical when set) and `{conversation}` replaced by
|
|
17
|
-
# a flat `role: text` rendering of the envelope's `turns` array
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
17
|
+
# a flat `role: text` rendering of the envelope's `turns` array.
|
|
18
|
+
# Task 213 strips the prior `[tool: name(args)]` markers — they
|
|
19
|
+
# taught the recorder to copy the shape into prose. The initialMessage
|
|
20
|
+
# is a plain string, not a JSON-stringified envelope; no instruction
|
|
21
|
+
# prose, no leading JSON.
|
|
21
22
|
# - The hook emits exactly one `[turn-recorder] envelope ...` line and
|
|
22
23
|
# one `[turn-recorder] substitution ...` line before `spawn-request`.
|
|
23
24
|
# The substitution line carries `schemaBytes`, `conversationBytes`,
|
|
@@ -406,10 +407,12 @@ else
|
|
|
406
407
|
fail "case-6 multi-block collapse wrong ($MULTI_OK): $MULTI_BODY"
|
|
407
408
|
fi
|
|
408
409
|
|
|
409
|
-
# --- Case 7 (Task 177): tool-call pairing
|
|
410
|
-
# Assistant tool_use
|
|
411
|
-
# `toolCalls` entry on the assistant turn
|
|
412
|
-
#
|
|
410
|
+
# --- Case 7 (Task 177 + Task 213): tool-call pairing kept in envelope, -
|
|
411
|
+
# stripped from the transcript. Assistant tool_use + user tool_result still
|
|
412
|
+
# fold into one `toolCalls` entry on the assistant turn (envelope-level
|
|
413
|
+
# observability via `[turn-recorder] envelope … toolCallTurns=N`). The
|
|
414
|
+
# rendered transcript carries NO `[tool: …]` markers — pre-213 they taught
|
|
415
|
+
# the recorder to emit `<tool_call>` as text.
|
|
413
416
|
TOOL_TRANSCRIPT=$(mktemp); TMPFILES+=("$TOOL_TRANSCRIPT")
|
|
414
417
|
python3 - "$TOOL_TRANSCRIPT" <<'PY'
|
|
415
418
|
import json, sys
|
|
@@ -454,14 +457,13 @@ try:
|
|
|
454
457
|
# text body), one carrying "done".
|
|
455
458
|
conds.append(("two-asst-lines", msg.count("\nassistant: ") == 2))
|
|
456
459
|
conds.append(("has-asst-done", "\nassistant: done" in msg))
|
|
457
|
-
#
|
|
458
|
-
|
|
459
|
-
conds.append(("
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
conds.append(("
|
|
463
|
-
# The tool_result content does
|
|
464
|
-
# input-only markers; output goes nowhere in the filled prompt).
|
|
460
|
+
# Task 213 — no `[tool: …]` markup in the rendered transcript.
|
|
461
|
+
conds.append(("no-tool-marker", "[tool:" not in msg))
|
|
462
|
+
conds.append(("no-tool-input-leak", "Smalleys" not in msg))
|
|
463
|
+
# The owning assistant turn lands as an empty-bodied line: the tool_use
|
|
464
|
+
# block contributed no text, and the marker that used to fill it is gone.
|
|
465
|
+
conds.append(("has-empty-asst-line", "\nassistant: \n" in msg))
|
|
466
|
+
# The tool_result content does not bleed in either.
|
|
465
467
|
conds.append(("no-output-leakage", "ok: wrote LocalBusiness" not in msg))
|
|
466
468
|
failed = [name for name, ok in conds if not ok]
|
|
467
469
|
print("yes" if not failed else "no:"+repr(failed))
|
|
@@ -469,7 +471,7 @@ except Exception as e:
|
|
|
469
471
|
print("parse-fail:"+str(e))
|
|
470
472
|
' 2>/dev/null)
|
|
471
473
|
if [[ "$TOOL_OK" == "yes" ]]; then
|
|
472
|
-
pass "case-7 assistant tool_use
|
|
474
|
+
pass "case-7 assistant tool_use leaves no [tool: …] markup in transcript; tool_result does not create a separate user line; toolCallTurns count survives via envelope log"
|
|
473
475
|
else
|
|
474
476
|
fail "case-7 tool pairing wrong ($TOOL_OK): $TOOL_BODY"
|
|
475
477
|
fi
|
|
@@ -21,6 +21,40 @@ TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"'
|
|
|
21
21
|
# ---------------------------------------------------------------------------
|
|
22
22
|
if [ "$AGENT_TYPE" = "admin" ]; then
|
|
23
23
|
|
|
24
|
+
# ── Task 213: memory-write/memory-update revocation ──────────────────────
|
|
25
|
+
# The admin agent does not write to the graph; the Stop-hook recorder
|
|
26
|
+
# (database-operator specialist) owns these tools. Session d2aaa85e
|
|
27
|
+
# 2026-05-21 showed admin emitting `tool_use: memory-update`, filling in
|
|
28
|
+
# when the recorder produced no tool_use. `pty-spawner.ts` already passes
|
|
29
|
+
# `--disallowed-tools` on the spawn argv; this hook is the observability
|
|
30
|
+
# surface — every relapse lands in server.log as `[admin-tool-revoke]`,
|
|
31
|
+
# which the `--disallowed-tools` path itself does not produce.
|
|
32
|
+
case "$TOOL_NAME" in
|
|
33
|
+
mcp__plugin_memory_memory__memory-write|mcp__plugin_memory_memory__memory-update)
|
|
34
|
+
SESSION_ID=$(printf '%s' "$INPUT" | python3 -c '
|
|
35
|
+
import sys, json
|
|
36
|
+
try:
|
|
37
|
+
d = json.load(sys.stdin)
|
|
38
|
+
print(d.get("session_id", "") or "unknown")
|
|
39
|
+
except Exception:
|
|
40
|
+
print("unknown")
|
|
41
|
+
' 2>/dev/null || echo "unknown")
|
|
42
|
+
UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
|
|
43
|
+
if [ -n "$UI_PORT" ]; then
|
|
44
|
+
curl -sS -o /dev/null -X POST \
|
|
45
|
+
-H 'Content-Type: application/json' \
|
|
46
|
+
--max-time 2 \
|
|
47
|
+
--data "$(python3 -c '
|
|
48
|
+
import sys, json
|
|
49
|
+
print(json.dumps({"tag":"admin-tool-revoke","level":"info","line":f"role=admin sessionId={sys.argv[1]} tool={sys.argv[2]} result=rejected source=permission-hook"}))
|
|
50
|
+
' "$SESSION_ID" "$TOOL_NAME")" \
|
|
51
|
+
"http://127.0.0.1:${UI_PORT}/api/admin/log-ingest" 2>/dev/null || true
|
|
52
|
+
fi
|
|
53
|
+
echo "Blocked: admin agent does not write to the graph. The Stop-hook database-operator recorder owns memory-write and memory-update." >&2
|
|
54
|
+
exit 2
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
24
58
|
# ── Code directory protection ────────────────────────────────────────────
|
|
25
59
|
case "$TOOL_NAME" in
|
|
26
60
|
Write|Edit)
|
|
@@ -336,9 +336,13 @@ fi
|
|
|
336
336
|
# `{conversation}` placeholders);
|
|
337
337
|
# - reads schema-base.md (and concatenates the vertical when set);
|
|
338
338
|
# - renders the envelope's `turns` array into a flat transcript — one
|
|
339
|
-
# line per turn, `role: text
|
|
340
|
-
#
|
|
341
|
-
#
|
|
339
|
+
# line per turn, `role: text`. The recorder is the only writer to the
|
|
340
|
+
# graph; rendering the operator's tool_use blocks inline taught the
|
|
341
|
+
# model (Task 213 evidence in sessions d2aaa85e / 481799d1) to copy
|
|
342
|
+
# the shape and emit `<tool_call>{…}</tool_call>` as text instead of
|
|
343
|
+
# real tool_use blocks. The envelope's `toolCalls` are still captured
|
|
344
|
+
# for the `[turn-recorder] envelope … toolCallTurns=N` observability
|
|
345
|
+
# line; they no longer reach the spawn prompt;
|
|
342
346
|
# - substitutes both placeholders in the paragraph;
|
|
343
347
|
# - emits one tab-separated line of byte counts, then the
|
|
344
348
|
# spawn-body JSON containing the filled paragraph as `initialMessage`.
|
|
@@ -372,19 +376,7 @@ lines = []
|
|
|
372
376
|
for t in turns:
|
|
373
377
|
role = t.get("role", "?")
|
|
374
378
|
text_t = t.get("text", "") or ""
|
|
375
|
-
|
|
376
|
-
for tc in (t.get("toolCalls") or []):
|
|
377
|
-
tool = tc.get("tool", "?")
|
|
378
|
-
inp = tc.get("input")
|
|
379
|
-
try:
|
|
380
|
-
inp_s = json.dumps(inp, ensure_ascii=False) if inp is not None else ""
|
|
381
|
-
except Exception:
|
|
382
|
-
inp_s = str(inp)
|
|
383
|
-
marks.append(f"[tool: {tool}({inp_s})]")
|
|
384
|
-
if marks:
|
|
385
|
-
lines.append(f"{role}: {text_t} {' '.join(marks)}".rstrip())
|
|
386
|
-
else:
|
|
387
|
-
lines.append(f"{role}: {text_t}")
|
|
379
|
+
lines.append(f"{role}: {text_t}")
|
|
388
380
|
# Wrap the transcript block in newlines so each turn lands on its own
|
|
389
381
|
# line in the filled prompt — the template's `{conversation}` sits
|
|
390
382
|
# mid-sentence, so the leading newline separates the first transcript
|
|
@@ -17,9 +17,9 @@ Applies when the business owner wants to control who can access a gated or paid
|
|
|
17
17
|
|
|
18
18
|
## Constraints
|
|
19
19
|
|
|
20
|
-
- **Admin-scope only.**
|
|
20
|
+
- **Admin-scope only.** Every AccessGrant node lands with `scope: "admin"`. Credential data must never be visible to public agents — the recorder enforces the scope tag on its write turn.
|
|
21
21
|
- **Agent must exist.** Before any grant operation, verify the target agent directory exists under `agents/` and read its `config.json`.
|
|
22
|
-
- **Confirm before mutating.** Present a `confirm` component before
|
|
22
|
+
- **Confirm before mutating.** Present a `confirm` component before grants are created, access is revoked, or access modes change. The recorder writes off the confirmed transcript.
|
|
23
23
|
- **Domain required for email invitations.** Resolve the public hostname via `tunnel-status`. Without a confirmed hostname, magic link URLs cannot be constructed.
|
|
24
24
|
- **Deterministic grant queries.** AccessGrant nodes are not vector-indexed. Query them via `cypher-shell` (password at `~/.maxy/.neo4j-password`), not via `memory-search`.
|
|
25
25
|
|
|
@@ -25,7 +25,7 @@ Use these exact property names when creating or updating grants:
|
|
|
25
25
|
| `contactValue` | string | Yes | No | Email address (lowercase) or phone number (E.164) |
|
|
26
26
|
| `status` | string | Yes | Yes | `"invited"` / `"active"` / `"expired"` / `"revoked"` |
|
|
27
27
|
| `expiresAt` | datetime | Yes | Yes | ISO 8601 datetime. `null` means no expiry |
|
|
28
|
-
| `createdAt` | datetime | Auto | No |
|
|
28
|
+
| `createdAt` | datetime | Auto | No | Stamped at write time by the recorder |
|
|
29
29
|
| `magicToken` | string | Email only | Yes | Cryptographically random UUID. Nulled after first use |
|
|
30
30
|
| `magicTokenExpiresAt` | datetime | Email only | Yes | 15 minutes from generation |
|
|
31
31
|
| `otpCode` | string | Phone only | Yes | Random 6-digit numeric code |
|
|
@@ -34,11 +34,9 @@ Use these exact property names when creating or updating grants:
|
|
|
34
34
|
| `otpAttempts` | integer | Phone only | Yes | Failed OTP entries. Max 3 before regeneration required |
|
|
35
35
|
| `scope` | `"admin"` | Yes | No | **Mandatory.** Omitting scope exposes grants to public agents |
|
|
36
36
|
|
|
37
|
-
###
|
|
37
|
+
### Recorder write shape
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`memory-update` requires `nodeId` (element ID from a prior query), `properties` (fields to update), and `accountId`. Always include all three.
|
|
39
|
+
Each AccessGrant write the recorder produces names a label (`AccessGrant`), the property bag from the table above, the operator's `accountId`, `scope: "admin"`, and the inbound `HAS_ACCESS` relationship from the Person. Admin's job is to confirm those five things in conversation; the recorder reads them off the transcript and writes the node. Updates name the existing `AccessGrant` by element ID (from the most recent `cypher-shell` lookup) and the new properties.
|
|
42
40
|
|
|
43
41
|
### Querying Grants
|
|
44
42
|
|
|
@@ -71,24 +69,11 @@ RETURN elementId(p) AS personId, p.givenName AS name
|
|
|
71
69
|
|
|
72
70
|
### Person node
|
|
73
71
|
|
|
74
|
-
Search for an existing Person by email using a `cypher-shell` query. If found, reuse its element ID. If not found,
|
|
75
|
-
|
|
76
|
-
- Labels: `["Person"]`
|
|
77
|
-
- Properties: `givenName` (if the admin provided a name), `email` (lowercase)
|
|
78
|
-
- `accountId`: from `account.json`
|
|
79
|
-
- `scope`: `"admin"`
|
|
72
|
+
Search for an existing Person by email using a `cypher-shell` query. If found, reuse its element ID. If not found, name the new Person in the confirmation — labels `["Person"]`, `givenName` (when the admin provided a name), `email` (lowercase), `accountId` from `account.json`, `scope: "admin"`. The recorder writes it on its next turn.
|
|
80
73
|
|
|
81
74
|
### AccessGrant node
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- Labels: `["AccessGrant"]`
|
|
86
|
-
- Properties: all fields from the data model table. Set `status` to `"invited"`, generate the `magicToken` (random UUID), calculate `magicTokenExpiresAt` (15 minutes from now), and calculate `expiresAt` from the admin's requested duration (or `null` for no expiry).
|
|
87
|
-
- `accountId`: from `account.json`
|
|
88
|
-
- `scope`: `"admin"`
|
|
89
|
-
- Relationships: `[{ type: "HAS_ACCESS", direction: "incoming", targetNodeId: "<person-element-id>" }]`
|
|
90
|
-
|
|
91
|
-
The `incoming` direction creates `(Person)-[:HAS_ACCESS]->(AccessGrant)`.
|
|
76
|
+
Confirm the grant with the operator carrying all fields from the data model table — `status: "invited"`, the freshly-generated `magicToken` (random UUID), the `magicTokenExpiresAt` (15 minutes from now), and the `expiresAt` from the admin's requested duration (or `null` for no expiry) — together with `accountId` from `account.json`, `scope: "admin"`, and the inbound `HAS_ACCESS` edge from the Person. The `incoming` direction yields `(Person)-[:HAS_ACCESS]->(AccessGrant)` once the recorder writes it.
|
|
92
77
|
|
|
93
78
|
### Magic link
|
|
94
79
|
|
|
@@ -160,15 +145,15 @@ If no grants exist for the agent, say so in text rather than emitting an empty l
|
|
|
160
145
|
|
|
161
146
|
**Outcome:** Grant status is `"revoked"`. The visitor is blocked on their next login attempt.
|
|
162
147
|
|
|
163
|
-
Find the grant via `cypher-shell` to get its element ID.
|
|
148
|
+
Find the grant via `cypher-shell` to get its element ID. Confirm the revocation with the operator naming the element ID and the new `status: "revoked"`; the recorder writes the status change in its next turn.
|
|
164
149
|
|
|
165
|
-
Present a `confirm` component before
|
|
150
|
+
Present a `confirm` component before the revocation lands. Explain that active sessions continue until they naturally expire (max 24 hours) — revocation takes effect on the visitor's next login.
|
|
166
151
|
|
|
167
152
|
## Extend Access
|
|
168
153
|
|
|
169
154
|
**Outcome:** Grant's `expiresAt` is pushed forward by the requested duration.
|
|
170
155
|
|
|
171
|
-
Find the grant via `cypher-shell`. Calculate the new `expiresAt` by adding the requested duration to the current `expiresAt` (or to now, if the grant has already expired).
|
|
156
|
+
Find the grant via `cypher-shell`. Calculate the new `expiresAt` by adding the requested duration to the current `expiresAt` (or to now, if the grant has already expired). Confirm the new `expiresAt` and the grant's element ID with the operator; the recorder folds the new property in.
|
|
172
157
|
|
|
173
158
|
Present a `confirm` component showing the current expiry date and the new expiry date after extension.
|
|
174
159
|
|
|
@@ -192,6 +177,6 @@ Present a `confirm` component before changing, explaining the current mode, the
|
|
|
192
177
|
|
|
193
178
|
Find the existing grant via `cypher-shell`. The grant must be in `invited` or `active` status.
|
|
194
179
|
|
|
195
|
-
**For email grants:** Generate a new UUID for `magicToken`, set `magicTokenExpiresAt` to 15 minutes from now.
|
|
180
|
+
**For email grants:** Generate a new UUID for `magicToken`, set `magicTokenExpiresAt` to 15 minutes from now. Confirm both with the operator and the grant's element ID — the recorder folds them in. Send the invitation email using the same format as the original invite.
|
|
196
181
|
|
|
197
|
-
**For phone grants:** Generate a new 6-digit `otpCode`, reset `otpAttempts` to `0`, set `otpExpiresAt` to 10 minutes from now.
|
|
182
|
+
**For phone grants:** Generate a new 6-digit `otpCode`, reset `otpAttempts` to `0`, set `otpExpiresAt` to 10 minutes from now. Confirm all three with the operator and the grant's element ID — the recorder folds them in. Then deliver the new OTP via the SMS delivery command described in the "Invite by Phone" section.
|
|
@@ -10,28 +10,28 @@ Applies when the business owner wants to set up, complete, or update their busin
|
|
|
10
10
|
## When to Activate
|
|
11
11
|
|
|
12
12
|
- Admin asks to set up, edit, or complete their business profile
|
|
13
|
-
- Onboarding step 9 delegates here on first run **only when the operator picked `business-owner` mode**. Personal mode bootstraps a `Person`
|
|
13
|
+
- Onboarding step 9 delegates here on first run **only when the operator picked `business-owner` mode**. Personal mode bootstraps a personal-profile `Person` directly inside step 9 and does NOT invoke this skill.
|
|
14
14
|
- Session-start graph check shows a `LocalBusiness` node with missing data (no hours, no services, empty description)
|
|
15
|
-
- A
|
|
15
|
+
- A recorder cycle reports `no-admin-user` or `no-local-business` — the graph lacks the prerequisite node. (For personal-mode accounts, this signal should not fire because the personal-profile `Person` already satisfies the precondition — if it does fire, the personal-profile node is missing or its `role` is wrong.)
|
|
16
16
|
- Admin asks questions like "update my address", "add our opening hours", "change the phone number"
|
|
17
17
|
|
|
18
18
|
## First-run path (no LocalBusiness yet)
|
|
19
19
|
|
|
20
|
-
The `AdminUser` and the operator's personal-profile `Person` are written deterministically at PIN setup time, so this skill no longer creates the AdminUser. When onboarding step 9 invokes this skill in `business-owner` mode, or when
|
|
20
|
+
The `AdminUser` and the operator's personal-profile `Person` are written deterministically at PIN setup time, so this skill no longer creates the AdminUser. When onboarding step 9 invokes this skill in `business-owner` mode, or when the operator's graph has no `LocalBusiness`:
|
|
21
21
|
|
|
22
|
-
1. **LocalBusiness.** Ask the
|
|
23
|
-
2. **Capture the remaining domains** (address, hours, services, FAQs, brand assets)
|
|
22
|
+
1. **LocalBusiness.** Ask the operator for the business name first — this is the only mandatory property. Confirm the name and the OWNS link to the AdminUser. The recorder writes the `LocalBusiness` node with `scope: "shared"` and the OWNS edge in its next turn.
|
|
23
|
+
2. **Capture the remaining domains** (address, hours, services, FAQs, brand assets) in conversation. After the operator confirms each batch, the recorder folds those properties onto the `LocalBusiness` and adds the related nodes (`OpeningHoursSpecification`, `Service`, `FAQPage`, `ImageObject`).
|
|
24
24
|
|
|
25
|
-
Adapt the order if the
|
|
25
|
+
Adapt the order if the operator provides data in a different sequence — the invariant is: the `LocalBusiness` is named and confirmed before any related-node batch is discussed. Confirm each batch with the operator before moving on; the recorder reads the confirmation off the transcript.
|
|
26
26
|
|
|
27
27
|
## Constraints
|
|
28
28
|
|
|
29
|
-
- **Schema-first.** Before
|
|
30
|
-
- **Read-first.** Search for the existing `LocalBusiness` node and its related entities before asking the
|
|
31
|
-
- **Confirm before
|
|
29
|
+
- **Schema-first.** Before discussing a batch, load `memory/references/schema-base.md` via `plugin-read`. If `businessType` is known on the `LocalBusiness` node, also load the matching vertical schema. Confirm which schemas were loaded so the recorder uses the right property names.
|
|
30
|
+
- **Read-first.** Search for the existing `LocalBusiness` node and its related entities before asking the operator anything. Do not re-ask for data that is already in the graph — reference it and confirm it is still correct. On first run (no `LocalBusiness` yet), skip the search and proceed with the first-run path above.
|
|
31
|
+
- **Confirm before recording.** Present each batch of data back to the operator and get explicit confirmation. The recorder writes off the confirmed transcript; unconfirmed prose is not a record.
|
|
32
32
|
- **Exact property names.** Use Schema.org camelCase property names exactly as defined in the loaded schema reference. No synonyms, no abbreviations.
|
|
33
|
-
- **No placeholders.** Never
|
|
34
|
-
- **Conversational, not a form.** Adapt to what the
|
|
33
|
+
- **No placeholders.** Never propose empty strings, "TBD", or "unknown" as property values — they degrade search quality via poor embeddings. If a value is unknown, skip it.
|
|
34
|
+
- **Conversational, not a form.** Adapt to what the operator provides. Accept partial input and infer where reasonable (e.g. "9-5 weekdays" means Mon-Fri 09:00-17:00). Allow skipping sections and returning later.
|
|
35
35
|
|
|
36
36
|
## Data Domains
|
|
37
37
|
|
|
@@ -46,8 +46,8 @@ Six categories of operational data, each mapping to graph nodes defined in `sche
|
|
|
46
46
|
| FAQs (question + answer pairs) | `FAQPage` → `Question` | `HAS_FAQ`, then `CONTAINS` |
|
|
47
47
|
| Services (name, description, offers/pricing) | `Service` | `OFFERS` (from LocalBusiness) |
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Recording Nodes
|
|
50
50
|
|
|
51
|
-
When
|
|
51
|
+
When the operator confirms a new related node (hours, FAQs, assets, services), make sure the conversation names the `LocalBusiness` it attaches to and the edge type — the recorder reads this off the transcript and writes the node with the right adjacency. For FAQ questions, that means the operator confirms the `FAQPage` first, then the individual `Question`s that hang off it via `CONTAINS`.
|
|
52
52
|
|
|
53
|
-
When
|
|
53
|
+
When the operator changes existing data (a phone number, an address), reference the existing node by name in conversation; the recorder finds it via search and folds the new properties in.
|
|
@@ -67,7 +67,7 @@ During creation (Step 7) and editing, offer image upload when the user has an im
|
|
|
67
67
|
|
|
68
68
|
Nodes in the graph can be directly tagged for a specific agent by adding the agent's slug to the node's `agents` array property. This is the explicit tagging mode — the admin decides exactly which nodes an agent can see. Direct tagging and keyword subscriptions (`knowledgeKeywords`) are independent routing mechanisms — each is configured and approved separately, and neither implies the other.
|
|
69
69
|
|
|
70
|
-
To tag nodes:
|
|
70
|
+
To tag nodes: read the node's current `agents` array via `memory-search` (no `agentSlug` param), confirm with the operator which nodes get the new slug, and the recorder folds the slug onto each `agents` array in its next turn. To untag: same shape — read the current array, confirm removal, and the recorder rewrites the array without the slug. To see what's tagged for an agent: search with `memory-search` (no `agentSlug` param) and identify results whose `agents` property contains the target slug.
|
|
71
71
|
|
|
72
72
|
### Model Options
|
|
73
73
|
|
|
@@ -189,8 +189,8 @@ After creation, no template metadata persists in the agent's files. The resultin
|
|
|
189
189
|
|
|
190
190
|
Ask the user whether any specific knowledge nodes should be tagged with this agent's slug. Explain that direct tagging makes specific nodes always visible to this agent regardless of keyword matches, and is useful for content that doesn't match any keyword but should still be available. If keyword subscriptions already cover the desired content, tagging is unnecessary.
|
|
191
191
|
|
|
192
|
-
- If the user approves: present the knowledge discovery results from Step 4 for selection,
|
|
193
|
-
- If the user declines: skip tagging entirely. Do not
|
|
192
|
+
- If the user approves: present the knowledge discovery results from Step 4 for selection, read each node's current `agents` array first, and confirm the new slug-tag set with the user. The recorder folds the slug onto each node's `agents` array in its next turn.
|
|
193
|
+
- If the user declines: skip tagging entirely. Do not propose tagging without explicit approval.
|
|
194
194
|
9. **KNOWLEDGE.md generation** — populate from the now-tagged set plus keyword matches using the `update-knowledge` skill workflow
|
|
195
195
|
10. Write `config.json` with selected model, plugins, status "active", `liveMemory`, and `knowledgeKeywords`. This is the last gated write — placed after IDENTITY.md, SOUL.md, and KNOWLEDGE.md to prevent cascade failure if one gate stalls.
|
|
196
196
|
11. Check context budget — auto-summarise if over threshold
|
|
@@ -226,7 +226,7 @@ For agent image changes:
|
|
|
226
226
|
|
|
227
227
|
For knowledge scope changes:
|
|
228
228
|
- Show currently tagged nodes (search `memory-search` without `agentSlug`, identify results whose `agents` property contains the agent's slug) and active keyword subscriptions (from `config.json`).
|
|
229
|
-
- Allow adding/removing direct tags
|
|
229
|
+
- Allow adding/removing direct tags. Read each node's current `agents` array first, confirm the change with the operator, and the recorder updates the arrays in its next turn.
|
|
230
230
|
- Allow adding/removing keyword subscriptions (update `knowledgeKeywords` in `config.json`).
|
|
231
231
|
- Allow toggling `liveMemory` on/off (update `config.json`).
|
|
232
232
|
- After changes, offer to refresh KNOWLEDGE.md using the `update-knowledge` skill.
|
|
@@ -248,7 +248,7 @@ Deletion removes the entire agent directory (`agents/{slug}/`) — not individua
|
|
|
248
248
|
- Verify the directory no longer exists
|
|
249
249
|
- Report what was removed (list the files that were in the directory)
|
|
250
250
|
|
|
251
|
-
**Graph tag cleanup (optional):** Knowledge nodes in the graph may have the deleted agent's slug in their `agents` array. These tags become orphaned after deletion — no agent will query them, so they are inert. Offer the
|
|
251
|
+
**Graph tag cleanup (optional):** Knowledge nodes in the graph may have the deleted agent's slug in their `agents` array. These tags become orphaned after deletion — no agent will query them, so they are inert. Offer the operator the choice: clean up the orphaned tags (read each affected node's current `agents` array first, confirm the slug removal, and the recorder rewrites the arrays in its next turn) or leave them. If the operator declines, note that the tags are harmless and can be cleaned up later.
|
|
252
252
|
|
|
253
253
|
### Set as Default
|
|
254
254
|
|
|
@@ -108,7 +108,7 @@ You are not user-facing and your text goes nowhere — do not emit it.
|
|
|
108
108
|
At fire time the Stop hook substitutes both placeholders and sends the filled paragraph as `initialMessage` — a plain string, not a JSON envelope, no instruction prose, no leading object.
|
|
109
109
|
|
|
110
110
|
- `{schema}` ← the concatenated text of `platform/plugins/memory/references/schema-base.md` plus, when `brand.json#vertical` names a vertical file that exists in the same directory, that file's content joined with a blank-line separator (Task 193). When the brand declares `vertical: null`, the field is missing, or the named file is absent, the schema is `schema-base.md` alone — never loud-fail here; Task 193 owns the brand-config gate.
|
|
111
|
-
- `{conversation}` ← a flat rendering of the envelope's `turns` array. Format: one line per turn, `role: text`, wrapped in leading and trailing newlines so each line lands on its own line in the filled prompt.
|
|
111
|
+
- `{conversation}` ← a flat rendering of the envelope's `turns` array. Format: one line per turn, `role: text`, wrapped in leading and trailing newlines so each line lands on its own line in the filled prompt. Task 213 strips the prior inline `[tool: name(args)]` markers on assistant turns — they taught the recorder to copy the shape and emit `<tool_call>` as text instead of real `tool_use` blocks (evidence in sessions d2aaa85e / 481799d1). Assistant turns that only called tools land as an empty-bodied `assistant: ` line; the `toolCallTurns` counter on `[turn-recorder] envelope …` remains the independent observability surface. The user record carrying only `tool_result` blocks does not create a separate transcript line; thinking-only records contribute nothing.
|
|
112
112
|
|
|
113
113
|
The envelope walker still runs (so `[turn-recorder] envelope ...` continues to land for observability and the `trigger-skipped reason=conversation-empty` gate still fires on a turns-empty transcript), but its JSON shape never reaches the model.
|
|
114
114
|
|
|
@@ -200,7 +200,7 @@ Three log-line shapes, all keyed on the same recorder `sessionId`, distinguish t
|
|
|
200
200
|
```
|
|
201
201
|
[pty-spawn-tool-inventory] sessionId=<id> specialist=<name> argv-tools=<n> mcp-listed-tools=<n> exposed=<csv> not-exposed=<csv>
|
|
202
202
|
[recorder-session-init] sessionId=<id> specialist=<name> toolsExposedCount=<n> toolsExposed=<csv> toolsArgvCount=<n> toolsArgvMissingFromExposed=<csv|—> source=<probe>
|
|
203
|
-
[recorder-turn] sessionId=<id> turnIndex=<n> stopReason=<r> textBytes=<n>
|
|
203
|
+
[recorder-turn] sessionId=<id> turnIndex=<n> stopReason=<r> textBytes=<n> text=<full json-escaped> toolUseNames=<csv|—> containsToolCallText=true|false
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
- `[pty-spawn-tool-inventory]` (Task 178) measures the **framework** layer — the argv built and the MCP servers' published tools. Argv-tools count must be 11; mcp-listed-tools must be ≥ 11.
|
|
@@ -209,7 +209,7 @@ Three log-line shapes, all keyed on the same recorder `sessionId`, distinguish t
|
|
|
209
209
|
- `source=headless-argv-fallback` ⇒ Task 185 finding (2026-05-20): headless `claude --verbose` PTY mode (which the database-operator recorder uses) does **not** write any tool-list record to the JSONL — none of the three probe shapes ever appears. The tail falls back to the spawner's `--allowed-tools` argv as the exposed set, independently verified by `[pty-spawn-tool-inventory]`'s `exposed=` field. Treat this source identically to a positive JSONL match: the argv set is authoritative for headless spawns.
|
|
210
210
|
- `toolsArgvMissingFromExposed=—` ⇒ all argv tools made it through; Outcome A is ruled out.
|
|
211
211
|
- Any non-`—` value enumerates the names the SDK dropped and is the diagnosis. Only meaningful when `source` is JSONL-derived; under `headless-argv-fallback` the exposed set equals argv by construction, so this field is always `—`.
|
|
212
|
-
- `[recorder-turn]` measures the **model** layer — Haiku's verbatim words (`textHead`) and the tool names it actually called (`toolUseNames`). `toolUseNames=—` paired with `toolsArgvMissingFromExposed=—` is the Outcome B signature: tools present, model abstained.
|
|
212
|
+
- `[recorder-turn]` measures the **model** layer — Haiku's verbatim words (Task 213: full `text=`, not the prior 240-char `textHead`) and the tool names it actually called (`toolUseNames`). `toolUseNames=—` paired with `toolsArgvMissingFromExposed=—` is the Outcome B signature: tools present, model abstained. `containsToolCallText=true` flags any assistant text that contains `<tool_call>`, `<function_calls>`, or `</invoke>` — the recorder model copying the tool-call shape into prose (the regression Task 213 closed).
|
|
213
213
|
|
|
214
214
|
The three lines are emitted into `server.log` via the platform UI's `/api/admin/log-ingest` loopback route, so a single grep against `server.log` answers the question for any past, present, or future recorder spawn:
|
|
215
215
|
|
|
@@ -222,7 +222,7 @@ grep -E "\[(pty-spawn-tool-inventory|recorder-session-init|recorder-turn|mcp:(me
|
|
|
222
222
|
|
|
223
223
|
1. The single `[pty-spawn-tool-inventory]` line proves what the framework provisioned. If `argv-tools < 11`, the bug is upstream of this evidence chain — the agent file or the spawn site.
|
|
224
224
|
2. The single `[recorder-session-init]` line proves what the model received. If `toolsArgvMissingFromExposed` is non-`—`, the named tools were dropped by the CLI/SDK; that is Outcome A and the fix lives in the `--mcp-config` resolution or the CLI/SDK handshake.
|
|
225
|
-
3. Each `[recorder-turn]` line records one assistant turn. If `toolUseNames=—` across every turn AND `toolsArgvMissingFromExposed=—`, the model could call the tools and chose not to; `
|
|
225
|
+
3. Each `[recorder-turn]` line records one assistant turn. If `toolUseNames=—` across every turn AND `toolsArgvMissingFromExposed=—`, the model could call the tools and chose not to; `text=` carries the model's verbatim reasoning (Task 213 dropped the 240-char head cap so the whole turn is observable from server.log) and is the next surface to read. A `containsToolCallText=true` row is the recorder-relapse signature: the model emitted tool-call shape as prose instead of a real `tool_use` block.
|
|
226
226
|
|
|
227
227
|
**Loopback gate.** The `/api/admin/log-ingest` route is loopback-only on both the socket address and (when present) the `X-Forwarded-For` header. A LAN client whose request reaches the route via a proxy still gets a 403 because XFF tokens are required to be loopback when XFF is set. Without this gate the evidence chain would be worthless — any LAN client could forge `[recorder-session-init]` lines under a spoofed sessionId.
|
|
228
228
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonl-tail.d.ts","sourceRoot":"","sources":["../src/jsonl-tail.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"jsonl-tail.d.ts","sourceRoot":"","sources":["../src/jsonl-tail.ts"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAWxC;;;uDAGuD;AACvD,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAEjG,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,aAAa,CAAA;IAC1B,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,SAAS;IACxB;;qDAEiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,GAAG,IAAI,CAAA;IAC9D;;;8CAG0C;IAC1C,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACzD,iDAAiD;IACjD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,0EAA0E;IAC1E,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CACxG;AAkBD;;oDAEoD;AACpD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAY3F;AA4GD,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,CAkL9D"}
|
|
@@ -22,11 +22,17 @@
|
|
|
22
22
|
// with argvTools as the exposed set and tag the source.
|
|
23
23
|
//
|
|
24
24
|
// Line 2 — `[recorder-turn] sessionId=<id> turnIndex=<n> stopReason=<r>
|
|
25
|
-
// textBytes=<n>
|
|
26
|
-
// toolUseNames=<csv
|
|
25
|
+
// textBytes=<n> text=<full json-escaped>
|
|
26
|
+
// toolUseNames=<csv|—> containsToolCallText=true|false`
|
|
27
27
|
// Emitted once per newly-appended `assistant` record. Records with the
|
|
28
28
|
// same `message.id` collapse to one line (only the first sighting
|
|
29
29
|
// emits). Records that contain only `thinking` blocks emit nothing.
|
|
30
|
+
// `text=` is the full assistant text, json-escaped — Task 213 dropped
|
|
31
|
+
// the 240-char head cap so a recorder relapse that emits
|
|
32
|
+
// `<tool_call>{…}</tool_call>` as text is observable from server.log
|
|
33
|
+
// without downloading the jsonl. `containsToolCallText=true` flags any
|
|
34
|
+
// assistant text containing `<tool_call>`, `<function_calls>`, or
|
|
35
|
+
// `</invoke>` — the recorder model copying tool-call shape into prose.
|
|
30
36
|
//
|
|
31
37
|
// The cursor is append-only: `cursorOffset` is the byte position past the
|
|
32
38
|
// last `\n` we successfully parsed; the trailing fragment lives in
|
|
@@ -35,7 +41,13 @@
|
|
|
35
41
|
// record — exactly the failure mode this task closes.
|
|
36
42
|
import { existsSync, openSync, closeSync, readSync, statSync } from 'node:fs';
|
|
37
43
|
const HEAD_READ_CHUNK = 64 * 1024;
|
|
38
|
-
|
|
44
|
+
// Task 213 — regex against assistant text. Matches `<tool_call>`,
|
|
45
|
+
// `<function_calls>`, and `</invoke>` — three shapes the recorder model
|
|
46
|
+
// emitted as prose in sessions d2aaa85e / 481799d1 (and similar evidence
|
|
47
|
+
// in 596a42bc, c3539844, 4d4775bd, bad79fb9, 06ac2eb3, 6828bae0,
|
|
48
|
+
// c4c10cf3, 5cb8fd16) instead of real `tool_use` blocks. `\b` against the
|
|
49
|
+
// `>` boundary keeps the match precise.
|
|
50
|
+
const TOOL_CALL_TEXT_PATTERN = /<tool_call\b|<function_calls\b|<\/invoke\b/;
|
|
39
51
|
/** Production implementation of LogLinePoster. POSTs to the brand UI's
|
|
40
52
|
* loopback log-ingest route. Fire-and-forget; failure is logged via the
|
|
41
53
|
* manager's own logger and otherwise swallowed. */
|
|
@@ -118,18 +130,10 @@ function probeInitTools(rec) {
|
|
|
118
130
|
/** JSON-escape a string for inclusion in a log line. Renders control
|
|
119
131
|
* characters as `\uXXXX`, quotes the result. Callers strip the quotes
|
|
120
132
|
* if they want; this module keeps them so the boundaries of
|
|
121
|
-
* `
|
|
133
|
+
* `text=...` are explicit on screen. */
|
|
122
134
|
function jsonEscape(s) {
|
|
123
135
|
return JSON.stringify(s);
|
|
124
136
|
}
|
|
125
|
-
/** First N CHARACTERS (not bytes) of a string. The 240-char cap is per
|
|
126
|
-
* spec; we operate on UTF-16 code units which is what JavaScript's
|
|
127
|
-
* default substring semantics already give us. */
|
|
128
|
-
function head(s, max) {
|
|
129
|
-
if (s.length <= max)
|
|
130
|
-
return s;
|
|
131
|
-
return s.slice(0, max);
|
|
132
|
-
}
|
|
133
137
|
function summarizeAssistantRecord(rec) {
|
|
134
138
|
if (rec.type !== 'assistant')
|
|
135
139
|
return null;
|
|
@@ -210,11 +214,13 @@ export function createJsonlTail(deps) {
|
|
|
210
214
|
state.nextTurnIndex += 1;
|
|
211
215
|
const textBytes = Buffer.byteLength(summary.text, 'utf8');
|
|
212
216
|
const namesField = summary.toolUseNames.length === 0 ? '—' : summary.toolUseNames.join(',');
|
|
217
|
+
const containsToolCallText = TOOL_CALL_TEXT_PATTERN.test(summary.text);
|
|
213
218
|
const line = `sessionId=${state.sessionId} turnIndex=${idx}` +
|
|
214
219
|
` stopReason=${summary.stopReason || '—'}` +
|
|
215
220
|
` textBytes=${textBytes}` +
|
|
216
|
-
`
|
|
217
|
-
` toolUseNames=${namesField}
|
|
221
|
+
` text=${jsonEscape(summary.text)}` +
|
|
222
|
+
` toolUseNames=${namesField}` +
|
|
223
|
+
` containsToolCallText=${containsToolCallText}`;
|
|
218
224
|
deps.postLogLine('recorder-turn', 'info', line);
|
|
219
225
|
}
|
|
220
226
|
function processRecord(state, rec) {
|