Commit 16cf0c8
sentinel: slice 2 — agent-ready dispatch (notification hook, manual run, agent endpoints)
Wires up everything that can be wired without the actual Sentinel
agent service existing. The agent (slice 3) will plug into these
endpoints unchanged when it ships.
Backend:
1. SentinelRun gains state machine columns (added via sync_schema):
- outcome string widens to: pending | running | incident | no_action | error
- started_at, completed_at: nullable timestamps for the agent's
state transitions
- manual_prompt: nullable text for trigger_type=manual runs (the
operator's prompt from the Run-Now modal)
- is_terminal property: convenience for "has this run completed?"
2. New module app/core/sentinel_dispatch.py with the single dispatch
gate that every code path funnels through:
- _is_camera_in_scope: cameras absent from the scope dict default
to in-scope (matches frontend isCameraInScope behaviour)
- _schedule_allows_now: parses schedule_mode + schedule_start/end
+ active_days against the org's IANA timezone (Setting key
"timezone", defaults to UTC); handles wrap-around windows like
22:00 → 06:00
- cap enforcement: 300 runs / month, hard-coded for now (slice 5
surfaces it as a per-plan setting)
- maybe_dispatch_for_notification: best-effort, never raises —
a failed dispatch must NEVER block the underlying notification
from being delivered
- dispatch_manual_run: skips schedule + scope checks (the
operator overrode them by clicking) but still cap-enforces
3. notifications.create_notification() now calls
maybe_dispatch_for_notification() after the email side-channel.
When motion / incident_created fires AND Sentinel is enabled with
that trigger on AND the camera is in scope AND the schedule
allows it AND we're under the cap, a pending sentinel_runs row
gets inserted.
4. Four new endpoints in app/api/sentinel.py:
- POST /api/sentinel/runs/manual: operator-initiated run from the
"Run now" button. Pro Plus only; cap-enforced; audited.
- POST /api/sentinel/runs/{id}/start: agent claims a pending run
(transitions outcome=running). Idempotent. Auth: agent key.
- POST /api/sentinel/runs/{id}/complete: agent reports terminal
outcome + tool trace. Idempotent (re-call with same body is a
no-op once terminal). Auth: agent key.
- GET /api/sentinel/runs/pending: cross-org polling endpoint for
the agent to discover work, FIFO, surfaces org_id so the agent
knows which MCP key to use. Auth: agent key.
/runs/pending is registered BEFORE /runs/{run_id} because FastAPI
matches routes in registration order; otherwise the literal path
would 404 with run_id="pending".
5. New auth scheme: SENTINEL_AGENT_KEY env var. Service-to-service
secret in the X-Sentinel-Agent-Key header. Hard-rejects every
request when not configured, which is the right behaviour in
environments where the agent isn't deployed yet (current state).
6. /runs response stats expanded:
pending count, runs_this_month, monthly_cap, remaining_this_month.
Frontend:
1. New api.js helper: dispatchSentinelManualRun(getToken, {prompt,
cameraId}). POSTs to /runs/manual.
2. SentinelPage.jsx:
- Run-Now button enabled when interactive (not loading, not
plan-gated). Click opens the manual-run modal (re-added).
Submit POSTs to /manual, prepends the new pending row to the
timeline, optimistically bumps the stats counters, toasts.
- 429 cap-reached response surfaced as an explicit toast.
- Outcome chip helpers + outcomeDotClass extended to handle
pending and running states.
- Timeline + history-table runs render placeholder italic text
("Agent is investigating…", "Waiting for the agent to pick this
up") for pending/running rows that don't have a real summary
yet.
- Drawer surfaces the operator's manual_prompt when present, plus
dedicated outcome blurbs for pending/running.
3. CSS:
- .sentinel-outcome-chip-pending / -running (blue family)
- .sentinel-timeline-dot-pending / -running with the existing
pulse keyframe in blue — visually communicates "the agent
should pick this up imminently"
- .sentinel-timeline-summary-muted: italic muted style for the
placeholder lines
Verified locally:
- Full backend test suite (549 tests) green
- All 8 sentinel routes registered, /runs/pending matches before
/runs/{run_id}
- sync_schema added the 3 new columns to existing sentinel_runs
table on first boot
- Ruff lint clean (after auto-fix of import sort + the manual ones
from the prior commit)
What's still deferred to slice 3:
- Actually building the agent service (separate Fly app)
- Choosing polling vs. webhook delivery (both contracts are now in
place server-side; agent picks one)
- LLM inference + MCP tool calls
- Real reasoning summary + tool_trace populated by the agent
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 5b6e855 commit 16cf0c8
8 files changed
Lines changed: 789 additions & 38 deletions
File tree
- backend/app
- api
- core
- models
- frontend/src
- pages
- services
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
594 | 594 | | |
595 | 595 | | |
596 | 596 | | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
597 | 614 | | |
598 | 615 | | |
599 | 616 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
| 34 | + | |
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
40 | 41 | | |
41 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
42 | 48 | | |
43 | 49 | | |
44 | 50 | | |
| |||
102 | 108 | | |
103 | 109 | | |
104 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
105 | 138 | | |
106 | 139 | | |
107 | 140 | | |
| |||
235 | 268 | | |
236 | 269 | | |
237 | 270 | | |
238 | | - | |
239 | | - | |
| 271 | + | |
240 | 272 | | |
241 | 273 | | |
242 | 274 | | |
| |||
245 | 277 | | |
246 | 278 | | |
247 | 279 | | |
| 280 | + | |
| 281 | + | |
248 | 282 | | |
249 | 283 | | |
250 | 284 | | |
251 | 285 | | |
252 | 286 | | |
253 | 287 | | |
254 | 288 | | |
| 289 | + | |
255 | 290 | | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
256 | 294 | | |
257 | 295 | | |
258 | 296 | | |
259 | 297 | | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
260 | 337 | | |
261 | 338 | | |
262 | 339 | | |
| |||
273 | 350 | | |
274 | 351 | | |
275 | 352 | | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
141 | 153 | | |
142 | 154 | | |
0 commit comments