Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
7a4c165f4e
Merge 815a2017c2 into a576ad5218 2026-05-20 17:27:38 +00:00
18 changed files with 47 additions and 205 deletions

View File

@ -43,7 +43,6 @@ from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateTopic,
)
from frigate.const import REDACTED_CREDENTIAL_SENTINEL
from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector
from frigate.genai import PROVIDERS, load_providers
from frigate.jobs.media_sync import (
@ -62,11 +61,7 @@ from frigate.util.builtin import (
process_config_query_string,
update_yaml_file_bulk,
)
from frigate.util.config import (
apply_section_update,
find_config_file,
redact_credential,
)
from frigate.util.config import apply_section_update, find_config_file
from frigate.util.schema import get_config_schema
from frigate.util.services import (
get_nvidia_driver_info,
@ -289,24 +284,26 @@ def config(request: Request):
if request.headers.get("remote-role") != "admin":
config.pop("environment_vars", None)
# redact mqtt credentials
redact_credential(config["mqtt"], "password")
# remove mqtt credentials
config["mqtt"].pop("password", None)
config["mqtt"].pop("user", None)
# redact proxy secret
redact_credential(config["proxy"], "auth_secret")
# remove the proxy secret
config["proxy"].pop("auth_secret", None)
# redact genai api keys
for _genai_name, genai_cfg in config.get("genai", {}).items():
# remove genai api keys
for genai_name, genai_cfg in config.get("genai", {}).items():
if isinstance(genai_cfg, dict):
redact_credential(genai_cfg, "api_key")
genai_cfg.pop("api_key", None)
for camera_name, camera in request.app.frigate_config.cameras.items():
camera_dict = config["cameras"][camera_name]
# redact onvif credentials
# remove onvif credentials
onvif_dict = camera_dict.get("onvif", {})
if onvif_dict:
redact_credential(onvif_dict, "password")
onvif_dict.pop("user", None)
onvif_dict.pop("password", None)
# clean paths
for input in camera_dict.get("ffmpeg", {}).get("inputs", []):
@ -683,10 +680,6 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo
_restore_masked_camera_paths(body.config_data, request.app.frigate_config)
updates = flatten_config_data(body.config_data)
updates = {k: ("" if v is None else v) for k, v in updates.items()}
# Drop any field whose value is still the redaction sentinel
updates = {
k: v for k, v in updates.items() if v != REDACTED_CREDENTIAL_SENTINEL
}
if not updates:
return JSONResponse(
@ -797,13 +790,6 @@ def config_set(request: Request, body: AppConfigSetBody):
updates = flatten_config_data(body.config_data)
# Convert None values to empty strings for deletion (e.g., when deleting masks)
updates = {k: ("" if v is None else v) for k, v in updates.items()}
# Drop sentinel-valued fields so untouched credential
# placeholders don't clobber the saved YAML value.
updates = {
k: v
for k, v in updates.items()
if v != REDACTED_CREDENTIAL_SENTINEL
}
if not updates:
return JSONResponse(

View File

@ -86,15 +86,10 @@ class DebugReplayStopResponse(BaseModel):
async def start_debug_replay(request: Request, body: DebugReplayStartBody):
"""Start a debug replay session asynchronously."""
replay_manager = request.app.replay_manager
internal_port = request.app.frigate_config.networking.listen.internal
if type(internal_port) is str:
internal_port = int(internal_port.split(":")[-1])
source = RecordingDebugReplaySource(
source_camera=body.camera,
start_ts=body.start_time,
end_ts=body.end_time,
internal_port=internal_port,
)
try:

View File

@ -21,8 +21,6 @@ PLUS_API_HOST = "https://api.frigate.video"
SHM_FRAMES_VAR = "SHM_MAX_FRAMES"
REDACTED_CREDENTIAL_SENTINEL = "__FRIGATE_SAVED_CREDENTIAL__"
# Attribute & Object constants
DEFAULT_ATTRIBUTE_LABEL_MAP = {

View File

@ -245,23 +245,11 @@ class DebugReplayManager:
"frame_shape",
"raw_mask",
"mask",
"enabled_in_config",
"improved_contrast_enabled",
"rasterized_mask",
}
)
if source_config.motion.mask:
motion_dict["mask"] = {
mask_id: (
mask_cfg.model_dump(
exclude={"raw_coordinates", "enabled_in_config"}
)
if mask_cfg is not None
else None
)
for mask_id, mask_cfg in source_config.motion.mask.items()
}
return {
"enabled": True,
"ffmpeg": {

View File

@ -63,8 +63,8 @@ Describe the scene based on observable actions and movements, evaluate the activ
## Analysis Guidelines
When forming your description:
- **Treat "Objects in Scene" as the list of tracked subjects to describe.** Do not introduce additional people or vehicles that are not present in this list. You may freely reference other items, surfaces, and environmental details visible in the frames when describing what the listed subjects are doing.
- **Describe the most likely activity from visible cues across the sequence** the subject's path, what they are carrying, and what they interact with. Avoid asserting completed outcomes you do not observe; describe in-progress actions rather than results.
- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list.
- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence.
- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity).
- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects.
- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved.

View File

@ -1,4 +1,4 @@
"""Debug replay startup job: ffmpeg remux + camera config publish.
"""Debug replay startup job: ffmpeg concat + camera config publish.
The runner orchestrates the async portion of starting a debug replay
session. The DebugReplayManager (in frigate.debug_replay) owns session
@ -153,22 +153,15 @@ class DebugReplaySource(ABC):
class RecordingDebugReplaySource(DebugReplaySource):
"""Replay source backed by the Recordings table.
Feeds ffmpeg the internal VOD endpoint so segments with mismatched
SPS/PPS (e.g. across day/night transitions) stitch cleanly via HLS
discontinuities.
Builds a concat playlist of recording files covering the time range
and feeds it to ffmpeg's concat demuxer.
"""
def __init__(
self,
source_camera: str,
start_ts: float,
end_ts: float,
internal_port: int,
) -> None:
def __init__(self, source_camera: str, start_ts: float, end_ts: float) -> None:
self._camera = source_camera
self._start_ts = start_ts
self._end_ts = end_ts
self._internal_port = internal_port
self._concat_file: Optional[str] = None
@property
def source_camera(self) -> str:
@ -192,16 +185,18 @@ class RecordingDebugReplaySource(DebugReplaySource):
)
def ffmpeg_input_args(self, working_dir: str) -> list[str]:
playlist_url = (
f"http://127.0.0.1:{self._internal_port}/vod/{self._camera}"
f"/start/{self._start_ts}/end/{self._end_ts}/index.m3u8"
)
return [
"-protocol_whitelist",
"pipe,file,http,tcp",
"-i",
playlist_url,
]
replay_name = f"{REPLAY_CAMERA_PREFIX}{self._camera}"
concat_file = os.path.join(working_dir, f"{replay_name}_concat.txt")
recordings = query_recordings(self._camera, self._start_ts, self._end_ts)
with open(concat_file, "w") as f:
for recording in recordings:
f.write(f"file '{recording.path}'\n")
self._concat_file = concat_file
return ["-f", "concat", "-safe", "0", "-i", concat_file]
def cleanup(self, working_dir: str) -> None:
if self._concat_file:
_remove_silent(self._concat_file)
class ExportDebugReplaySource(DebugReplaySource):

View File

@ -2,7 +2,6 @@ from unittest.mock import Mock, patch
import frigate.genai
from frigate.config import GenAIProviderEnum
from frigate.const import REDACTED_CREDENTIAL_SENTINEL
from frigate.genai import GenAIClient
from frigate.models import Event, Recordings, ReviewSegment
from frigate.stats.emitter import StatsEmitter
@ -76,20 +75,6 @@ class TestHttpApp(BaseTestHttp):
assert response.status_code == 200
assert app.frigate_config.cameras["front_door"].objects.track == ["person"]
####################################################################################################################
################################### Credential redaction sentinel ################################################
####################################################################################################################
def test_config_response_redacts_mqtt_password_with_sentinel(self):
self.minimal_config["mqtt"]["user"] = "mqttuser"
self.minimal_config["mqtt"]["password"] = "supersecret"
app = super().create_app()
with AuthTestClient(app) as client:
response = client.get("/config")
assert response.status_code == 200
mqtt = response.json()["mqtt"]
assert mqtt["password"] == REDACTED_CREDENTIAL_SENTINEL
####################################################################################################################
################################### POST /genai/probe Endpoint ##################################################
####################################################################################################################

View File

@ -101,10 +101,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
with self.assertRaises(ValueError):
start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="missing",
start_ts=100.0,
end_ts=200.0,
internal_port=5000,
source_camera="missing", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
@ -115,10 +112,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
with self.assertRaises(ValueError):
start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="front",
start_ts=200.0,
end_ts=100.0,
internal_port=5000,
source_camera="front", start_ts=200.0, end_ts=100.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
@ -132,10 +126,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
with self.assertRaises(ValueError):
start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
internal_port=5000,
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
@ -165,10 +156,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
):
job_id = start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
internal_port=5000,
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
@ -205,10 +193,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
):
start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
internal_port=5000,
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
@ -218,10 +203,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
with self.assertRaises(RuntimeError):
start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
internal_port=5000,
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
@ -289,10 +271,7 @@ class TestRunnerHappyPath(unittest.TestCase):
):
start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
internal_port=5000,
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
@ -363,10 +342,7 @@ class TestRunnerFailurePath(unittest.TestCase):
):
start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
internal_port=5000,
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,
@ -444,10 +420,7 @@ class TestRunnerCancellation(unittest.TestCase):
):
start_debug_replay_job(
source=RecordingDebugReplaySource(
source_camera="front",
start_ts=100.0,
end_ts=200.0,
internal_port=5000,
source_camera="front", start_ts=100.0, end_ts=200.0
),
frigate_config=self.frigate_config,
config_publisher=self.publisher,

View File

@ -8,7 +8,7 @@ from typing import Any, Optional, Union
from ruamel.yaml import YAML
from frigate.const import CONFIG_DIR, EXPORT_DIR, REDACTED_CREDENTIAL_SENTINEL
from frigate.const import CONFIG_DIR, EXPORT_DIR
from frigate.util.builtin import deep_merge
from frigate.util.services import get_video_properties
@ -18,21 +18,6 @@ CURRENT_CONFIG_VERSION = "0.18-0"
DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml")
def redact_credential(obj: dict[str, Any], key: str) -> None:
"""Replace obj[key] with the redaction sentinel if a value is saved, else drop.
Used when shaping the /config response so saved credentials never leave
the server. The frontend recognizes REDACTED_CREDENTIAL_SENTINEL, renders
the field as empty with a "saved — leave blank to keep" placeholder, and
/config/set strips it from any incoming payload so the YAML value is
preserved when the user doesn't touch the field.
"""
if obj.get(key):
obj[key] = REDACTED_CREDENTIAL_SENTINEL
else:
obj.pop(key, None)
def find_config_file() -> str:
config_path = os.environ.get("CONFIG_FILE", DEFAULT_CONFIG_FILE)

View File

@ -316,8 +316,5 @@
"pixels": "{{area}}px"
},
"no_items": "No items",
"validation_errors": "Validation Errors",
"credentialField": {
"savedPlaceholder": "Saved — leave blank to keep current"
}
"validation_errors": "Validation Errors"
}

View File

@ -24,7 +24,6 @@ const genai: SectionConfigOverrides = {
"ui:widget": "genaiRoles",
},
"*.api_key": {
"ui:widget": "password",
"ui:options": { size: "lg" },
},
"*.base_url": {

View File

@ -64,7 +64,6 @@ const mqtt: SectionConfigOverrides = {
liveValidate: true,
uiSchema: {
password: {
"ui:widget": "password",
"ui:options": { size: "xs" },
},
},

View File

@ -29,9 +29,6 @@ const onvif: SectionConfigOverrides = {
host: {
"ui:options": { size: "sm" },
},
password: {
"ui:widget": "password",
},
profile: {
"ui:widget": "onvifProfile",
},

View File

@ -18,7 +18,6 @@ const proxy: SectionConfigOverrides = {
"ui:options": { size: "lg" },
},
auth_secret: {
"ui:widget": "password",
"ui:options": { size: "md" },
},
header_map: {

View File

@ -3,10 +3,8 @@ import type { WidgetProps } from "@rjsf/utils";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { LuEye, LuEyeOff } from "react-icons/lu";
import { cn } from "@/lib/utils";
import { REDACTED_CREDENTIAL_SENTINEL } from "@/lib/const";
import { getSizedFieldClassName } from "../utils";
export function PasswordWidget(props: WidgetProps) {
@ -23,31 +21,17 @@ export function PasswordWidget(props: WidgetProps) {
options,
} = props;
const { t } = useTranslation(["common"]);
const [showPassword, setShowPassword] = useState(false);
const fieldClassName = getSizedFieldClassName(options, "sm");
// When the backend returns the sentinel, hide it visually and prompt the
// user that a value is already saved. The value stays as the sentinel in
// form state — backend /config/set strips it so the saved YAML is
// preserved when the user doesn't touch the field.
const isRedacted = value === REDACTED_CREDENTIAL_SENTINEL;
const displayValue = isRedacted ? "" : (value ?? "");
const effectivePlaceholder = isRedacted
? t("credentialField.savedPlaceholder", {
ns: "common",
defaultValue: "Saved — leave blank to keep current",
})
: placeholder || "";
return (
<div className={cn("relative", fieldClassName)}>
<Input
id={id}
type={showPassword ? "text" : "password"}
value={displayValue}
value={value ?? ""}
disabled={disabled || readonly}
placeholder={effectivePlaceholder}
placeholder={placeholder || ""}
onChange={(e) =>
onChange(e.target.value === "" ? undefined : e.target.value)
}
@ -62,7 +46,7 @@ export function PasswordWidget(props: WidgetProps) {
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
disabled={disabled || isRedacted}
disabled={disabled}
>
{showPassword ? (
<LuEyeOff className="h-4 w-4" />

View File

@ -1,16 +1,6 @@
/** ONNX embedding models that require local model downloads. GenAI providers are not in this list. */
export const JINA_EMBEDDING_MODELS = ["jinav1", "jinav2"] as const;
/**
* Sentinel the backend substitutes for saved credentials (api keys,
* passwords, secrets) in /config responses. The credential widget renders
* this value as an empty input with a "saved — leave blank to keep" hint,
* and stripRedactedCredentials() removes any field still equal to this
* value before sending a config/set payload so the saved YAML value is
* preserved. Mirror of frigate.const.REDACTED_CREDENTIAL_SENTINEL.
*/
export const REDACTED_CREDENTIAL_SENTINEL = "__FRIGATE_SAVED_CREDENTIAL__";
export const ANNOTATION_OFFSET_MIN = -10000;
export const ANNOTATION_OFFSET_MAX = 5000;
export const ANNOTATION_OFFSET_STEP = 50;

View File

@ -121,7 +121,7 @@ export default function Replay() {
mutate: refreshStatus,
isLoading,
} = useSWR<DebugReplayStatus>("debug_replay/status", {
refreshInterval: (latestData) => (latestData?.live_ready ? 0 : 1000),
refreshInterval: 1000,
});
const { payload: replayJob } =
useJobStatus<DebugReplayJobResults>("debug_replay");

View File

@ -11,7 +11,6 @@ import isEqual from "lodash/isEqual";
import mergeWith from "lodash/mergeWith";
import set from "lodash/set";
import { isJsonObject } from "@/lib/utils";
import { REDACTED_CREDENTIAL_SENTINEL } from "@/lib/const";
import { applySchemaDefaults } from "@/lib/config-schema";
import { normalizeConfigValue } from "@/hooks/use-config-override";
import {
@ -30,33 +29,6 @@ import type {
import type { SectionConfig } from "../components/config-form/sections/BaseSection";
import { sectionConfigs } from "../components/config-form/sectionConfigs";
/**
* Recursively strip any key whose value is the redaction sentinel from a
* config_data payload. Use just before sending to /config/set so untouched
* credential placeholder fields don't clobber the saved YAML value. Mutates
* and returns the input.
*/
export function stripRedactedCredentials<T>(value: T): T {
if (Array.isArray(value)) {
for (const item of value) {
stripRedactedCredentials(item);
}
return value;
}
if (value && typeof value === "object") {
const obj = value as Record<string, unknown>;
for (const key of Object.keys(obj)) {
const v = obj[key];
if (v === REDACTED_CREDENTIAL_SENTINEL) {
delete obj[key];
} else if (v && typeof v === "object") {
stripRedactedCredentials(v);
}
}
}
return value;
}
// ---------------------------------------------------------------------------
// cameraUpdateTopicMap — maps config section paths to MQTT/WS update topics
// ---------------------------------------------------------------------------