Commit graph

15 commits

Author SHA1 Message Date
유창욱
949c6d05c3 feat(bundle): pin + hash + SBOM + verify offline bundle integrity (P5-4)
The offline bundle shipped unpinned pip deps with no digests, no SBOM,
and no signature — an air-gap target can't re-resolve from PyPI, so a
truncated or tampered wheel would install silently.

build_offline_bundle.sh now emits, from the resolved wheelhouse:
- SHA256SUMS  — sha256 of every wheel + model
- requirements.lock — exact name==version pins for a reproducible rebuild
- sbom.json  — CycloneDX 1.5 components (name, version, pypi purl, sha256)
- SHA256SUMS.asc — optional detached GPG signature when SIGN_KEY is set
MANIFEST advertises the integrity set and whether it is signed.

install_offline.sh verifies `sha256sum -c SHA256SUMS` (and the signature,
if present) BEFORE any pip install, hard-failing on mismatch; older
bundles without the file pass with a warning.

The generation logic was verified end-to-end on a synthetic stage:
requirements.lock/SHA256SUMS correct, `sha256sum -c` passes, sbom.json is
valid CycloneDX JSON. Both scripts pass `bash -n`.

Tests (test_deploy_artifacts.py): pinned integrity manifest is emitted
(+ optional SIGN_KEY), installer verifies integrity before install.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 10:18:57 +09:00
유창욱
1fb7a5da58 fix(bundle): carry the Hailo HEF in the offline bundle, ONNX optional (P5-3)
The air-gapped bundle never fetched the Hailo HEF, so an offline Hailo
install had no pose model. Two latent breaks also made the offline path
unusable regardless of P5-3:

- build [2/5] ran `fetch_movenet` unconditionally; with the MoveNet
  source unreachable that aborted the whole build under `set -e`.
- install_offline copied only `*.onnx`, so even a HEF placed in the
  bundle never reached $PREFIX/models for the backend gate to find.

Fixes:
- build_offline_bundle.sh fetches the $HAILO_ARCH (default hailo8) HEF
  via edge.tools.fetch_hailo_hef (pinned sha256, aborts on mismatch) and
  stages yolov8s_pose.hef. MoveNet ONNX is now optional (guarded fetch →
  Hailo-only bundle if unavailable). onnxruntime moves to a best-effort
  optional wheel. MANIFEST lists models + available pose_backends.
- install_offline.sh copies *.hef (and tolerates a missing *.onnx).

Tests (test_deploy_artifacts.py): bundle fetches+stages the HEF, MoveNet
is optional (guarded), installer copies the HEF. Both scripts pass
`bash -n`. Full python suite: 152 passed, 2 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 10:15:13 +09:00
유창욱
99070f5e87 fix(cloud): don't swallow snapshot failures, clean orphan JPEGs (P4-7)
The ingest snapshot loop silently `continue`d on re-encode and file-write
failures (evidence lost with a 201), and a DB-index throw after the file
landed left an orphan JPEG on disk with no row — or bubbled into a 500
that discarded an already-stored event.

Extracts storeSnapshot(): re-encode → write file → index, in that order
so bytes exist before a row points at them. Every failure is error_log'd
instead of swallowed; a DB-index failure after the write @unlinks the
orphan file. A snapshot problem no longer fails the ingest — the event is
already persisted, so the 201 stands.

Test: testSnapshotIndexFailureCleansOrphanFileButKeepsEvent uses a
throwing snapshot repo and asserts 201, the event is stored, and no *.jpg
is left in the snapshot dir.

phpunit: 90 passing (4 skipped: PDO-backed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 10:08:28 +09:00
유창욱
9a0ff1fc5e feat(cloud): cursor pagination for the event list (P4-5)
The event list was hard-capped at the newest 100 with no way to reach
older events. Adds keyset pagination:

- EventRepository::page(limit, severity, beforeTs, beforeId) on both the
  PDO and in-memory implementations, ordered (occurred_at DESC,
  event_id DESC). The cursor is the last row's (occurred_at, event_id),
  so pages stay stable as new events arrive at the head — no overlap, no
  skips. occurred_at is bound as a string in SQL to keep DECIMAL
  precision.
- EventListController reads `limit` (default 50, capped 200) and `cursor`
  ("occurred_at:event_id", malformed input ignored) and returns
  `next_cursor` only on a full page. recent() is retained for existing
  callers; the response gains next_cursor additively.

The live-tail dashboard keeps consuming page 1; a history-browse UI can
build on the same cursor. Test: testListPaginatesWithCursor.

phpunit: 89 passing (4 skipped: PDO-backed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 10:05:52 +09:00
유창욱
d0ba621b42 fix(cloud): verify login CSRF + zone ownership 400 not 500 (P4-8, P4-6)
P4-8: AuthController::login now verifies the CSRF token the login form
already emits (Csrf::check over session vs posted field), returning 403
before the user lookup. Closes a login-CSRF / session-fixation hole where
a forged cross-site POST could silently log a victim into an attacker's
account. Tests seed the session+body token; adds
testLoginRejectsMissingOrWrongCsrf.

P4-6: ZoneConfigController::save wraps the repository write in
try/catch(\RuntimeException) so an ownership/device mismatch is a 400
(invalid_zone) client error, not an uncaught 500. Adds
testSaveOwnershipMismatchIs400NotError with a throwing repo double.

phpunit: 88 passing (4 skipped: PDO-backed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 10:00:04 +09:00
유창욱
c66b034921 fix(cloud): logout order, url normalization, length caps, package hygiene (P4-9/10/11, P5-6)
- P4-9 [L] logout() rotates the session id before wiping (was regenerate-after-destroy → warning).
- P4-10 [L] EventValidator caps device_id/camera_id (64) + zone_id (32) → 400 not a MySQL
  'Data too long' 500 the edge retries forever.
- P4-11 [L] Request::pathFor collapses leading slashes so '//api/events' isn't misrouted to '/events'.
- P5-6 [L] cafe24 build prunes dev caches (.ruff_cache/.impeccable/__pycache__/…) from the package.

python + phpunit (86) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:48:35 +09:00
유창욱
36b1f94571 fix(install): arch guard + CSI device access (P5-2, P5-5)
- P5-2 [H] build_offline_bundle.sh native branch hard-fails when uname -m != TARGET_ARCH
  (an x86 build was producing an aarch64-named tarball of x86_64 wheels).
- P5-5 [H] systemd unit adds DeviceAllow char-media + /dev/dma_heap for the CSI camera —
  picamera2/libcamera on Pi 5 needs them or the unit denies its own camera. OFF-HW: verify on Pi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:38:18 +09:00
유창욱
7a86545367 feat(delivery-audit): privacy — face-blur sizing + model integrity (P3-1, P3-3)
- P3-1 [H] _head_boxes sizes the anonymization region from the person bbox (scales with
  distance) + bbox-top fallback when the nose keypoint is low-confidence; a fixed ~40px
  patch had left close faces identifiable. test_faceblur.py.
- P3-3 [H] fetch_movenet.py rewritten to stage-validate (reject HTML/LFS-pointer, optional
  pinned sha256+size, fail loud) per the SHA256 spec requirement. Discovery: the old PINTO
  raw URL is DEAD (GitHub 404 → it was silently saving an HTML page as the .onnx). Non-blocking
  for the Hailo delivery path; the CPU MoveNet fallback needs a valid MOVENET_ONNX_URL pinned.

python + phpunit suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:35:02 +09:00
유창욱
1187df5628 feat(delivery-audit): PIPA retention + cloud hardening (P3-2, P4-1..4)
- P3-2 [H] snapshot retention (7 days): snapshot_retention_days config;
  SnapshotRepository::purgeOlderThan (interface + Pdo + double, files-then-rows,
  bounded batch); opportunistic purge in the ingest path (Cafe24 has no cron).
- P4-1 [H] index.php sets display_errors=0 + a global exception handler → generic
  {"error":"internal"} + error_log (no DSN/db_user leak); ships www/move_detect/.user.ini.
- P4-2 [H] ConfigController + HeartbeatController take an optional rate limiter +
  clientIp; pre-auth per-IP anti-flood → 429 (were unthrottled).
- P4-3 [M] PdoNonceRepository::remember + PdoRateLimiterRepository::hit opportunistically
  reap expired rows (Cafe24 has no cron; tables grew unbounded).
- P4-4 [M] Router::dispatch rawurldecodes captured params (Korean/space camera_id could
  save zones but never load them).

Tests: python green; phpunit 84 green. See docs/delivery-audit.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:28:16 +09:00
유창욱
9386dfc580 feat(delivery): install-path fixes + cafe24 package + delivery-audit hardening
Three intertwined bodies of work developed and tested together this session.
Full test suite green: python (edge) + phpunit (cloud).

## Edge/Pi install path
- setup_online.sh: apt-lock retry (apt-daily contention), Type=exec unit +
  StartLimitIntervalSec in [Unit], journalctl-on-failure, crash-loop detection.
- onnxruntime is now best-effort (no sdist / no 32-bit-ARM wheel); [6/8] probes the
  real backend and hard-fails if none works.
- Hailo HEF auto-fetch: edge/tools/fetch_hailo_hef.py (pinned sha256, refuses to guess
  8 vs 8L, HAILO_ARCH override for serial-less boards); hailo_pose.py run() timeout fix.
- install_offline.sh: mirrors the backend gate; onnxruntime optional.

## Cafe24 cloud package (cloud/deploy/)
- build_cafe24_package.sh emits move_detect_app/ (private, one folder) + www/move_detect/
  (docroot subdir); no-dev classmap autoloader; preflight.php; hardened .htaccess.
- Subdirectory mount support: Request::baseFrom/pathFor strip the mount prefix; index.php
  walks up to find the app root; views/JS prefix every URL with APP_BASE; session cookie
  scoped to the mount.

## Delivery audit — silent-failure & correctness (docs/delivery-audit.md)
All 4 blockers + all Phase-1 silent-failure + Phase-2 HIGH correctness:
- Edge cloud clients send a custom User-Agent (cafe24 WAF blocks python-requests) and
  allow_redirects=False.
- Daemon fails loud on blank CAFE24_URL/DEVICE_SECRET; run_loop logs swallowed exceptions;
  heartbeat carries real fps + per-camera health; UvcSource reopens a dead camera;
  offline queue is size-capped and drops poison rows; TARGET_FPS/QUEUE_DB validated.
- Zone coordinates normalized 0..1 on both sides (were canvas-px vs camera-px — every zone
  misplaced); pinch zones emit PINCH/HIGH (were downgraded to WARNING).
- Fall detection uses bbox cues when keypoints are low-confidence (occluded fallen person);
  ghost incident not cancelled by a bystander; displacement normalized per-second (FPS-invariant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:07:33 +09:00
유창욱
a946d0bee1 docs: pilot runbook + gated MoveNet smoke test 2026-07-12 11:49:07 +09:00
유창욱
e1f9f91110 docs: P1 pilot integration plan (wire edge daemon + CPU pose + detail view)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne4rU4FToTLHcBUMUvUnT1
2026-07-12 10:35:21 +09:00
유창욱
65344f6ea6 docs: P1-B cloud ingest + dashboard implementation plan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne4rU4FToTLHcBUMUvUnT1
2026-07-11 23:12:04 +09:00
유창욱
2ac26d4091 feat(edge): hardware adapter stubs + Pi bring-up/benchmark doc 2026-07-11 21:54:24 +09:00
유창욱
a4f2ead211 docs: workplace safety AI CCTV design spec v2.0 + P1-A edge core plan
- 설계 스펙 v2.0 (다관점 리뷰 반영: 기절경로 분리, 시점강건 낙상, 인증/RBAC, 평가셋, fps 3티어, 컴플라이언스, 증거 무결성)
- P1-A 엣지 감지 코어 구현계획 (16 태스크 TDD)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne4rU4FToTLHcBUMUvUnT1
2026-07-11 20:08:01 +09:00