_resolve_scoped_artifact_path validates the filename against traversal, but _base_root and _session_artifacts_dir use user_id and session_id directly in Path() construction without the same check.
A user_id or session_id containing ../ segments builds a scope root outside root_dir. The filename guard then validates against the already-escaped scope root, so it doesn't catch the escape.
GcsArtifactService and InMemoryArtifactService use string keys, not filesystem paths - only FileArtifactService is affected.