@rubytech/create-maxy-code 0.1.286 → 0.1.288
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/dist/__tests__/cloudflared-slice.test.js +3 -2
- package/dist/__tests__/cloudflared-version-pin.test.js +24 -0
- package/dist/__tests__/samba-provision.test.js +84 -16
- package/dist/index.js +29 -12
- package/dist/port-resolution.js +4 -3
- package/dist/samba-provision.js +81 -24
- package/dist/uninstall.js +16 -11
- package/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +52 -3
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +55 -0
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +29 -13
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +60 -11
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +30 -17
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +8 -4
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +18 -2
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
- package/payload/platform/plugins/docs/references/deployment.md +1 -1
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/samba.md +25 -12
- package/payload/platform/plugins/whatsapp/PLUGIN.md +4 -0
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +1 -1
- package/payload/platform/scripts/__tests__/prompt-optimiser-audit.test.sh +59 -0
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +47 -10
- package/payload/platform/scripts/installer-device-verify.sh +74 -3
- package/payload/platform/scripts/prompt-optimiser-audit.sh +82 -0
- package/payload/platform/scripts/resume-tunnel.sh +45 -28
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +46 -7
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +49 -9
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +70 -17
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/server/{chunk-QHD5TKLQ.js → chunk-EAJMRICW.js} +21 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/AdminShell-DkP2rJdF.js +1 -0
- package/payload/server/public/assets/{Checkbox-DlwyBGbH.js → Checkbox-CsQq7YPC.js} +1 -1
- package/payload/server/public/assets/{admin-sjHlmwFk.js → admin-C_bkVvXh.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BheZXEzK.js → architectureDiagram-Q4EWVU46-PGePlVpb.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-dYNElb4l.js → blockDiagram-DXYQGD6D-BJCbJRRs.js} +1 -1
- package/payload/server/public/assets/{browser-Dx1D0tI_.js → browser-DynPFG_A.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Q6g8vHmX.js → c4Diagram-AHTNJAMY-DcMhi4gF.js} +1 -1
- package/payload/server/public/assets/channel-idaGQqlP.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-D-Cn77jA.js → chunk-336JU56O-0vhS0MI-.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-BAE0JfL6.js → chunk-426QAEUC-DbVU-p-q.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-BI2HTCey.js → chunk-4TB4RGXK-dgx3qIz_.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-BsWXvze8.js → chunk-5FUZZQ4R-BglTosdT.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-DWO5wTr8.js → chunk-5PVQY5BW-hZB3AxUF.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-BM4DLBML.js → chunk-EDXVE4YY-DXqLJyYa.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-aOt2Re1y.js → chunk-ENJZ2VHE-BD4I_a54.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-_ioQjgQ1.js → chunk-ICPOFSXX-BdQx0Ahc.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-DkuLduF1.js → chunk-OYMX7WX6-BDjtbZvS.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-1ESGrUQ6.js → chunk-U2HBQHQK-hYN7A3g_.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-C0MUqJKJ.js → chunk-X2U36JSP-D3njJ7WG.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-BjIcv7r1.js → chunk-YZCP3GAM-BfZKEcvU.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-LW116-Vw.js → chunk-ZZ45TVLE-DLJMNqLn.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-CmRFpirU.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DFy8xZjP.js +1 -0
- package/payload/server/public/assets/clone-kFPktUPQ.js +1 -0
- package/payload/server/public/assets/{dagre-CxO2ywW2.js → dagre-DVLE4-m6.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-DcPqQxUG.js → dagre-KV5264BT-DGjcX5uz.js} +1 -1
- package/payload/server/public/assets/{data-Ca6hPEtp.js → data-D1FFhz2k.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-B25vZ2cO.js → diagram-5BDNPKRD-CQI2o8Cv.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-p7maXzYr.js → diagram-G4DWMVQ6-C8kXsEdQ.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-Deu8Io1I.js → diagram-MMDJMWI5-DaWrNg_c.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-Cp3l9iiP.js → diagram-TYMM5635-Ql9QPUxX.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-D8_SMZL-.js → erDiagram-SMLLAGMA-B2fBZhPo.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-pECrNCIH.js → flowDiagram-DWJPFMVM-BA0LiOm5.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DAbNRh0p.js → ganttDiagram-T4ZO3ILL-CBKdEgUk.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CHVyAvfu.js → gitGraphDiagram-UUTBAWPF-BIyEfuwB.js} +1 -1
- package/payload/server/public/assets/{graph-ChJs-qY8.js → graph-BxWBNsFs.js} +1 -1
- package/payload/server/public/assets/{graph-labels-B-BTHvfh.js → graph-labels-DSLdDXoF.js} +1 -1
- package/payload/server/public/assets/{graphlib-DS0srZLX.js → graphlib-Cv4SvsQN.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-lioeuA4m.js → infoDiagram-42DDH7IO-DovHLDHz.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-Dq2MKHxM.js → ishikawaDiagram-UXIWVN3A-haFza9s4.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-BTC5zuDZ.js → journeyDiagram-VCZTEJTY-l-8tJ21z.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DQDOg3tk.js → kanban-definition-6JOO6SKY-DSZUvFLs.js} +1 -1
- package/payload/server/public/assets/{line---rBtN__.js → line-mfTElknw.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-BivSpldT.js → mermaid-parser.core-4temHHPS.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-2mrA7rmo.js → mermaid.core-DO2iS9my.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CduFYM-3.js → mindmap-definition-QFDTVHPH-Cymf2IGG.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-s68UJf6f.js → pieDiagram-DEJITSTG-_2tmuvMb.js} +1 -1
- package/payload/server/public/assets/{public-B2WWbnkK.js → public-Rz_GzOrN.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-ykHWuSCg.js → quadrantDiagram-34T5L4WZ-CVCrGnBi.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DaRlmBk1.js → requirementDiagram-MS252O5E-Bc-o8i8Z.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CjSaDmJD.js → sankeyDiagram-XADWPNL6-DrTBxqE6.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-DJcun441.js → sequenceDiagram-FGHM5R23-BjVjSsUJ.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DMLxc8L0.js → stateDiagram-FHFEXIEX-DWAJP6ly.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BHDyrxpj.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Ls4Sv2EN.js → timeline-definition-GMOUNBTQ-Br_nbteH.js} +1 -1
- package/payload/server/public/assets/{useSelectionMode-BiULiLka.css → useSelectionMode-BLb-o9RX.css} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-NpboV334.js → vennDiagram-DHZGUBPP-D_ozeKV6.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-s71AWI0_.js → wardleyDiagram-NUSXRM2D-B87ZKC2B.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DYjbwXgQ.js → xychartDiagram-5P7HB3ND-DosRlk2T.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +5 -5
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +269 -81
- package/payload/server/public/assets/AdminShell-D06jh8Lz.js +0 -1
- package/payload/server/public/assets/channel-yIp47StE.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DGPa3QR8.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-De6lAWAi.js +0 -1
- package/payload/server/public/assets/clone-C29IA5qA.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-a-wkzuCq.js +0 -1
- /package/payload/server/public/assets/{useSelectionMode-DMNfbxHW.js → useSelectionMode-hZ5bdL8b.js} +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Reconciliation audit for the per-turn directive store (Task 746). For each
|
|
3
|
+
# session it emits `[prompt-optimiser-audit] session=<id> injected=<N> stored=<M>`
|
|
4
|
+
# (N = breadcrumb lines, M = content files) and names any breadcrumb whose
|
|
5
|
+
# file= path is missing on disk (the precise silent loss).
|
|
6
|
+
set -u
|
|
7
|
+
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/prompt-optimiser-audit.sh"
|
|
8
|
+
[[ -x "$SCRIPT" ]] || { echo "FAIL: $SCRIPT not executable" >&2; exit 1; }
|
|
9
|
+
PASS=0; FAIL=0
|
|
10
|
+
|
|
11
|
+
LD=$(mktemp -d)
|
|
12
|
+
mkdir -p "$LD/prompt-optimiser-directives/sessA"
|
|
13
|
+
f1="$LD/prompt-optimiser-directives/sessA/100-11.txt"; printf 'one' > "$f1"
|
|
14
|
+
f2="$LD/prompt-optimiser-directives/sessA/101-12.txt"; printf 'two' > "$f2"
|
|
15
|
+
crumb="$LD/prompt-optimiser-directive.log"
|
|
16
|
+
printf 'T [prompt-optimiser-directive] injected len=3 sha256=x file=%s session=sessA\n' "$f1" >> "$crumb"
|
|
17
|
+
printf 'T [prompt-optimiser-directive] injected len=3 sha256=y file=%s session=sessA\n' "$f2" >> "$crumb"
|
|
18
|
+
|
|
19
|
+
# Healthy: 2 breadcrumbs, 2 files.
|
|
20
|
+
out=$(bash "$SCRIPT" --log-dir "$LD" sessA 2>/dev/null)
|
|
21
|
+
if printf '%s' "$out" | grep -q "\[prompt-optimiser-audit\] session=sessA injected=2 stored=2"; then
|
|
22
|
+
echo "PASS: healthy session reconciles injected==stored"; PASS=$((PASS+1))
|
|
23
|
+
else
|
|
24
|
+
echo "FAIL: healthy reconcile wrong: $out" >&2; FAIL=$((FAIL+1))
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Leak: delete one content file -> injected=2 stored=1 + a missing-file line.
|
|
28
|
+
rm -f "$f2"
|
|
29
|
+
out=$(bash "$SCRIPT" --log-dir "$LD" sessA 2>/dev/null)
|
|
30
|
+
if printf '%s' "$out" | grep -q "session=sessA injected=2 stored=1" \
|
|
31
|
+
&& printf '%s' "$out" | grep -q "missing-file=$f2"; then
|
|
32
|
+
echo "PASS: deleted file flagged as leak (injected>stored + missing-file)"; PASS=$((PASS+1))
|
|
33
|
+
else
|
|
34
|
+
echo "FAIL: leak not flagged: $out" >&2; FAIL=$((FAIL+1))
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# No argument -> walks every session in the breadcrumb log.
|
|
38
|
+
printf 'T [prompt-optimiser-directive] injected len=1 sha256=z file=- session=sessB\n' >> "$crumb"
|
|
39
|
+
out=$(bash "$SCRIPT" --log-dir "$LD" 2>/dev/null)
|
|
40
|
+
if printf '%s' "$out" | grep -q "session=sessA " && printf '%s' "$out" | grep -q "session=sessB "; then
|
|
41
|
+
echo "PASS: no-arg mode walks all sessions"; PASS=$((PASS+1))
|
|
42
|
+
else
|
|
43
|
+
echo "FAIL: no-arg mode missed a session: $out" >&2; FAIL=$((FAIL+1))
|
|
44
|
+
fi
|
|
45
|
+
# An explicitly-named session with no breadcrumbs reconciles cleanly to 0==0 on
|
|
46
|
+
# a single line (regression: grep -c's exit-1 must not double-emit "0\n0").
|
|
47
|
+
out=$(bash "$SCRIPT" --log-dir "$LD" sessZ 2>/dev/null); rc=$?
|
|
48
|
+
nlines=$(printf '%s\n' "$out" | grep -c 'session=sessZ injected=')
|
|
49
|
+
if [[ "$rc" -eq 0 ]] \
|
|
50
|
+
&& printf '%s' "$out" | grep -Fqx "[prompt-optimiser-audit] session=sessZ injected=0 stored=0" \
|
|
51
|
+
&& [[ "$nlines" -eq 1 ]]; then
|
|
52
|
+
echo "PASS: empty named session -> single clean injected=0 stored=0, rc=0"; PASS=$((PASS+1))
|
|
53
|
+
else
|
|
54
|
+
echo "FAIL: empty named session mis-reported (rc=$rc lines=$nlines out=$out)" >&2; FAIL=$((FAIL+1))
|
|
55
|
+
fi
|
|
56
|
+
rm -rf "$LD"
|
|
57
|
+
|
|
58
|
+
echo "----"; echo "PASS=$PASS FAIL=$FAIL"
|
|
59
|
+
[[ "$FAIL" -eq 0 ]]
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Tests for resume-tunnel.sh
|
|
2
|
+
# Tests for resume-tunnel.sh supervised-service spawn path (Task 602 + Task 757).
|
|
3
|
+
# Task 602 decoupled the connector into its own transient unit under
|
|
4
|
+
# cloudflared.slice; Task 757 changed that unit from a --scope (no restart
|
|
5
|
+
# policy) to a --service with Restart=always and --no-autoupdate.
|
|
3
6
|
# Stubs systemd-run, systemctl, and cloudflared binaries via a fake $PATH entry.
|
|
4
7
|
set -uo pipefail
|
|
5
8
|
|
|
@@ -80,10 +83,12 @@ exit 0
|
|
|
80
83
|
SH
|
|
81
84
|
chmod +x "$bin_dir/systemctl"
|
|
82
85
|
|
|
83
|
-
# Stub systemd-run:
|
|
86
|
+
# Stub systemd-run: echoes every arg (so tests can assert the unit name, slice,
|
|
87
|
+
# restart properties, and the connector argv), optionally writes connection line.
|
|
84
88
|
cat > "$bin_dir/systemd-run" <<SH
|
|
85
89
|
#!/bin/bash
|
|
86
90
|
for arg in "\$@"; do
|
|
91
|
+
echo "[stub] arg=\$arg"
|
|
87
92
|
case "\$arg" in
|
|
88
93
|
--unit=*) echo "[stub] unit=\${arg#--unit=}" ;;
|
|
89
94
|
--slice=*) echo "[stub] slice=\${arg#--slice=}" ;;
|
|
@@ -120,7 +125,7 @@ case_skip_when_scope_active() {
|
|
|
120
125
|
if [[ $rc -ne 0 ]]; then
|
|
121
126
|
echo " expected exit 0, got $rc"; return 1
|
|
122
127
|
fi
|
|
123
|
-
if ! echo "$log_content" | grep -q "already running.*
|
|
128
|
+
if ! echo "$log_content" | grep -q "already running.*service.*cloudflared-maxy-code.service"; then
|
|
124
129
|
echo " skip branch missing expected log line"
|
|
125
130
|
echo "$log_content"
|
|
126
131
|
return 1
|
|
@@ -149,8 +154,8 @@ case_spawn_succeeds() {
|
|
|
149
154
|
if [[ $rc -ne 0 ]]; then
|
|
150
155
|
echo " expected exit 0, got $rc"; return 1
|
|
151
156
|
fi
|
|
152
|
-
if ! echo "$log_content" | grep -q "spawned
|
|
153
|
-
echo " missing spawned
|
|
157
|
+
if ! echo "$log_content" | grep -q "spawned service=cloudflared-maxy-code.service"; then
|
|
158
|
+
echo " missing spawned service log line"; echo "$log_content"; return 1
|
|
154
159
|
fi
|
|
155
160
|
if ! echo "$log_content" | grep -q "tunnel verified"; then
|
|
156
161
|
echo " missing tunnel verified log line"; echo "$log_content"; return 1
|
|
@@ -186,7 +191,7 @@ case_spawn_no_connection() {
|
|
|
186
191
|
return 0
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
# CASE 4:
|
|
194
|
+
# CASE 4: service unit name derived correctly from configDir (.real-agent → cloudflared-real-agent.service)
|
|
190
195
|
case_scope_unit_name_from_config_dir() {
|
|
191
196
|
local root
|
|
192
197
|
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
@@ -207,8 +212,8 @@ case_scope_unit_name_from_config_dir() {
|
|
|
207
212
|
if [[ $rc -ne 0 ]]; then
|
|
208
213
|
echo " expected exit 0, got $rc"; return 1
|
|
209
214
|
fi
|
|
210
|
-
if ! echo "$log_content" | grep -q "spawned
|
|
211
|
-
echo "
|
|
215
|
+
if ! echo "$log_content" | grep -q "spawned service=cloudflared-real-agent.service"; then
|
|
216
|
+
echo " service unit name not derived correctly from .real-agent"
|
|
212
217
|
echo "$log_content"; return 1
|
|
213
218
|
fi
|
|
214
219
|
return 0
|
|
@@ -240,11 +245,43 @@ case_slice_flag_present() {
|
|
|
240
245
|
return 0
|
|
241
246
|
}
|
|
242
247
|
|
|
243
|
-
|
|
248
|
+
# CASE 6: supervision contract — Restart=always, crash-loop limits, --no-autoupdate,
|
|
249
|
+
# and the unit is a .service (Task 757), all reaching systemd-run.
|
|
250
|
+
case_supervision_contract() {
|
|
251
|
+
local root
|
|
252
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
253
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
254
|
+
|
|
255
|
+
local log="$root/home/.maxy-code/logs/cloudflared.log"
|
|
256
|
+
|
|
257
|
+
HOME="$root/home" \
|
|
258
|
+
PATH="$root/bin:$PATH" \
|
|
259
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
260
|
+
bash "$RESUME"
|
|
261
|
+
|
|
262
|
+
local log_content
|
|
263
|
+
log_content=$(cat "$log" 2>/dev/null || echo "")
|
|
264
|
+
cleanup "$root"
|
|
265
|
+
|
|
266
|
+
local missing=""
|
|
267
|
+
echo "$log_content" | grep -q "\[stub\] arg=--unit=cloudflared-maxy-code.service" || missing="$missing unit=.service"
|
|
268
|
+
echo "$log_content" | grep -q "\[stub\] arg=Restart=always" || missing="$missing Restart=always"
|
|
269
|
+
echo "$log_content" | grep -q "\[stub\] arg=StartLimitIntervalSec=300" || missing="$missing StartLimitIntervalSec"
|
|
270
|
+
echo "$log_content" | grep -q "\[stub\] arg=StartLimitBurst=5" || missing="$missing StartLimitBurst"
|
|
271
|
+
echo "$log_content" | grep -q "\[stub\] arg=--no-autoupdate" || missing="$missing --no-autoupdate"
|
|
272
|
+
if [[ -n "$missing" ]]; then
|
|
273
|
+
echo " systemd-run argv missing:$missing"
|
|
274
|
+
echo "$log_content"; return 1
|
|
275
|
+
fi
|
|
276
|
+
return 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
run_case "skip when service active" case_skip_when_scope_active
|
|
244
280
|
run_case "spawn succeeds and logs verified" case_spawn_succeeds
|
|
245
281
|
run_case "spawn with no connection logs ERROR exits 0" case_spawn_no_connection
|
|
246
|
-
run_case "
|
|
282
|
+
run_case "service unit name derived from configDir" case_scope_unit_name_from_config_dir
|
|
247
283
|
run_case "slice flag passed to systemd-run" case_slice_flag_present
|
|
284
|
+
run_case "supervision contract: restart-always + no-autoupdate + .service" case_supervision_contract
|
|
248
285
|
|
|
249
286
|
echo ""
|
|
250
287
|
echo "Results: $PASS passed, $FAIL failed"
|
|
@@ -229,11 +229,82 @@ REMOTE
|
|
|
229
229
|
CHECK_RC=$?
|
|
230
230
|
set -e
|
|
231
231
|
|
|
232
|
-
if [[ $CHECK_RC -
|
|
233
|
-
echo "OK $NAME — CDP banner present" | tee -a "$SUMMARY_LOG"
|
|
234
|
-
else
|
|
232
|
+
if [[ $CHECK_RC -ne 0 ]]; then
|
|
235
233
|
echo "FAIL $NAME — CDP banner missing (rc=$CHECK_RC, see $SUMMARY_LOG)" | tee -a "$SUMMARY_LOG"
|
|
236
234
|
FAILED=1
|
|
235
|
+
continue
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
# Step 3 — standing Samba socket audit (Task 755). Reconcile the *actual*
|
|
239
|
+
# listening sockets against the bind invariant, independent of any install
|
|
240
|
+
# log, so this catches a drifted or re-opened box as well as a fresh
|
|
241
|
+
# mis-provision. Two assertions:
|
|
242
|
+
# • smbd (TCP :445/:139) listens only on loopback or a private LAN address.
|
|
243
|
+
# A public or wildcard (0.0.0.0 / [::] / public IP) listener is internet
|
|
244
|
+
# exposure — the BSI-reported defect. Private LAN listeners are expected
|
|
245
|
+
# on a Pi (LAN+loopback); loopback-only is expected on a cloud box. The
|
|
246
|
+
# private-vs-public test mirrors isPrivateIPv4() in samba-provision.ts.
|
|
247
|
+
# • nmbd is not active (the NetBIOS DDoS-reflection service is never run).
|
|
248
|
+
# Matched by port, so no root is needed to read process names.
|
|
249
|
+
#
|
|
250
|
+
# Loaded via `read -d ''` rather than `$(cat <<…)`: the audit body contains a
|
|
251
|
+
# `case` and process substitution, whose parens break bash 3.2's command-
|
|
252
|
+
# substitution scanner (operators may run this script from a macOS bash 3.2).
|
|
253
|
+
# The body itself runs on the device's modern bash over ssh.
|
|
254
|
+
IFS= read -r -d '' SOCKET_AUDIT <<'REMOTE' || true
|
|
255
|
+
set -uo pipefail
|
|
256
|
+
|
|
257
|
+
is_private_v4() {
|
|
258
|
+
local ip="$1" a b _rest
|
|
259
|
+
IFS=. read -r a b _rest <<<"$ip"
|
|
260
|
+
[[ "$a" =~ ^[0-9]+$ && "$b" =~ ^[0-9]+$ ]] || return 1
|
|
261
|
+
(( a == 10 )) && return 0
|
|
262
|
+
(( a == 172 && b >= 16 && b <= 31 )) && return 0
|
|
263
|
+
(( a == 192 && b == 168 )) && return 0
|
|
264
|
+
(( a == 100 && b >= 64 && b <= 127 )) && return 0
|
|
265
|
+
(( a == 169 && b == 254 )) && return 0
|
|
266
|
+
return 1
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
BAD=""
|
|
270
|
+
while read -r localaddr; do
|
|
271
|
+
[[ -z "$localaddr" ]] && continue
|
|
272
|
+
host="${localaddr%:*}" # strip :port
|
|
273
|
+
host="${host#'['}"; host="${host%']'}" # strip IPv6 brackets (quoted: '[' is a glob class)
|
|
274
|
+
case "$host" in
|
|
275
|
+
127.*|::1) continue ;; # loopback — allowed
|
|
276
|
+
esac
|
|
277
|
+
if [[ "$host" == *.*.*.* ]] && is_private_v4 "$host"; then
|
|
278
|
+
continue # private IPv4 LAN (Pi) — allowed
|
|
279
|
+
fi
|
|
280
|
+
BAD+="$localaddr"$'\n'
|
|
281
|
+
done < <(ss -tuln 2>/dev/null | awk '{print $5}' | grep -E ':(445|139)$' || true)
|
|
282
|
+
|
|
283
|
+
NMBD=$(systemctl is-active nmbd 2>/dev/null || true)
|
|
284
|
+
FAIL=0
|
|
285
|
+
if [[ -n "${BAD//$'\n'/}" ]]; then
|
|
286
|
+
echo "smbd-exposed-listener:"
|
|
287
|
+
printf '%s' "$BAD"
|
|
288
|
+
FAIL=1
|
|
289
|
+
fi
|
|
290
|
+
if [[ "$NMBD" == "active" ]]; then
|
|
291
|
+
echo "nmbd-active"
|
|
292
|
+
FAIL=1
|
|
293
|
+
fi
|
|
294
|
+
[[ $FAIL -eq 0 ]] && echo "socket-audit-ok smbd=loopback-or-private nmbd=$NMBD"
|
|
295
|
+
exit $FAIL
|
|
296
|
+
REMOTE
|
|
297
|
+
|
|
298
|
+
set +e
|
|
299
|
+
run_ssh "$TARGET" "$PASS" "$SOCKET_AUDIT" >>"$SUMMARY_LOG" 2>&1
|
|
300
|
+
AUDIT_RC=$?
|
|
301
|
+
set -e
|
|
302
|
+
|
|
303
|
+
if [[ $AUDIT_RC -eq 0 ]]; then
|
|
304
|
+
echo "OK $NAME — CDP banner present; smbd loopback/LAN-only, nmbd inactive" | tee -a "$SUMMARY_LOG"
|
|
305
|
+
else
|
|
306
|
+
echo "FAIL $NAME — Samba socket audit failed (rc=$AUDIT_RC, see $SUMMARY_LOG)" | tee -a "$SUMMARY_LOG"
|
|
307
|
+
FAILED=1
|
|
237
308
|
fi
|
|
238
309
|
done
|
|
239
310
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# prompt-optimiser-audit.sh — reconcile per-turn directive-injection breadcrumbs
|
|
3
|
+
# against the stored content files (Task 746). A missing content write emits no
|
|
4
|
+
# error on its own, so this standing check is the leak detector: for each
|
|
5
|
+
# session it prints
|
|
6
|
+
# [prompt-optimiser-audit] session=<id> injected=<N> stored=<M>
|
|
7
|
+
# where N is the count of breadcrumb injection lines and M is the count of
|
|
8
|
+
# content files. N != M is the leak signature. Every breadcrumb whose file=
|
|
9
|
+
# path is absent on disk is named on its own `missing-file=` line.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# prompt-optimiser-audit.sh [--log-dir <dir>] [<session_id>]
|
|
13
|
+
# With no <session_id> it walks every session that appears in the breadcrumb
|
|
14
|
+
# log. --log-dir overrides the resolved account logs dir (used by tests); the
|
|
15
|
+
# default resolves the single account's logs dir, the same dir the hook writes.
|
|
16
|
+
#
|
|
17
|
+
# Exit codes:
|
|
18
|
+
# 0 every reported session reconciles (injected == stored)
|
|
19
|
+
# 1 at least one session leaks (injected != stored)
|
|
20
|
+
# 2 no log dir could be resolved
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
LOG_DIR=""
|
|
24
|
+
SESSION=""
|
|
25
|
+
while [ $# -gt 0 ]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--log-dir) LOG_DIR="$2"; shift 2 ;;
|
|
28
|
+
*) SESSION="$1"; shift ;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
# Resolve the account logs dir when not overridden: {installDir}/data/accounts/*/logs.
|
|
33
|
+
if [ -z "$LOG_DIR" ]; then
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
PLATFORM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
36
|
+
INSTALL_DIR="$(dirname "$PLATFORM_ROOT")"
|
|
37
|
+
for d in "$INSTALL_DIR"/data/accounts/*/logs; do
|
|
38
|
+
[ -d "$d" ] && LOG_DIR="$d" && break
|
|
39
|
+
done
|
|
40
|
+
fi
|
|
41
|
+
if [ -z "$LOG_DIR" ] || [ ! -d "$LOG_DIR" ]; then
|
|
42
|
+
echo "[prompt-optimiser-audit] no-log-dir resolved=${LOG_DIR:-<none>}" >&2
|
|
43
|
+
exit 2
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
CRUMB="$LOG_DIR/prompt-optimiser-directive.log"
|
|
47
|
+
STORE_ROOT="$LOG_DIR/prompt-optimiser-directives"
|
|
48
|
+
|
|
49
|
+
# Sessions to report: the explicit arg, else every distinct non-"?" session in
|
|
50
|
+
# the breadcrumb log (the breadcrumb's session= is always the last field).
|
|
51
|
+
sessions() {
|
|
52
|
+
if [ -n "$SESSION" ]; then
|
|
53
|
+
echo "$SESSION"; return
|
|
54
|
+
fi
|
|
55
|
+
[ -f "$CRUMB" ] || return
|
|
56
|
+
grep -o 'session=[^ ]*$' "$CRUMB" 2>/dev/null | cut -d= -f2 | grep -v '^?$' | sort -u
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
rc=0
|
|
60
|
+
while IFS= read -r sid; do
|
|
61
|
+
[ -n "$sid" ] || continue
|
|
62
|
+
injected=0
|
|
63
|
+
# grep -c prints a count AND exits 1 on zero matches, so a `|| echo 0` would
|
|
64
|
+
# double-emit "0\n0"; capture the count and default an empty/unreadable file.
|
|
65
|
+
if [ -f "$CRUMB" ]; then
|
|
66
|
+
injected=$(grep -c "session=$sid\$" "$CRUMB" 2>/dev/null)
|
|
67
|
+
injected=${injected:-0}
|
|
68
|
+
fi
|
|
69
|
+
stored=0
|
|
70
|
+
[ -d "$STORE_ROOT/$sid" ] && stored=$(find "$STORE_ROOT/$sid" -maxdepth 1 -name '*.txt' 2>/dev/null | wc -l | tr -d ' ')
|
|
71
|
+
echo "[prompt-optimiser-audit] session=$sid injected=$injected stored=$stored"
|
|
72
|
+
[ "$injected" != "$stored" ] && rc=1
|
|
73
|
+
# Name every breadcrumb whose stored file is absent.
|
|
74
|
+
if [ -f "$CRUMB" ]; then
|
|
75
|
+
grep "session=$sid\$" "$CRUMB" 2>/dev/null | grep -o 'file=[^ ]*' | cut -d= -f2- | while IFS= read -r f; do
|
|
76
|
+
[ "$f" = "-" ] && continue
|
|
77
|
+
[ -e "$f" ] || echo "[prompt-optimiser-audit] session=$sid missing-file=$f"
|
|
78
|
+
done
|
|
79
|
+
fi
|
|
80
|
+
done < <(sessions)
|
|
81
|
+
|
|
82
|
+
exit "$rc"
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# resume-tunnel.sh — Resume the Cloudflare tunnel on brand-service start.
|
|
3
3
|
#
|
|
4
|
-
# Spawns cloudflared into its own transient systemd
|
|
4
|
+
# Spawns cloudflared into its own transient systemd SERVICE (cloudflared-<brand>.service)
|
|
5
5
|
# under cloudflared.slice, completely decoupled from the brand service's cgroup.
|
|
6
|
-
# A brand-service restart can no longer reap the tunnel
|
|
6
|
+
# A brand-service restart can no longer reap the tunnel (Task 602's cgroup
|
|
7
|
+
# decoupling). The unit is a Restart=always service (Task 757), so systemd
|
|
8
|
+
# resurrects the connector on ANY exit — crash, OOM, manual kill — and the
|
|
9
|
+
# connector runs with --no-autoupdate so it never replaces itself; the binary
|
|
10
|
+
# version is installer-owned. This is still the single spawner: only the unit
|
|
11
|
+
# type changed from a transient --scope (no restart policy) to a supervised
|
|
12
|
+
# --service. No second spawner is introduced.
|
|
7
13
|
#
|
|
8
14
|
# Called as ExecStartPre in maxy.service. All failures exit 0 — this script must
|
|
9
15
|
# never prevent maxy.service from starting.
|
|
@@ -49,16 +55,16 @@ if [ -z "$TUNNEL_ID" ]; then
|
|
|
49
55
|
exit 0
|
|
50
56
|
fi
|
|
51
57
|
|
|
52
|
-
# Derive
|
|
53
|
-
# .maxy-code → cloudflared-maxy-code.
|
|
58
|
+
# Derive service unit name: strip leading dot from CONFIG_DIR.
|
|
59
|
+
# .maxy-code → cloudflared-maxy-code.service
|
|
54
60
|
BRAND="${CONFIG_DIR#.}"
|
|
55
|
-
|
|
61
|
+
SERVICE_UNIT="cloudflared-${BRAND}.service"
|
|
56
62
|
|
|
57
|
-
# Liveness check: query the
|
|
58
|
-
# The
|
|
59
|
-
if systemctl --user is-active "$
|
|
60
|
-
|
|
61
|
-
log "Tunnel already running (
|
|
63
|
+
# Liveness check: query the service unit state, not a recorded PID.
|
|
64
|
+
# The service is owned by user-systemd, fully outside the brand service's cgroup.
|
|
65
|
+
if systemctl --user is-active "$SERVICE_UNIT" >/dev/null 2>&1; then
|
|
66
|
+
SERVICE_PID=$(systemctl --user show "$SERVICE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
|
|
67
|
+
log "Tunnel already running (service $SERVICE_UNIT active, MainPID $SERVICE_PID) — skipping resume"
|
|
62
68
|
exit 0
|
|
63
69
|
fi
|
|
64
70
|
|
|
@@ -82,26 +88,37 @@ log "Resuming tunnel $TUNNEL_ID for $DOMAIN..."
|
|
|
82
88
|
mkdir -p "$LOG_DIR"
|
|
83
89
|
LOG_OFFSET=$(wc -c < "$LOG_FILE" 2>/dev/null | awk '{print $1+0}' || echo 0)
|
|
84
90
|
|
|
85
|
-
# Spawn into a transient
|
|
86
|
-
#
|
|
87
|
-
# systemd
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
+
# Spawn into a SUPERVISED transient service under cloudflared.slice (Task 757).
|
|
92
|
+
# Unlike a --scope (no restart policy), a Restart=always service is resurrected
|
|
93
|
+
# by systemd on ANY exit — crash, OOM, manual kill. --no-autoupdate stops the
|
|
94
|
+
# connector replacing its own binary, so the version stays installer-owned.
|
|
95
|
+
# StartLimitIntervalSec/StartLimitBurst trip a genuinely broken binary to
|
|
96
|
+
# `failed` instead of looping forever (5 starts within 300s → failed).
|
|
97
|
+
# A service's stdout/stderr go to the journal, not this shell, so route them to
|
|
98
|
+
# LOG_FILE via StandardOutput/StandardError=append so the verification gate
|
|
99
|
+
# below and the operator's cloudflared.log keep working. systemd-run returns
|
|
100
|
+
# once the unit is active (it does not block for the process lifetime), so there
|
|
101
|
+
# is no shell child to background. --quiet suppresses systemd-run's "Running as
|
|
102
|
+
# unit:" line; the trailing redirect only captures any error output it prints.
|
|
91
103
|
systemd-run \
|
|
92
104
|
--user \
|
|
93
|
-
--scope \
|
|
94
105
|
--quiet \
|
|
95
|
-
--unit="$
|
|
106
|
+
--unit="$SERVICE_UNIT" \
|
|
96
107
|
--slice=cloudflared \
|
|
108
|
+
-p Restart=always \
|
|
109
|
+
-p RestartSec=2 \
|
|
110
|
+
-p StartLimitIntervalSec=300 \
|
|
111
|
+
-p StartLimitBurst=5 \
|
|
97
112
|
-p TimeoutStopSec=15 \
|
|
113
|
+
-p "StandardOutput=append:$LOG_FILE" \
|
|
114
|
+
-p "StandardError=append:$LOG_FILE" \
|
|
98
115
|
-- \
|
|
99
|
-
"$CLOUDFLARED_BIN" --origincert "$CERT_PATH" --config "$CONFIG_PATH" tunnel run \
|
|
100
|
-
>> "$LOG_FILE" 2>&1
|
|
116
|
+
"$CLOUDFLARED_BIN" --no-autoupdate --origincert "$CERT_PATH" --config "$CONFIG_PATH" tunnel run \
|
|
117
|
+
>> "$LOG_FILE" 2>&1
|
|
101
118
|
|
|
102
|
-
# Log spawn receipt so the operator can distinguish the
|
|
103
|
-
|
|
104
|
-
log "spawned
|
|
119
|
+
# Log spawn receipt so the operator can distinguish the supervised path from legacy nohup.
|
|
120
|
+
SERVICE_PID=$(systemctl --user show "$SERVICE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
|
|
121
|
+
log "spawned service=$SERVICE_UNIT slice=cloudflared main-pid=$SERVICE_PID"
|
|
105
122
|
|
|
106
123
|
# Post-spawn connection verification gate.
|
|
107
124
|
# Polls only new log content (from LOG_OFFSET onwards) to avoid false positives
|
|
@@ -127,16 +144,16 @@ done
|
|
|
127
144
|
if [ "$VERIFIED" -eq 1 ]; then
|
|
128
145
|
ELAPSED=$(( $(date +%s) - VERIFY_START ))
|
|
129
146
|
CONN_COUNT=$(tail -c "+$((LOG_OFFSET + 1))" "$LOG_FILE" 2>/dev/null | grep -c "Registered tunnel connection" || echo "?")
|
|
130
|
-
log "tunnel verified
|
|
147
|
+
log "tunnel verified service=$SERVICE_UNIT connections=$CONN_COUNT ms=$(( ELAPSED * 1000 ))"
|
|
131
148
|
else
|
|
132
|
-
log "ERROR: tunnel
|
|
149
|
+
log "ERROR: tunnel service started but no edge connection within ${VERIFY_DEADLINE}s — service=$SERVICE_UNIT"
|
|
133
150
|
fi
|
|
134
151
|
|
|
135
|
-
# Update state file: keep pid/startedAt for observability, add
|
|
152
|
+
# Update state file: keep pid/startedAt for observability, add serviceUnit as control signal.
|
|
136
153
|
NEW_STATE=$(echo "$STATE" | jq \
|
|
137
|
-
--arg unit "$
|
|
154
|
+
--arg unit "$SERVICE_UNIT" \
|
|
138
155
|
--argjson ts "$(date +%s)000" \
|
|
139
|
-
'.
|
|
156
|
+
'.serviceUnit = $unit | .startedAt = $ts' 2>/dev/null || echo "")
|
|
140
157
|
|
|
141
158
|
if [ -n "$NEW_STATE" ] && echo "$NEW_STATE" > "$STATE_FILE.tmp" 2>/dev/null && mv "$STATE_FILE.tmp" "$STATE_FILE" 2>/dev/null; then
|
|
142
159
|
:
|
|
@@ -16,16 +16,34 @@ export interface InboundPayload {
|
|
|
16
16
|
* mirror of platform/ui's MediaRef — this package never cross-imports it. */
|
|
17
17
|
media: MediaInbound[];
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Task 753 — the standing select-and-dispatch instruction prepended to every
|
|
21
|
+
* channel turn. The inbound message is demoted to payload and the turn becomes
|
|
22
|
+
* a routing question: pick the owning specialist (dispatch via the Agent tool),
|
|
23
|
+
* else load the owning skill, else answer directly. Freestyle inline authoring
|
|
24
|
+
* is the `direct` floor, reached only when nothing owns the work — which is why
|
|
25
|
+
* a greeting/confirmation needs no separate skip: it falls through to `direct`.
|
|
26
|
+
* Exported so the unit tests assert the exact wrapped shape.
|
|
27
|
+
*/
|
|
28
|
+
export declare const REFRAME_PREFIX: string;
|
|
29
|
+
/** Task 753 — the reframe decision for one inbound: the wrapped `content` the
|
|
30
|
+
* agent receives, and `payloadBytes`, the byte length of the inner payload
|
|
31
|
+
* (message + woven media notes, excluding the wrapper) for the observability
|
|
32
|
+
* line. */
|
|
33
|
+
export interface ComposedInbound {
|
|
34
|
+
content: string;
|
|
35
|
+
payloadBytes: number;
|
|
36
|
+
}
|
|
19
37
|
/**
|
|
20
38
|
* Shape an inbound WhatsApp payload as a Claude Code channel notification.
|
|
21
39
|
*
|
|
22
|
-
* The event carries ONLY the message text — no `meta`. The sender,
|
|
23
|
-
* and WhatsApp message id are all held server-side (the reply is
|
|
24
|
-
* server's fixed sender), so none of it needs to reach the
|
|
25
|
-
* Two reasons: (1) data minimisation — the operator's phone
|
|
26
|
-
* handed to Claude; (2) a bare event
|
|
27
|
-
*
|
|
28
|
-
*
|
|
40
|
+
* The event carries ONLY the reframed message text — no `meta`. The sender,
|
|
41
|
+
* session id, and WhatsApp message id are all held server-side (the reply is
|
|
42
|
+
* pinned to the server's fixed sender), so none of it needs to reach the
|
|
43
|
+
* model's context. Two reasons: (1) data minimisation — the operator's phone
|
|
44
|
+
* number is never handed to Claude; (2) a bare event gives the agent nothing to
|
|
45
|
+
* narrate, so it acts on the message instead of announcing "incoming message
|
|
46
|
+
* from +44 …".
|
|
29
47
|
*/
|
|
30
48
|
export declare function buildInboundNotification(p: InboundPayload): {
|
|
31
49
|
method: "notifications/claude/channel";
|
|
@@ -33,6 +51,15 @@ export declare function buildInboundNotification(p: InboundPayload): {
|
|
|
33
51
|
content: string;
|
|
34
52
|
};
|
|
35
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Task 753 — wrap the composed payload in {@link REFRAME_PREFIX} so the agent
|
|
56
|
+
* routes the request instead of authoring it inline. Every channel turn is
|
|
57
|
+
* wrapped — there is no deterministic trivia-skip (operator decision): a
|
|
58
|
+
* greeting is handled by the cascade's `direct` branch, not by a pre-classifier
|
|
59
|
+
* here. Returns the wrapped `content` plus `payloadBytes` (the inner payload's
|
|
60
|
+
* byte length) for the channel server's `[reframe]` observability line.
|
|
61
|
+
*/
|
|
62
|
+
export declare function composeAdminContent(p: InboundPayload): ComposedInbound;
|
|
36
63
|
/**
|
|
37
64
|
* Defence in depth: this server serves exactly one sender. Even though the
|
|
38
65
|
* gateway only streams that sender's messages, refuse anything else before
|
|
@@ -51,6 +78,18 @@ export declare function buildReplyPostBody(mySenderId: string, text: string): {
|
|
|
51
78
|
senderId: string;
|
|
52
79
|
text: string;
|
|
53
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* Build the body for POST /wa-channel/reply-document — the file-reply the
|
|
83
|
+
* channel server posts when the agent calls the `reply-document` tool. Like the
|
|
84
|
+
* text reply, the senderId is the server's fixed sender (from spawn env), never
|
|
85
|
+
* model-supplied, so a compromised turn cannot redirect a document to a
|
|
86
|
+
* different recipient. The caption is omitted entirely when absent.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildReplyDocumentPostBody(mySenderId: string, files: string[], caption?: string): {
|
|
89
|
+
senderId: string;
|
|
90
|
+
files: string[];
|
|
91
|
+
caption?: string;
|
|
92
|
+
};
|
|
54
93
|
/**
|
|
55
94
|
* Build the body for POST /wa-channel/received — the receipt ack the channel
|
|
56
95
|
* server sends after it has emitted a drained inbound to the agent. It confirms
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification.d.ts","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB;;;kFAG8E;IAC9E,KAAK,EAAE,YAAY,EAAE,CAAA;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,cAAc;;;;;EAOzD;AA0BD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAElF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvG;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAE3C;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAC/B,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,GAChF,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED;;;;gCAIgC;AAChC,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAMrE"}
|
|
1
|
+
{"version":3,"file":"notification.d.ts","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB;;;kFAG8E;IAC9E,KAAK,EAAE,YAAY,EAAE,CAAA;CACtB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,QAIY,CAAA;AAEvC;;;YAGY;AACZ,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,cAAc;;;;;EAOzD;AA0BD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,cAAc,GAAG,eAAe,CAMtE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAElF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvG;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE,MAAM,GACf;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAIzD;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAE3C;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,EAC/B,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,GAChF,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED;;;;gCAIgC;AAChC,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAMrE"}
|
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
// Pure helpers for the WhatsApp channel server. No MCP SDK import here, so
|
|
2
2
|
// these can be unit-tested without the SDK installed and so the server's
|
|
3
3
|
// transport wiring (server.ts) stays separable from its message shaping.
|
|
4
|
+
/**
|
|
5
|
+
* Task 753 — the standing select-and-dispatch instruction prepended to every
|
|
6
|
+
* channel turn. The inbound message is demoted to payload and the turn becomes
|
|
7
|
+
* a routing question: pick the owning specialist (dispatch via the Agent tool),
|
|
8
|
+
* else load the owning skill, else answer directly. Freestyle inline authoring
|
|
9
|
+
* is the `direct` floor, reached only when nothing owns the work — which is why
|
|
10
|
+
* a greeting/confirmation needs no separate skip: it falls through to `direct`.
|
|
11
|
+
* Exported so the unit tests assert the exact wrapped shape.
|
|
12
|
+
*/
|
|
13
|
+
export const REFRAME_PREFIX = 'Choose the installed specialist that owns this request and dispatch it with the Agent tool. ' +
|
|
14
|
+
'If no specialist fits, load the owning skill. ' +
|
|
15
|
+
'If neither fits (a greeting, a quick question, a confirmation, a direct continuation), just answer. ' +
|
|
16
|
+
'Then carry it out. Request follows:';
|
|
4
17
|
/**
|
|
5
18
|
* Shape an inbound WhatsApp payload as a Claude Code channel notification.
|
|
6
19
|
*
|
|
7
|
-
* The event carries ONLY the message text — no `meta`. The sender,
|
|
8
|
-
* and WhatsApp message id are all held server-side (the reply is
|
|
9
|
-
* server's fixed sender), so none of it needs to reach the
|
|
10
|
-
* Two reasons: (1) data minimisation — the operator's phone
|
|
11
|
-
* handed to Claude; (2) a bare event
|
|
12
|
-
*
|
|
13
|
-
*
|
|
20
|
+
* The event carries ONLY the reframed message text — no `meta`. The sender,
|
|
21
|
+
* session id, and WhatsApp message id are all held server-side (the reply is
|
|
22
|
+
* pinned to the server's fixed sender), so none of it needs to reach the
|
|
23
|
+
* model's context. Two reasons: (1) data minimisation — the operator's phone
|
|
24
|
+
* number is never handed to Claude; (2) a bare event gives the agent nothing to
|
|
25
|
+
* narrate, so it acts on the message instead of announcing "incoming message
|
|
26
|
+
* from +44 …".
|
|
14
27
|
*/
|
|
15
28
|
export function buildInboundNotification(p) {
|
|
16
29
|
return {
|
|
17
30
|
method: 'notifications/claude/channel',
|
|
18
31
|
params: {
|
|
19
|
-
content: composeAdminContent(p),
|
|
32
|
+
content: composeAdminContent(p).content,
|
|
20
33
|
},
|
|
21
34
|
};
|
|
22
35
|
}
|
|
@@ -30,7 +43,7 @@ export function buildInboundNotification(p) {
|
|
|
30
43
|
* `p.text`. Only the `/tmp` file paths are added to Claude's context — never
|
|
31
44
|
* the sender number, preserving the data-minimisation invariant above.
|
|
32
45
|
*/
|
|
33
|
-
function
|
|
46
|
+
function composePayload(p) {
|
|
34
47
|
const openable = (p.media ?? []).filter((m) => m.type !== 'audio');
|
|
35
48
|
if (openable.length === 0)
|
|
36
49
|
return p.text;
|
|
@@ -41,6 +54,21 @@ function composeAdminContent(p) {
|
|
|
41
54
|
const body = notes.join('\n\n');
|
|
42
55
|
return caption ? `${caption}\n\n${body}` : body;
|
|
43
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Task 753 — wrap the composed payload in {@link REFRAME_PREFIX} so the agent
|
|
59
|
+
* routes the request instead of authoring it inline. Every channel turn is
|
|
60
|
+
* wrapped — there is no deterministic trivia-skip (operator decision): a
|
|
61
|
+
* greeting is handled by the cascade's `direct` branch, not by a pre-classifier
|
|
62
|
+
* here. Returns the wrapped `content` plus `payloadBytes` (the inner payload's
|
|
63
|
+
* byte length) for the channel server's `[reframe]` observability line.
|
|
64
|
+
*/
|
|
65
|
+
export function composeAdminContent(p) {
|
|
66
|
+
const payload = composePayload(p);
|
|
67
|
+
return {
|
|
68
|
+
content: `${REFRAME_PREFIX}\n\n${payload}`,
|
|
69
|
+
payloadBytes: Buffer.byteLength(payload, 'utf8'),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
44
72
|
/**
|
|
45
73
|
* Defence in depth: this server serves exactly one sender. Even though the
|
|
46
74
|
* gateway only streams that sender's messages, refuse anything else before
|
|
@@ -58,6 +86,18 @@ export function acceptsSender(mySenderId, p) {
|
|
|
58
86
|
export function buildReplyPostBody(mySenderId, text) {
|
|
59
87
|
return { senderId: mySenderId, text };
|
|
60
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Build the body for POST /wa-channel/reply-document — the file-reply the
|
|
91
|
+
* channel server posts when the agent calls the `reply-document` tool. Like the
|
|
92
|
+
* text reply, the senderId is the server's fixed sender (from spawn env), never
|
|
93
|
+
* model-supplied, so a compromised turn cannot redirect a document to a
|
|
94
|
+
* different recipient. The caption is omitted entirely when absent.
|
|
95
|
+
*/
|
|
96
|
+
export function buildReplyDocumentPostBody(mySenderId, files, caption) {
|
|
97
|
+
return caption !== undefined
|
|
98
|
+
? { senderId: mySenderId, files, caption }
|
|
99
|
+
: { senderId: mySenderId, files };
|
|
100
|
+
}
|
|
61
101
|
/**
|
|
62
102
|
* Build the body for POST /wa-channel/received — the receipt ack the channel
|
|
63
103
|
* server sends after it has emitted a drained inbound to the agent. It confirms
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification.js","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,yEAAyE;AACzE,yEAAyE;AAsBzE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,CAAiB;IACxD,OAAO;QACL,MAAM,EAAE,8BAAuC;QAC/C,MAAM,EAAE;YACN,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"notification.js","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"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"}
|