@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.
Files changed (107) hide show
  1. package/dist/__tests__/cloudflared-slice.test.js +3 -2
  2. package/dist/__tests__/cloudflared-version-pin.test.js +24 -0
  3. package/dist/__tests__/samba-provision.test.js +84 -16
  4. package/dist/index.js +29 -12
  5. package/dist/port-resolution.js +4 -3
  6. package/dist/samba-provision.js +81 -24
  7. package/dist/uninstall.js +16 -11
  8. package/package.json +1 -1
  9. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  10. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +52 -3
  11. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +55 -0
  12. package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +29 -13
  13. package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +60 -11
  14. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +30 -17
  15. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +8 -4
  16. package/payload/platform/plugins/cloudflare/references/manual-setup.md +18 -2
  17. package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
  18. package/payload/platform/plugins/docs/references/deployment.md +1 -1
  19. package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
  20. package/payload/platform/plugins/docs/references/samba.md +25 -12
  21. package/payload/platform/plugins/whatsapp/PLUGIN.md +4 -0
  22. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +1 -1
  23. package/payload/platform/scripts/__tests__/prompt-optimiser-audit.test.sh +59 -0
  24. package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +47 -10
  25. package/payload/platform/scripts/installer-device-verify.sh +74 -3
  26. package/payload/platform/scripts/prompt-optimiser-audit.sh +82 -0
  27. package/payload/platform/scripts/resume-tunnel.sh +45 -28
  28. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +46 -7
  29. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  30. package/payload/platform/services/whatsapp-channel/dist/notification.js +49 -9
  31. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  32. package/payload/platform/services/whatsapp-channel/dist/server.js +70 -17
  33. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  34. package/payload/server/{chunk-QHD5TKLQ.js → chunk-EAJMRICW.js} +21 -0
  35. package/payload/server/maxy-edge.js +1 -1
  36. package/payload/server/public/assets/AdminShell-DkP2rJdF.js +1 -0
  37. package/payload/server/public/assets/{Checkbox-DlwyBGbH.js → Checkbox-CsQq7YPC.js} +1 -1
  38. package/payload/server/public/assets/{admin-sjHlmwFk.js → admin-C_bkVvXh.js} +1 -1
  39. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BheZXEzK.js → architectureDiagram-Q4EWVU46-PGePlVpb.js} +1 -1
  40. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-dYNElb4l.js → blockDiagram-DXYQGD6D-BJCbJRRs.js} +1 -1
  41. package/payload/server/public/assets/{browser-Dx1D0tI_.js → browser-DynPFG_A.js} +1 -1
  42. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Q6g8vHmX.js → c4Diagram-AHTNJAMY-DcMhi4gF.js} +1 -1
  43. package/payload/server/public/assets/channel-idaGQqlP.js +1 -0
  44. package/payload/server/public/assets/{chunk-336JU56O-D-Cn77jA.js → chunk-336JU56O-0vhS0MI-.js} +2 -2
  45. package/payload/server/public/assets/{chunk-426QAEUC-BAE0JfL6.js → chunk-426QAEUC-DbVU-p-q.js} +1 -1
  46. package/payload/server/public/assets/{chunk-4TB4RGXK-BI2HTCey.js → chunk-4TB4RGXK-dgx3qIz_.js} +1 -1
  47. package/payload/server/public/assets/{chunk-5FUZZQ4R-BsWXvze8.js → chunk-5FUZZQ4R-BglTosdT.js} +1 -1
  48. package/payload/server/public/assets/{chunk-5PVQY5BW-DWO5wTr8.js → chunk-5PVQY5BW-hZB3AxUF.js} +1 -1
  49. package/payload/server/public/assets/{chunk-EDXVE4YY-BM4DLBML.js → chunk-EDXVE4YY-DXqLJyYa.js} +1 -1
  50. package/payload/server/public/assets/{chunk-ENJZ2VHE-aOt2Re1y.js → chunk-ENJZ2VHE-BD4I_a54.js} +1 -1
  51. package/payload/server/public/assets/{chunk-ICPOFSXX-_ioQjgQ1.js → chunk-ICPOFSXX-BdQx0Ahc.js} +1 -1
  52. package/payload/server/public/assets/{chunk-OYMX7WX6-DkuLduF1.js → chunk-OYMX7WX6-BDjtbZvS.js} +1 -1
  53. package/payload/server/public/assets/{chunk-U2HBQHQK-1ESGrUQ6.js → chunk-U2HBQHQK-hYN7A3g_.js} +1 -1
  54. package/payload/server/public/assets/{chunk-X2U36JSP-C0MUqJKJ.js → chunk-X2U36JSP-D3njJ7WG.js} +1 -1
  55. package/payload/server/public/assets/{chunk-YZCP3GAM-BjIcv7r1.js → chunk-YZCP3GAM-BfZKEcvU.js} +1 -1
  56. package/payload/server/public/assets/{chunk-ZZ45TVLE-LW116-Vw.js → chunk-ZZ45TVLE-DLJMNqLn.js} +1 -1
  57. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CmRFpirU.js +1 -0
  58. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DFy8xZjP.js +1 -0
  59. package/payload/server/public/assets/clone-kFPktUPQ.js +1 -0
  60. package/payload/server/public/assets/{dagre-CxO2ywW2.js → dagre-DVLE4-m6.js} +1 -1
  61. package/payload/server/public/assets/{dagre-KV5264BT-DcPqQxUG.js → dagre-KV5264BT-DGjcX5uz.js} +1 -1
  62. package/payload/server/public/assets/{data-Ca6hPEtp.js → data-D1FFhz2k.js} +1 -1
  63. package/payload/server/public/assets/{diagram-5BDNPKRD-B25vZ2cO.js → diagram-5BDNPKRD-CQI2o8Cv.js} +1 -1
  64. package/payload/server/public/assets/{diagram-G4DWMVQ6-p7maXzYr.js → diagram-G4DWMVQ6-C8kXsEdQ.js} +1 -1
  65. package/payload/server/public/assets/{diagram-MMDJMWI5-Deu8Io1I.js → diagram-MMDJMWI5-DaWrNg_c.js} +1 -1
  66. package/payload/server/public/assets/{diagram-TYMM5635-Cp3l9iiP.js → diagram-TYMM5635-Ql9QPUxX.js} +1 -1
  67. package/payload/server/public/assets/{erDiagram-SMLLAGMA-D8_SMZL-.js → erDiagram-SMLLAGMA-B2fBZhPo.js} +1 -1
  68. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-pECrNCIH.js → flowDiagram-DWJPFMVM-BA0LiOm5.js} +1 -1
  69. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DAbNRh0p.js → ganttDiagram-T4ZO3ILL-CBKdEgUk.js} +1 -1
  70. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CHVyAvfu.js → gitGraphDiagram-UUTBAWPF-BIyEfuwB.js} +1 -1
  71. package/payload/server/public/assets/{graph-ChJs-qY8.js → graph-BxWBNsFs.js} +1 -1
  72. package/payload/server/public/assets/{graph-labels-B-BTHvfh.js → graph-labels-DSLdDXoF.js} +1 -1
  73. package/payload/server/public/assets/{graphlib-DS0srZLX.js → graphlib-Cv4SvsQN.js} +1 -1
  74. package/payload/server/public/assets/{infoDiagram-42DDH7IO-lioeuA4m.js → infoDiagram-42DDH7IO-DovHLDHz.js} +1 -1
  75. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-Dq2MKHxM.js → ishikawaDiagram-UXIWVN3A-haFza9s4.js} +1 -1
  76. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-BTC5zuDZ.js → journeyDiagram-VCZTEJTY-l-8tJ21z.js} +1 -1
  77. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DQDOg3tk.js → kanban-definition-6JOO6SKY-DSZUvFLs.js} +1 -1
  78. package/payload/server/public/assets/{line---rBtN__.js → line-mfTElknw.js} +1 -1
  79. package/payload/server/public/assets/{mermaid-parser.core-BivSpldT.js → mermaid-parser.core-4temHHPS.js} +1 -1
  80. package/payload/server/public/assets/{mermaid.core-2mrA7rmo.js → mermaid.core-DO2iS9my.js} +3 -3
  81. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CduFYM-3.js → mindmap-definition-QFDTVHPH-Cymf2IGG.js} +1 -1
  82. package/payload/server/public/assets/{pieDiagram-DEJITSTG-s68UJf6f.js → pieDiagram-DEJITSTG-_2tmuvMb.js} +1 -1
  83. package/payload/server/public/assets/{public-B2WWbnkK.js → public-Rz_GzOrN.js} +3 -3
  84. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-ykHWuSCg.js → quadrantDiagram-34T5L4WZ-CVCrGnBi.js} +1 -1
  85. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DaRlmBk1.js → requirementDiagram-MS252O5E-Bc-o8i8Z.js} +1 -1
  86. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-CjSaDmJD.js → sankeyDiagram-XADWPNL6-DrTBxqE6.js} +1 -1
  87. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-DJcun441.js → sequenceDiagram-FGHM5R23-BjVjSsUJ.js} +1 -1
  88. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DMLxc8L0.js → stateDiagram-FHFEXIEX-DWAJP6ly.js} +1 -1
  89. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BHDyrxpj.js +1 -0
  90. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Ls4Sv2EN.js → timeline-definition-GMOUNBTQ-Br_nbteH.js} +1 -1
  91. package/payload/server/public/assets/{useSelectionMode-BiULiLka.css → useSelectionMode-BLb-o9RX.css} +1 -1
  92. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-NpboV334.js → vennDiagram-DHZGUBPP-D_ozeKV6.js} +1 -1
  93. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-s71AWI0_.js → wardleyDiagram-NUSXRM2D-B87ZKC2B.js} +1 -1
  94. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DYjbwXgQ.js → xychartDiagram-5P7HB3ND-DosRlk2T.js} +1 -1
  95. package/payload/server/public/browser.html +4 -4
  96. package/payload/server/public/data.html +5 -5
  97. package/payload/server/public/graph.html +6 -6
  98. package/payload/server/public/index.html +5 -5
  99. package/payload/server/public/public.html +4 -4
  100. package/payload/server/server.js +269 -81
  101. package/payload/server/public/assets/AdminShell-D06jh8Lz.js +0 -1
  102. package/payload/server/public/assets/channel-yIp47StE.js +0 -1
  103. package/payload/server/public/assets/classDiagram-6PBFFD2Q-DGPa3QR8.js +0 -1
  104. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-De6lAWAi.js +0 -1
  105. package/payload/server/public/assets/clone-C29IA5qA.js +0 -1
  106. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-a-wkzuCq.js +0 -1
  107. /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 scope-based spawn path (Task 602).
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: logs its --unit= and --slice= args, optionally writes connection line.
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.*scope.*cloudflared-maxy-code"; then
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 scope=cloudflared-maxy-code.scope"; then
153
- echo " missing spawned scope log line"; echo "$log_content"; return 1
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: scope unit name derived correctly from configDir (.real-agent → cloudflared-real-agent.scope)
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 scope=cloudflared-real-agent.scope"; then
211
- echo " scope unit name not derived correctly from .real-agent"
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
- run_case "skip when scope active" case_skip_when_scope_active
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 "scope unit name derived from configDir" case_scope_unit_name_from_config_dir
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 -eq 0 ]]; then
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 scope (cloudflared-<brand>.scope)
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. Task 602.
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 scope unit name: strip leading dot from CONFIG_DIR.
53
- # .maxy-code → cloudflared-maxy-code.scope
58
+ # Derive service unit name: strip leading dot from CONFIG_DIR.
59
+ # .maxy-code → cloudflared-maxy-code.service
54
60
  BRAND="${CONFIG_DIR#.}"
55
- SCOPE_UNIT="cloudflared-${BRAND}.scope"
61
+ SERVICE_UNIT="cloudflared-${BRAND}.service"
56
62
 
57
- # Liveness check: query the scope unit state, not a recorded PID.
58
- # The scope is owned by user-systemd, fully outside the brand service's cgroup.
59
- if systemctl --user is-active "$SCOPE_UNIT" >/dev/null 2>&1; then
60
- SCOPE_PID=$(systemctl --user show "$SCOPE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
61
- log "Tunnel already running (scope $SCOPE_UNIT active, MainPID $SCOPE_PID) — skipping resume"
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 scope under cloudflared.slice.
86
- # The scope is owned by user-systemd, not by the brand service's cgroup.
87
- # systemd-run --scope blocks until cloudflared exits (it is the scope leader),
88
- # so we background it. $? after & is always 0 (fork success); the verification
89
- # gate below detects actual spawn failures via connection timeout.
90
- # stdout/stderr of cloudflared append to LOG_FILE via the redirect.
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="$SCOPE_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 scoped path from legacy nohup.
103
- SCOPE_PID=$(systemctl --user show "$SCOPE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
104
- log "spawned scope=$SCOPE_UNIT slice=cloudflared main-pid=$SCOPE_PID"
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 scope=$SCOPE_UNIT connections=$CONN_COUNT ms=$(( ELAPSED * 1000 ))"
147
+ log "tunnel verified service=$SERVICE_UNIT connections=$CONN_COUNT ms=$(( ELAPSED * 1000 ))"
131
148
  else
132
- log "ERROR: tunnel scope started but no edge connection within ${VERIFY_DEADLINE}s — scope=$SCOPE_UNIT"
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 scopeUnit as control signal.
152
+ # Update state file: keep pid/startedAt for observability, add serviceUnit as control signal.
136
153
  NEW_STATE=$(echo "$STATE" | jq \
137
- --arg unit "$SCOPE_UNIT" \
154
+ --arg unit "$SERVICE_UNIT" \
138
155
  --argjson ts "$(date +%s)000" \
139
- '.scopeUnit = $unit | .startedAt = $ts' 2>/dev/null || echo "")
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, session id,
23
- * and WhatsApp message id are all held server-side (the reply is pinned to the
24
- * server's fixed sender), so none of it needs to reach the model's context.
25
- * Two reasons: (1) data minimisation — the operator's phone number is never
26
- * handed to Claude; (2) a bare event (just the text, like the fakechat demo)
27
- * gives the agent nothing to narrate, so it answers the message instead of
28
- * announcing "incoming message from +44 …".
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, session id,
8
- * and WhatsApp message id are all held server-side (the reply is pinned to the
9
- * server's fixed sender), so none of it needs to reach the model's context.
10
- * Two reasons: (1) data minimisation — the operator's phone number is never
11
- * handed to Claude; (2) a bare event (just the text, like the fakechat demo)
12
- * gives the agent nothing to narrate, so it answers the message instead of
13
- * announcing "incoming message from +44 …".
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 composeAdminContent(p) {
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;SAChC;KACF,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,mBAAmB,CAAC,CAAiB;IAC5C,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAA;IAClE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,IAAI,CAAA;IACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CACxB,CAAC,CAAC,EAAE,EAAE,CACJ,qBAAqB,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,EAAE;QACxC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI;QAC3C,gDAAgD,CACnD,CAAA;IACD,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC/B,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,CAAuB;IACvE,OAAO,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAA;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAkB,EAAE,IAAY;IACjE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;AACvC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAAkB,EAClB,WAAmB;IAEnB,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,CAAA;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAA+B,EAC/B,IAAiF;IAEjF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,MAAM,OAAO,EAAE;YAAE,OAAO,IAAI,CAAA;QAChC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,GAAG,CAAC;YAAE,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC3D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;gCAIgC;AAChC,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,IAAa;IAC3D,MAAM,CAAC,GAAG,IAAsD,CAAA;IAChE,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;IAClF,IAAI,CAAC,MAAM;QAAE,OAAO,2BAA2B,MAAM,EAAE,CAAA;IACvD,IAAI,CAAC,EAAE,QAAQ,KAAK,IAAI;QAAE,OAAO,+CAA+C,CAAA;IAChF,OAAO,UAAU,MAAM,EAAE,CAAA;AAC3B,CAAC"}
1
+ {"version":3,"file":"notification.js","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,yEAAyE;AACzE,yEAAyE;AAsBzE;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,cAAc,GACzB,8FAA8F;IAC9F,gDAAgD;IAChD,sGAAsG;IACtG,qCAAqC,CAAA;AAWvC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,CAAiB;IACxD,OAAO;QACL,MAAM,EAAE,8BAAuC;QAC/C,MAAM,EAAE;YACN,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC,OAAO;SACxC;KACF,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,cAAc,CAAC,CAAiB;IACvC,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAA;IAClE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC,IAAI,CAAA;IACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CACxB,CAAC,CAAC,EAAE,EAAE,CACJ,qBAAqB,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,EAAE;QACxC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI;QAC3C,gDAAgD,CACnD,CAAA;IACD,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC/B,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,CAAiB;IACnD,MAAM,OAAO,GAAG,cAAc,CAAC,CAAC,CAAC,CAAA;IACjC,OAAO;QACL,OAAO,EAAE,GAAG,cAAc,OAAO,OAAO,EAAE;QAC1C,YAAY,EAAE,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC;KACjD,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,CAAuB;IACvE,OAAO,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAA;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAkB,EAAE,IAAY;IACjE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;AACvC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,UAAkB,EAClB,KAAe,EACf,OAAgB;IAEhB,OAAO,OAAO,KAAK,SAAS;QAC1B,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE;QAC1C,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAAkB,EAClB,WAAmB;IAEnB,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,CAAA;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAA+B,EAC/B,IAAiF;IAEjF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,MAAM,OAAO,EAAE;YAAE,OAAO,IAAI,CAAA;QAChC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,GAAG,CAAC;YAAE,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC3D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;gCAIgC;AAChC,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,IAAa;IAC3D,MAAM,CAAC,GAAG,IAAsD,CAAA;IAChE,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,KAAK,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;IAClF,IAAI,CAAC,MAAM;QAAE,OAAO,2BAA2B,MAAM,EAAE,CAAA;IACvD,IAAI,CAAC,EAAE,QAAQ,KAAK,IAAI;QAAE,OAAO,+CAA+C,CAAA;IAChF,OAAO,UAAU,MAAM,EAAE,CAAA;AAC3B,CAAC"}