@trillboards/edge-sdk 0.2.140 → 0.2.150

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 (188) hide show
  1. package/README.md +2 -2
  2. package/deploy/detector/pyproject.toml +20 -2
  3. package/deploy/detector/tests/test_execution.py +124 -0
  4. package/deploy/detector/tests/test_model_identity.py +80 -0
  5. package/deploy/detector/tests/test_registry.py +18 -0
  6. package/deploy/detector/tests/test_server.py +3 -1
  7. package/deploy/detector/tests/test_version_stamp.py +4 -1
  8. package/deploy/detector/trillboards_detector/_version.py +2 -2
  9. package/deploy/detector/trillboards_detector/cli.py +3 -0
  10. package/deploy/detector/trillboards_detector/detections.py +7 -0
  11. package/deploy/detector/trillboards_detector/errors.py +4 -0
  12. package/deploy/detector/trillboards_detector/execution.py +109 -0
  13. package/deploy/detector/trillboards_detector/loader.py +6 -3
  14. package/deploy/detector/trillboards_detector/registry.py +27 -5
  15. package/deploy/detector/trillboards_detector/server.py +29 -2
  16. package/deploy/detector/trillboards_detector/session.py +15 -3
  17. package/deploy/litertlm/test_trillboards_litertlm_shim.py +81 -0
  18. package/deploy/litertlm/test_verify_model_proof.py +30 -0
  19. package/deploy/litertlm/trillboards_litertlm_shim.py +150 -1
  20. package/deploy/litertlm/verify_model_proof.py +30 -0
  21. package/deploy/macos/build-macos-pkg.sh +10 -10
  22. package/deploy/macos/scripts/setup-detector.sh +2 -2
  23. package/deploy/macos/scripts/setup-litertlm.sh +66 -30
  24. package/deploy/macos/test-setup-litertlm-reinstall.sh +57 -4
  25. package/deploy/systemd/setup-detector.sh +2 -2
  26. package/deploy/systemd/setup-litertlm.sh +51 -27
  27. package/deploy/windows/build-windows.ps1 +10 -6
  28. package/deploy/windows/bundle-windows-installer.ps1 +3 -3
  29. package/deploy/windows/e2e/test-local-sense.ps1 +6 -5
  30. package/deploy/windows/install-update.ps1 +52 -9
  31. package/deploy/windows/set-executable-icon.ps1 +24 -5
  32. package/deploy/windows/setup-detector.ps1 +142 -4
  33. package/deploy/windows/setup-litertlm.ps1 +53 -10
  34. package/dist/AudienceAnalyzeClient.d.ts +10 -0
  35. package/dist/AudienceAnalyzeClient.d.ts.map +1 -1
  36. package/dist/AudienceAnalyzeClient.js +5 -4
  37. package/dist/AudienceAnalyzeClient.js.map +1 -1
  38. package/dist/CloudSensingManager.d.ts +38 -1
  39. package/dist/CloudSensingManager.d.ts.map +1 -1
  40. package/dist/CloudSensingManager.js +403 -25
  41. package/dist/CloudSensingManager.js.map +1 -1
  42. package/dist/DetectorFieldProducer.d.ts +5 -1
  43. package/dist/DetectorFieldProducer.d.ts.map +1 -1
  44. package/dist/DetectorFieldProducer.js +8 -1
  45. package/dist/DetectorFieldProducer.js.map +1 -1
  46. package/dist/DurableVlmCandidateSpool.d.ts +83 -2
  47. package/dist/DurableVlmCandidateSpool.d.ts.map +1 -1
  48. package/dist/DurableVlmCandidateSpool.js +596 -30
  49. package/dist/DurableVlmCandidateSpool.js.map +1 -1
  50. package/dist/EdgeAgent.js +1 -1
  51. package/dist/FrameGroupingShadow.d.ts +51 -0
  52. package/dist/FrameGroupingShadow.d.ts.map +1 -0
  53. package/dist/FrameGroupingShadow.js +158 -0
  54. package/dist/FrameGroupingShadow.js.map +1 -0
  55. package/dist/ImageClassifierFieldProducer.d.ts +36 -0
  56. package/dist/ImageClassifierFieldProducer.d.ts.map +1 -0
  57. package/dist/ImageClassifierFieldProducer.js +150 -0
  58. package/dist/ImageClassifierFieldProducer.js.map +1 -0
  59. package/dist/LiteRtVlmProvider.d.ts +5 -0
  60. package/dist/LiteRtVlmProvider.d.ts.map +1 -1
  61. package/dist/LiteRtVlmProvider.js +72 -7
  62. package/dist/LiteRtVlmProvider.js.map +1 -1
  63. package/dist/LocalDetectorClient.d.ts +6 -0
  64. package/dist/LocalDetectorClient.d.ts.map +1 -1
  65. package/dist/LocalDetectorClient.js +8 -0
  66. package/dist/LocalDetectorClient.js.map +1 -1
  67. package/dist/OrderedFrameReview.d.ts +57 -0
  68. package/dist/OrderedFrameReview.d.ts.map +1 -0
  69. package/dist/OrderedFrameReview.js +121 -0
  70. package/dist/OrderedFrameReview.js.map +1 -0
  71. package/dist/OrderedReviewClient.d.ts +38 -0
  72. package/dist/OrderedReviewClient.d.ts.map +1 -0
  73. package/dist/OrderedReviewClient.js +121 -0
  74. package/dist/OrderedReviewClient.js.map +1 -0
  75. package/dist/OrderedSequenceStore.d.ts +69 -0
  76. package/dist/OrderedSequenceStore.d.ts.map +1 -0
  77. package/dist/OrderedSequenceStore.js +257 -0
  78. package/dist/OrderedSequenceStore.js.map +1 -0
  79. package/dist/ProfileReviewWork.d.ts +68 -0
  80. package/dist/ProfileReviewWork.d.ts.map +1 -0
  81. package/dist/ProfileReviewWork.js +150 -0
  82. package/dist/ProfileReviewWork.js.map +1 -0
  83. package/dist/RetainedFieldContract.d.ts +21 -0
  84. package/dist/RetainedFieldContract.d.ts.map +1 -0
  85. package/dist/RetainedFieldContract.js +92 -0
  86. package/dist/RetainedFieldContract.js.map +1 -0
  87. package/dist/ReviewModelUse.d.ts +7 -0
  88. package/dist/ReviewModelUse.d.ts.map +1 -0
  89. package/dist/ReviewModelUse.js +30 -0
  90. package/dist/ReviewModelUse.js.map +1 -0
  91. package/dist/ReviewPriority.d.ts +12 -0
  92. package/dist/ReviewPriority.d.ts.map +1 -0
  93. package/dist/ReviewPriority.js +45 -0
  94. package/dist/ReviewPriority.js.map +1 -0
  95. package/dist/RuntimeModelSelection.d.ts +10 -0
  96. package/dist/RuntimeModelSelection.d.ts.map +1 -0
  97. package/dist/RuntimeModelSelection.js +58 -0
  98. package/dist/RuntimeModelSelection.js.map +1 -0
  99. package/dist/SceneChangeDetector.d.ts +1 -1
  100. package/dist/SceneChangeDetector.d.ts.map +1 -1
  101. package/dist/SceneChangeDetector.js +18 -6
  102. package/dist/SceneChangeDetector.js.map +1 -1
  103. package/dist/ScopedVlmReview.d.ts +76 -0
  104. package/dist/ScopedVlmReview.d.ts.map +1 -0
  105. package/dist/ScopedVlmReview.js +267 -0
  106. package/dist/ScopedVlmReview.js.map +1 -0
  107. package/dist/SensingProfileClient.d.ts +50 -0
  108. package/dist/SensingProfileClient.d.ts.map +1 -1
  109. package/dist/SensingProfileClient.js +58 -2
  110. package/dist/SensingProfileClient.js.map +1 -1
  111. package/dist/TemporalReviewClient.d.ts +43 -0
  112. package/dist/TemporalReviewClient.d.ts.map +1 -0
  113. package/dist/TemporalReviewClient.js +117 -0
  114. package/dist/TemporalReviewClient.js.map +1 -0
  115. package/dist/TemporalReviewJournal.d.ts +91 -0
  116. package/dist/TemporalReviewJournal.d.ts.map +1 -0
  117. package/dist/TemporalReviewJournal.js +337 -0
  118. package/dist/TemporalReviewJournal.js.map +1 -0
  119. package/dist/UrgentReviewTriage.d.ts +35 -0
  120. package/dist/UrgentReviewTriage.d.ts.map +1 -0
  121. package/dist/UrgentReviewTriage.js +52 -0
  122. package/dist/UrgentReviewTriage.js.map +1 -0
  123. package/dist/VideoSegmentUploader.d.ts +293 -0
  124. package/dist/VideoSegmentUploader.d.ts.map +1 -0
  125. package/dist/VideoSegmentUploader.js +1002 -0
  126. package/dist/VideoSegmentUploader.js.map +1 -0
  127. package/dist/VlmCandidateBuffer.d.ts +19 -0
  128. package/dist/VlmCandidateBuffer.d.ts.map +1 -1
  129. package/dist/VlmCandidateBuffer.js.map +1 -1
  130. package/dist/autoSense.d.ts +2 -2
  131. package/dist/autoSense.d.ts.map +1 -1
  132. package/dist/autoSense.js +16 -11
  133. package/dist/autoSense.js.map +1 -1
  134. package/dist/cli.js +1 -1
  135. package/dist/cli.js.map +1 -1
  136. package/dist/config.d.ts +2 -0
  137. package/dist/config.d.ts.map +1 -1
  138. package/dist/config.js.map +1 -1
  139. package/dist/continuousReview.d.ts +71 -0
  140. package/dist/continuousReview.d.ts.map +1 -0
  141. package/dist/continuousReview.js +53 -0
  142. package/dist/continuousReview.js.map +1 -0
  143. package/dist/detectorCountReview.d.ts +9 -0
  144. package/dist/detectorCountReview.d.ts.map +1 -0
  145. package/dist/detectorCountReview.js +79 -0
  146. package/dist/detectorCountReview.js.map +1 -0
  147. package/dist/diagnose.d.ts +1 -1
  148. package/dist/diagnose.js +1 -1
  149. package/dist/installerOnboardingMode.d.ts.map +1 -1
  150. package/dist/installerOnboardingMode.js +5 -3
  151. package/dist/installerOnboardingMode.js.map +1 -1
  152. package/dist/localInferenceSchema.d.ts +8 -0
  153. package/dist/localInferenceSchema.d.ts.map +1 -1
  154. package/dist/localInferenceSchema.js +3 -3
  155. package/dist/localInferenceSchema.js.map +1 -1
  156. package/dist/localSenseE2eHarness.d.ts +12 -0
  157. package/dist/localSenseE2eHarness.d.ts.map +1 -1
  158. package/dist/localSenseE2eHarness.js +77 -4
  159. package/dist/localSenseE2eHarness.js.map +1 -1
  160. package/dist/localSensing.d.ts +91 -18
  161. package/dist/localSensing.d.ts.map +1 -1
  162. package/dist/localSensing.js +678 -153
  163. package/dist/localSensing.js.map +1 -1
  164. package/dist/mixedScopedCounts.d.ts +17 -0
  165. package/dist/mixedScopedCounts.d.ts.map +1 -0
  166. package/dist/mixedScopedCounts.js +90 -0
  167. package/dist/mixedScopedCounts.js.map +1 -0
  168. package/dist/sensingCatchRate.d.ts +9 -5
  169. package/dist/sensingCatchRate.d.ts.map +1 -1
  170. package/dist/sensingCatchRate.js +10 -6
  171. package/dist/sensingCatchRate.js.map +1 -1
  172. package/dist/sensingRunMode.d.ts +5 -0
  173. package/dist/sensingRunMode.d.ts.map +1 -1
  174. package/dist/sensingRunMode.js +56 -0
  175. package/dist/sensingRunMode.js.map +1 -1
  176. package/package.json +7 -4
  177. package/retained/ArchiveLifecycle.cjs +79 -0
  178. package/retained/ModelStageScheduler.cjs +66 -0
  179. package/retained/ProfileAuthorityClient.cjs +88 -0
  180. package/retained/RetainedCaptureClient.cjs +164 -0
  181. package/retained/WaitRecheckCaptureClient.cjs +109 -0
  182. package/retained/__tests__/archive-lifecycle.test.cjs +117 -0
  183. package/retained/__tests__/runtime.test.cjs +384 -0
  184. package/retained/alert-rule-policy.cjs +37 -0
  185. package/retained/manager.cjs +133 -0
  186. package/retained/pilot-profile-client.cjs +82 -0
  187. package/retained/temporal-grouping.cjs +78 -0
  188. package/retained/website-api-capability.cjs +122 -0
@@ -29,6 +29,9 @@ LA_DIR="$USER_HOME/Library/LaunchAgents"
29
29
  LOCAL_BIN="$USER_HOME/.local/bin"
30
30
  MODEL_ID="gemma-4-E2B-it.litertlm"
31
31
  HF_REPO="litert-community/gemma-4-E2B-it-litert-lm"
32
+ MODEL_REVISION="84b6978eff6e4eea02825bc2ee4ea48579f13109"
33
+ APPROVED_MODEL_SHA256="ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42"
34
+ MODEL_URL="https://huggingface.co/$HF_REPO/resolve/$MODEL_REVISION/$MODEL_ID"
32
35
  SERVE_LABEL="com.trillboards.litertlm"
33
36
  SERVE_PORT="9379"
34
37
  LITERT_PYTHON="${LITERT_LM_PYTHON:-3.12}"
@@ -245,16 +248,29 @@ install_litertlm_shim() {
245
248
  # Deliberately NOT a /v1/models check: that answers 200 without ever building an
246
249
  # engine, so it cannot tell a working GPU encoder from one that fails on first
247
250
  # use.
248
- verify_litertlm_vision() {
249
- _body="$LITERT_DIR/.vision-probe.json"
250
- printf '{"model":"%s,gpu","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
251
- "$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
252
- _code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
251
+ verify_litertlm_probe() {
252
+ local requested="$1" body response code tool_dir py verifier candidate valid=1
253
+ body="$LITERT_DIR/.identity-probe.json"
254
+ response="$LITERT_DIR/.identity-response.json"
255
+ printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
256
+ "$requested" "$LITERTLM_PROBE_JPEG_B64" > "$body" 2>/dev/null || return 1
257
+ code=$(curl -s -o "$response" -w '%{http_code}' --max-time 180 \
253
258
  -X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
254
- -H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
255
- rm -f "$_body" 2>/dev/null || true
256
- [ "$_code" = "200" ]
259
+ -H 'Content-Type: application/json' -H "X-Trillboards-Model-Sha256: $APPROVED_MODEL_SHA256" \
260
+ --data-binary @"$body" 2>/dev/null)
261
+ if [ "$code" = "200" ]; then
262
+ tool_dir="$("${UV:-$LOCAL_BIN/uv}" tool dir 2>/dev/null | tr -d '\r')"
263
+ py="$tool_dir/litert-lm/bin/python"
264
+ for candidate in "$SCRIPT_DIR/verify_model_proof.py" "$SCRIPT_DIR/../litertlm/verify_model_proof.py" "$SCRIPT_DIR/../../litertlm/verify_model_proof.py"; do
265
+ [ -f "$candidate" ] && { verifier="$candidate"; break; }
266
+ done
267
+ [ -n "${verifier:-}" ] && [ -x "$py" ] \
268
+ && "$py" "$verifier" "$response" "$requested" "$APPROVED_MODEL_SHA256" && valid=0
269
+ fi
270
+ rm -f "$body" "$response" 2>/dev/null || true
271
+ return "$valid"
257
272
  }
273
+ verify_litertlm_vision() { verify_litertlm_probe "$MODEL_ID,gpu"; }
258
274
 
259
275
  # The same probe, but asking for NO delegate suffix rather than `,gpu`.
260
276
  #
@@ -265,16 +281,7 @@ verify_litertlm_vision() {
265
281
  # carries no suffix at all, so a 200 means the box built an engine and ran a real
266
282
  # image through it under the config now on disk, which is exactly the claim each
267
283
  # rung needs.
268
- verify_litertlm_plain_inference() {
269
- _body="$LITERT_DIR/.plain-probe.json"
270
- printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
271
- "$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
272
- _code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
273
- -X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
274
- -H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
275
- rm -f "$_body" 2>/dev/null || true
276
- [ "$_code" = "200" ]
277
- }
284
+ verify_litertlm_plain_inference() { verify_litertlm_probe "$MODEL_ID"; }
278
285
 
279
286
  local_ai_healthy() {
280
287
  curl -fsS --max-time 3 "http://127.0.0.1:$SERVE_PORT/v1/models" 2>/dev/null \
@@ -334,11 +341,33 @@ verify_bundled_model() {
334
341
  [ -f "$BUNDLED_MODEL_SHA" ] || fail "bundled model checksum is missing"
335
342
  EXPECTED_SHA="$(awk 'NR == 1 { print $1 }' "$BUNDLED_MODEL_SHA" 2>/dev/null || echo '')"
336
343
  ACTUAL_SHA="$(shasum -a 256 "$BUNDLED_MODEL" 2>/dev/null | awk '{print $1}')"
337
- if [ -z "$EXPECTED_SHA" ] || [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
344
+ if [ "$EXPECTED_SHA" != "$APPROVED_MODEL_SHA256" ] || [ "$ACTUAL_SHA" != "$APPROVED_MODEL_SHA256" ]; then
338
345
  fail "bundled model checksum mismatch"
339
346
  fi
340
347
  log "verified bundled model SHA-256"
341
348
  }
349
+ installed_model_verified() {
350
+ [ -f "$STORE_DIR/model.litertlm" ] || return 1
351
+ [ "$(shasum -a 256 "$STORE_DIR/model.litertlm" 2>/dev/null | awk '{print $1}')" = "$APPROVED_MODEL_SHA256" ]
352
+ }
353
+ import_verified_model() {
354
+ local source="$1" old_sha rollback
355
+ [ "$(shasum -a 256 "$source" 2>/dev/null | awk '{print $1}')" = "$APPROVED_MODEL_SHA256" ] || return 1
356
+ if [ -f "$STORE_DIR/model.litertlm" ] && ! installed_model_verified; then
357
+ old_sha="$(shasum -a 256 "$STORE_DIR/model.litertlm" | awk '{print $1}')"
358
+ rollback="$STORE_DIR/rollback/$old_sha.litertlm"
359
+ mkdir -p "$STORE_DIR/rollback" || return 1
360
+ [ -f "$rollback" ] || cp "$STORE_DIR/model.litertlm" "$rollback" || return 1
361
+ [ "$(shasum -a 256 "$rollback" | awk '{print $1}')" = "$old_sha" ] || return 1
362
+ if launch_agent_loaded; then
363
+ launchctl bootout "gui/$(id -u)/$SERVE_LABEL" >>"$LOG" 2>&1 || return 1
364
+ wait_for_launch_agent_removal || return 1
365
+ fi
366
+ log "previous model preserved at $rollback"
367
+ fi
368
+ "$LITERTLM" import "$source" "$MODEL_ID" >>"$LOG" 2>&1 || return 1
369
+ installed_model_verified
370
+ }
342
371
 
343
372
  # One provisioning process per user. Re-opening the pairing helper is a normal
344
373
  # recovery path, so duplicate launches must collapse without duplicate downloads.
@@ -351,7 +380,7 @@ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
351
380
  log "LiteRT-LM provisioning is already running (pid $LOCK_PID)"
352
381
  if [ "$REQUIRE_BUNDLED_MODEL" = "1" ]; then
353
382
  for _attempt in $(seq 1 120); do
354
- local_ai_healthy && exit 0
383
+ local_ai_healthy && installed_model_verified && (verify_litertlm_vision || verify_litertlm_plain_inference) && exit 0
355
384
  sleep 2
356
385
  done
357
386
  fail "the existing local AI installation did not become healthy"
@@ -468,12 +497,12 @@ fi
468
497
 
469
498
  install_litertlm_shim || true
470
499
 
471
- # 3. Model - import the verified bundled file. Idempotent: skip entirely if
472
- # litert-lm already has the model. Engineering runs may use HF only with the
500
+ # 3. Model - skip only if the installed BYTES match the approved artifact.
501
+ # Engineering runs may use the immutable HF revision only with the
473
502
  # explicit TRILLBOARDS_ALLOW_MODEL_DOWNLOAD=1 opt-in.
474
503
  HAVE_MODEL=0
475
- if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
476
- log "model $MODEL_ID already present — skipping import"
504
+ if installed_model_verified; then
505
+ log "approved model $MODEL_ID already present — skipping import"
477
506
  HAVE_MODEL=1
478
507
  if [ -f "$BUNDLED_MODEL" ]; then
479
508
  verify_bundled_model
@@ -483,7 +512,7 @@ if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
483
512
  elif [ -f "$BUNDLED_MODEL" ]; then
484
513
  verify_bundled_model
485
514
  log "importing $MODEL_ID from BUNDLED file $BUNDLED_MODEL (no download) …"
486
- if "$LITERTLM" import "$BUNDLED_MODEL" "$MODEL_ID" >>"$LOG" 2>&1; then
515
+ if import_verified_model "$BUNDLED_MODEL"; then
487
516
  log "model imported from bundle"
488
517
  HAVE_MODEL=1
489
518
  # Reclaim the staged ~2.4 GB — the litert-lm store now holds the model.
@@ -497,12 +526,19 @@ fi
497
526
  if [ "$HAVE_MODEL" -eq 0 ]; then
498
527
  [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "required bundled model is missing"
499
528
  [ "$ALLOW_MODEL_DOWNLOAD" = "1" ] || fail "local AI model is missing and network fallback is disabled"
500
- log "importing $MODEL_ID from $HF_REPO (~2.4 GB, public repo) …"
501
- "$LITERTLM" import --from-huggingface-repo "$HF_REPO" "$MODEL_ID" >>"$LOG" 2>&1 \
502
- && { log "model imported from HF"; HAVE_MODEL=1; } \
503
- || log "WARN: model import failed (serve will retry on next run)"
529
+ mkdir -p "$STORE_DIR" || fail "model store is unavailable"
530
+ DOWNLOAD="$(mktemp "$STORE_DIR/approved-model.XXXXXX")" || fail "could not create a model download file"
531
+ log "downloading approved $MODEL_ID from immutable revision $MODEL_REVISION"
532
+ if curl -fL --retry 3 --output "$DOWNLOAD" "$MODEL_URL" >>"$LOG" 2>&1 && import_verified_model "$DOWNLOAD"; then
533
+ HAVE_MODEL=1
534
+ rm -f "$DOWNLOAD"
535
+ log "model imported from verified immutable revision"
536
+ else
537
+ rm -f "$DOWNLOAD"
538
+ fail "approved model download/import failed"
539
+ fi
504
540
  fi
505
- [ "$HAVE_MODEL" -eq 1 ] || { [ "$REQUIRE_BUNDLED_MODEL" = "1" ] && fail "local AI model is unavailable"; }
541
+ [ "$HAVE_MODEL" -eq 1 ] || fail "local AI model is unavailable"
506
542
 
507
543
  # Record the installed version next to the store model so the connector's
508
544
  # edge-core ModelManager seeds it (readBundledModelVersion) and does not
@@ -10,9 +10,29 @@ export HOME="$TEST_ROOT/home"
10
10
  export TRILLBOARDS_TEST_STATE_DIR="$TEST_ROOT/state"
11
11
  export LITERT_LM_DIR="$TEST_ROOT/store"
12
12
  export TRILLBOARDS_REQUIRE_BUNDLED_MODEL=1
13
+ export TRILLBOARDS_TEST_REAL_PYTHON="${TRILLBOARDS_TEST_REAL_PYTHON:-$(command -v python3)}"
13
14
  FAKE_BIN="$HOME/.local/bin"
14
15
  SHARE_DIR="$HOME/Library/Trillboards/edge/share"
15
16
  mkdir -p "$FAKE_BIN" "$SHARE_DIR" "$TRILLBOARDS_TEST_STATE_DIR"
17
+ mkdir -p "$TRILLBOARDS_TEST_STATE_DIR/tools/litert-lm/bin" "$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm"
18
+ printf 'fixture evaluated model\n' >"$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/model.litertlm"
19
+
20
+ # These are tiny installer fixtures, not model-inference/accuracy tests. Hashes
21
+ # are simulated from file CONTENT, so the same name with old bytes is rejected.
22
+ cat >"$FAKE_BIN/shasum" <<'EOF'
23
+ #!/bin/bash
24
+ file="${@: -1}"
25
+ if [ -f "$file" ] && [ "$(cat "$file")" = 'fixture evaluated model' ]; then
26
+ printf 'ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42 %s\n' "$file"
27
+ else
28
+ if [ -x /usr/bin/shasum ]; then /usr/bin/shasum "$@"; else /usr/bin/sha256sum "$file"; fi
29
+ fi
30
+ EOF
31
+ cat >"$TRILLBOARDS_TEST_STATE_DIR/tools/litert-lm/bin/python" <<'EOF'
32
+ #!/bin/bash
33
+ [ "${1:-}" != '-c' ] || exit 0
34
+ exec "$TRILLBOARDS_TEST_REAL_PYTHON" "$@"
35
+ EOF
16
36
 
17
37
  cat >"$FAKE_BIN/uname" <<'EOF'
18
38
  #!/bin/bash
@@ -25,14 +45,20 @@ EOF
25
45
 
26
46
  cat >"$FAKE_BIN/uv" <<'EOF'
27
47
  #!/bin/bash
48
+ [ "${1:-} ${2:-}" != 'tool dir' ] || printf '%s\n' "$TRILLBOARDS_TEST_STATE_DIR/tools"
28
49
  exit 0
29
50
  EOF
30
51
 
31
52
  cat >"$FAKE_BIN/litert-lm" <<'EOF'
32
53
  #!/bin/bash
33
- if [ "${1:-}" = "list" ]; then
34
- printf 'gemma-4-E2B-it.litertlm\n'
35
- fi
54
+ case "${1:-}" in
55
+ list) printf 'gemma-4-E2B-it.litertlm\n' ;;
56
+ --version) printf 'litert-lm 0.16.0\n' ;;
57
+ import)
58
+ cp "$2" "$LITERT_LM_DIR/models/$3/model.litertlm"
59
+ printf 'import\n' >>"$TRILLBOARDS_TEST_STATE_DIR/imports"
60
+ ;;
61
+ esac
36
62
  exit 0
37
63
  EOF
38
64
 
@@ -54,15 +80,21 @@ EOF
54
80
  cat >"$FAKE_BIN/curl" <<'EOF'
55
81
  #!/bin/bash
56
82
  body=""
83
+ response=""
57
84
  prev=""
58
85
  for arg in "$@"; do
59
86
  case "$prev" in
60
87
  --data-binary) body="${arg#@}" ;;
88
+ -o) response="$arg" ;;
61
89
  esac
62
90
  prev="$arg"
63
91
  done
64
92
  for arg in "$@"; do
65
93
  if [ "$arg" = "-w" ]; then
94
+ model="$(sed -n 's/.*"model":"\([^"]*\)".*/\1/p' "$body")"
95
+ case "$model" in *,gpu|*,cpu) ;; *) model="$model,cpu" ;; esac
96
+ sha="${TRILLBOARDS_TEST_PROOF_SHA:-ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42}"
97
+ printf '{"runtimeModel":{"version":1,"format":"litert-lm","sha256":"%s","runtimeModelId":"%s"}}' "$sha" "$model" >"$response"
66
98
  if [ "${TRILLBOARDS_TEST_FAIL_WHEN_SPECULATIVE:-0}" = "1" ] \
67
99
  && grep -Fq '"speculative_decoding":true' "$LITERT_LM_DIR/config.json" 2>/dev/null; then
68
100
  printf '500'
@@ -127,7 +159,8 @@ exit 0
127
159
  EOF
128
160
 
129
161
  chmod +x "$FAKE_BIN/uname" "$FAKE_BIN/uv" "$FAKE_BIN/litert-lm" \
130
- "$FAKE_BIN/curl" "$FAKE_BIN/sleep" "$FAKE_BIN/launchctl"
162
+ "$FAKE_BIN/curl" "$FAKE_BIN/sleep" "$FAKE_BIN/launchctl" "$FAKE_BIN/shasum" \
163
+ "$TRILLBOARDS_TEST_STATE_DIR/tools/litert-lm/bin/python"
131
164
 
132
165
  cat >"$SHARE_DIR/com.trillboards.litertlm.plist" <<'EOF'
133
166
  <plist><string>__LITERTLM__</string><string>__PORT__</string><string>__LOG_DIR__</string></plist>
@@ -247,5 +280,25 @@ fi
247
280
 
248
281
  unset TRILLBOARDS_TEST_FAIL_WHEN_SPECULATIVE
249
282
 
283
+ # An existing model under the SAME id must be replaced when its bytes differ,
284
+ # with a checked rollback copy. The fixture importer performs a real file copy.
285
+ printf 'fixture old model\n' >"$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/model.litertlm"
286
+ old_sha="$("$FAKE_BIN/shasum" -a 256 "$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/model.litertlm" | awk '{print $1}')"
287
+ mkdir -p "$SHARE_DIR/models"
288
+ printf 'fixture evaluated model\n' >"$SHARE_DIR/models/gemma-4-E2B-it.litertlm"
289
+ printf 'ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42\n' >"$SHARE_DIR/models/gemma-4-E2B-it.litertlm.sha256"
290
+ if ! OUTPUT4="$(/bin/bash "$SETUP" 2>&1)"; then printf '%s\n' "$OUTPUT4" >&2; exit 1; fi
291
+ [ "$(cat "$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/model.litertlm")" = 'fixture evaluated model' ]
292
+ [ "$("$FAKE_BIN/shasum" -a 256 "$LITERT_LM_DIR/models/gemma-4-E2B-it.litertlm/rollback/$old_sha.litertlm" | awk '{print $1}')" = "$old_sha" ]
293
+ [ "$(wc -l <"$TRILLBOARDS_TEST_STATE_DIR/imports" | tr -d ' ')" = '1' ]
294
+ # A correct store and a 200 inference must still fail when the loaded engine's
295
+ # identity differs. This catches a stale in-memory model after an upgrade.
296
+ export TRILLBOARDS_TEST_PROOF_SHA="181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c"
297
+ if OUTPUT5="$(/bin/bash "$SETUP" 2>&1)"; then
298
+ printf 'FAIL: setup accepted a stale runtime model proof\n' >&2
299
+ exit 1
300
+ fi
301
+ unset TRILLBOARDS_TEST_PROOF_SHA
302
+
250
303
  printf 'macOS LiteRT-LM reinstall race + engine-settings cascade regressions passed\n'
251
304
 
@@ -93,7 +93,7 @@ SOURCE="$(find_detector_source)" || { log "ERROR: detector package source not fo
93
93
  # re-downloads the pinned ~250 MB onnxruntime wheel on a shop PC every time, for
94
94
  # nothing — an exact pin cannot resolve to different bytes.
95
95
  log "installing/repairing trillboards-detector from $SOURCE with uv-managed Python $DETECTOR_PYTHON"
96
- if ! "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "$SOURCE" >>"$LOG" 2>&1; then
96
+ if ! "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "${SOURCE}[cpu]" >>"$LOG" 2>&1; then
97
97
  log "ERROR: trillboards-detector install failed; the cascade stays off"
98
98
  exit 0
99
99
  fi
@@ -126,7 +126,7 @@ elif [ "$HAVE_VERSION" != "$WANT_VERSION" ]; then
126
126
  # asking it again the same way is not a repair.
127
127
  log "WARN: the installed detector reports '$HAVE_VERSION' but the staged package is '$WANT_VERSION' - reinstalling from scratch"
128
128
  "$UV" tool uninstall trillboards-detector >>"$LOG" 2>&1 || true
129
- "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "$SOURCE" >>"$LOG" 2>&1 || true
129
+ "$UV" tool install --python "$DETECTOR_PYTHON" --managed-python --force --refresh-package trillboards-detector "${SOURCE}[cpu]" >>"$LOG" 2>&1 || true
130
130
  DETECTOR="$LOCAL_BIN/trillboards-detector"
131
131
  [ -x "$DETECTOR" ] || DETECTOR="$(command -v trillboards-detector || echo "$DETECTOR")"
132
132
  HAVE_VERSION="$(installed_detector_version)"
@@ -37,6 +37,9 @@ LOG_DIR="${TRILLBOARDS_LOG_DIR:-/var/log/trillboards}"
37
37
  LOCAL_BIN="$USER_HOME/.local/bin"
38
38
  MODEL_ID="gemma-4-E2B-it.litertlm"
39
39
  HF_REPO="litert-community/gemma-4-E2B-it-litert-lm"
40
+ MODEL_REVISION="84b6978eff6e4eea02825bc2ee4ea48579f13109"
41
+ APPROVED_MODEL_SHA256="ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42"
42
+ MODEL_URL="https://huggingface.co/$HF_REPO/resolve/$MODEL_REVISION/$MODEL_ID"
40
43
  SERVE_PORT="9379"
41
44
  SERVE_UNIT="trillboards-litertlm"
42
45
  LITERT_PYTHON="${LITERT_LM_PYTHON:-3.12}"
@@ -253,16 +256,29 @@ install_litertlm_shim() {
253
256
  # Deliberately NOT a /v1/models check: that answers 200 without ever building an
254
257
  # engine, so it cannot tell a working GPU encoder from one that fails on first
255
258
  # use.
256
- verify_litertlm_vision() {
257
- _body="$LITERT_DIR/.vision-probe.json"
258
- printf '{"model":"%s,gpu","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
259
- "$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
260
- _code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
259
+ verify_litertlm_probe() {
260
+ local requested="$1" body response code tool_dir py verifier candidate valid=1
261
+ body="$LITERT_DIR/.identity-probe.json"
262
+ response="$LITERT_DIR/.identity-response.json"
263
+ printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
264
+ "$requested" "$LITERTLM_PROBE_JPEG_B64" > "$body" 2>/dev/null || return 1
265
+ code=$(curl -s -o "$response" -w '%{http_code}' --max-time 180 \
261
266
  -X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
262
- -H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
263
- rm -f "$_body" 2>/dev/null || true
264
- [ "$_code" = "200" ]
267
+ -H 'Content-Type: application/json' -H "X-Trillboards-Model-Sha256: $APPROVED_MODEL_SHA256" \
268
+ --data-binary @"$body" 2>/dev/null)
269
+ if [ "$code" = "200" ]; then
270
+ tool_dir="$("${UV:-$LOCAL_BIN/uv}" tool dir 2>/dev/null | tr -d '\r')"
271
+ py="$tool_dir/litert-lm/bin/python"
272
+ for candidate in "$SCRIPT_DIR/verify_model_proof.py" "$SCRIPT_DIR/../litertlm/verify_model_proof.py" "$SCRIPT_DIR/../../litertlm/verify_model_proof.py"; do
273
+ [ -f "$candidate" ] && { verifier="$candidate"; break; }
274
+ done
275
+ [ -n "${verifier:-}" ] && [ -x "$py" ] \
276
+ && "$py" "$verifier" "$response" "$requested" "$APPROVED_MODEL_SHA256" && valid=0
277
+ fi
278
+ rm -f "$body" "$response" 2>/dev/null || true
279
+ return "$valid"
265
280
  }
281
+ verify_litertlm_vision() { verify_litertlm_probe "$MODEL_ID,gpu"; }
266
282
 
267
283
  # The same probe, but asking for NO delegate suffix rather than `,gpu`.
268
284
  #
@@ -273,16 +289,7 @@ verify_litertlm_vision() {
273
289
  # carries no suffix at all, so a 200 means the box built an engine and ran a real
274
290
  # image through it under the config now on disk, which is exactly the claim each
275
291
  # rung needs.
276
- verify_litertlm_plain_inference() {
277
- _body="$LITERT_DIR/.plain-probe.json"
278
- printf '{"model":"%s","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
279
- "$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
280
- _code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
281
- -X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
282
- -H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
283
- rm -f "$_body" 2>/dev/null || true
284
- [ "$_code" = "200" ]
285
- }
292
+ verify_litertlm_plain_inference() { verify_litertlm_probe "$MODEL_ID"; }
286
293
 
287
294
  local_ai_healthy() {
288
295
  curl -fsS --max-time 3 "http://127.0.0.1:$SERVE_PORT/v1/models" 2>/dev/null \
@@ -362,20 +369,37 @@ fi
362
369
 
363
370
  install_litertlm_shim || true
364
371
 
365
- # 3. Model. Idempotent: skip entirely if litert-lm already has it.
372
+ # 3. Model: a matching name is not a matching artifact. Upgrade from the immutable
373
+ # revision, preserve the old bytes, and verify the installed copy.
366
374
  HAVE_MODEL=0
367
- if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
368
- log "model $MODEL_ID already present — skipping import"
375
+ INSTALLED_SHA="$(sha256sum "$STORE_DIR/model.litertlm" 2>/dev/null | awk '{print $1}')"
376
+ if [ "$INSTALLED_SHA" = "$APPROVED_MODEL_SHA256" ]; then
377
+ log "approved model $MODEL_ID already present — skipping import"
369
378
  HAVE_MODEL=1
370
379
  else
371
380
  [ "$ALLOW_MODEL_DOWNLOAD" = "1" ] || fail "local AI model is missing and the network import is disabled"
372
- log "importing $MODEL_ID from $HF_REPO (~2.4 GB, public repo) — this takes a while"
373
- if "$LITERTLM" import --from-huggingface-repo "$HF_REPO" "$MODEL_ID" >>"$LOG" 2>&1; then
374
- log "model imported"
375
- HAVE_MODEL=1
376
- else
377
- fail "model import failed — the connector will WAIT (and beacon) until this succeeds"
381
+ mkdir -p "$STORE_DIR" || fail "model store is unavailable"
382
+ DOWNLOAD="$(mktemp "$STORE_DIR/approved-model.XXXXXX")" || fail "could not create a model download file"
383
+ log "downloading approved $MODEL_ID from immutable revision $MODEL_REVISION"
384
+ if ! curl -fL --retry 3 --output "$DOWNLOAD" "$MODEL_URL" >>"$LOG" 2>&1 \
385
+ || [ "$(sha256sum "$DOWNLOAD" | awk '{print $1}')" != "$APPROVED_MODEL_SHA256" ]; then
386
+ rm -f "$DOWNLOAD"
387
+ fail "approved model download/checksum failed"
388
+ fi
389
+ if [ -n "$INSTALLED_SHA" ]; then
390
+ ROLLBACK="$STORE_DIR/rollback/$INSTALLED_SHA.litertlm"
391
+ mkdir -p "$STORE_DIR/rollback" || fail "could not preserve previous model"
392
+ [ -f "$ROLLBACK" ] || cp "$STORE_DIR/model.litertlm" "$ROLLBACK" || fail "could not preserve previous model"
393
+ [ "$(sha256sum "$ROLLBACK" | awk '{print $1}')" = "$INSTALLED_SHA" ] || fail "previous model backup verification failed"
378
394
  fi
395
+ if ! "$LITERTLM" import "$DOWNLOAD" "$MODEL_ID" >>"$LOG" 2>&1 \
396
+ || [ "$(sha256sum "$STORE_DIR/model.litertlm" | awk '{print $1}')" != "$APPROVED_MODEL_SHA256" ]; then
397
+ rm -f "$DOWNLOAD"
398
+ fail "approved model import verification failed — the connector will WAIT until this succeeds"
399
+ fi
400
+ rm -f "$DOWNLOAD"
401
+ HAVE_MODEL=1
402
+ log "model imported from verified immutable revision"
379
403
  fi
380
404
 
381
405
  # Record the installed version next to the store model so the connector's
@@ -16,11 +16,11 @@
16
16
  bin\trillboards-pair.cmd first-run claim-code pairing helper
17
17
  lib\edge-sdk\dist\cli.js the connector CLI
18
18
  lib\node_modules\... runtime subset (edge-core + edge-platform-rtsp +
19
- edge-sdk + transitive deps), WITHOUT the heavy
20
- local ONNX/sharp/sqlite inference stack.
19
+ edge-sdk + transitive deps, including Sharp for
20
+ focused image masks), WITHOUT local ONNX/sqlite.
21
21
 
22
- The connector is loaded WITHOUT the heavy ONNX/sharp/sqlite stack (cli.ts
23
- lazy-requires EdgeAgent), so the bundle omits onnxruntime-node/sharp/
22
+ The connector is loaded WITHOUT the heavy ONNX/sqlite stack (cli.ts
23
+ lazy-requires EdgeAgent), so the bundle omits onnxruntime-node/
24
24
  better-sqlite3 — keeping the inner .exe small. The release workflow signs this
25
25
  connector installer, then bundle-windows-installer.ps1 wraps it with the
26
26
  pinned Gemma model in one customer-facing executable. The inner installer
@@ -107,6 +107,7 @@ Push-Location $WorkspaceRoot; npm run build; if ($LASTEXITCODE -ne 0) { Die "edg
107
107
  Log "Staging edge-sdk dist…"
108
108
  New-Item -ItemType Directory -Force -Path "$Payload\lib\edge-sdk" | Out-Null
109
109
  Copy-Item -Recurse -Force "$PkgRoot\dist" "$Payload\lib\edge-sdk\dist"
110
+ Copy-Item -Recurse -Force "$PkgRoot\retained" "$Payload\lib\edge-sdk\retained"
110
111
  Copy-Item -Force "$PkgRoot\package.json" "$Payload\lib\edge-sdk\package.json"
111
112
 
112
113
  # ─── 3. Runtime node_modules subset (NO heavy native stack) ────────────────────
@@ -123,9 +124,11 @@ Copy-Item -Force "$PkgRoot\package.json" "$Payload\lib\edge-sdk\package.json"
123
124
  # the job was killed at its timeout on EVERY release run, so the .exe never
124
125
  # published. /MT parallelises the small-file copy; /R:1 /W:1 bound retries so a
125
126
  # single transiently-locked file can't trigger robocopy's default 1e6×30s hang.
126
- Log "Staging runtime node_modules (excluding ONNX/sharp/sqlite/dev)…"
127
+ Log "Staging runtime node_modules (including focused-image masks; excluding ONNX/sqlite/dev)…"
127
128
  New-Item -ItemType Directory -Force -Path "$Payload\lib\node_modules" | Out-Null
128
- $xd = @("onnxruntime-node","sharp","@img","better-sqlite3","ffmpeg-static",".cache",".bin","typescript","jest","jest-cli","@jest","ts-jest","@types")
129
+ # Focused retained reviews use Sharp to mask pixels, not just prompt geometry.
130
+ # Keep its platform-native dependencies; detectors still use the local HTTP service.
131
+ $xd = @("onnxruntime-node","better-sqlite3","ffmpeg-static",".cache",".bin","typescript","jest","jest-cli","@jest","ts-jest","@types")
129
132
  $rcArgs = @("$WorkspaceRoot\node_modules", "$Payload\lib\node_modules", "/E", "/XJ", "/MT:16", "/R:1", "/W:1", "/NFL","/NDL","/NJH","/NJS","/NP")
130
133
  foreach ($d in $xd) { $rcArgs += @("/XD", $d) }
131
134
  robocopy @rcArgs | Out-Null
@@ -141,6 +144,7 @@ foreach ($p in @("edge-core","edge-sensing","edge-ads","edge-federated","edge-cl
141
144
  if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
142
145
  New-Item -ItemType Directory -Force -Path $dest | Out-Null
143
146
  if (Test-Path "$src\dist") { Copy-Item -Recurse -Force "$src\dist" "$dest\dist" }
147
+ if ($p -eq 'edge-sdk') { Copy-Item -Recurse -Force "$src\retained" "$dest\retained" }
144
148
  Copy-Item -Force "$src\package.json" "$dest\package.json"
145
149
  }
146
150
 
@@ -15,8 +15,8 @@ param(
15
15
  [Parameter(Mandatory = $true)][string]$InnerInstaller,
16
16
  [string]$OutputPath = "",
17
17
  [string]$ModelPath = "",
18
- [string]$ModelUrl = "https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/main/gemma-4-E2B-it.litertlm",
19
- [string]$ModelSha256 = "181938105e0eefd105961417e8da75903eacda102c4fce9ce90f50b97139a63c",
18
+ [string]$ModelUrl = "https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/84b6978eff6e4eea02825bc2ee4ea48579f13109/gemma-4-E2B-it.litertlm",
19
+ [string]$ModelSha256 = "ab7838cdfc8f77e54d8ca45eadceb20452d9f01e4bfade03e5dce27911b27e42",
20
20
  [string]$SevenZipSdkUrl = "https://www.7-zip.org/a/lzma2602.7z",
21
21
  [string]$SevenZipSdkSha256 = "2878c85f5f43a4a4e0952b1fd4e5fe097c1c143997a8047c7e1e788892aa9357",
22
22
  [string]$IconPath = ""
@@ -87,7 +87,7 @@ if (-not $sfxModule) { Fail "official 7zSD.sfx installer module was not found" }
87
87
  # Brand the executable module before appending the config and archive. This
88
88
  # keeps the SFX overlay byte-for-byte intact and the final Authenticode signing
89
89
  # step covers both the application icon and the customer payload.
90
- & (Join-Path $Here "set-executable-icon.ps1") -ExecutablePath $sfxModule.FullName -IconPath $IconPath
90
+ & (Join-Path $Here "set-executable-icon.ps1") -ExecutablePath $sfxModule.FullName -IconPath $IconPath -EnsurePerUserExecution
91
91
 
92
92
  $innerName = "TrillboardsEdgeSetup-core.exe"
93
93
  Copy-Item -LiteralPath $InnerInstaller -Destination (Join-Path $Stage $innerName) -Force
@@ -12,11 +12,12 @@
12
12
  wiring (DEFAULT, runs on EVERY CI event incl. PRs):
13
13
  Builds the connector, then runs the cross-platform Node wiring runner
14
14
  (scripts/run-local-sense-wiring-e2e.mjs) which drives the REAL built
15
- `cli.js local-sense` against a STUB :9379 VLM (returns a fixed valid Gemma
16
- JSON, person_count>=1) + a STUB ingest API, feeding the committed person
17
- fixture through the connector's real RTSP/ffmpeg path. Proves the
18
- connector → serve → preAnalyzed → assert plumbing WITHOUT the ~2.4 GB model.
19
- This is a STUB-VLM proof — NOT a real-model proof.
15
+ `cli.js local-sense` against STUB local VLM/detector services and an
16
+ authenticated retained capability/profile/ingest API, feeding the committed
17
+ fixture through real RTSP/ffmpeg. Checks declared field ownership, pinned
18
+ requests and original evidence/hash/capture time with the genuine retained
19
+ protocol parser/acknowledgement, WITHOUT downloading the real models.
20
+ This is a STUB-MODEL wiring proof — NOT an inference-accuracy proof.
20
21
 
21
22
  bench (workflow_dispatch only):
22
23
  Provisions exactly as 'real' does, then runs scripts/bench-local-vlm.mjs
@@ -56,12 +56,10 @@ function Register-Connector([string]$Root) {
56
56
  2026-08-19 Windows BOM incident, and it is why this is here rather than left
57
57
  to the repair path.
58
58
 
59
- RE-REGISTER, NOT RE-PROVISION. `setup-detector.ps1` runs `uv tool install
60
- --force`, which rebuilds a venv and needs the network; putting that inside an
61
- update transaction would give the upgrade a new way to be slow and a new way
62
- to fail. Re-registering the task is the whole of what has to change, and
63
- register-detector-task.ps1 already stops the running service, replaces the
64
- action and starts it again - so this both migrates and restarts it.
59
+ Re-register when the runtime source stamp matches. When it differs, run the
60
+ owned provisioner so a healthy old CPU-only service cannot survive a GPU
61
+ update indefinitely. The provisioner serializes replacement, verifies its
62
+ installed stamp and chooses one CPU/CUDA dependency set. Pairings stay intact.
65
63
 
66
64
  BEST EFFORT, ALWAYS. The detector is a cost optimisation: without it every
67
65
  frame goes to the VLM exactly as it does today. It must never be able to fail
@@ -77,6 +75,47 @@ function Update-DetectorTask([string]$Root) {
77
75
  # register-task.ps1 states for its own repair.
78
76
  $exe = ($task.Actions | Select-Object -First 1).Execute
79
77
  if (-not $exe) { return }
78
+ # Older updater array-splatting bound the text '-DetectorPath' positionally
79
+ # as the executable, shifting the real path into --port. Recover ONLY this
80
+ # known shape, under the current user's SID and exact local tool path.
81
+ if ($exe -eq '-DetectorPath') {
82
+ $taskUser = [string]$task.Principal.UserId
83
+ $taskSid = if ($taskUser -match '^S-1-') { $taskUser } else {
84
+ (New-Object System.Security.Principal.NTAccount($taskUser)).Translate([System.Security.Principal.SecurityIdentifier]).Value
85
+ }
86
+ $currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
87
+ $oldArgs = [string]($task.Actions | Select-Object -First 1).Arguments
88
+ $ownTool = Join-Path $env:USERPROFILE '.local\bin\trillboards-detector.exe'
89
+ if ($taskSid -ne $currentSid -or $oldArgs -notmatch '^serve --host 127\.0\.0\.1 --port (.+?) --log-file "-RamBudgetMb" --ram-budget-mb \d+$') {
90
+ throw 'Cannot verify ownership of the malformed detector task'
91
+ }
92
+ $recovered = $Matches[1]
93
+ if ([IO.Path]::GetFullPath($recovered) -ine [IO.Path]::GetFullPath($ownTool) -or -not (Test-Path -LiteralPath $ownTool)) {
94
+ throw 'Malformed detector task does not identify this user installation'
95
+ }
96
+ $exe = $ownTool
97
+ }
98
+
99
+ $detectorSource = Join-Path $Root 'bin\detector'
100
+ $project = Join-Path $detectorSource 'pyproject.toml'
101
+ $setup = Join-Path $Root 'bin\setup-detector.ps1'
102
+ if ((Test-Path -LiteralPath $project) -and (Test-Path -LiteralPath $setup)) {
103
+ $versionLine = @(Get-Content -LiteralPath $project | Where-Object { $_ -match '^version\s*=\s*"([^"]+)"\s*$' })
104
+ if ($versionLine.Count -ne 1 -or $versionLine[0] -notmatch '^version\s*=\s*"([^"]+)"\s*$') {
105
+ throw 'Cannot resolve the staged detector source version'
106
+ }
107
+ $stagedDetectorVersion = $Matches[1]
108
+ $installedDetectorVersion = ((& $exe --version 2>$null | Out-String).Trim() -split '\s+')[-1]
109
+ if ($LASTEXITCODE -ne 0 -or $installedDetectorVersion -ne $stagedDetectorVersion) {
110
+ Write-Host "Updating the managed detector runtime to $stagedDetectorVersion."
111
+ & $setup -SourcePath $detectorSource
112
+ $verifiedDetectorVersion = ((& $exe --version 2>$null | Out-String).Trim() -split '\s+')[-1]
113
+ if ($LASTEXITCODE -ne 0 -or $verifiedDetectorVersion -ne $stagedDetectorVersion) {
114
+ throw 'Detector runtime update did not verify; pairing preserved, local detector needs repair'
115
+ }
116
+ return # the owned provisioner already registered and restarted the task
117
+ }
118
+ }
80
119
 
81
120
  $register = Join-Path $Root "bin\register-detector-task.ps1"
82
121
  if (-not (Test-Path -LiteralPath $register)) { return }
@@ -93,12 +132,16 @@ function Update-DetectorTask([string]$Root) {
93
132
  if ($LASTEXITCODE -eq 0 -and $derived -match '^\d+$') { $budget = $derived }
94
133
  }
95
134
 
96
- $registerArgs = @('-DetectorPath', $exe)
97
- if ($budget) { $registerArgs += @('-RamBudgetMb', $budget) }
135
+ # A PowerShell SCRIPT takes named parameters from a hashtable. An array of
136
+ # flag strings is positional; it creates Execute='-DetectorPath' while
137
+ # reporting registration success. Native powershell.exe argument arrays
138
+ # elsewhere are a different boundary and must not be changed with this.
139
+ $registerArgs = @{ DetectorPath = $exe }
140
+ if ($budget) { $registerArgs.RamBudgetMb = $budget }
98
141
  & $register @registerArgs | Out-Null
99
142
  Write-Host "Detector task migrated$(if ($budget) { " (RAM ceiling $budget MiB)" })."
100
143
  } catch {
101
- Write-Warning "Could not migrate the detector task: $($_.Exception.Message). Sensing is unaffected; every frame keeps going to the VLM."
144
+ Write-Warning "Could not migrate the detector task: $($_.Exception.Message). Detector-backed profiles need the local detector repaired; camera pairing is preserved."
102
145
  }
103
146
  }
104
147